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

mendersoftware / gui / 913068613

pending completion
913068613

Pull #3803

gitlab-ci

web-flow
Merge pull request #3801 from mzedel/men-6383

MEN-6383 - device check in time
Pull Request #3803: staging alignment

4418 of 6435 branches covered (68.66%)

178 of 246 new or added lines in 27 files covered. (72.36%)

8352 of 10138 relevant lines covered (82.38%)

160.95 hits per line

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

83.88
/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 Tracking from '../tracking';
11
import { getDeviceAuth, getDeviceById } from './deviceActions';
12
import { saveGlobalSettings } from './userActions';
13

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

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

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

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

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

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

90
const isWithinFirstMonth = expirationDate => {
190✔
91
  if (!expirationDate) {
1!
92
    return false;
1✔
93
  }
94
  const endOfFirstMonth = new Date(expirationDate);
×
95
  endOfFirstMonth.setMonth(endOfFirstMonth.getMonth() - 11);
×
96
  return endOfFirstMonth > new Date();
×
97
};
98

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

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

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

157
        tasks.push(dispatch(saveGlobalSettings(newSettings)));
5✔
158
        return Promise.all(tasks);
5✔
159
      });
160
  };
161

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

201
const parseDeviceDeployment = ({
190✔
202
  deployment: { id, artifact_name: release, groups = [], name, device: deploymentDevice, status: deploymentStatus },
2✔
203
  device: { created, deleted, id: deviceId, finished, status, log }
204
}) => ({
2✔
205
  id,
206
  release,
207
  target: groups.length === 1 && groups[0] ? groups[0] : deploymentDevice ? deploymentDevice : name,
6!
208
  created,
209
  deleted,
210
  deviceId,
211
  finished,
212
  status,
213
  log,
214
  route: Object.values(DEPLOYMENT_ROUTES).reduce((accu, { key, states }) => {
215
    if (!accu) {
6✔
216
      return states.includes(deploymentStatus) ? key : accu;
2!
217
    }
218
    return accu;
4✔
219
  }, ''),
220
  deploymentStatus
221
});
222

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

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

249
export const getSingleDeployment = id => (dispatch, getState) =>
190✔
250
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}`).then(({ data }) => {
10✔
251
    const storedDeployment = getState().deployments.byId[id] || {};
9✔
252
    return Promise.resolve(
9✔
253
      dispatch({
254
        type: RECEIVE_DEPLOYMENT,
255
        deployment: {
256
          ...deploymentPrototype,
257
          ...storedDeployment,
258
          ...data,
259
          name: decodeURIComponent(data.name)
260
        }
261
      })
262
    );
263
  });
264

265
export const getDeviceLog = (deploymentId, deviceId) => (dispatch, getState) =>
190✔
266
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${deploymentId}/devices/${deviceId}/log`)
1✔
267
    .catch(e => {
268
      console.log('no log here');
×
269
      console.log(e);
×
270
      return Promise.reject();
×
271
    })
272
    .then(({ data: log }) => {
273
      const stateDeployment = getState().deployments.byId[deploymentId];
1✔
274
      const deployment = {
1✔
275
        ...stateDeployment,
276
        devices: {
277
          ...stateDeployment.devices,
278
          [deviceId]: {
279
            ...stateDeployment.devices[deviceId],
280
            log
281
          }
282
        }
283
      };
284
      return Promise.all([
1✔
285
        Promise.resolve(
286
          dispatch({
287
            type: RECEIVE_DEPLOYMENT_DEVICE_LOG,
288
            deployment
289
          })
290
        ),
291
        Promise.resolve(log)
292
      ]);
293
    });
294

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

326
export const updateDeploymentControlMap = (deploymentId, update_control_map) => dispatch =>
190✔
327
  GeneralApi.patch(`${deploymentsApiUrl}/deployments/${deploymentId}`, { update_control_map })
1✔
328
    .catch(err => commonErrorHandler(err, 'There was an error while updating the deployment status:', dispatch))
×
329
    .then(() => Promise.resolve(dispatch(getSingleDeployment(deploymentId))));
1✔
330

331
export const setDeploymentsState = selection => (dispatch, getState) => {
190✔
332
  // eslint-disable-next-line no-unused-vars
333
  const { page, perPage, ...selectionState } = selection;
23✔
334
  const currentState = getState().deployments.selectionState;
23✔
335
  let nextState = {
23✔
336
    ...currentState,
337
    ...selectionState,
338
    ...Object.keys(DEPLOYMENT_STATES).reduce((accu, item) => {
339
      accu[item] = {
92✔
340
        ...currentState[item],
341
        ...selectionState[item]
342
      };
343
      return accu;
92✔
344
    }, {}),
345
    general: {
346
      ...currentState.general,
347
      ...selectionState.general
348
    }
349
  };
350
  let tasks = [dispatch({ type: SET_DEPLOYMENTS_STATE, state: nextState })];
23✔
351
  if (nextState.selectedId && currentState.selectedId !== nextState.selectedId) {
23✔
352
    tasks.push(dispatch(getSingleDeployment(nextState.selectedId)));
2✔
353
  }
354
  return Promise.all(tasks);
23✔
355
};
356

357
const deltaAttributeMappings = [
190✔
358
  { here: 'compressionLevel', there: 'compression_level' },
359
  { here: 'disableChecksum', there: 'disable_checksum' },
360
  { here: 'disableDecompression', there: 'disable_external_decompression' },
361
  { here: 'sourceWindow', there: 'source_window_size' },
362
  { here: 'inputWindow', there: 'input_window_size' },
363
  { here: 'duplicatesWindow', there: 'compression_duplicates_window' },
364
  { here: 'instructionBuffer', there: 'instruction_buffer_size' }
365
];
366

367
const mapExternalDeltaConfig = (config = {}) =>
190!
368
  deltaAttributeMappings.reduce((accu, { here, there }) => {
10✔
369
    if (config[there] !== undefined) {
70✔
370
      accu[here] = config[there];
55✔
371
    }
372
    return accu;
70✔
373
  }, {});
374

375
export const getDeploymentsConfig = () => (dispatch, getState) =>
190✔
376
  GeneralApi.get(`${deploymentsApiUrl}/config`).then(({ data }) => {
7✔
377
    const oldConfig = getState().deployments.config;
5✔
378
    const { delta = {} } = data;
5!
379
    const { binary_delta = {}, binary_delta_limits = {} } = delta;
5!
380
    const { xdelta_args = {}, timeout: timeoutConfig = oldConfig.binaryDelta.timeout } = binary_delta;
5!
381
    const { xdelta_args_limits = {}, timeout: timeoutLimit = oldConfig.binaryDeltaLimits.timeout } = binary_delta_limits;
5!
382
    const config = {
5✔
383
      ...oldConfig,
384
      hasDelta: Boolean(delta.enabled),
385
      binaryDelta: {
386
        ...oldConfig.binaryDelta,
387
        timeout: timeoutConfig,
388
        ...mapExternalDeltaConfig(xdelta_args)
389
      },
390
      binaryDeltaLimits: {
391
        ...oldConfig.binaryDeltaLimits,
392
        timeout: timeoutLimit,
393
        ...mapExternalDeltaConfig(xdelta_args_limits)
394
      }
395
    };
396
    return Promise.resolve(dispatch({ type: DeploymentConstants.SET_DEPLOYMENTS_CONFIG, config }));
5✔
397
  });
398

399
// traverse a source object and remove undefined & empty object properties to only return an attribute if there really is content worth sending
400
const deepClean = source =>
190✔
401
  Object.entries(source).reduce((accu, [key, value]) => {
2✔
402
    if (value !== undefined) {
9!
403
      let cleanedValue = typeof value === 'object' ? deepClean(value) : value;
9✔
404
      if (cleanedValue === undefined || (typeof cleanedValue === 'object' && isEmpty(cleanedValue))) {
9!
405
        return accu;
×
406
      }
407
      accu = { ...(accu ?? {}), [key]: cleanedValue };
9✔
408
    }
409
    return accu;
9✔
410
  }, undefined);
411

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