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

mendersoftware / gui / 919001084

pending completion
919001084

Pull #3839

gitlab-ci

mzedel
revert: "chore: bump node from 20.2.0-alpine to 20.3.1-alpine"

This reverts commit cbfcd7663.

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3839: Combined PRs

4399 of 6397 branches covered (68.77%)

8302 of 10074 relevant lines covered (82.41%)

162.96 hits per line

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

84.52
/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, TIMEOUTS, 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: 'check_in_time' },
54
  { scope: 'system', attribute: 'group' },
55
  { scope: 'tags', attribute: 'name' }
56
];
57

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

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

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

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

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

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

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

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

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

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

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

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

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

290
export const selectGroup =
291
  (group, filters = []) =>
187✔
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) {
5✔
296
      return;
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
const getEarliestTs = (dateA = '', dateB = '') => (!dateA || !dateB ? dateA || dateB : dateA < dateB ? dateA : dateB);
256!
310
const getLatestTs = (dateA = '', dateB = '') => (!dateA || !dateB ? dateA || dateB : dateA >= dateB ? dateA : dateB);
384✔
311

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1018
export const deleteAuthset = (deviceId, authId) => (dispatch, getState) =>
187✔
1019
  GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}/auth/${authId}`)
1✔
1020
    .then(() => Promise.all([dispatch(setSnackbar('Device authorization status was updated successfully'))]))
1✔
1021
    .catch(err => commonErrorHandler(err, 'There was a problem updating the device authorization status:', dispatch))
×
1022
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))))
1✔
1023
    .finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })));
1✔
1024

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

1036
export const decommissionDevice = (deviceId, authId) => (dispatch, getState) =>
187✔
1037
  GeneralApi.delete(`${deviceAuthV2}/devices/${deviceId}`)
1✔
1038
    .then(() => Promise.resolve(dispatch(setSnackbar('Device was decommissioned successfully'))))
1✔
1039
    .catch(err => commonErrorHandler(err, 'There was a problem decommissioning the device:', dispatch))
×
1040
    .then(() => Promise.resolve(dispatch(maybeUpdateDevicesByStatus(deviceId, authId))))
1✔
1041
    // trigger reset of device list list!
1042
    .finally(() => dispatch(setDeviceListState({ refreshTrigger: !getState().devices.deviceList.refreshTrigger })));
1✔
1043

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

1063
export const setDeviceConfig = (deviceId, config) => dispatch =>
187✔
1064
  GeneralApi.put(`${deviceConfig}/${deviceId}`, config)
1✔
1065
    .catch(err => commonErrorHandler(err, `There was an error setting the configuration for device ${deviceId}.`, dispatch, commonErrorFallback))
×
1066
    .then(() => Promise.resolve(dispatch(getDeviceConfig(deviceId))));
1✔
1067

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

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

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

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

1151
const prepareSearchArguments = ({ filters, group, state, status }) => {
187✔
1152
  const { filterTerms } = convertDeviceListStateToFilters({ filters, group, offlineThreshold: state.app.offlineThreshold, selectedIssues: [], status });
4✔
1153
  const { columnSelection = [] } = getUserSettings(state);
4!
1154
  const selectedAttributes = columnSelection.map(column => ({ attribute: column.key, scope: column.scope }));
4✔
1155
  const attributes = [...defaultAttributes, { scope: 'identity', attribute: getIdAttribute(state).attribute }, ...selectedAttributes];
4✔
1156
  return { attributes, filterTerms };
4✔
1157
};
1158

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

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

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

1229
export const geoAttributes = ['geo-lat', 'geo-lon'].map(attribute => ({ attribute, scope: 'inventory' }));
374✔
1230
export const getDevicesInBounds = (bounds, group) => (dispatch, getState) => {
187✔
1231
  const state = getState();
×
1232
  const { filterTerms } = convertDeviceListStateToFilters({
×
1233
    group: group === DeviceConstants.ALL_DEVICES ? undefined : group,
×
1234
    groups: state.devices.groups,
1235
    status: DEVICE_STATES.accepted
1236
  });
1237
  return GeneralApi.post(getSearchEndpoint(state.app.features.hasReporting), {
×
1238
    page: 1,
1239
    per_page: MAX_PAGE_SIZE,
1240
    filters: filterTerms,
1241
    attributes: geoAttributes,
1242
    geo_bounding_box_filter: {
1243
      geo_bounding_box: {
1244
        location: {
1245
          top_left: { lat: bounds._northEast.lat, lon: bounds._southWest.lng },
1246
          bottom_right: { lat: bounds._southWest.lat, lon: bounds._northEast.lng }
1247
        }
1248
      }
1249
    }
1250
  }).then(({ data }) => {
1251
    const { devicesById } = reduceReceivedDevices(data, [], getState());
×
1252
    return Promise.resolve(dispatch({ type: DeviceConstants.RECEIVE_DEVICES, devicesById }));
×
1253
  });
1254
};
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