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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

83.74
/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 } 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 { saveGlobalSettings } from './userActions';
12

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

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

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

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

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

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

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

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

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

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

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

161
export const getDeploymentDevices =
162
  (id, options = {}) =>
190✔
163
  (dispatch, getState) => {
2✔
164
    const { page = defaultPage, perPage = defaultPerPage } = options;
2✔
165
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}/devices/list?deployment_id=${id}&page=${page}&per_page=${perPage}`).then(response => {
2✔
166
      const { devices: deploymentDevices = {} } = getState().deployments.byId[id] || {};
1!
167
      const devices = response.data.reduce((accu, item) => {
1✔
168
        accu[item.id] = item;
1✔
169
        const log = (deploymentDevices[item.id] || {}).log;
1!
170
        if (log) {
1!
171
          accu[item.id].log = log;
×
172
        }
173
        return accu;
1✔
174
      }, {});
175
      return Promise.resolve(
1✔
176
        dispatch({
177
          type: RECEIVE_DEPLOYMENT_DEVICES,
178
          deploymentId: id,
179
          devices,
180
          selectedDeviceIds: Object.keys(devices),
181
          totalDeviceCount: Number(response.headers[headerNames.total])
182
        })
183
      );
184
    });
185
  };
186

187
const parseDeviceDeployment = ({
190✔
188
  deployment: { id, artifact_name: release, groups = [], name, device: deploymentDevice, status: deploymentStatus },
2✔
189
  device: { created, deleted, id: deviceId, finished, status, log }
190
}) => ({
2✔
191
  id,
192
  release,
193
  target: groups.length === 1 && groups[0] ? groups[0] : deploymentDevice ? deploymentDevice : name,
6!
194
  created,
195
  deleted,
196
  deviceId,
197
  finished,
198
  status,
199
  log,
200
  route: Object.values(DEPLOYMENT_ROUTES).reduce((accu, { key, states }) => {
201
    if (!accu) {
6✔
202
      return states.includes(deploymentStatus) ? key : accu;
2!
203
    }
204
    return accu;
4✔
205
  }, ''),
206
  deploymentStatus
207
});
208

209
export const getDeviceDeployments =
210
  (deviceId, options = {}) =>
190✔
211
  (dispatch, getState) => {
2✔
212
    const { filterSelection = [], page = defaultPage, perPage = defaultPerPage } = options;
2✔
213
    const filters = filterSelection.map(item => `&status=${item}`).join('');
2✔
214
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/devices/${deviceId}?page=${page}&per_page=${perPage}${filters}`)
2✔
215
      .then(({ data, headers }) =>
216
        Promise.resolve(
2✔
217
          dispatch({
218
            type: RECEIVE_DEVICE,
219
            device: {
220
              ...getState().devices.byId[deviceId],
221
              deviceDeployments: data.map(parseDeviceDeployment),
222
              deploymentsCount: Number(headers[headerNames.total])
223
            }
224
          })
225
        )
226
      )
227
      .catch(err => commonErrorHandler(err, 'There was an error retrieving the device deployment history:', dispatch));
×
228
  };
229

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

235
export const getSingleDeployment = id => (dispatch, getState) =>
190✔
236
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}`).then(({ data }) => {
10✔
237
    const storedDeployment = getState().deployments.byId[id] || {};
9✔
238
    return Promise.resolve(
9✔
239
      dispatch({
240
        type: RECEIVE_DEPLOYMENT,
241
        deployment: {
242
          ...deploymentPrototype,
243
          ...storedDeployment,
244
          ...data,
245
          name: decodeURIComponent(data.name)
246
        }
247
      })
248
    );
249
  });
250

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

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

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

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

343
const deltaAttributeMappings = [
190✔
344
  { here: 'compressionLevel', there: 'compression_level' },
345
  { here: 'disableChecksum', there: 'disable_checksum' },
346
  { here: 'disableDecompression', there: 'disable_external_decompression' },
347
  { here: 'sourceWindow', there: 'source_window_size' },
348
  { here: 'inputWindow', there: 'input_window_size' },
349
  { here: 'duplicatesWindow', there: 'compression_duplicates_window' },
350
  { here: 'instructionBuffer', there: 'instruction_buffer_size' }
351
];
352

353
const mapExternalDeltaConfig = (config = {}) =>
190!
354
  deltaAttributeMappings.reduce((accu, { here, there }) => {
10✔
355
    if (config[there] !== undefined) {
70✔
356
      accu[here] = config[there];
55✔
357
    }
358
    return accu;
70✔
359
  }, {});
360

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

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

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