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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

90.43
/src/js/actions/deploymentActions.js
1
/*eslint import/namespace: ['error', { allowComputed: true }]*/
2
import isUUID from 'validator/lib/isUUID';
3

4
import { commonErrorHandler, setSnackbar } from '../actions/appActions';
5
import GeneralApi, { apiUrl, headerNames } from '../api/general-api';
6
import { SORTING_OPTIONS, TIMEOUTS } from '../constants/appConstants';
7
import * as DeploymentConstants from '../constants/deploymentConstants';
8
import { DEVICE_LIST_DEFAULTS, RECEIVE_DEVICE } from '../constants/deviceConstants';
9
import { deepCompare, isEmpty, standardizePhases, startTimeSort } from '../helpers';
10
import { getDevicesById } from '../selectors';
11
import Tracking from '../tracking';
12
import { getDeviceAuth, getDeviceById, mapTermsToFilters } from './deviceActions';
13
import { saveGlobalSettings } from './userActions';
14

15
export const deploymentsApiUrl = `${apiUrl.v1}/deployments`;
184✔
16
export const deploymentsApiUrlV2 = `${apiUrl.v2}/deployments`;
184✔
17

18
const {
19
  CREATE_DEPLOYMENT,
20
  DEPLOYMENT_ROUTES,
21
  DEPLOYMENT_STATES,
22
  DEPLOYMENT_TYPES,
23
  deploymentPrototype,
24
  RECEIVE_DEPLOYMENT_DEVICE_LOG,
25
  RECEIVE_DEPLOYMENT_DEVICES,
26
  RECEIVE_DEPLOYMENT,
27
  RECEIVE_DEPLOYMENTS,
28
  REMOVE_DEPLOYMENT,
29
  SET_DEPLOYMENTS_STATE
30
} = DeploymentConstants;
184✔
31

32
// default per page until pagination and counting integrated
33
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
184✔
34

35
export const deriveDeploymentGroup = ({ filter = {}, group, groups = [], name }) => (group || (groups.length === 1 && !isUUID(name)) ? groups[0] : filter.name);
982!
36

37
const transformDeployments = (deployments, deploymentsById) =>
184✔
38
  deployments.sort(startTimeSort).reduce(
496✔
39
    (accu, item) => {
40
      const filter = item.filter ?? {};
982✔
41
      let deployment = {
982✔
42
        ...deploymentPrototype,
43
        ...deploymentsById[item.id],
44
        ...item,
45
        filter: item.filter ? { ...filter, name: filter.name ?? filter.id, filters: mapTermsToFilters(filter.terms) } : undefined,
982!
46
        name: decodeURIComponent(item.name)
47
      };
48
      // deriving the group in a second step to potentially make use of the merged data from the existing group state + the decoded name
49
      deployment = { ...deployment, group: deriveDeploymentGroup(deployment) };
982✔
50
      accu.deployments[item.id] = deployment;
982✔
51
      accu.deploymentIds.push(item.id);
982✔
52
      return accu;
982✔
53
    },
54
    { deployments: {}, deploymentIds: [] }
55
  );
56

57
/*Deployments */
58
export const getDeploymentsByStatus =
59
  (status, page = defaultPage, per_page = defaultPerPage, startDate, endDate, group, type, shouldSelect = true, sort = SORTING_OPTIONS.desc) =>
184✔
60
  (dispatch, getState) => {
486✔
61
    const created_after = startDate ? `&created_after=${startDate}` : '';
486✔
62
    const created_before = endDate ? `&created_before=${endDate}` : '';
486!
63
    const search = group ? `&search=${group}` : '';
486✔
64
    const typeFilter = type ? `&type=${type}` : '';
486✔
65
    return GeneralApi.get(
486✔
66
      `${deploymentsApiUrl}/deployments?status=${status}&per_page=${per_page}&page=${page}${created_after}${created_before}${search}${typeFilter}&sort=${sort}`
67
    ).then(res => {
68
      const { deployments, deploymentIds } = transformDeployments(res.data, getState().deployments.byId);
486✔
69
      const total = Number(res.headers[headerNames.total]);
486✔
70
      let tasks = [
486✔
71
        dispatch({ type: RECEIVE_DEPLOYMENTS, deployments }),
72
        dispatch({
73
          type: DeploymentConstants[`RECEIVE_${status.toUpperCase()}_DEPLOYMENTS`],
74
          deploymentIds,
75
          status,
76
          total: !(startDate || endDate || group || type) ? total : getState().deployments.byStatus[status].total
2,411✔
77
        })
78
      ];
79
      tasks = deploymentIds.reduce((accu, deploymentId) => {
486✔
80
        if (deployments[deploymentId].type === DEPLOYMENT_TYPES.configuration) {
972!
UNCOV
81
          accu.push(dispatch(getSingleDeployment(deploymentId)));
×
82
        }
83
        return accu;
972✔
84
      }, tasks);
85
      if (shouldSelect) {
486✔
86
        tasks.push(dispatch({ type: DeploymentConstants[`SELECT_${status.toUpperCase()}_DEPLOYMENTS`], deploymentIds, status, total }));
465✔
87
      }
88
      tasks.push({ deploymentIds, total });
486✔
89
      return Promise.all(tasks);
486✔
90
    });
91
  };
92

93
const isWithinFirstMonth = expirationDate => {
184✔
94
  if (!expirationDate) {
1!
95
    return false;
1✔
96
  }
UNCOV
97
  const endOfFirstMonth = new Date(expirationDate);
×
UNCOV
98
  endOfFirstMonth.setMonth(endOfFirstMonth.getMonth() - 11);
×
UNCOV
99
  return endOfFirstMonth > new Date();
×
100
};
101

102
const trackDeploymentCreation = (totalDeploymentCount, hasDeployments, trial_expiration) => {
184✔
103
  Tracking.event({ category: 'deployments', action: 'create' });
5✔
104
  if (!totalDeploymentCount) {
5✔
105
    if (!hasDeployments) {
1!
106
      Tracking.event({ category: 'deployments', action: 'create_initial_deployment' });
1✔
107
      if (isWithinFirstMonth(trial_expiration)) {
1!
UNCOV
108
        Tracking.event({ category: 'deployments', action: 'create_initial_deployment_first_month' });
×
109
      }
110
    }
111
    Tracking.event({ category: 'deployments', action: 'create_initial_deployment_user' });
1✔
112
  }
113
};
114

115
const MAX_PREVIOUS_PHASES_COUNT = 5;
184✔
116
export const createDeployment =
117
  (newDeployment, hasNewRetryDefault = false) =>
184✔
118
  (dispatch, getState) => {
5✔
119
    let request;
120
    if (newDeployment.filter_id) {
5✔
121
      request = GeneralApi.post(`${deploymentsApiUrlV2}/deployments`, newDeployment);
1✔
122
    } else if (newDeployment.group) {
4✔
123
      request = GeneralApi.post(`${deploymentsApiUrl}/deployments/group/${newDeployment.group}`, newDeployment);
1✔
124
    } else {
125
      request = GeneralApi.post(`${deploymentsApiUrl}/deployments`, newDeployment);
3✔
126
    }
127
    const totalDeploymentCount = Object.values(getState().deployments.byStatus).reduce((accu, item) => accu + item.total, 0);
20✔
128
    const { hasDeployments } = getState().users.globalSettings;
5✔
129
    const { trial_expiration } = getState().organization.organization;
5✔
130
    return request
5✔
UNCOV
131
      .catch(err => commonErrorHandler(err, 'Error creating deployment.', dispatch))
×
132
      .then(data => {
133
        const lastslashindex = data.headers.location.lastIndexOf('/');
5✔
134
        const deploymentId = data.headers.location.substring(lastslashindex + 1);
5✔
135
        const deployment = {
5✔
136
          ...newDeployment,
137
          devices: newDeployment.devices ? newDeployment.devices.map(id => ({ id, status: 'pending' })) : [],
1✔
138
          statistics: { status: {} }
139
        };
140
        let tasks = [
5✔
141
          dispatch({ type: CREATE_DEPLOYMENT, deployment, deploymentId }),
142
          dispatch(getSingleDeployment(deploymentId)),
143
          dispatch(setSnackbar('Deployment created successfully', TIMEOUTS.fiveSeconds))
144
        ];
145
        // track in GA
146
        trackDeploymentCreation(totalDeploymentCount, hasDeployments, trial_expiration);
5✔
147

148
        const { phases, retries } = newDeployment;
5✔
149
        const { previousPhases = [], retries: previousRetries = 0 } = getState().users.globalSettings;
5✔
150
        let newSettings = { retries: hasNewRetryDefault ? retries : previousRetries, hasDeployments: true };
5✔
151
        if (phases) {
5✔
152
          const standardPhases = standardizePhases(phases);
1✔
153
          let prevPhases = previousPhases.map(standardizePhases);
1✔
154
          if (!prevPhases.find(previousPhaseList => previousPhaseList.every(oldPhase => standardPhases.find(phase => deepCompare(phase, oldPhase))))) {
3!
155
            prevPhases.push(standardPhases);
1✔
156
          }
157
          newSettings.previousPhases = prevPhases.slice(-1 * MAX_PREVIOUS_PHASES_COUNT);
1✔
158
        }
159

160
        tasks.push(dispatch(saveGlobalSettings(newSettings)));
5✔
161
        return Promise.all(tasks);
5✔
162
      });
163
  };
164

165
export const getDeploymentDevices =
166
  (id, options = {}) =>
184✔
167
  (dispatch, getState) => {
3✔
168
    const { page = defaultPage, perPage = defaultPerPage } = options;
3✔
169
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}/devices/list?deployment_id=${id}&page=${page}&per_page=${perPage}`).then(response => {
3✔
170
      const { devices: deploymentDevices = {} } = getState().deployments.byId[id] || {};
3!
171
      const devices = response.data.reduce((accu, item) => {
3✔
172
        accu[item.id] = item;
3✔
173
        const log = (deploymentDevices[item.id] || {}).log;
3!
174
        if (log) {
3!
UNCOV
175
          accu[item.id].log = log;
×
176
        }
177
        return accu;
3✔
178
      }, {});
179
      const selectedDeviceIds = Object.keys(devices);
3✔
180
      let tasks = [
3✔
181
        dispatch({
182
          type: RECEIVE_DEPLOYMENT_DEVICES,
183
          deploymentId: id,
184
          devices,
185
          selectedDeviceIds,
186
          totalDeviceCount: Number(response.headers[headerNames.total])
187
        })
188
      ];
189
      const devicesById = getDevicesById(getState());
3✔
190
      // only update those that have changed & lack data
191
      const lackingData = selectedDeviceIds.reduce((accu, deviceId) => {
3✔
192
        const device = devicesById[deviceId];
3✔
193
        if (!device || !device.identity_data || !device.attributes || Object.keys(device.attributes).length === 0) {
3!
UNCOV
194
          accu.push(deviceId);
×
195
        }
196
        return accu;
3✔
197
      }, []);
198
      // get device artifact, inventory and identity details not listed in schedule data
199
      tasks = lackingData.reduce((accu, deviceId) => [...accu, dispatch(getDeviceById(deviceId)), dispatch(getDeviceAuth(deviceId))], tasks);
3✔
200
      return Promise.all(tasks);
3✔
201
    });
202
  };
203

204
const parseDeviceDeployment = ({
184✔
205
  deployment: { id, artifact_name: release, status: deploymentStatus },
206
  device: { created, deleted, id: deviceId, finished, status, log }
207
}) => ({
4✔
208
  id,
209
  release,
210
  created,
211
  deleted,
212
  deviceId,
213
  finished,
214
  status,
215
  log,
216
  route: Object.values(DEPLOYMENT_ROUTES).reduce((accu, { key, states }) => {
217
    if (!accu) {
12✔
218
      return states.includes(deploymentStatus) ? key : accu;
4!
219
    }
220
    return accu;
8✔
221
  }, '')
222
});
223

224
export const getDeviceDeployments =
225
  (deviceId, options = {}) =>
184✔
226
  (dispatch, getState) => {
4✔
227
    const { filterSelection = [], page = defaultPage, perPage = defaultPerPage } = options;
4✔
228
    const filters = filterSelection.map(item => `&status=${item}`).join('');
4✔
229
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/devices/${deviceId}?page=${page}&per_page=${perPage}${filters}`)
4✔
230
      .then(({ data, headers }) =>
231
        Promise.resolve(
4✔
232
          dispatch({
233
            type: RECEIVE_DEVICE,
234
            device: {
235
              ...getState().devices.byId[deviceId],
236
              deviceDeployments: data.map(parseDeviceDeployment),
237
              deploymentsCount: Number(headers[headerNames.total])
238
            }
239
          })
240
        )
241
      )
UNCOV
242
      .catch(err => commonErrorHandler(err, 'There was an error retrieving the device deployment history:', dispatch));
×
243
  };
244

245
export const resetDeviceDeployments = deviceId => dispatch =>
184✔
246
  GeneralApi.delete(`${deploymentsApiUrl}/deployments/devices/${deviceId}/history`)
1✔
247
    .then(() => Promise.resolve(dispatch(getDeviceDeployments(deviceId))))
1✔
UNCOV
248
    .catch(err => commonErrorHandler(err, 'There was an error resetting the device deployment history:', dispatch));
×
249

250
export const getSingleDeployment = id => (dispatch, getState) =>
184✔
251
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}`).then(({ data }) => {
10✔
252
    const { deployments } = transformDeployments([data], getState().deployments.byId);
10✔
253
    return Promise.resolve(dispatch({ type: RECEIVE_DEPLOYMENT, deployment: deployments[id] }));
10✔
254
  });
255

256
export const getDeviceLog = (deploymentId, deviceId) => (dispatch, getState) =>
184✔
257
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${deploymentId}/devices/${deviceId}/log`)
1✔
258
    .catch(e => {
UNCOV
259
      console.log('no log here', e);
×
UNCOV
260
      return Promise.reject();
×
261
    })
262
    .then(({ data: log }) => {
263
      const stateDeployment = getState().deployments.byId[deploymentId];
1✔
264
      const deployment = {
1✔
265
        ...stateDeployment,
266
        devices: {
267
          ...stateDeployment.devices,
268
          [deviceId]: {
269
            ...stateDeployment.devices[deviceId],
270
            log
271
          }
272
        }
273
      };
274
      return Promise.all([
1✔
275
        Promise.resolve(
276
          dispatch({
277
            type: RECEIVE_DEPLOYMENT_DEVICE_LOG,
278
            deployment
279
          })
280
        ),
281
        Promise.resolve(log)
282
      ]);
283
    });
284

285
export const abortDeployment = deploymentId => (dispatch, getState) =>
184✔
286
  GeneralApi.put(`${deploymentsApiUrl}/deployments/${deploymentId}/status`, { status: 'aborted' })
2✔
287
    .then(() => {
288
      const state = getState();
1✔
289
      let status = DEPLOYMENT_STATES.pending;
1✔
290
      let index = state.deployments.byStatus.pending.deploymentIds.findIndex(id => id === deploymentId);
1✔
291
      if (index < 0) {
1!
292
        status = DEPLOYMENT_STATES.inprogress;
1✔
293
        index = state.deployments.byStatus.inprogress.deploymentIds.findIndex(id => id === deploymentId);
1✔
294
      }
295
      const deploymentIds = [
1✔
296
        ...state.deployments.byStatus[status].deploymentIds.slice(0, index),
297
        ...state.deployments.byStatus[status].deploymentIds.slice(index + 1)
298
      ];
299
      const deployments = deploymentIds.reduce((accu, id) => {
1✔
UNCOV
300
        accu[id] = state.deployments.byId[id];
×
UNCOV
301
        return accu;
×
302
      }, {});
303
      const total = Math.max(state.deployments.byStatus[status].total - 1, 0);
1✔
304
      return Promise.all([
1✔
305
        dispatch({ type: RECEIVE_DEPLOYMENTS, deployments }),
306
        dispatch({ type: DeploymentConstants[`RECEIVE_${status.toUpperCase()}_DEPLOYMENTS`], deploymentIds, status, total }),
307
        dispatch({
308
          type: REMOVE_DEPLOYMENT,
309
          deploymentId
310
        }),
311
        dispatch(setSnackbar('The deployment was successfully aborted'))
312
      ]);
313
    })
314
    .catch(err => commonErrorHandler(err, 'There was an error while aborting the deployment:', dispatch));
1✔
315

316
export const updateDeploymentControlMap = (deploymentId, update_control_map) => dispatch =>
184✔
317
  GeneralApi.patch(`${deploymentsApiUrl}/deployments/${deploymentId}`, { update_control_map })
1✔
UNCOV
318
    .catch(err => commonErrorHandler(err, 'There was an error while updating the deployment status:', dispatch))
×
319
    .then(() => Promise.resolve(dispatch(getSingleDeployment(deploymentId))));
1✔
320

321
export const setDeploymentsState = selection => (dispatch, getState) => {
184✔
322
  // eslint-disable-next-line no-unused-vars
323
  const { page, perPage, ...selectionState } = selection;
27✔
324
  const currentState = getState().deployments.selectionState;
27✔
325
  let nextState = {
27✔
326
    ...currentState,
327
    ...selectionState,
328
    ...Object.keys(DEPLOYMENT_STATES).reduce((accu, item) => {
329
      accu[item] = {
108✔
330
        ...currentState[item],
331
        ...selectionState[item]
332
      };
333
      return accu;
108✔
334
    }, {}),
335
    general: {
336
      ...currentState.general,
337
      ...selectionState.general
338
    }
339
  };
340
  let tasks = [dispatch({ type: SET_DEPLOYMENTS_STATE, state: nextState })];
27✔
341
  if (nextState.selectedId && currentState.selectedId !== nextState.selectedId) {
27✔
342
    tasks.push(dispatch(getSingleDeployment(nextState.selectedId)));
2✔
343
  }
344
  return Promise.all(tasks);
27✔
345
};
346

347
const deltaAttributeMappings = [
184✔
348
  { here: 'compressionLevel', there: 'compression_level' },
349
  { here: 'disableChecksum', there: 'disable_checksum' },
350
  { here: 'disableDecompression', there: 'disable_external_decompression' },
351
  { here: 'sourceWindow', there: 'source_window_size' },
352
  { here: 'inputWindow', there: 'input_window_size' },
353
  { here: 'duplicatesWindow', there: 'compression_duplicates_window' },
354
  { here: 'instructionBuffer', there: 'instruction_buffer_size' }
355
];
356

357
const mapExternalDeltaConfig = (config = {}) =>
184!
358
  deltaAttributeMappings.reduce((accu, { here, there }) => {
12✔
359
    if (config[there] !== undefined) {
84✔
360
      accu[here] = config[there];
66✔
361
    }
362
    return accu;
84✔
363
  }, {});
364

365
export const getDeploymentsConfig = () => (dispatch, getState) =>
184✔
366
  GeneralApi.get(`${deploymentsApiUrl}/config`).then(({ data }) => {
6✔
367
    const oldConfig = getState().deployments.config;
6✔
368
    const { delta = {} } = data;
6!
369
    const { binary_delta = {}, binary_delta_limits = {} } = delta;
6!
370
    const { xdelta_args = {}, timeout: timeoutConfig = oldConfig.binaryDelta.timeout } = binary_delta;
6!
371
    const { xdelta_args_limits = {}, timeout: timeoutLimit = oldConfig.binaryDeltaLimits.timeout } = binary_delta_limits;
6!
372
    const config = {
6✔
373
      ...oldConfig,
374
      hasDelta: Boolean(delta.enabled),
375
      binaryDelta: {
376
        ...oldConfig.binaryDelta,
377
        timeout: timeoutConfig,
378
        ...mapExternalDeltaConfig(xdelta_args)
379
      },
380
      binaryDeltaLimits: {
381
        ...oldConfig.binaryDeltaLimits,
382
        timeout: timeoutLimit,
383
        ...mapExternalDeltaConfig(xdelta_args_limits)
384
      }
385
    };
386
    return Promise.resolve(dispatch({ type: DeploymentConstants.SET_DEPLOYMENTS_CONFIG, config }));
6✔
387
  });
388

389
// traverse a source object and remove undefined & empty object properties to only return an attribute if there really is content worth sending
390
const deepClean = source =>
184✔
391
  Object.entries(source).reduce((accu, [key, value]) => {
2✔
392
    if (value !== undefined) {
9!
393
      let cleanedValue = typeof value === 'object' ? deepClean(value) : value;
9✔
394
      if (cleanedValue === undefined || (typeof cleanedValue === 'object' && isEmpty(cleanedValue))) {
9!
UNCOV
395
        return accu;
×
396
      }
397
      accu = { ...(accu ?? {}), [key]: cleanedValue };
9✔
398
    }
399
    return accu;
9✔
400
  }, undefined);
401

402
export const saveDeltaDeploymentsConfig = config => (dispatch, getState) => {
184✔
403
  const configChange = {
1✔
404
    timeout: config.timeout,
405
    xdelta_args: deltaAttributeMappings.reduce((accu, { here, there }) => {
406
      if (config[here] !== undefined) {
7!
407
        accu[there] = config[here];
7✔
408
      }
409
      return accu;
7✔
410
    }, {})
411
  };
412
  const result = deepClean(configChange);
1✔
413
  if (!result) {
1!
UNCOV
414
    return Promise.resolve();
×
415
  }
416
  return GeneralApi.put(`${deploymentsApiUrl}/config/binary_delta`, result)
1✔
UNCOV
417
    .catch(err => commonErrorHandler(err, 'There was a problem storing your delta artifact generation configuration.', dispatch))
×
418
    .then(() => {
419
      const oldConfig = getState().deployments.config;
1✔
420
      const newConfig = {
1✔
421
        ...oldConfig,
422
        binaryDelta: {
423
          ...oldConfig.binaryDelta,
424
          ...config
425
        }
426
      };
427
      return Promise.all([
1✔
428
        dispatch({ type: DeploymentConstants.SET_DEPLOYMENTS_CONFIG, config: newConfig }),
429
        dispatch(setSnackbar('Settings saved successfully'))
430
      ]);
431
    });
432
};
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