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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 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);
570!
36

37
const transformDeployments = (deployments, deploymentsById) =>
183✔
38
  deployments.sort(startTimeSort).reduce(
291✔
39
    (accu, item) => {
40
      const filter = item.filter ?? {};
570✔
41
      let deployment = {
570✔
42
        ...deploymentPrototype,
43
        ...deploymentsById[item.id],
44
        ...item,
45
        filter: item.filter ? { ...filter, name: filter.name ?? filter.id, filters: mapTermsToFilters(filter.terms) } : undefined,
570!
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) };
570✔
50
      accu.deployments[item.id] = deployment;
570✔
51
      accu.deploymentIds.push(item.id);
570✔
52
      return accu;
570✔
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) => {
6✔
168
    const { page = defaultPage, perPage = defaultPerPage } = options;
6✔
169
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}/devices/list?deployment_id=${id}&page=${page}&per_page=${perPage}`).then(response => {
6✔
170
      const { devices: deploymentDevices = {} } = getState().deployments.byId[id] || {};
5!
171
      const devices = response.data.reduce((accu, item) => {
5✔
172
        accu[item.id] = item;
5✔
173
        const log = (deploymentDevices[item.id] || {}).log;
5!
174
        if (log) {
5!
175
          accu[item.id].log = log;
×
176
        }
177
        return accu;
5✔
178
      }, {});
179
      const selectedDeviceIds = Object.keys(devices);
5✔
180
      let tasks = [
5✔
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());
5✔
190
      // only update those that have changed & lack data
191
      const lackingData = selectedDeviceIds.reduce((accu, deviceId) => {
5✔
192
        const device = devicesById[deviceId];
5✔
193
        if (!device || !device.identity_data || !device.attributes || Object.keys(device.attributes).length === 0) {
5!
194
          accu.push(deviceId);
×
195
        }
196
        return accu;
5✔
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);
5✔
200
      return Promise.all(tasks);
5✔
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 }) => {
13✔
254
    const { deployments } = transformDeployments([data], getState().deployments.byId);
12✔
255
    return Promise.resolve(dispatch({ type: RECEIVE_DEPLOYMENT, deployment: deployments[id] }));
12✔
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