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

mendersoftware / gui / 1493849842

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

Pull #4531

gitlab-ci

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

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

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

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

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

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

4486 of 6422 branches covered (69.85%)

8551 of 10246 relevant lines covered (83.46%)

151.3 hits per line

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

89.86
/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 { filtersFilter } from '../components/devices/widgets/filters';
17
import { SORTING_OPTIONS, TIMEOUTS, UPLOAD_PROGRESS, emptyChartSelection, yes } from '../constants/appConstants';
18
import * as DeviceConstants from '../constants/deviceConstants';
19
import { rootfsImageVersion } from '../constants/releaseConstants';
20
import { attributeDuplicateFilter, deepCompare, extractErrorMessage, getSnackbarMessage, mapDeviceAttributes } from '../helpers';
21
import {
22
  getDeviceById as getDeviceByIdSelector,
23
  getDeviceFilters,
24
  getDeviceTwinIntegrations,
25
  getGroups as getGroupsSelector,
26
  getIdAttribute,
27
  getTenantCapabilities,
28
  getUserCapabilities,
29
  getUserSettings
30
} from '../selectors';
31
import { chartColorPalette } from '../themes/Mender';
32
import { getDeviceMonitorConfig, getLatestDeviceAlerts } from './monitorActions';
33

34
const { DEVICE_FILTERING_OPTIONS, DEVICE_STATES, DEVICE_LIST_DEFAULTS, UNGROUPED_GROUP, emptyFilter } = DeviceConstants;
184✔
35
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
184✔
36

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

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

60
export const getSearchEndpoint = hasReporting => (hasReporting ? `${reportingApiUrl}/devices/search` : `${inventoryApiUrlV2}/filters/search`);
900!
61

62
const getAttrsEndpoint = hasReporting => (hasReporting ? `${reportingApiUrl}/devices/search/attributes` : `${inventoryApiUrlV2}/filters/attributes`);
184✔
63

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

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

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

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

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

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

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

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

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

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

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

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

310
const getEarliestTs = (dateA = '', dateB = '') => (!dateA || !dateB ? dateA || dateB : dateA < dateB ? dateA : dateB);
490!
311

312
const reduceReceivedDevices = (devices, ids, state, status) =>
184✔
313
  devices.reduce(
201✔
314
    (accu, device) => {
315
      const stateDevice = getDeviceByIdSelector(state, device.id);
245✔
316
      const {
317
        attributes: storedAttributes = {},
4✔
318
        identity_data: storedIdentity = {},
4✔
319
        monitor: storedMonitor = {},
178✔
320
        tags: storedTags = {},
178✔
321
        group: storedGroup
322
      } = stateDevice;
245✔
323
      const { identity, inventory, monitor, system = {}, tags } = mapDeviceAttributes(device.attributes);
245!
324
      device.tags = { ...storedTags, ...tags };
245✔
325
      device.group = system.group ?? storedGroup;
245✔
326
      device.monitor = { ...storedMonitor, ...monitor };
245✔
327
      device.identity_data = { ...storedIdentity, ...identity, ...(device.identity_data ? device.identity_data : {}) };
245✔
328
      device.status = status ? status : device.status || identity.status;
245✔
329
      device.check_in_time_rounded = system.check_in_time ?? stateDevice.check_in_time_rounded;
245✔
330
      device.check_in_time_exact = device.check_in_time ?? stateDevice.check_in_time_exact;
245✔
331
      device.created_ts = getEarliestTs(getEarliestTs(system.created_ts, device.created_ts), stateDevice.created_ts);
245✔
332
      device.updated_ts = device.attributes ? device.updated_ts : stateDevice.updated_ts;
245✔
333
      device.isNew = new Date(device.created_ts) > new Date(state.app.newThreshold);
245✔
334
      device.isOffline = new Date(device.check_in_time_rounded) < new Date(state.app.offlineThreshold) || device.check_in_time_rounded === undefined;
245✔
335
      // 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
336
      // for device_type and artifact_name, potentially overwriting existing info, so rely on stored information instead if there are no attributes
337
      device.attributes = device.attributes ? { ...storedAttributes, ...inventory } : storedAttributes;
245✔
338
      accu.devicesById[device.id] = { ...stateDevice, ...device };
245✔
339
      accu.ids.push(device.id);
245✔
340
      return accu;
245✔
341
    },
342
    { ids, devicesById: {} }
343
  );
344

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

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

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

455
export const setDeviceFilters = filters => (dispatch, getState) => {
184✔
456
  if (deepCompare(filters, getDeviceFilters(getState()))) {
6✔
457
    return Promise.resolve();
2✔
458
  }
459
  return Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_FILTERS, filters }));
4✔
460
};
461

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

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

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

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

561
export const getAllDeviceCounts = () => dispatch =>
368✔
562
  Promise.all([DEVICE_STATES.accepted, DEVICE_STATES.pending].map(status => dispatch(getDeviceCount(status))));
736✔
563

564
export const getDeviceLimit = () => dispatch =>
184✔
565
  GeneralApi.get(`${deviceAuthV2}/limits/max_devices`).then(res =>
10✔
566
    dispatch({
10✔
567
      type: DeviceConstants.SET_DEVICE_LIMIT,
568
      limit: res.data.limit
569
    })
570
  );
571

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

616
const convertIssueOptionsToFilters = (issuesSelection, filtersState = {}) =>
184!
617
  issuesSelection.map(item => {
104✔
618
    if (typeof DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule.value === 'function') {
11✔
619
      return { ...DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule, value: DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule.value(filtersState) };
5✔
620
    }
621
    return DeviceConstants.DEVICE_ISSUE_OPTIONS[item].filterRule;
6✔
622
  });
623

624
export const convertDeviceListStateToFilters = ({ filters = [], group, groups = { byId: {} }, offlineThreshold, selectedIssues = [], status }) => {
184!
625
  let applicableFilters = [...filters];
104✔
626
  if (typeof group === 'string' && !(groups.byId[group]?.filters || applicableFilters).length) {
104✔
627
    applicableFilters.push({ key: 'group', value: group, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'system' });
5✔
628
  }
629
  const nonMonitorFilters = applicableFilters.filter(
104✔
630
    filter =>
631
      !Object.values(DeviceConstants.DEVICE_ISSUE_OPTIONS).some(
43✔
632
        ({ filterRule }) => filter.scope !== 'inventory' && filterRule.scope === filter.scope && filterRule.key === filter.key
258✔
633
      )
634
  );
635
  const deviceIssueFilters = convertIssueOptionsToFilters(selectedIssues, { offlineThreshold });
104✔
636
  applicableFilters = [...nonMonitorFilters, ...deviceIssueFilters];
104✔
637
  const effectiveFilters = status
104✔
638
    ? [...applicableFilters, { key: 'status', value: status, operator: DEVICE_FILTERING_OPTIONS.$eq.key, scope: 'identity' }]
639
    : applicableFilters;
640
  return { applicableFilters: nonMonitorFilters, filterTerms: mapFiltersToTerms(effectiveFilters) };
104✔
641
};
642

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

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

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

768
const ATTRIBUTE_LIST_CUTOFF = 100;
184✔
769
const attributeReducer = (attributes = []) =>
184!
770
  attributes.slice(0, ATTRIBUTE_LIST_CUTOFF).reduce(
25✔
771
    (accu, { name, scope }) => {
772
      if (!accu[scope]) {
500!
773
        accu[scope] = [];
×
774
      }
775
      accu[scope].push(name);
500✔
776
      return accu;
500✔
777
    },
778
    { identity: [], inventory: [], system: [], tags: [] }
779
  );
780

781
export const getDeviceAttributes = () => (dispatch, getState) =>
184✔
782
  GeneralApi.get(getAttrsEndpoint(getState().app.features.hasReporting)).then(({ data }) => {
23✔
783
    // TODO: remove the array fallback once the inventory attributes endpoint is fixed
784
    const { identity: identityAttributes, inventory: inventoryAttributes, system: systemAttributes, tags: tagAttributes } = attributeReducer(data || []);
23!
785
    return dispatch({
23✔
786
      type: DeviceConstants.SET_FILTER_ATTRIBUTES,
787
      attributes: { identityAttributes, inventoryAttributes, systemAttributes, tagAttributes }
788
    });
789
  });
790

791
export const getReportingLimits = () => dispatch =>
184✔
792
  GeneralApi.get(`${reportingApiUrl}/devices/attributes`)
2✔
793
    .catch(err => commonErrorHandler(err, `filterable attributes limit & usage could not be retrieved.`, dispatch, commonErrorFallback))
×
794
    .then(({ data }) => {
795
      const { attributes, count, limit } = data;
2✔
796
      const groupedAttributes = attributeReducer(attributes);
2✔
797
      return Promise.resolve(dispatch({ type: DeviceConstants.SET_FILTERABLES_CONFIG, count, limit, attributes: groupedAttributes }));
2✔
798
    });
799

800
export const ensureVersionString = (software, fallback) =>
184✔
801
  software.length && software !== 'artifact_name' ? (software.endsWith('.version') ? software : `${software}.version`) : fallback;
1!
802

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

818
export const defaultReportType = 'distribution';
184✔
819
export const defaultReports = [{ ...emptyChartSelection, group: null, attribute: 'artifact_name', type: defaultReportType }];
184✔
820

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

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

863
const deriveReportsData = () => (dispatch, getState) => {
184✔
864
  const state = getState();
25✔
865
  const {
866
    groups: { byId: groupsById },
867
    byId,
868
    byStatus: {
869
      accepted: { total }
870
    }
871
  } = state.devices;
25✔
872
  const reports =
873
    getUserSettings(state).reports || state.users.globalSettings[`${state.users.currentUser}-reports`] || (Object.keys(byId).length ? defaultReports : []);
25!
874
  const newReports = reports.map(report => initializeDistributionData(report, groupsById, byId, total));
25✔
875
  return Promise.resolve(dispatch({ type: DeviceConstants.SET_DEVICE_REPORTS, reports: newReports }));
25✔
876
};
877

878
export const getReportsDataWithoutBackendSupport = () => (dispatch, getState) =>
184✔
879
  Promise.all([dispatch(getAllDevicesByStatus(DEVICE_STATES.accepted)), dispatch(getGroups()), dispatch(getDynamicGroups())]).then(() => {
13✔
880
    const { dynamic: dynamicGroups, static: staticGroups } = getGroupsSelector(getState());
13✔
881
    return Promise.all([
13✔
882
      ...staticGroups.map(group => dispatch(getAllGroupDevices(group))),
12✔
883
      ...dynamicGroups.map(group => dispatch(getAllDynamicGroupDevices(group)))
12✔
884
    ]).then(() => dispatch(deriveReportsData()));
13✔
885
  });
886

887
export const getDeviceConnect = id => dispatch =>
184✔
888
  GeneralApi.get(`${deviceConnect}/devices/${id}`).then(({ data }) => {
2✔
889
    let tasks = [
1✔
890
      dispatch({
891
        type: DeviceConstants.RECEIVE_DEVICE_CONNECT,
892
        device: { connect_status: data.status, connect_updated_ts: data.updated_ts, id }
893
      })
894
    ];
895
    tasks.push(Promise.resolve(data));
1✔
896
    return Promise.all(tasks);
1✔
897
  });
898

899
export const getSessionDetails = (sessionId, deviceId, userId, startDate, endDate) => () => {
184✔
900
  const createdAfter = startDate ? `&created_after=${Math.round(Date.parse(startDate) / 1000)}` : '';
4✔
901
  const createdBefore = endDate ? `&created_before=${Math.round(Date.parse(endDate) / 1000)}` : '';
4✔
902
  const objectSearch = `&object_id=${deviceId}`;
4✔
903
  return GeneralApi.get(`${auditLogsApiUrl}/logs?per_page=500${createdAfter}${createdBefore}&actor_id=${userId}${objectSearch}`).then(
4✔
904
    ({ data: auditLogEntries }) => {
905
      const { start, end } = auditLogEntries.reduce(
4✔
906
        (accu, item) => {
907
          if (item.meta?.session_id?.includes(sessionId)) {
4!
908
            accu.start = new Date(item.action.startsWith('open') ? item.time : accu.start);
4!
909
            accu.end = new Date(item.action.startsWith('close') ? item.time : accu.end);
4!
910
          }
911
          return accu;
4✔
912
        },
913
        { start: startDate || endDate, end: endDate || startDate }
12✔
914
      );
915
      return Promise.resolve({ start, end });
4✔
916
    }
917
  );
918
};
919

920
export const getDeviceFileDownloadLink = (deviceId, path) => () =>
184✔
921
  Promise.resolve(`${deviceConnect}/devices/${deviceId}/download?path=${encodeURIComponent(path)}`);
1✔
922

923
export const deviceFileUpload = (deviceId, path, file) => (dispatch, getState) => {
184✔
924
  var formData = new FormData();
1✔
925
  formData.append('path', path);
1✔
926
  formData.append('file', file);
1✔
927
  const uploadId = uuid();
1✔
928
  const cancelSource = new AbortController();
1✔
929
  const uploads = { ...getState().app.uploads, [uploadId]: { inprogress: true, uploadProgress: 0, cancelSource } };
1✔
930
  return Promise.all([
1✔
931
    dispatch(setSnackbar('Uploading file')),
932
    dispatch({ type: UPLOAD_PROGRESS, uploads }),
933
    GeneralApi.uploadPut(`${deviceConnect}/devices/${deviceId}/upload`, formData, e => dispatch(progress(e, uploadId)), cancelSource.signal)
×
934
  ])
935
    .then(() => Promise.resolve(dispatch(setSnackbar('Upload successful', TIMEOUTS.fiveSeconds))))
1✔
936
    .catch(err => {
937
      if (isCancel(err)) {
×
938
        return dispatch(setSnackbar('The upload has been cancelled', TIMEOUTS.fiveSeconds));
×
939
      }
940
      return commonErrorHandler(err, `Error uploading file to device.`, dispatch);
×
941
    })
942
    .finally(() => dispatch(cleanUpUpload(uploadId)));
1✔
943
};
944

945
export const getDeviceAuth = id => dispatch =>
184✔
946
  Promise.resolve(dispatch(getDevicesWithAuth([{ id }]))).then(results => {
6✔
947
    if (results[results.length - 1]) {
6!
948
      return Promise.resolve(results[results.length - 1][0]);
6✔
949
    }
950
    return Promise.resolve();
×
951
  });
952

953
export const getDevicesWithAuth = devices => (dispatch, getState) =>
184✔
954
  devices.length
79✔
955
    ? GeneralApi.get(`${deviceAuthV2}/devices?id=${devices.map(device => device.id).join('&id=')}`)
105✔
956
        .then(({ data: receivedDevices }) => {
957
          const { devicesById } = reduceReceivedDevices(receivedDevices, [], getState());
78✔
958
          return Promise.all([dispatch({ type: DeviceConstants.RECEIVE_DEVICES, devicesById }), Promise.resolve(receivedDevices)]);
78✔
959
        })
960
        .catch(err => commonErrorHandler(err, `Error: ${err}`, dispatch))
×
961
    : Promise.resolve([[], []]);
962

963
const maybeUpdateDevicesByStatus = (deviceId, authId) => (dispatch, getState) => {
184✔
964
  const devicesState = getState().devices;
4✔
965
  const device = devicesState.byId[deviceId];
4✔
966
  const hasMultipleAuthSets = authId ? device.auth_sets.filter(authset => authset.id !== authId).length > 0 : false;
4✔
967
  if (!hasMultipleAuthSets && Object.values(DEVICE_STATES).includes(device.status)) {
4!
968
    const deviceIds = devicesState.byStatus[device.status].deviceIds.filter(id => id !== deviceId);
8✔
969
    return Promise.resolve(
4✔
970
      dispatch({
971
        type: DeviceConstants[`SET_${device.status.toUpperCase()}_DEVICES`],
972
        deviceIds,
973
        forceUpdate: true,
974
        status: device.status,
975
        total: Math.max(0, devicesState.byStatus[device.status].total - 1)
976
      })
977
    );
978
  }
979
  return Promise.resolve();
×
980
};
981

982
export const updateDeviceAuth = (deviceId, authId, status) => (dispatch, getState) =>
184✔
983
  GeneralApi.put(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}/status`, { status })
2✔
984
    .then(() => Promise.all([dispatch(getDeviceAuth(deviceId)), dispatch(setSnackbar('Device authorization status was updated successfully'))]))
2✔
985
    .catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
×
986
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))))
2✔
987
    .finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })));
2✔
988

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

1025
export const deleteAuthset = (deviceId, authId) => (dispatch, getState) =>
184✔
1026
  GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}`)
1✔
1027
    .then(() => Promise.all([dispatch(setSnackbar('Device authorization status was updated successfully'))]))
1✔
1028
    .catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
×
1029
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))))
1✔
1030
    .finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })));
1✔
1031

1032
export const preauthDevice = authset => dispatch =>
184✔
1033
  GeneralApi.post(`${deviceAuthV2}/devices`, authset)
4✔
1034
    .catch(err => {
1035
      if (err.response.status === 409) {
1!
1036
        return Promise.reject('A device with a matching identity data set already exists');
1✔
1037
      }
1038
      commonErrorHandler(err, 'The device could not be added:', dispatch);
×
1039
      return Promise.reject();
×
1040
    })
1041
    .then(() => Promise.resolve(dispatch(setSnackbar('Device was successfully added to the preauthorization list', TIMEOUTS.fiveSeconds))));
3✔
1042

1043
export const decommissionDevice = (deviceId, authId) => (dispatch, getState) =>
184✔
1044
  GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}`)
1✔
1045
    .then(() => Promise.resolve(dispatch(setSnackbar('Device was decommissioned successfully'))))
1✔
1046
    .catch(err => commonErrorHandler(err, 'There was a problem decommissioning the device:', dispatch))
×
1047
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))))
1✔
1048
    // trigger reset of device list list!
1049
    .finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })));
1✔
1050

1051
export const getDeviceConfig = deviceId => dispatch =>
184✔
1052
  GeneralApi.get(`${deviceConfig}/${deviceId}`)
3✔
1053
    .then(({ data }) => {
1054
      let tasks = [
2✔
1055
        dispatch({
1056
          type: DeviceConstants.RECEIVE_DEVICE_CONFIG,
1057
          device: { id: deviceId, config: data }
1058
        })
1059
      ];
1060
      tasks.push(Promise.resolve(data));
2✔
1061
      return Promise.all(tasks);
2✔
1062
    })
1063
    .catch(err => {
1064
      // 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
1065
      if (err.response?.data?.error.status_code !== 404) {
1!
1066
        return commonErrorHandler(err, `There was an error retrieving the configuration for device ${deviceId}.`, dispatch, commonErrorFallback);
×
1067
      }
1068
    });
1069

1070
export const setDeviceConfig = (deviceId, config) => dispatch =>
184✔
1071
  GeneralApi.put(`${deviceConfig}/${deviceId}`, config)
4✔
1072
    .catch(err => commonErrorHandler(err, `There was an error setting the configuration for device ${deviceId}.`, dispatch, commonErrorFallback))
2✔
1073
    .then(() => Promise.resolve(dispatch(getDeviceConfig(deviceId))));
1✔
1074

1075
export const applyDeviceConfig = (deviceId, configDeploymentConfiguration, isDefault, config) => (dispatch, getState) =>
184✔
1076
  GeneralApi.post(`${deviceConfig}/${deviceId}/deploy`, configDeploymentConfiguration)
1✔
1077
    .catch(err => commonErrorHandler(err, `There was an error deploying the configuration to device ${deviceId}.`, dispatch, commonErrorFallback))
×
1078
    .then(({ data }) => {
1079
      const device = getDeviceByIdSelector(getState(), deviceId);
1✔
1080
      const { canManageUsers } = getUserCapabilities(getState());
1✔
1081
      let tasks = [
1✔
1082
        dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device: { ...device, config: { ...device.config, deployment_id: '' } } }),
1083
        new Promise(resolve => setTimeout(() => resolve(dispatch(getSingleDeployment(data.deployment_id))), TIMEOUTS.oneSecond))
1✔
1084
      ];
1085
      if (isDefault && canManageUsers) {
1!
1086
        const { previous } = getState().users.globalSettings.defaultDeviceConfig ?? {};
×
1087
        tasks.push(dispatch(saveGlobalSettings({ defaultDeviceConfig: { current: config, previous } })));
×
1088
      }
1089
      return Promise.all(tasks);
1✔
1090
    });
1091

1092
export const setDeviceTags = (deviceId, tags) => dispatch =>
184✔
1093
  // to prevent tag set failures, retrieve the device & use the freshest etag we can get
1094
  Promise.resolve(dispatch(getDeviceById(deviceId))).then(device => {
2✔
1095
    const headers = device.etag ? { 'If-Match': device.etag } : {};
2!
1096
    return GeneralApi.put(
2✔
1097
      `${inventoryApiUrl}/devices/${deviceId}/tags`,
1098
      Object.entries(tags).map(([name, value]) => ({ name, value })),
2✔
1099
      { headers }
1100
    )
1101
      .catch(err => commonErrorHandler(err, `There was an error setting tags for device ${deviceId}.`, dispatch, 'Please check your connection.'))
×
1102
      .then(() => Promise.all([dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device: { ...device, tags } }), dispatch(setSnackbar('Device name changed'))]));
2✔
1103
  });
1104

1105
export const getDeviceTwin = (deviceId, integration) => (dispatch, getState) => {
184✔
1106
  let providerResult = {};
3✔
1107
  return GeneralApi.get(`${iotManagerBaseURL}/devices/${deviceId}/state`)
3✔
1108
    .then(({ data }) => {
1109
      providerResult = { ...data, twinError: '' };
2✔
1110
    })
1111
    .catch(err => {
1112
      providerResult = {
×
1113
        twinError: `There was an error getting the ${DeviceConstants.EXTERNAL_PROVIDER[
1114
          integration.provider
1115
        ].twinTitle.toLowerCase()} for device ${deviceId}. ${err}`
1116
      };
1117
    })
1118
    .finally(() =>
1119
      Promise.resolve(
2✔
1120
        dispatch({
1121
          type: DeviceConstants.RECEIVE_DEVICE,
1122
          device: {
1123
            ...getState().devices.byId[deviceId],
1124
            twinsByIntegration: {
1125
              ...getState().devices.byId[deviceId].twinsByIntegration,
1126
              ...providerResult
1127
            }
1128
          }
1129
        })
1130
      )
1131
    );
1132
};
1133

1134
export const setDeviceTwin = (deviceId, integration, settings) => (dispatch, getState) =>
184✔
1135
  GeneralApi.put(`${iotManagerBaseURL}/devices/${deviceId}/state/${integration.id}`, { desired: settings })
1✔
1136
    .catch(err =>
1137
      commonErrorHandler(
×
1138
        err,
1139
        `There was an error updating the ${DeviceConstants.EXTERNAL_PROVIDER[integration.provider].twinTitle.toLowerCase()} for device ${deviceId}.`,
1140
        dispatch
1141
      )
1142
    )
1143
    .then(() => {
1144
      const { twinsByIntegration = {} } = getState().devices.byId[deviceId];
1✔
1145
      const { [integration.id]: currentState = {} } = twinsByIntegration;
1✔
1146
      return Promise.resolve(
1✔
1147
        dispatch({
1148
          type: DeviceConstants.RECEIVE_DEVICE,
1149
          device: {
1150
            ...getState().devices.byId[deviceId],
1151
            twinsByIntegration: {
1152
              ...twinsByIntegration,
1153
              [integration.id]: {
1154
                ...currentState,
1155
                desired: settings
1156
              }
1157
            }
1158
          }
1159
        })
1160
      );
1161
    });
1162

1163
const prepareSearchArguments = ({ filters, group, state, status }) => {
184✔
1164
  const { filterTerms } = convertDeviceListStateToFilters({ filters, group, offlineThreshold: state.app.offlineThreshold, selectedIssues: [], status });
4✔
1165
  const { columnSelection = [] } = getUserSettings(state);
4!
1166
  const selectedAttributes = columnSelection.map(column => ({ attribute: column.key, scope: column.scope }));
4✔
1167
  const attributes = [...defaultAttributes, { scope: 'identity', attribute: getIdAttribute(state).attribute }, ...selectedAttributes];
4✔
1168
  return { attributes, filterTerms };
4✔
1169
};
1170

1171
export const getSystemDevices =
1172
  (id, options = {}) =>
184✔
1173
  (dispatch, getState) => {
1✔
1174
    const { page = defaultPage, perPage = defaultPerPage, sortOptions = [] } = options;
1✔
1175
    const state = getState();
1✔
1176
    let device = getDeviceByIdSelector(state, id);
1✔
1177
    const { attributes: deviceAttributes = {} } = device;
1!
1178
    const { mender_gateway_system_id = '' } = deviceAttributes;
1✔
1179
    const { hasFullFiltering } = getTenantCapabilities(state);
1✔
1180
    if (!hasFullFiltering) {
1!
1181
      return Promise.resolve();
×
1182
    }
1183
    const filters = [
1✔
1184
      { ...emptyFilter, key: 'mender_is_gateway', operator: DEVICE_FILTERING_OPTIONS.$ne.key, value: 'true', scope: 'inventory' },
1185
      { ...emptyFilter, key: 'mender_gateway_system_id', value: mender_gateway_system_id, scope: 'inventory' }
1186
    ];
1187
    const { attributes, filterTerms } = prepareSearchArguments({ filters, state });
1✔
1188

1189
    return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
1✔
1190
      page,
1191
      per_page: perPage,
1192
      filters: filterTerms,
1193
      sort: sortOptions,
1194
      attributes
1195
    })
1196
      .catch(err => commonErrorHandler(err, `There was an error getting system devices device ${id}.`, dispatch, 'Please check your connection.'))
×
1197
      .then(({ data, headers }) => {
1198
        const state = getState();
1✔
1199
        const { devicesById, ids } = reduceReceivedDevices(data, [], state);
1✔
1200
        const device = {
1✔
1201
          ...state.devices.byId[id],
1202
          systemDeviceIds: ids,
1203
          systemDeviceTotal: Number(headers[headerNames.total])
1204
        };
1205
        return Promise.resolve(
1✔
1206
          dispatch({
1207
            type: DeviceConstants.RECEIVE_DEVICES,
1208
            devicesById: {
1209
              ...devicesById,
1210
              [id]: device
1211
            }
1212
          })
1213
        );
1214
      });
1215
  };
1216

1217
export const getGatewayDevices = deviceId => (dispatch, getState) => {
184✔
1218
  const state = getState();
1✔
1219
  let device = getDeviceByIdSelector(state, deviceId);
1✔
1220
  const { attributes = {} } = device;
1!
1221
  const { mender_gateway_system_id = '' } = attributes;
1!
1222
  const filters = [
1✔
1223
    { ...emptyFilter, key: 'id', operator: DEVICE_FILTERING_OPTIONS.$ne.key, value: deviceId, scope: 'identity' },
1224
    { ...emptyFilter, key: 'mender_is_gateway', value: 'true', scope: 'inventory' },
1225
    { ...emptyFilter, key: 'mender_gateway_system_id', value: mender_gateway_system_id, scope: 'inventory' }
1226
  ];
1227
  const { attributes: attributeSelection, filterTerms } = prepareSearchArguments({ filters, state });
1✔
1228
  return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
1✔
1229
    page: 1,
1230
    per_page: MAX_PAGE_SIZE,
1231
    filters: filterTerms,
1232
    attributes: attributeSelection
1233
  }).then(({ data }) => {
1234
    const { ids } = reduceReceivedDevices(data, [], getState());
1✔
1235
    let tasks = ids.map(deviceId => dispatch(getDeviceInfo(deviceId)));
1✔
1236
    tasks.push(dispatch({ type: DeviceConstants.RECEIVE_DEVICE, device: { ...getState().devices.byId[deviceId], gatewayIds: ids } }));
1✔
1237
    return Promise.all(tasks);
1✔
1238
  });
1239
};
1240

1241
export const geoAttributes = ['geo-lat', 'geo-lon'].map(attribute => ({ attribute, scope: 'inventory' }));
368✔
1242
export const getDevicesInBounds = (bounds, group) => (dispatch, getState) => {
184✔
1243
  const state = getState();
×
1244
  const { filterTerms } = convertDeviceListStateToFilters({
×
1245
    group: group === DeviceConstants.ALL_DEVICES ? undefined : group,
×
1246
    groups: state.devices.groups,
1247
    status: DEVICE_STATES.accepted
1248
  });
1249
  return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
×
1250
    page: 1,
1251
    per_page: MAX_PAGE_SIZE,
1252
    filters: filterTerms,
1253
    attributes: geoAttributes,
1254
    geo_bounding_box_filter: {
1255
      geo_bounding_box: {
1256
        location: {
1257
          top_left: { lat: bounds._northEast.lat, lon: bounds._southWest.lng },
1258
          bottom_right: { lat: bounds._southWest.lat, lon: bounds._northEast.lng }
1259
        }
1260
      }
1261
    }
1262
  }).then(({ data }) => {
1263
    const { devicesById } = reduceReceivedDevices(data, [], getState());
×
1264
    return Promise.resolve(dispatch({ type: DeviceConstants.RECEIVE_DEVICES, devicesById }));
×
1265
  });
1266
};
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