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

mendersoftware / gui / 914712491

pending completion
914712491

Pull #3798

gitlab-ci

mzedel
refac: refactored signup page to make better use of form capabilities

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3798: MEN-3530 - refactored forms

4359 of 6322 branches covered (68.95%)

92 of 99 new or added lines in 11 files covered. (92.93%)

1715 existing lines in 159 files now uncovered.

8203 of 9941 relevant lines covered (82.52%)

150.06 hits per line

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

84.63
/src/js/actions/deviceActions.js
1
/*eslint import/namespace: ['error', { allowComputed: true }]*/
2
import React from 'react';
3
import { Link } from 'react-router-dom';
4

5
import { isCancel } from 'axios';
6
import pluralize from 'pluralize';
7
import { v4 as uuid } from 'uuid';
8

9
import { commonErrorFallback, commonErrorHandler, setSnackbar } from '../actions/appActions';
10
import { getSingleDeployment } from '../actions/deploymentActions';
11
import { auditLogsApiUrl } from '../actions/organizationActions';
12
import { cleanUpUpload, progress } from '../actions/releaseActions';
13
import { saveGlobalSettings } from '../actions/userActions';
14
import GeneralApi, { MAX_PAGE_SIZE, apiUrl, headerNames } from '../api/general-api';
15
import { routes, sortingAlternatives } from '../components/devices/base-devices';
16
import { SORTING_OPTIONS, UPLOAD_PROGRESS, emptyChartSelection, yes } from '../constants/appConstants';
17
import * as DeviceConstants from '../constants/deviceConstants';
18
import { rootfsImageVersion } from '../constants/releaseConstants';
19
import { attributeDuplicateFilter, deepCompare, extractErrorMessage, getSnackbarMessage, mapDeviceAttributes } from '../helpers';
20
import {
21
  getDeviceFilters,
22
  getDeviceTwinIntegrations,
23
  getGroups as getGroupsSelector,
24
  getIdAttribute,
25
  getTenantCapabilities,
26
  getUserCapabilities,
27
  getUserSettings
28
} from '../selectors';
29
import { chartColorPalette } from '../themes/Mender';
30
import { getDeviceMonitorConfig, getLatestDeviceAlerts } from './monitorActions';
31

32
const { DEVICE_FILTERING_OPTIONS, DEVICE_STATES, DEVICE_LIST_DEFAULTS, UNGROUPED_GROUP, emptyFilter } = DeviceConstants;
187✔
33
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
187✔
34

35
export const deviceAuthV2 = `${apiUrl.v2}/devauth`;
187✔
36
export const deviceConnect = `${apiUrl.v1}/deviceconnect`;
187✔
37
export const inventoryApiUrl = `${apiUrl.v1}/inventory`;
187✔
38
export const inventoryApiUrlV2 = `${apiUrl.v2}/inventory`;
187✔
39
export const deviceConfig = `${apiUrl.v1}/deviceconfig/configurations/device`;
187✔
40
export const reportingApiUrl = `${apiUrl.v1}/reporting`;
187✔
41
export const iotManagerBaseURL = `${apiUrl.v1}/iot-manager`;
187✔
42

43
const defaultAttributes = [
187✔
44
  { scope: 'identity', attribute: 'status' },
45
  { scope: 'inventory', attribute: 'artifact_name' },
46
  { scope: 'inventory', attribute: 'device_type' },
47
  { scope: 'inventory', attribute: 'mender_is_gateway' },
48
  { scope: 'inventory', attribute: 'mender_gateway_system_id' },
49
  { scope: 'inventory', attribute: rootfsImageVersion },
50
  { scope: 'monitor', attribute: 'alerts' },
51
  { scope: 'system', attribute: 'created_ts' },
52
  { scope: 'system', attribute: 'updated_ts' },
53
  { scope: 'system', attribute: 'group' },
54
  { scope: 'tags', attribute: 'name' }
55
];
56

57
export const getSearchEndpoint = hasReporting => (hasReporting ? `${reportingApiUrl}/devices/search` : `${inventoryApiUrlV2}/filters/search`);
187!
58

59
const getAttrsEndpoint = hasReporting => (hasReporting ? `${reportingApiUrl}/devices/search/attributes` : `${inventoryApiUrlV2}/filters/attributes`);
187✔
60

61
export const getGroups = () => (dispatch, getState) =>
187✔
62
  GeneralApi.get(`${inventoryApiUrl}/groups`).then(res => {
27✔
63
    const state = getState().devices.groups.byId;
27✔
64
    const dynamicGroups = Object.entries(state).reduce((accu, [id, group]) => {
27✔
65
      if (group.id || (group.filters?.length && id !== UNGROUPED_GROUP.id)) {
50✔
66
        accu[id] = group;
24✔
67
      }
68
      return accu;
50✔
69
    }, {});
70
    const groups = res.data.reduce((accu, group) => {
27✔
71
      accu[group] = { deviceIds: [], filters: [], total: 0, ...state[group] };
27✔
72
      return accu;
27✔
73
    }, dynamicGroups);
74
    const filters = [{ key: 'group', value: res.data, operator: DEVICE_FILTERING_OPTIONS.$nin.key, scope: 'system' }];
27✔
75
    return Promise.all([
27✔
76
      dispatch({ type: DeviceConstants.RECEIVE_GROUPS, groups }),
77
      dispatch(getDevicesByStatus(undefined, { filterSelection: filters, group: 0, page: 1, perPage: 1 }))
78
    ]).then(promises => {
79
      const ungroupedDevices = promises[promises.length - 1] || [];
25!
80
      const result = ungroupedDevices[ungroupedDevices.length - 1] || {};
25!
81
      if (!result.total) {
25!
UNCOV
82
        return Promise.resolve();
×
83
      }
84
      return Promise.resolve(
25✔
85
        dispatch({
86
          type: DeviceConstants.ADD_DYNAMIC_GROUP,
87
          groupName: UNGROUPED_GROUP.id,
88
          group: {
89
            deviceIds: [],
90
            total: 0,
91
            ...getState().devices.groups.byId[UNGROUPED_GROUP.id],
92
            filters: [{ key: 'group', value: res.data, operator: DEVICE_FILTERING_OPTIONS.$nin.key, scope: 'system' }]
93
          }
94
        })
95
      );
96
    });
97
  });
98

99
export const addDevicesToGroup = (group, deviceIds, isCreation) => dispatch =>
187✔
100
  GeneralApi.patch(`${inventoryApiUrl}/groups/${group}/devices`, deviceIds)
2✔
101
    .then(() => dispatch({ type: DeviceConstants.ADD_TO_GROUP, group, deviceIds }))
2✔
102
    .finally(() => (isCreation ? Promise.resolve(dispatch(getGroups())) : {}));
2✔
103

104
export const removeDevicesFromGroup = (group, deviceIds) => dispatch =>
187✔
105
  GeneralApi.delete(`${inventoryApiUrl}/groups/${group}/devices`, deviceIds).then(() =>
1✔
106
    Promise.all([
1✔
107
      dispatch({
108
        type: DeviceConstants.REMOVE_FROM_GROUP,
109
        group,
110
        deviceIds
111
      }),
112
      dispatch(setSnackbar(`The ${pluralize('devices', deviceIds.length)} ${pluralize('were', deviceIds.length)} removed from the group`, 5000))
113
    ])
114
  );
115

116
const getGroupNotification = (newGroup, selectedGroup) => {
187✔
117
  const successMessage = 'The group was updated successfully';
3✔
118
  if (newGroup === selectedGroup) {
3✔
119
    return [successMessage, 5000];
1✔
120
  }
121
  return [
2✔
122
    <>
123
      {successMessage} - <Link to={`/devices?inventory=group:eq:${newGroup}`}>click here</Link> to see it.
124
    </>,
125
    5000,
126
    undefined,
127
    undefined,
128
    () => {}
129
  ];
130
};
131

132
export const addStaticGroup = (group, devices) => (dispatch, getState) =>
187✔
133
  Promise.resolve(
1✔
134
    dispatch(
135
      addDevicesToGroup(
136
        group,
137
        devices.map(({ id }) => id),
1✔
138
        true
139
      )
140
    )
141
  )
142
    .then(() =>
143
      Promise.resolve(
1✔
144
        dispatch({
145
          type: DeviceConstants.ADD_STATIC_GROUP,
146
          group: { deviceIds: [], total: 0, filters: [], ...getState().devices.groups.byId[group] },
147
          groupName: group
148
        })
149
      ).then(() =>
150
        Promise.all([
1✔
151
          dispatch(setDeviceListState({ selectedId: undefined, setOnly: true })),
152
          dispatch(getGroups()),
153
          dispatch(setSnackbar(...getGroupNotification(group, getState().devices.groups.selectedGroup)))
154
        ])
155
      )
156
    )
UNCOV
157
    .catch(err => commonErrorHandler(err, `Group could not be updated:`, dispatch));
×
158

159
export const removeStaticGroup = groupName => (dispatch, getState) => {
187✔
160
  return GeneralApi.delete(`${inventoryApiUrl}/groups/${groupName}`).then(() => {
1✔
161
    const selectedGroup = getState().devices.groups.selectedGroup === groupName ? undefined : getState().devices.groups.selectedGroup;
1!
162
    // eslint-disable-next-line no-unused-vars
163
    const { [groupName]: removal, ...groups } = getState().devices.groups.byId;
1✔
164
    return Promise.all([
1✔
165
      dispatch({
166
        type: DeviceConstants.REMOVE_STATIC_GROUP,
167
        groups
168
      }),
169
      dispatch(getGroups()),
170
      dispatch(selectGroup(selectedGroup)),
171
      dispatch(setSnackbar('Group was removed successfully', 5000))
172
    ]);
173
  });
174
};
175

176
// for some reason these functions can not be stored in the deviceConstants...
177
const filterProcessors = {
187✔
UNCOV
178
  $gt: val => Number(val) || val,
×
UNCOV
179
  $gte: val => Number(val) || val,
×
180
  $lt: val => Number(val) || val,
2✔
UNCOV
181
  $lte: val => Number(val) || val,
×
UNCOV
182
  $in: val => ('' + val).split(',').map(i => i.trim()),
×
183
  $nin: val => ('' + val).split(',').map(i => i.trim()),
28✔
184
  $exists: yes,
UNCOV
185
  $nexists: () => false
×
186
};
187
const filterAliases = {
187✔
188
  $nexists: { alias: DEVICE_FILTERING_OPTIONS.$exists.key, value: false }
189
};
190
const mapFiltersToTerms = filters =>
187✔
191
  filters.map(filter => ({
131✔
192
    scope: filter.scope,
193
    attribute: filter.key,
194
    type: filterAliases[filter.operator]?.alias || filter.operator,
262✔
195
    value: filterProcessors.hasOwnProperty(filter.operator) ? filterProcessors[filter.operator](filter.value) : filter.value
131✔
196
  }));
197
const mapTermsToFilters = terms =>
187✔
198
  terms.map(term => {
25✔
199
    const aliasedFilter = Object.entries(filterAliases).find(
75✔
200
      aliasDefinition => aliasDefinition[1].alias === term.type && aliasDefinition[1].value === term.value
75✔
201
    );
202
    const operator = aliasedFilter ? aliasedFilter[0] : term.type;
75✔
203
    return { scope: term.scope, key: term.attribute, operator, value: term.value };
75✔
204
  });
205

206
export const getDynamicGroups = () => (dispatch, getState) =>
187✔
207
  GeneralApi.get(`${inventoryApiUrlV2}/filters?per_page=${MAX_PAGE_SIZE}`)
25✔
208
    .then(({ data: filters }) => {
209
      const state = getState().devices.groups.byId;
25✔
210
      const staticGroups = Object.entries(state).reduce((accu, [id, group]) => {
25✔
211
        if (!(group.id || group.filters?.length)) {
47✔
212
          accu[id] = group;
25✔
213
        }
214
        return accu;
47✔
215
      }, {});
216
      const groups = (filters || []).reduce((accu, filter) => {
25!
217
        accu[filter.name] = {
25✔
218
          deviceIds: [],
219
          total: 0,
220
          ...state[filter.name],
221
          id: filter.id,
222
          filters: mapTermsToFilters(filter.terms)
223
        };
224
        return accu;
25✔
225
      }, staticGroups);
226
      return Promise.resolve(dispatch({ type: DeviceConstants.RECEIVE_DYNAMIC_GROUPS, groups }));
25✔
227
    })
UNCOV
228
    .catch(() => console.log('Dynamic group retrieval failed - likely accessing a non-enterprise backend'));
×
229

230
export const addDynamicGroup = (groupName, filterPredicates) => (dispatch, getState) =>
187✔
231
  GeneralApi.post(`${inventoryApiUrlV2}/filters`, { name: groupName, terms: mapFiltersToTerms(filterPredicates) })
2✔
232
    .then(res =>
233
      Promise.resolve(
2✔
234
        dispatch({
235
          type: DeviceConstants.ADD_DYNAMIC_GROUP,
236
          groupName,
237
          group: {
238
            deviceIds: [],
239
            total: 0,
240
            ...getState().devices.groups.byId[groupName],
241
            id: res.headers[headerNames.location].substring(res.headers[headerNames.location].lastIndexOf('/') + 1),
242
            filters: filterPredicates
243
          }
244
        })
245
      ).then(() => {
246
        const { cleanedFilters } = getGroupFilters(groupName, getState().devices.groups);
2✔
247
        return Promise.all([
2✔
248
          dispatch(setDeviceFilters(cleanedFilters)),
249
          dispatch(setSnackbar(...getGroupNotification(groupName, getState().devices.groups.selectedGroup))),
250
          dispatch(getDynamicGroups())
251
        ]);
252
      })
253
    )
UNCOV
254
    .catch(err => commonErrorHandler(err, `Group could not be updated:`, dispatch));
×
255

256
export const updateDynamicGroup = (groupName, filterPredicates) => (dispatch, getState) => {
187✔
257
  const filterId = getState().devices.groups.byId[groupName].id;
1✔
258
  return GeneralApi.delete(`${inventoryApiUrlV2}/filters/${filterId}`).then(() => Promise.resolve(dispatch(addDynamicGroup(groupName, filterPredicates))));
1✔
259
};
260

261
export const removeDynamicGroup = groupName => (dispatch, getState) => {
187✔
262
  let groups = { ...getState().devices.groups.byId };
1✔
263
  const filterId = groups[groupName].id;
1✔
264
  const selectedGroup = getState().devices.groups.selectedGroup === groupName ? undefined : getState().devices.groups.selectedGroup;
1!
265
  return Promise.all([GeneralApi.delete(`${inventoryApiUrlV2}/filters/${filterId}`), dispatch(selectGroup(selectedGroup))]).then(() => {
1✔
266
    delete groups[groupName];
1✔
267
    return Promise.all([
1✔
268
      dispatch({
269
        type: DeviceConstants.REMOVE_DYNAMIC_GROUP,
270
        groups
271
      }),
272
      dispatch(setSnackbar('Group was removed successfully', 5000))
273
    ]);
274
  });
275
};
276
/*
277
 * Device inventory functions
278
 */
279
const getGroupFilters = (group, groupsState, filters = []) => {
187✔
280
  const groupName = group === UNGROUPED_GROUP.id || group === UNGROUPED_GROUP.name ? UNGROUPED_GROUP.id : group;
10!
281
  const selectedGroup = groupsState.byId[groupName];
10✔
282
  const groupFilterLength = selectedGroup?.filters?.length || 0;
10✔
283
  const cleanedFilters = groupFilterLength
10✔
284
    ? (filters.length ? filters : selectedGroup.filters).filter((item, index, array) => array.findIndex(filter => deepCompare(filter, item)) == index)
5✔
285
    : filters;
286
  return { cleanedFilters, groupName, selectedGroup, groupFilterLength };
10✔
287
};
288

289
export const selectGroup =
290
  (group, filters = []) =>
187✔
291
  (dispatch, getState) => {
5✔
292
    const { cleanedFilters, groupName, selectedGroup, groupFilterLength } = getGroupFilters(group, getState().devices.groups, filters);
5✔
293
    const state = getState();
5✔
294
    if (state.devices.groups.selectedGroup === groupName && filters.length === 0 && !groupFilterLength) {
5✔
295
      return;
2✔
296
    }
297
    let tasks = [];
3✔
298
    if (groupFilterLength) {
3✔
299
      tasks.push(dispatch(setDeviceFilters(cleanedFilters)));
2✔
300
    } else {
301
      tasks.push(dispatch(setDeviceFilters(filters)));
1✔
302
      tasks.push(dispatch(getGroupDevices(groupName, { perPage: 1, shouldIncludeAllStates: true })));
1✔
303
    }
304
    const selectedGroupName = selectedGroup || !Object.keys(state.devices.groups.byId).length ? groupName : undefined;
3!
305
    tasks.push(dispatch({ type: DeviceConstants.SELECT_GROUP, group: selectedGroupName }));
3✔
306
    return Promise.all(tasks);
3✔
307
  };
308
const getEarliestTs = (dateA = '', dateB = '') => (!dateA || !dateB ? dateA || dateB : dateA < dateB ? dateA : dateB);
252!
309
const getLatestTs = (dateA = '', dateB = '') => (!dateA || !dateB ? dateA || dateB : dateA >= dateB ? dateA : dateB);
252✔
310

311
const reduceReceivedDevices = (devices, ids, state, status) =>
187✔
312
  devices.reduce(
125✔
313
    (accu, device) => {
314
      const stateDevice = state.devices.byId[device.id] || {};
126✔
315
      const {
316
        attributes: storedAttributes = {},
3✔
317
        identity_data: storedIdentity = {},
3✔
318
        monitor: storedMonitor = {},
102✔
319
        tags: storedTags = {},
102✔
320
        group: storedGroup
321
      } = stateDevice;
126✔
322
      const { identity, inventory, monitor, system = {}, tags } = mapDeviceAttributes(device.attributes);
126!
323
      // all the other mapped attributes return as empty objects if there are no attributes to map, but identity will be initialized with an empty state
324
      // for device_type and artifact_name, potentially overwriting existing info, so rely on stored information instead if there are no attributes
325
      device.attributes = device.attributes ? { ...storedAttributes, ...inventory } : storedAttributes;
126✔
326
      device.tags = { ...storedTags, ...tags };
126✔
327
      device.group = system.group ?? storedGroup;
126✔
328
      device.monitor = { ...storedMonitor, ...monitor };
126✔
329
      device.identity_data = { ...storedIdentity, ...identity, ...(device.identity_data ? device.identity_data : {}) };
126✔
330
      device.status = status ? status : device.status || identity.status;
126✔
331
      device.created_ts = getEarliestTs(getEarliestTs(system.created_ts, device.created_ts), stateDevice.created_ts);
126✔
332
      device.updated_ts = getLatestTs(getLatestTs(system.updated_ts, device.updated_ts), stateDevice.updated_ts);
126✔
333
      device.isOffline = new Date(device.updated_ts) < new Date(state.app.offlineThreshold);
126✔
334
      accu.devicesById[device.id] = { ...stateDevice, ...device };
126✔
335
      accu.ids.push(device.id);
126✔
336
      return accu;
126✔
337
    },
338
    { ids, devicesById: {} }
339
  );
340

341
export const getGroupDevices =
342
  (group, options = {}) =>
187✔
343
  (dispatch, getState) => {
3✔
344
    const { shouldIncludeAllStates, ...remainder } = options;
3✔
345
    const { cleanedFilters: filterSelection } = getGroupFilters(group, getState().devices.groups);
3✔
346
    return Promise.resolve(
3✔
347
      dispatch(getDevicesByStatus(shouldIncludeAllStates ? undefined : DEVICE_STATES.accepted, { ...remainder, filterSelection, group }))
3✔
348
    ).then(results => {
349
      if (!group) {
3✔
350
        return Promise.resolve();
1✔
351
      }
352
      const { deviceAccu, total } = results[results.length - 1];
2✔
353
      const stateGroup = getState().devices.groups.byId[group];
2✔
354
      if (!stateGroup && !total && !deviceAccu.ids.length) {
2!
UNCOV
355
        return Promise.resolve();
×
356
      }
357
      return Promise.resolve(
2✔
358
        dispatch({
359
          type: DeviceConstants.RECEIVE_GROUP_DEVICES,
360
          group: {
361
            filters: [],
362
            ...stateGroup,
363
            deviceIds: deviceAccu.ids.length === total || deviceAccu.ids.length > stateGroup?.deviceIds ? deviceAccu.ids : stateGroup.deviceIds,
6!
364
            total
365
          },
366
          groupName: group
367
        })
368
      );
369
    });
370
  };
371

372
export const getAllGroupDevices = (group, shouldIncludeAllStates) => (dispatch, getState) => {
187✔
373
  if (!group || (!!group && (!getState().devices.groups.byId[group] || getState().devices.groups.byId[group].filters.length))) {
14✔
374
    return Promise.resolve();
13✔
375
  }
376
  const { attributes, filterTerms } = prepareSearchArguments({
1✔
377
    filters: [],
378
    group,
379
    state: getState(),
380
    status: shouldIncludeAllStates ? undefined : DEVICE_STATES.accepted
1!
381
  });
382
  const getAllDevices = (perPage = MAX_PAGE_SIZE, page = defaultPage, devices = []) =>
1✔
383
    GeneralApi.post(getSearchEndpoint(getState().app.features.hasReporting), {
1✔
384
      page,
385
      per_page: perPage,
386
      filters: filterTerms,
387
      attributes
388
    }).then(res => {
389
      const state = getState();
1✔
390
      const deviceAccu = reduceReceivedDevices(res.data, devices, state);
1✔
391
      dispatch({
1✔
392
        type: DeviceConstants.RECEIVE_DEVICES,
393
        devicesById: deviceAccu.devicesById
394
      });
395
      const total = Number(res.headers[headerNames.total]);
1✔
396
      if (total > perPage * page) {
1!
UNCOV
397
        return getAllDevices(perPage, page + 1, deviceAccu.ids);
×
398
      }
399
      return Promise.resolve(
1✔
400
        dispatch({
401
          type: DeviceConstants.RECEIVE_GROUP_DEVICES,
402
          group: {
403
            filters: [],
404
            ...state.devices.groups.byId[group],
405
            deviceIds: deviceAccu.ids,
406
            total: deviceAccu.ids.length
407
          },
408
          groupName: group
409
        })
410
      );
411
    });
412
  return getAllDevices();
1✔
413
};
414

415
export const getAllDynamicGroupDevices = group => (dispatch, getState) => {
187✔
416
  if (!!group && (!getState().devices.groups.byId[group] || !getState().devices.groups.byId[group].filters.length)) {
14✔
417
    return Promise.resolve();
13✔
418
  }
419
  const { attributes, filterTerms: filters } = prepareSearchArguments({
1✔
420
    filters: getState().devices.groups.byId[group].filters,
421
    state: getState(),
422
    status: DEVICE_STATES.accepted
423
  });
424
  const getAllDevices = (perPage = MAX_PAGE_SIZE, page = defaultPage, devices = []) =>
1✔
425
    GeneralApi.post(getSearchEndpoint(getState().app.features.hasReporting), { page, per_page: perPage, filters, attributes }).then(res => {
1✔
426
      const state = getState();
1✔
427
      const deviceAccu = reduceReceivedDevices(res.data, devices, state);
1✔
428
      dispatch({
1✔
429
        type: DeviceConstants.RECEIVE_DEVICES,
430
        devicesById: deviceAccu.devicesById
431
      });
432
      const total = Number(res.headers[headerNames.total]);
1✔
433
      if (total > deviceAccu.ids.length) {
1!
UNCOV
434
        return getAllDevices(perPage, page + 1, deviceAccu.ids);
×
435
      }
436
      return Promise.resolve(
1✔
437
        dispatch({
438
          type: DeviceConstants.RECEIVE_GROUP_DEVICES,
439
          group: {
440
            ...state.devices.groups.byId[group],
441
            deviceIds: deviceAccu.ids,
442
            total
443
          },
444
          groupName: group
445
        })
446
      );
447
    });
448
  return getAllDevices();
1✔
449
};
450

451
export const setDeviceFilters = filters => (dispatch, getState) => {
187✔
452
  if (deepCompare(filters, getDeviceFilters(getState()))) {
6✔
453
    return Promise.resolve();
2✔
454
  }
455
  return Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_FILTERS, filters }));
4✔
456
};
457

458
export const getDeviceById = id => (dispatch, getState) =>
187✔
459
  GeneralApi.get(`${inventoryApiUrl}/devices/${id}`)
7✔
460
    .then(res => {
461
      const device = reduceReceivedDevices([res.data], [], getState()).devicesById[id];
5✔
462
      device.etag = res.headers.etag;
5✔
463
      dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device });
5✔
464
      return Promise.resolve(device);
5✔
465
    })
466
    .catch(err => {
UNCOV
467
      const errMsg = extractErrorMessage(err);
×
UNCOV
468
      if (errMsg.includes('Not Found')) {
×
UNCOV
469
        console.log(`${id} does not have any inventory information`);
×
UNCOV
470
        const device = reduceReceivedDevices(
×
471
          [
472
            {
473
              id,
474
              attributes: [
475
                { name: 'status', value: 'decomissioned', scope: 'identity' },
476
                { name: 'decomissioned', value: 'true', scope: 'inventory' }
477
              ]
478
            }
479
          ],
480
          [],
481
          getState()
482
        ).devicesById[id];
UNCOV
483
        dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device });
×
484
      }
485
    });
486

487
export const getDeviceInfo = deviceId => (dispatch, getState) => {
187✔
488
  const device = getState().devices.byId[deviceId] || {};
2!
489
  const { hasDeviceConfig, hasDeviceConnect, hasMonitor } = getTenantCapabilities(getState());
2✔
490
  const { canConfigure } = getUserCapabilities(getState());
2✔
491
  const integrations = getDeviceTwinIntegrations(getState());
2✔
492
  let tasks = [dispatch(getDeviceAuth(deviceId)), ...integrations.map(integration => dispatch(getDeviceTwin(deviceId, integration)))];
2✔
493
  if (hasDeviceConfig && canConfigure && [DEVICE_STATES.accepted, DEVICE_STATES.preauth].includes(device.status)) {
2✔
494
    tasks.push(dispatch(getDeviceConfig(deviceId)));
1✔
495
  }
496
  if (device.status === DEVICE_STATES.accepted) {
2!
497
    // Get full device identity details for single selected device
498
    tasks.push(dispatch(getDeviceById(deviceId)));
2✔
499
    if (hasDeviceConnect) {
2!
500
      tasks.push(dispatch(getDeviceConnect(deviceId)));
2✔
501
    }
502
    if (hasMonitor) {
2✔
503
      tasks.push(dispatch(getLatestDeviceAlerts(deviceId)));
1✔
504
      tasks.push(dispatch(getDeviceMonitorConfig(deviceId)));
1✔
505
    }
506
  }
507
  return Promise.all(tasks);
2✔
508
};
509

510
const deriveInactiveDevices = deviceIds => (dispatch, getState) => {
187✔
511
  const yesterday = new Date();
1✔
512
  yesterday.setDate(yesterday.getDate() - 1);
1✔
513
  const yesterdaysIsoString = yesterday.toISOString();
1✔
514
  const state = getState().devices;
1✔
515
  // now boil the list down to the ones that were not updated since yesterday
516
  const devices = deviceIds.reduce(
1✔
517
    (accu, id) => {
518
      const device = state.byId[id];
2✔
519
      if (device && device.updated_ts > yesterdaysIsoString) {
2!
UNCOV
520
        accu.active.push(id);
×
521
      } else {
522
        accu.inactive.push(id);
2✔
523
      }
524
      return accu;
2✔
525
    },
526
    { active: [], inactive: [] }
527
  );
528
  return dispatch({
1✔
529
    type: DeviceConstants.SET_INACTIVE_DEVICES,
530
    activeDeviceTotal: devices.active.length,
531
    inactiveDeviceTotal: devices.inactive.length
532
  });
533
};
534

535
/*
536
    Device Auth + admission
537
  */
538
export const getDeviceCount = status => (dispatch, getState) =>
187✔
539
  GeneralApi.post(getSearchEndpoint(getState().app.features.hasReporting), {
46✔
540
    page: 1,
541
    per_page: 1,
542
    filters: mapFiltersToTerms([{ key: 'status', value: status, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'identity' }]),
543
    attributes: defaultAttributes
544
  }).then(response => {
545
    const count = Number(response.headers[headerNames.total]);
42✔
546
    switch (status) {
42!
547
      case DEVICE_STATES.accepted:
548
      case DEVICE_STATES.pending:
549
      case DEVICE_STATES.preauth:
550
      case DEVICE_STATES.rejected:
551
        return dispatch({ type: DeviceConstants[`SET_${status.toUpperCase()}_DEVICES_COUNT`], count, status });
42✔
552
      default:
UNCOV
553
        return dispatch({ type: DeviceConstants.SET_TOTAL_DEVICES, count });
×
554
    }
555
  });
556

557
export const getAllDeviceCounts = () => dispatch =>
187✔
558
  Promise.all([DEVICE_STATES.accepted, DEVICE_STATES.pending].map(status => dispatch(getDeviceCount(status))));
2✔
559

560
export const getDeviceLimit = () => dispatch =>
187✔
561
  GeneralApi.get(`${deviceAuthV2}/limits/max_devices`).then(res =>
6✔
562
    dispatch({
6✔
563
      type: DeviceConstants.SET_DEVICE_LIMIT,
564
      limit: res.data.limit
565
    })
566
  );
567

568
export const setDeviceListState =
569
  (selectionState, shouldSelectDevices = true) =>
187✔
570
  (dispatch, getState) => {
13✔
571
    const currentState = getState().devices.deviceList;
13✔
572
    let nextState = {
13✔
573
      ...currentState,
574
      setOnly: false,
575
      ...selectionState,
576
      sort: { ...currentState.sort, ...selectionState.sort }
577
    };
578
    let tasks = [];
13✔
579
    // eslint-disable-next-line no-unused-vars
580
    const { isLoading: currentLoading, deviceIds: currentDevices, selection: currentSelection, ...currentRequestState } = currentState;
13✔
581
    // eslint-disable-next-line no-unused-vars
582
    const { isLoading: nextLoading, deviceIds: nextDevices, selection: nextSelection, ...nextRequestState } = nextState;
13✔
583
    if (!nextState.setOnly && !deepCompare(currentRequestState, nextRequestState)) {
13✔
584
      const { direction: sortDown = SORTING_OPTIONS.desc, key: sortCol, scope: sortScope } = nextState.sort ?? {};
9!
585
      const sortBy = sortCol ? [{ attribute: sortCol, order: sortDown, scope: sortScope }] : undefined;
9!
586
      if (sortCol && sortingAlternatives[sortCol]) {
9!
UNCOV
587
        sortBy.push({ ...sortBy[0], attribute: sortingAlternatives[sortCol] });
×
588
      }
589
      const applicableSelectedState = nextState.state === routes.allDevices.key ? undefined : nextState.state;
9!
590
      nextState.isLoading = true;
9✔
591
      tasks.push(
9✔
592
        dispatch(getDevicesByStatus(applicableSelectedState, { ...nextState, sortOptions: sortBy }))
593
          .then(results => {
594
            const { deviceAccu, total } = results[results.length - 1];
7✔
595
            const devicesState = shouldSelectDevices
7!
596
              ? { ...getState().devices.deviceList, deviceIds: deviceAccu.ids, total, isLoading: false }
597
              : { ...getState().devices.deviceList, isLoading: false };
598
            return Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_LIST_STATE, state: devicesState }));
7✔
599
          })
600
          // whatever happens, change "loading" back to null
601
          .catch(() =>
UNCOV
602
            Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_LIST_STATE, state: { ...getState().devices.deviceList, isLoading: false } }))
×
603
          )
604
      );
605
    }
606
    tasks.push(dispatch({ type: DeviceConstants.SET_DEVICE_LIST_STATE, state: nextState }));
13✔
607
    return Promise.all(tasks);
13✔
608
  };
609

610
const convertIssueOptionsToFilters = (issuesSelection, filtersState = {}) =>
187!
611
  issuesSelection.map(item => {
73✔
612
    if (typeof DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule.value === 'function') {
5✔
613
      return { ...DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule, value: DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule.value(filtersState) };
2✔
614
    }
615
    return DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule;
3✔
616
  });
617

618
export const convertDeviceListStateToFilters = ({ filters = [], group, groups = { byId: {} }, offlineThreshold, selectedIssues = [], status }) => {
187!
619
  let applicableFilters = [...filters];
73✔
620
  if (typeof group === 'string' && !(groups.byId[group]?.filters || applicableFilters).length) {
73✔
621
    applicableFilters.push({ key: 'group', value: group, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'system' });
4✔
622
  }
623
  const nonMonitorFilters = applicableFilters.filter(
73✔
624
    filter =>
625
      !Object.values(DeviceConstants.DEVICE_ISSUE_OPTIONS).some(
40✔
626
        ({ filterRule }) => filter.scope !== 'inventory' && filterRule.scope === filter.scope && filterRule.key === filter.key
240✔
627
      )
628
  );
629
  const deviceIssueFilters = convertIssueOptionsToFilters(selectedIssues, { offlineThreshold });
73✔
630
  applicableFilters = [...nonMonitorFilters, ...deviceIssueFilters];
73✔
631
  const effectiveFilters = status
73✔
632
    ? [...applicableFilters, { key: 'status', value: status, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'identity' }]
633
    : applicableFilters;
634
  return { applicableFilters: nonMonitorFilters, filterTerms: mapFiltersToTerms(effectiveFilters) };
73✔
635
};
636

637
// get devices from inventory
638
export const getDevicesByStatus =
639
  (status, options = {}) =>
187✔
640
  (dispatch, getState) => {
63✔
641
    const { filterSelection, group, selectedIssues = [], page = defaultPage, perPage = defaultPerPage, sortOptions = [], selectedAttributes = [] } = options;
63✔
642
    const { applicableFilters, filterTerms } = convertDeviceListStateToFilters({
63✔
643
      filters: filterSelection ?? getDeviceFilters(getState()),
96✔
644
      group: group ?? getState().devices.groups.selectedGroup,
97✔
645
      groups: getState().devices.groups,
646
      offlineThreshold: getState().app.offlineThreshold,
647
      selectedIssues,
648
      status
649
    });
650
    const attributes = [...defaultAttributes, { scope: 'identity', attribute: getIdAttribute(getState()).attribute || 'id' }, ...selectedAttributes];
63!
651
    return GeneralApi.post(getSearchEndpoint(getState().app.features.hasReporting), {
63✔
652
      page,
653
      per_page: perPage,
654
      filters: filterTerms,
655
      sort: sortOptions,
656
      attributes
657
    })
658
      .then(response => {
659
        const state = getState();
60✔
660
        const deviceAccu = reduceReceivedDevices(response.data, [], state, status);
60✔
661
        let total = !applicableFilters.length ? Number(response.headers[headerNames.total]) : null;
60✔
662
        if (status && state.devices.byStatus[status].total === deviceAccu.ids.length) {
60✔
663
          total = deviceAccu.ids.length;
29✔
664
        }
665
        let tasks = [
60✔
666
          dispatch({
667
            type: DeviceConstants.RECEIVE_DEVICES,
668
            devicesById: deviceAccu.devicesById
669
          })
670
        ];
671
        if (status) {
60✔
672
          tasks.push(
33✔
673
            dispatch({
674
              type: DeviceConstants[`SET_${status.toUpperCase()}_DEVICES`],
675
              deviceIds: deviceAccu.ids,
676
              status,
677
              total
678
            })
679
          );
680
        }
681
        // for each device, get device identity info
682
        const receivedDevices = Object.values(deviceAccu.devicesById);
60✔
683
        if (receivedDevices.length) {
60✔
684
          tasks.push(dispatch(getDevicesWithAuth(receivedDevices)));
50✔
685
        }
686
        tasks.push(Promise.resolve({ deviceAccu, total: Number(response.headers[headerNames.total]) }));
60✔
687
        return Promise.all(tasks);
60✔
688
      })
UNCOV
689
      .catch(err => commonErrorHandler(err, `${status} devices couldn't be loaded.`, dispatch, commonErrorFallback));
×
690
  };
691

692
export const getAllDevicesByStatus = status => (dispatch, getState) => {
187✔
693
  const attributes = [...defaultAttributes, { scope: 'identity', attribute: getIdAttribute(getState()).attribute || 'id' }];
1!
694
  const getAllDevices = (perPage = MAX_PAGE_SIZE, page = 1, devices = []) =>
1✔
695
    GeneralApi.post(getSearchEndpoint(getState().app.features.hasReporting), {
1✔
696
      page,
697
      per_page: perPage,
698
      filters: mapFiltersToTerms([{ key: 'status', value: status, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'identity' }]),
699
      attributes
700
    }).then(res => {
701
      const state = getState();
1✔
702
      const deviceAccu = reduceReceivedDevices(res.data, devices, state, status);
1✔
703
      dispatch({
1✔
704
        type: DeviceConstants.RECEIVE_DEVICES,
705
        devicesById: deviceAccu.devicesById
706
      });
707
      const total = Number(res.headers[headerNames.total]);
1✔
708
      if (total > state.deployments.deploymentDeviceLimit) {
1!
UNCOV
709
        return Promise.resolve();
×
710
      }
711
      if (total > perPage * page) {
1!
UNCOV
712
        return getAllDevices(perPage, page + 1, deviceAccu.ids);
×
713
      }
714
      let tasks = [
1✔
715
        dispatch({
716
          type: DeviceConstants[`SET_${status.toUpperCase()}_DEVICES`],
717
          deviceIds: deviceAccu.ids,
718
          forceUpdate: true,
719
          status,
720
          total: deviceAccu.ids.length
721
        })
722
      ];
723
      if (status === DEVICE_STATES.accepted && deviceAccu.ids.length === total) {
1!
724
        tasks.push(dispatch(deriveInactiveDevices(deviceAccu.ids)));
1✔
725
      }
726
      return Promise.all(tasks);
1✔
727
    });
728
  return getAllDevices();
1✔
729
};
730

731
export const searchDevices =
732
  (passedOptions = {}) =>
187!
733
  (dispatch, getState) => {
2✔
734
    const state = getState();
2✔
735
    let options = { ...state.app.searchState, ...passedOptions };
2✔
736
    const { page = defaultPage, searchTerm, sortOptions = [] } = options;
2✔
737
    const { columnSelection = [] } = getUserSettings(state);
2!
738
    const selectedAttributes = columnSelection.map(column => ({ attribute: column.key, scope: column.scope }));
2✔
739
    const attributes = attributeDuplicateFilter(
2✔
740
      [...defaultAttributes, { scope: 'identity', attribute: getIdAttribute(state).attribute }, ...selectedAttributes],
741
      'attribute'
742
    );
743
    return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
2✔
744
      page,
745
      per_page: 10,
746
      filters: [],
747
      sort: sortOptions,
748
      text: searchTerm,
749
      attributes
750
    })
751
      .then(response => {
752
        const deviceAccu = reduceReceivedDevices(response.data, [], getState());
2✔
753
        return Promise.all([
2✔
754
          dispatch({ type: DeviceConstants.RECEIVE_DEVICES, devicesById: deviceAccu.devicesById }),
755
          Promise.resolve({ deviceIds: deviceAccu.ids, searchTotal: Number(response.headers[headerNames.total]) })
756
        ]);
757
      })
UNCOV
758
      .catch(err => commonErrorHandler(err, `devices couldn't be searched.`, dispatch, commonErrorFallback));
×
759
  };
760

761
const ATTRIBUTE_LIST_CUTOFF = 100;
187✔
762
const attributeReducer = (attributes = []) =>
187!
763
  attributes.slice(0, ATTRIBUTE_LIST_CUTOFF).reduce(
20✔
764
    (accu, { name, scope }) => {
765
      if (!accu[scope]) {
400!
UNCOV
766
        accu[scope] = [];
×
767
      }
768
      accu[scope].push(name);
400✔
769
      return accu;
400✔
770
    },
771
    { identity: [], inventory: [], system: [], tags: [] }
772
  );
773

774
export const getDeviceAttributes = () => (dispatch, getState) =>
187✔
775
  GeneralApi.get(getAttrsEndpoint(getState().app.features.hasReporting)).then(({ data }) => {
22✔
776
    // TODO: remove the array fallback once the inventory attributes endpoint is fixed
777
    const { identity: identityAttributes, inventory: inventoryAttributes, system: systemAttributes, tags: tagAttributes } = attributeReducer(data || []);
19!
778
    return dispatch({
19✔
779
      type: DeviceConstants.SET_FILTER_ATTRIBUTES,
780
      attributes: { identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }
781
    });
782
  });
783

784
export const getReportingLimits = () => dispatch =>
187✔
785
  GeneralApi.get(`${reportingApiUrl}/devices/attributes`)
2✔
UNCOV
786
    .catch(err => commonErrorHandler(err, `filterable attributes limit & usage could not be retrieved.`, dispatch, commonErrorFallback))
×
787
    .then(({ data }) => {
788
      const { attributes, count, limit } = data;
1✔
789
      const groupedAttributes = attributeReducer(attributes);
1✔
790
      return Promise.resolve(dispatch({ type: DeviceConstants.SET_FILTERABLES_CONFIG, count, limit, attributes: groupedAttributes }));
1✔
791
    });
792

793
export const ensureVersionString = (software, fallback) =>
187✔
794
  software.length && software !== 'artifact_name' ? (software.endsWith('.version') ? software : `${software}.version`) : fallback;
1!
795

796
const getSingleReportData = (reportConfig, groups) => {
187✔
797
  const { attribute, group, software = '' } = reportConfig;
1!
798
  const filters = [{ key: 'status', scope: 'identity', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: 'accepted' }];
1✔
799
  if (group) {
1!
UNCOV
800
    const staticGroupFilter = { key: 'group', scope: 'system', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: group };
×
UNCOV
801
    const { cleanedFilters: groupFilters } = getGroupFilters(group, groups);
×
UNCOV
802
    filters.push(...(groupFilters.length ? groupFilters : [staticGroupFilter]));
×
803
  }
804
  const aggregationAttribute = ensureVersionString(software, attribute);
1✔
805
  return GeneralApi.post(`${reportingApiUrl}/devices/aggregate`, {
1✔
806
    aggregations: [{ attribute: aggregationAttribute, name: '*', scope: 'inventory', size: chartColorPalette.length }],
807
    filters: mapFiltersToTerms(filters)
808
  }).then(({ data }) => ({ data, reportConfig }));
1✔
809
};
810

811
export const defaultReportType = 'distribution';
187✔
812
export const defaultReports = [{ ...emptyChartSelection, group: null, attribute: 'artifact_name', type: defaultReportType }];
187✔
813

814
export const getReportsData = () => (dispatch, getState) => {
187✔
815
  const state = getState();
1✔
816
  const reports =
817
    getUserSettings(state).reports ||
1✔
818
    state.users.globalSettings[`${state.users.currentUser}-reports`] ||
819
    (Object.keys(state.devices.byId).length ? defaultReports : []);
1!
820
  return Promise.all(reports.map(report => getSingleReportData(report, getState().devices.groups))).then(results => {
1✔
821
    const devicesState = getState().devices;
1✔
822
    const totalDeviceCount = devicesState.byStatus.accepted.total;
1✔
823
    const newReports = results.map(({ data, reportConfig }) => {
1✔
824
      let { items, other_count } = data[0];
1✔
825
      const { attribute, group, software = '' } = reportConfig;
1!
826
      const dataCount = items.reduce((accu, item) => accu + item.count, 0);
2✔
827
      // the following is needed to show reports including both old (artifact_name) & current style (rootfs-image.version) device software
828
      const otherCount = !group && (software === rootfsImageVersion || attribute === 'artifact_name') ? totalDeviceCount - dataCount : other_count;
1!
829
      return { items, otherCount, total: otherCount + dataCount };
1✔
830
    });
831
    return Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_REPORTS, reports: newReports }));
1✔
832
  });
833
};
834

835
const initializeDistributionData = (report, groups, devices, totalDeviceCount) => {
187✔
836
  const { attribute, group = '', software = '' } = report;
13!
837
  const effectiveAttribute = software ? software : attribute;
13!
838
  const { deviceIds, total = 0 } = groups[group] || {};
13✔
839
  const relevantDevices = groups[group] ? deviceIds.map(id => devices[id]) : Object.values(devices);
13!
840
  const distributionByAttribute = relevantDevices.reduce((accu, item) => {
13✔
841
    if (!item.attributes || item.status !== DEVICE_STATES.accepted) return accu;
25✔
842
    if (!accu[item.attributes[effectiveAttribute]]) {
19✔
843
      accu[item.attributes[effectiveAttribute]] = 0;
13✔
844
    }
845
    accu[item.attributes[effectiveAttribute]] = accu[item.attributes[effectiveAttribute]] + 1;
19✔
846
    return accu;
19✔
847
  }, {});
848
  const distributionByAttributeSorted = Object.entries(distributionByAttribute).sort((pairA, pairB) => pairB[1] - pairA[1]);
13✔
849
  const items = distributionByAttributeSorted.map(([key, count]) => ({ key, count }));
13✔
850
  const dataCount = items.reduce((accu, item) => accu + item.count, 0);
13✔
851
  // the following is needed to show reports including both old (artifact_name) & current style (rootfs-image.version) device software
852
  const otherCount = (groups[group] ? total : totalDeviceCount) - dataCount;
13!
853
  return { items, otherCount, total: otherCount + dataCount };
13✔
854
};
855

856
export const deriveReportsData = () => (dispatch, getState) =>
187✔
857
  Promise.all([dispatch(getGroups()), dispatch(getDynamicGroups())]).then(() => {
14✔
858
    const { dynamic: dynamicGroups, static: staticGroups } = getGroupsSelector(getState());
13✔
859
    return Promise.all([
13✔
860
      ...staticGroups.map(group => dispatch(getAllGroupDevices(group))),
13✔
861
      ...dynamicGroups.map(group => dispatch(getAllDynamicGroupDevices(group)))
13✔
862
    ]).then(() => {
863
      const state = getState();
13✔
864
      const {
865
        groups: { byId: groupsById },
866
        byId,
867
        byStatus: {
868
          accepted: { total }
869
        }
870
      } = state.devices;
13✔
871
      const reports =
872
        getUserSettings(state).reports || state.users.globalSettings[`${state.users.currentUser}-reports`] || (Object.keys(byId).length ? defaultReports : []);
13!
873
      const newReports = reports.map(report => initializeDistributionData(report, groupsById, byId, total));
13✔
874
      return Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_REPORTS, reports: newReports }));
13✔
875
    });
876
  });
877

878
export const getDeviceConnect = id => dispatch =>
187✔
879
  GeneralApi.get(`${deviceConnect}/devices/${id}`).then(({ data }) => {
2✔
880
    let tasks = [
1✔
881
      dispatch({
882
        type: DeviceConstants.RECEIVE_DEVICE_CONNECT,
883
        device: { connect_status: data.status, connect_updated_ts: data.updated_ts, id }
884
      })
885
    ];
886
    tasks.push(Promise.resolve(data));
1✔
887
    return Promise.all(tasks);
1✔
888
  });
889

890
export const getSessionDetails = (sessionId, deviceId, userId, startDate, endDate) => () => {
187✔
891
  const createdAfter = startDate ? `&created_after=${Math.round(Date.parse(startDate) / 1000)}` : '';
5✔
892
  const createdBefore = endDate ? `&created_before=${Math.round(Date.parse(endDate) / 1000)}` : '';
5✔
893
  const objectSearch = `&object_id=${deviceId}`;
5✔
894
  return GeneralApi.get(`${auditLogsApiUrl}/logs?per_page=500${createdAfter}${createdBefore}&actor_id=${userId}${objectSearch}`).then(
5✔
895
    ({ data: auditLogEntries }) => {
896
      const { start, end } = auditLogEntries.reduce(
5✔
897
        (accu, item) => {
898
          if (item.meta?.session_id?.includes(sessionId)) {
5!
899
            accu.start = new Date(item.action.startsWith('open') ? item.time : accu.start);
5!
900
            accu.end = new Date(item.action.startsWith('close') ? item.time : accu.end);
5!
901
          }
902
          return accu;
5✔
903
        },
904
        { start: startDate || endDate, end: endDate || startDate }
15✔
905
      );
906
      return Promise.resolve({ start, end });
5✔
907
    }
908
  );
909
};
910

911
export const getDeviceFileDownloadLink = (deviceId, path) => () =>
187✔
912
  Promise.resolve(`${deviceConnect}/devices/${deviceId}/download?path=${encodeURIComponent(path)}`);
1✔
913

914
export const deviceFileUpload = (deviceId, path, file) => (dispatch, getState) => {
187✔
915
  var formData = new FormData();
1✔
916
  formData.append('path', path);
1✔
917
  formData.append('file', file);
1✔
918
  const uploadId = uuid();
1✔
919
  const cancelSource = new AbortController();
1✔
920
  const uploads = { ...getState().app.uploads, [uploadId]: { inprogress: true, uploadProgress: 0, cancelSource } };
1✔
921
  return Promise.all([
1✔
922
    dispatch(setSnackbar('Uploading file')),
923
    dispatch({ type: UPLOAD_PROGRESS, uploads }),
UNCOV
924
    GeneralApi.uploadPut(`${deviceConnect}/devices/${deviceId}/upload`, formData, e => dispatch(progress(e, uploadId)), cancelSource.signal)
×
925
  ])
926
    .then(() => Promise.resolve(dispatch(setSnackbar('Upload successful', 5000))))
1✔
927
    .catch(err => {
UNCOV
928
      if (isCancel(err)) {
×
UNCOV
929
        return dispatch(setSnackbar('The upload has been cancelled', 5000));
×
930
      }
UNCOV
931
      return commonErrorHandler(err, `Error uploading file to device.`, dispatch);
×
932
    })
933
    .finally(() => dispatch(cleanUpUpload(uploadId)));
1✔
934
};
935

936
export const getDeviceAuth = id => dispatch =>
187✔
937
  Promise.resolve(dispatch(getDevicesWithAuth([{ id }]))).then(results => {
7✔
938
    if (results[results.length - 1]) {
5!
939
      return Promise.resolve(results[results.length - 1][0]);
5✔
940
    }
UNCOV
941
    return Promise.resolve();
×
942
  });
943

944
export const getDevicesWithAuth = devices => (dispatch, getState) =>
187✔
945
  devices.length
59✔
946
    ? GeneralApi.get(`${deviceAuthV2}/devices?id=${devices.map(device => device.id).join('&id=')}`)
59✔
947
        .then(({ data: receivedDevices }) => {
948
          const { devicesById } = reduceReceivedDevices(receivedDevices, [], getState());
53✔
949
          return Promise.all([dispatch({ type: DeviceConstants.RECEIVE_DEVICES, devicesById }), Promise.resolve(receivedDevices)]);
53✔
950
        })
UNCOV
951
        .catch(err => commonErrorHandler(err, `Error: ${err}`, dispatch))
×
952
    : Promise.resolve([[], []]);
953

954
const maybeUpdateDevicesByStatus = (deviceId, authId) => (dispatch, getState) => {
187✔
955
  const devicesState = getState().devices;
4✔
956
  const device = devicesState.byId[deviceId];
4✔
957
  const hasMultipleAuthSets = authId ? device.auth_sets.filter(authset => authset.id !== authId).length > 0 : false;
4✔
958
  if (!hasMultipleAuthSets && Object.values(DEVICE_STATES).includes(device.status)) {
4!
959
    const deviceIds = devicesState.byStatus[device.status].deviceIds.filter(id => id !== deviceId);
8✔
960
    return Promise.resolve(
4✔
961
      dispatch({
962
        type: DeviceConstants[`SET_${device.status.toUpperCase()}_DEVICES`],
963
        deviceIds,
964
        forceUpdate: true,
965
        status: device.status,
966
        total: Math.max(0, devicesState.byStatus[device.status].total - 1)
967
      })
968
    );
969
  }
UNCOV
970
  return Promise.resolve();
×
971
};
972

973
export const updateDeviceAuth = (deviceId, authId, status) => dispatch =>
187✔
974
  GeneralApi.put(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}/status`, { status })
2✔
975
    .then(() => Promise.all([dispatch(getDeviceAuth(deviceId)), dispatch(setSnackbar('Device authorization status was updated successfully'))]))
2✔
UNCOV
976
    .catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
×
977
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))));
2✔
978

979
export const updateDevicesAuth = (deviceIds, status) => (dispatch, getState) => {
187✔
980
  let devices = getState().devices.byId;
1✔
981
  const deviceIdsWithoutAuth = deviceIds.reduce((accu, id) => (devices[id].auth_sets ? accu : [...accu, { id }]), []);
2!
982
  return dispatch(getDevicesWithAuth(deviceIdsWithoutAuth)).then(() => {
1✔
983
    devices = getState().devices.byId;
1✔
984
    // for each device, get id and id of authset & make api call to accept
985
    // if >1 authset, skip instead
986
    const deviceAuthUpdates = deviceIds.map(id => {
1✔
987
      const device = devices[id];
2✔
988
      if (device.auth_sets.length !== 1) {
2✔
989
        return Promise.reject();
1✔
990
      }
991
      // api call device.id and device.authsets[0].id
992
      return dispatch(updateDeviceAuth(device.id, device.auth_sets[0].id, status)).catch(err =>
1✔
UNCOV
993
        commonErrorHandler(err, 'The action was stopped as there was a problem updating a device authorization status: ', dispatch)
×
994
      );
995
    });
996
    return Promise.allSettled(deviceAuthUpdates).then(results => {
1✔
997
      const { skipped, count } = results.reduce(
1✔
998
        (accu, item) => {
999
          if (item.status === 'rejected') {
2✔
1000
            accu.skipped = accu.skipped + 1;
1✔
1001
          } else {
1002
            accu.count = accu.count + 1;
1✔
1003
          }
1004
          return accu;
2✔
1005
        },
1006
        { skipped: 0, count: 0 }
1007
      );
1008
      const message = getSnackbarMessage(skipped, count);
1✔
1009
      // break if an error occurs, display status up til this point before error message
1010
      return dispatch(setSnackbar(message));
1✔
1011
    });
1012
  });
1013
};
1014

1015
export const deleteAuthset = (deviceId, authId) => dispatch =>
187✔
1016
  GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}`)
1✔
1017
    .then(() => Promise.all([dispatch(setSnackbar('Device authorization status was updated successfully'))]))
1✔
UNCOV
1018
    .catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
×
1019
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))));
1✔
1020

1021
export const preauthDevice = authset => dispatch =>
187✔
1022
  GeneralApi.post(`${deviceAuthV2}/devices`, authset)
2✔
1023
    .catch(err => {
1024
      if (err.response.status === 409) {
1!
1025
        return Promise.reject('A device with a matching identity data set already exists');
1✔
1026
      }
UNCOV
1027
      commonErrorHandler(err, 'The device could not be added:', dispatch);
×
UNCOV
1028
      return Promise.reject();
×
1029
    })
1030
    .then(() => Promise.resolve(dispatch(setSnackbar('Device was successfully added to the preauthorization list', 5000))));
1✔
1031

1032
export const decommissionDevice = (deviceId, authId) => dispatch =>
187✔
1033
  GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}`)
1✔
1034
    .then(() => Promise.resolve(dispatch(setSnackbar('Device was decommissioned successfully'))))
1✔
UNCOV
1035
    .catch(err => commonErrorHandler(err, 'There was a problem decommissioning the device:', dispatch))
×
1036
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))));
1✔
1037

1038
export const getDeviceConfig = deviceId => dispatch =>
187✔
1039
  GeneralApi.get(`${deviceConfig}/${deviceId}`)
4✔
1040
    .then(({ data }) => {
1041
      let tasks = [
2✔
1042
        dispatch({
1043
          type: DeviceConstants.RECEIVE_DEVICE_CONFIG,
1044
          device: { id: deviceId, config: data }
1045
        })
1046
      ];
1047
      tasks.push(Promise.resolve(data));
2✔
1048
      return Promise.all(tasks);
2✔
1049
    })
1050
    .catch(err => {
1051
      // if we get a proper error response we most likely queried a device without an existing config check-in and we can just ignore the call
1052
      if (err.response?.data?.error.status_code !== 404) {
1!
UNCOV
1053
        return commonErrorHandler(err, `There was an error retrieving the configuration for device ${deviceId}.`, dispatch, commonErrorFallback);
×
1054
      }
1055
    });
1056

1057
export const setDeviceConfig = (deviceId, config) => dispatch =>
187✔
1058
  GeneralApi.put(`${deviceConfig}/${deviceId}`, config)
1✔
UNCOV
1059
    .catch(err => commonErrorHandler(err, `There was an error setting the configuration for device ${deviceId}.`, dispatch, commonErrorFallback))
×
1060
    .then(() => Promise.resolve(dispatch(getDeviceConfig(deviceId))));
1✔
1061

1062
export const applyDeviceConfig = (deviceId, configDeploymentConfiguration, isDefault, config) => (dispatch, getState) =>
187✔
1063
  GeneralApi.post(`${deviceConfig}/${deviceId}/deploy`, configDeploymentConfiguration)
1✔
UNCOV
1064
    .catch(err => commonErrorHandler(err, `There was an error deploying the configuration to device ${deviceId}.`, dispatch, commonErrorFallback))
×
1065
    .then(({ data }) => {
1066
      let tasks = [dispatch(getSingleDeployment(data.deployment_id))];
1✔
1067
      if (isDefault) {
1!
UNCOV
1068
        const { previous } = getState().users.globalSettings.defaultDeviceConfig;
×
UNCOV
1069
        tasks.push(dispatch(saveGlobalSettings({ defaultDeviceConfig: { current: config, previous } })));
×
1070
      }
1071
      return Promise.all(tasks);
1✔
1072
    });
1073

1074
export const setDeviceTags = (deviceId, tags) => dispatch =>
187✔
1075
  // to prevent tag set failures, retrieve the device & use the freshest etag we can get
1076
  Promise.resolve(dispatch(getDeviceById(deviceId))).then(device => {
3✔
1077
    const headers = device.etag ? { 'If-Match': device.etag } : {};
3!
1078
    return GeneralApi.put(
3✔
1079
      `${inventoryApiUrl}/devices/${deviceId}/tags`,
1080
      Object.entries(tags).map(([name, value]) => ({ name, value })),
3✔
1081
      { headers }
1082
    )
UNCOV
1083
      .catch(err => commonErrorHandler(err, `There was an error setting tags for device ${deviceId}.`, dispatch, 'Please check your connection.'))
×
1084
      .then(() => Promise.all([dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device: { ...device, tags } }), dispatch(setSnackbar('Device name changed'))]));
2✔
1085
  });
1086

1087
export const getDeviceTwin = (deviceId, integration) => (dispatch, getState) => {
187✔
1088
  let providerResult = {};
3✔
1089
  return GeneralApi.get(`${iotManagerBaseURL}/devices/${deviceId}/state`)
3✔
1090
    .then(({ data }) => {
1091
      providerResult = { ...data, twinError: '' };
2✔
1092
    })
1093
    .catch(err => {
UNCOV
1094
      providerResult = {
×
1095
        twinError: `There was an error getting the ${DeviceConstants.EXTERNAL_PROVIDER[
1096
          integration.provider
1097
        ].twinTitle.toLowerCase()} for device ${deviceId}. ${err}`
1098
      };
1099
    })
1100
    .finally(() =>
1101
      Promise.resolve(
2✔
1102
        dispatch({
1103
          type: DeviceConstants.RECEIVE_DEVICE,
1104
          device: {
1105
            ...getState().devices.byId[deviceId],
1106
            twinsByIntegration: {
1107
              ...getState().devices.byId[deviceId].twinsByIntegration,
1108
              ...providerResult
1109
            }
1110
          }
1111
        })
1112
      )
1113
    );
1114
};
1115

1116
export const setDeviceTwin = (deviceId, integration, settings) => (dispatch, getState) =>
187✔
1117
  GeneralApi.put(`${iotManagerBaseURL}/devices/${deviceId}/state/${integration.id}`, { desired: settings })
1✔
1118
    .catch(err =>
UNCOV
1119
      commonErrorHandler(
×
1120
        err,
1121
        `There was an error updating the ${DeviceConstants.EXTERNAL_PROVIDER[integration.provider].twinTitle.toLowerCase()} for device ${deviceId}.`,
1122
        dispatch
1123
      )
1124
    )
1125
    .then(() => {
1126
      const { twinsByIntegration = {} } = getState().devices.byId[deviceId];
1✔
1127
      const { [integration.id]: currentState = {} } = twinsByIntegration;
1✔
1128
      return Promise.resolve(
1✔
1129
        dispatch({
1130
          type: DeviceConstants.RECEIVE_DEVICE,
1131
          device: {
1132
            ...getState().devices.byId[deviceId],
1133
            twinsByIntegration: {
1134
              ...twinsByIntegration,
1135
              [integration.id]: {
1136
                ...currentState,
1137
                desired: settings
1138
              }
1139
            }
1140
          }
1141
        })
1142
      );
1143
    });
1144

1145
const prepareSearchArguments = ({ filters, group, state, status }) => {
187✔
1146
  const { filterTerms } = convertDeviceListStateToFilters({ filters, group, offlineThreshold: state.app.offlineThreshold, selectedIssues: [], status });
4✔
1147
  const { columnSelection = [] } = getUserSettings(state);
4!
1148
  const selectedAttributes = columnSelection.map(column => ({ attribute: column.key, scope: column.scope }));
4✔
1149
  const attributes = [...defaultAttributes, { scope: 'identity', attribute: getIdAttribute(state).attribute }, ...selectedAttributes];
4✔
1150
  return { attributes, filterTerms };
4✔
1151
};
1152

1153
export const getSystemDevices =
1154
  (id, options = {}) =>
187✔
1155
  (dispatch, getState) => {
1✔
1156
    const { page = defaultPage, perPage = defaultPerPage, sortOptions = [] } = options;
1✔
1157
    const state = getState();
1✔
1158
    let device = state.devices.byId[id];
1✔
1159
    const { attributes: deviceAttributes = {} } = device;
1!
1160
    const { mender_gateway_system_id = '' } = deviceAttributes;
1✔
1161
    const { hasFullFiltering } = getTenantCapabilities(state);
1✔
1162
    if (!hasFullFiltering) {
1!
UNCOV
1163
      return Promise.resolve();
×
1164
    }
1165
    const filters = [
1✔
1166
      { ...emptyFilter, key: 'mender_is_gateway', operator: DEVICE_FILTERING_OPTIONS.$ne.key, value: 'true', scope: 'inventory' },
1167
      { ...emptyFilter, key: 'mender_gateway_system_id', value: mender_gateway_system_id, scope: 'inventory' }
1168
    ];
1169
    const { attributes, filterTerms } = prepareSearchArguments({ filters, state });
1✔
1170

1171
    return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
1✔
1172
      page,
1173
      per_page: perPage,
1174
      filters: filterTerms,
1175
      sort: sortOptions,
1176
      attributes
1177
    })
UNCOV
1178
      .catch(err => commonErrorHandler(err, `There was an error getting system devices device ${id}.`, dispatch, 'Please check your connection.'))
×
1179
      .then(({ data, headers }) => {
1180
        const state = getState();
1✔
1181
        const { devicesById, ids } = reduceReceivedDevices(data, [], state);
1✔
1182
        const device = {
1✔
1183
          ...state.devices.byId[id],
1184
          systemDeviceIds: ids,
1185
          systemDeviceTotal: Number(headers[headerNames.total])
1186
        };
1187
        return Promise.resolve(
1✔
1188
          dispatch({
1189
            type: DeviceConstants.RECEIVE_DEVICES,
1190
            devicesById: {
1191
              ...devicesById,
1192
              [id]: device
1193
            }
1194
          })
1195
        );
1196
      });
1197
  };
1198

1199
export const getGatewayDevices = deviceId => (dispatch, getState) => {
187✔
1200
  const state = getState();
1✔
1201
  let device = state.devices.byId[deviceId];
1✔
1202
  const { attributes = {} } = device;
1!
1203
  const { mender_gateway_system_id = '' } = attributes;
1!
1204
  const filters = [
1✔
1205
    { ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$ne.key, value: deviceId, scope: 'identity' },
1206
    { ...emptyFilter, key: 'mender_is_gateway', value: 'true', scope: 'inventory' },
1207
    { ...emptyFilter, key: 'mender_gateway_system_id', value: mender_gateway_system_id, scope: 'inventory' }
1208
  ];
1209
  const { attributes: attributeSelection, filterTerms } = prepareSearchArguments({ filters, state });
1✔
1210
  return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
1✔
1211
    page: 1,
1212
    per_page: MAX_PAGE_SIZE,
1213
    filters: filterTerms,
1214
    attributes: attributeSelection
1215
  }).then(({ data }) => {
1216
    const { ids } = reduceReceivedDevices(data, [], getState());
1✔
1217
    let tasks = ids.map(deviceId => dispatch(getDeviceInfo(deviceId)));
1✔
1218
    tasks.push(dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device: { ...getState().devices.byId[deviceId], gatewayIds: ids } }));
1✔
1219
    return Promise.all(tasks);
1✔
1220
  });
1221
};
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