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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 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`;
183✔
16
export const deploymentsApiUrlV2 = `${apiUrl.v2}/deployments`;
183✔
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;
183✔
31

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

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

37
const transformDeployments = (deployments, deploymentsById) =>
183✔
38
  deployments.sort(startTimeSort).reduce(
288✔
39
    (accu, item) => {
40
      const filter = item.filter ?? {};
567✔
41
      let deployment = {
567✔
42
        ...deploymentPrototype,
43
        ...deploymentsById[item.id],
44
        ...item,
45
        filter: item.filter ? { ...filter, name: filter.name ?? filter.id, filters: mapTermsToFilters(filter.terms) } : undefined,
567!
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) };
567✔
50
      accu.deployments[item.id] = deployment;
567✔
51
      accu.deploymentIds.push(item.id);
567✔
52
      return accu;
567✔
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) =>
183✔
60
  (dispatch, getState) => {
288✔
61
    const created_after = startDate ? `&created_after=${startDate}` : '';
288✔
62
    const created_before = endDate ? `&created_before=${endDate}` : '';
288!
63
    const search = group ? `&search=${group}` : '';
288✔
64
    const typeFilter = type ? `&type=${type}` : '';
288✔
65
    return GeneralApi.get(
288✔
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);
279✔
69
      const total = Number(res.headers[headerNames.total]);
279✔
70
      let tasks = [
279✔
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
1,385✔
77
        })
78
      ];
79
      tasks = deploymentIds.reduce((accu, deploymentId) => {
279✔
80
        if (deployments[deploymentId].type === DEPLOYMENT_TYPES.configuration) {
558!
81
          accu.push(dispatch(getSingleDeployment(deploymentId)));
×
82
        }
83
        return accu;
558✔
84
      }, tasks);
85
      if (shouldSelect) {
279✔
86
        tasks.push(dispatch({ type: DeploymentConstants[`SELECT_${status.toUpperCase()}_DEPLOYMENTS`], deploymentIds, status, total }));
266✔
87
      }
88
      tasks.push({ deploymentIds, total });
279✔
89
      return Promise.all(tasks);
279✔
90
    });
91
  };
92

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

102
const trackDeploymentCreation = (totalDeploymentCount, hasDeployments, trial_expiration) => {
183✔
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!
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;
183✔
116
export const createDeployment =
117
  (newDeployment, hasNewRetryDefault = false) =>
183✔
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✔
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 = {}) =>
183✔
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] || {};
2!
171
      const devices = response.data.reduce((accu, item) => {
2✔
172
        accu[item.id] = item;
2✔
173
        const log = (deploymentDevices[item.id] || {}).log;
2!
174
        if (log) {
2!
175
          accu[item.id].log = log;
×
176
        }
177
        return accu;
2✔
178
      }, {});
179
      const selectedDeviceIds = Object.keys(devices);
2✔
180
      let tasks = [
2✔
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());
2✔
190
      // only update those that have changed & lack data
191
      const lackingData = selectedDeviceIds.reduce((accu, deviceId) => {
2✔
192
        const device = devicesById[deviceId];
2✔
193
        if (!device || !device.identity_data || !device.attributes || Object.keys(device.attributes).length === 0) {
2!
194
          accu.push(deviceId);
×
195
        }
196
        return accu;
2✔
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);
2✔
200
      return Promise.all(tasks);
2✔
201
    });
202
  };
203

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

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

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

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

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

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

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

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

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

359
const mapExternalDeltaConfig = (config = {}) =>
183!
360
  deltaAttributeMappings.reduce((accu, { here, there }) => {
10✔
361
    if (config[there] !== undefined) {
70✔
362
      accu[here] = config[there];
55✔
363
    }
364
    return accu;
70✔
365
  }, {});
366

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

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

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