• 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

85.13
/src/js/actions/releaseActions.js
1
// Copyright 2019 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import { isCancel } from 'axios';
15
import { v4 as uuid } from 'uuid';
16

17
import { commonErrorFallback, commonErrorHandler, setSnackbar } from '../actions/appActions';
18
import GeneralApi, { headerNames } from '../api/general-api';
19
import { SORTING_OPTIONS, TIMEOUTS, UPLOAD_PROGRESS } from '../constants/appConstants';
20
import { DEVICE_LIST_DEFAULTS, emptyFilter } from '../constants/deviceConstants';
21
import * as ReleaseConstants from '../constants/releaseConstants';
22
import { customSort, deepCompare, duplicateFilter, extractSoftwareItem } from '../helpers';
23
import { formatReleases } from '../utils/locationutils';
24
import { deploymentsApiUrl, deploymentsApiUrlV2 } from './deploymentActions';
25
import { convertDeviceListStateToFilters, getSearchEndpoint } from './deviceActions';
26

27
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
183✔
28

29
const flattenRelease = (release, stateRelease) => {
183✔
30
  const updatedArtifacts = release.artifacts?.sort(customSort(1, 'modified')) || [];
542✔
31
  const { artifacts, deviceTypes, modified } = updatedArtifacts.reduce(
542✔
32
    (accu, item) => {
33
      accu.deviceTypes.push(...item.device_types_compatible);
541✔
34
      const stateArtifact = stateRelease.artifacts?.find(releaseArtifact => releaseArtifact.id === item.id) || {};
541✔
35
      accu.modified = accu.modified ? accu.modified : item.modified;
541!
36
      accu.artifacts.push({
541✔
37
        ...stateArtifact,
38
        ...item
39
      });
40
      return accu;
541✔
41
    },
42
    { artifacts: [], deviceTypes: [], modified: undefined }
43
  );
44
  return { ...stateRelease, ...release, artifacts, device_types_compatible: deviceTypes.filter(duplicateFilter), modified };
542✔
45
};
46

47
const reduceReceivedReleases = (releases, stateReleasesById) =>
183✔
48
  releases.reduce((accu, release) => {
20✔
49
    const stateRelease = stateReleasesById[release.name] || {};
535✔
50
    accu[release.name] = flattenRelease(release, stateRelease);
535✔
51
    return accu;
535✔
52
  }, {});
53

54
const findArtifactIndexInRelease = (releases, id) =>
183✔
55
  Object.values(releases).reduce(
10✔
56
    (accu, item) => {
57
      let index = item.artifacts.findIndex(releaseArtifact => releaseArtifact.id === id);
70✔
58
      if (index > -1) {
70!
59
        accu = { release: item, index };
70✔
60
      }
61
      return accu;
70✔
62
    },
63
    { release: null, index: -1 }
64
  );
65

66
/* Artifacts */
67
export const getArtifactInstallCount = id => (dispatch, getState) => {
183✔
68
  let { release, index } = findArtifactIndexInRelease(getState().releases.byId, id);
3✔
69
  if (!release || index === -1) {
3!
70
    return;
×
71
  }
72
  const releaseArtifacts = [...release.artifacts];
3✔
73
  const artifact = releaseArtifacts[index];
3✔
74
  const { key, name, version } = extractSoftwareItem(artifact.artifact_provides) ?? {};
3!
75
  const attribute = `${key}${name ? `.${name}` : ''}.version`;
3!
76
  const { filterTerms } = convertDeviceListStateToFilters({
3✔
77
    filters: [{ ...emptyFilter, key: attribute, value: version, scope: 'inventory' }]
78
  });
79
  return GeneralApi.post(getSearchEndpoint(getState().app.features.hasReporting), {
3✔
80
    page: 1,
81
    per_page: 1,
82
    filters: filterTerms,
83
    attributes: [{ scope: 'identity', attribute: 'status' }]
84
  })
85
    .catch(err => commonErrorHandler(err, `Retrieving artifact installation count failed:`, dispatch, commonErrorFallback))
×
86
    .then(({ headers }) => {
87
      let { release, index } = findArtifactIndexInRelease(getState().releases.byId, id);
2✔
88
      if (!release || index === -1) {
2!
89
        return;
×
90
      }
91
      const installCount = Number(headers[headerNames.total]);
2✔
92
      const releaseArtifacts = [...release.artifacts];
2✔
93
      releaseArtifacts[index] = { ...releaseArtifacts[index], installCount };
2✔
94
      release = {
2✔
95
        ...release,
96
        artifacts: releaseArtifacts
97
      };
98
      return dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release });
2✔
99
    });
100
};
101

102
export const getArtifactUrl = id => (dispatch, getState) =>
183✔
103
  GeneralApi.get(`${deploymentsApiUrl}/artifacts/${id}/download`).then(response => {
3✔
104
    const state = getState();
2✔
105
    let { release, index } = findArtifactIndexInRelease(state.releases.byId, id);
2✔
106
    if (!release || index === -1) {
2!
107
      return dispatch(getReleases());
×
108
    }
109
    const releaseArtifacts = [...release.artifacts];
2✔
110
    releaseArtifacts[index] = {
2✔
111
      ...releaseArtifacts[index],
112
      url: response.data.uri
113
    };
114
    release = {
2✔
115
      ...release,
116
      artifacts: releaseArtifacts
117
    };
118
    return dispatch({ type: ReleaseConstants.ARTIFACTS_SET_ARTIFACT_URL, release });
2✔
119
  });
120

121
export const cleanUpUpload = uploadId => (dispatch, getState) => {
183✔
122
  // eslint-disable-next-line no-unused-vars
123
  const { [uploadId]: current, ...remainder } = getState().app.uploadsById;
4✔
124
  return Promise.resolve(dispatch({ type: UPLOAD_PROGRESS, uploads: remainder }));
4✔
125
};
126

127
export const createArtifact = (meta, file) => (dispatch, getState) => {
183✔
128
  let formData = Object.entries(meta).reduce((accu, [key, value]) => {
2✔
129
    if (Array.isArray(value)) {
8✔
130
      accu.append(key, value.join(','));
2✔
131
    } else if (value instanceof Object) {
6✔
132
      accu.append(key, JSON.stringify(value));
2✔
133
    } else {
134
      accu.append(key, value);
4✔
135
    }
136
    return accu;
8✔
137
  }, new FormData());
138
  formData.append('type', ReleaseConstants.ARTIFACT_GENERATION_TYPE.SINGLE_FILE);
2✔
139
  formData.append('file', file);
2✔
140
  const uploadId = uuid();
2✔
141
  const cancelSource = new AbortController();
2✔
142
  const uploads = { ...getState().app.uploadsById, [uploadId]: { name: file.name, size: file.size, uploadProgress: 0, cancelSource } };
2✔
143
  return Promise.all([
2✔
144
    dispatch(setSnackbar('Generating artifact')),
145
    dispatch({ type: UPLOAD_PROGRESS, uploads }),
146
    GeneralApi.upload(`${deploymentsApiUrl}/artifacts/generate`, formData, e => dispatch(progress(e, uploadId)), cancelSource.signal)
×
147
  ])
148
    .then(() => {
149
      setTimeout(() => {
1✔
150
        dispatch(getReleases());
1✔
151
        dispatch(selectRelease(meta.name));
1✔
152
      }, TIMEOUTS.oneSecond);
153
      return Promise.resolve(dispatch(setSnackbar('Upload successful', TIMEOUTS.fiveSeconds)));
1✔
154
    })
155
    .catch(err => {
156
      if (isCancel(err)) {
×
157
        return dispatch(setSnackbar('The artifact generation has been cancelled', TIMEOUTS.fiveSeconds));
×
158
      }
159
      return commonErrorHandler(err, `Artifact couldn't be generated.`, dispatch);
×
160
    })
161
    .finally(() => dispatch(cleanUpUpload(uploadId)));
1✔
162
};
163

164
export const uploadArtifact = (meta, file) => (dispatch, getState) => {
183✔
165
  let formData = new FormData();
2✔
166
  formData.append('size', file.size);
2✔
167
  formData.append('description', meta.description);
2✔
168
  formData.append('artifact', file);
2✔
169
  const uploadId = uuid();
2✔
170
  const cancelSource = new AbortController();
2✔
171
  const uploads = { ...getState().app.uploadsById, [uploadId]: { name: file.name, size: file.size, uploadProgress: 0, cancelSource } };
2✔
172
  return Promise.all([
2✔
173
    dispatch(setSnackbar('Uploading artifact')),
174
    dispatch({ type: UPLOAD_PROGRESS, uploads }),
175
    GeneralApi.upload(`${deploymentsApiUrl}/artifacts`, formData, e => dispatch(progress(e, uploadId)), cancelSource.signal)
×
176
  ])
177
    .then(() => {
178
      const tasks = [dispatch(setSnackbar('Upload successful', TIMEOUTS.fiveSeconds)), dispatch(getReleases())];
2✔
179
      if (meta.name) {
2✔
180
        tasks.push(dispatch(selectRelease(meta.name)));
1✔
181
      }
182
      return Promise.all(tasks);
2✔
183
    })
184
    .catch(err => {
185
      if (isCancel(err)) {
×
186
        return dispatch(setSnackbar('The upload has been cancelled', TIMEOUTS.fiveSeconds));
×
187
      }
188
      return commonErrorHandler(err, `Artifact couldn't be uploaded.`, dispatch);
×
189
    })
190
    .finally(() => dispatch(cleanUpUpload(uploadId)));
2✔
191
};
192

193
export const progress = (e, uploadId) => (dispatch, getState) => {
183✔
194
  let uploadProgress = (e.loaded / e.total) * 100;
×
195
  uploadProgress = uploadProgress < 50 ? Math.ceil(uploadProgress) : Math.round(uploadProgress);
×
196
  const uploads = { ...getState().app.uploadsById, [uploadId]: { ...getState().app.uploadsById[uploadId], uploadProgress } };
×
197
  return dispatch({ type: UPLOAD_PROGRESS, uploads });
×
198
};
199

200
export const cancelFileUpload = id => (dispatch, getState) => {
183✔
201
  const { [id]: current, ...remainder } = getState().app.uploadsById;
×
202
  current.cancelSource.abort();
×
203
  return Promise.resolve(dispatch({ type: UPLOAD_PROGRESS, uploads: remainder }));
×
204
};
205

206
export const editArtifact = (id, body) => (dispatch, getState) =>
183✔
207
  GeneralApi.put(`${deploymentsApiUrl}/artifacts/${id}`, body)
1✔
208
    .catch(err => commonErrorHandler(err, `Artifact details couldn't be updated.`, dispatch))
×
209
    .then(() => {
210
      const state = getState();
1✔
211
      let { release, index } = findArtifactIndexInRelease(state.releases.byId, id);
1✔
212
      if (!release || index === -1) {
1!
213
        return dispatch(getReleases());
×
214
      }
215
      release.artifacts[index].description = body.description;
1✔
216
      return Promise.all([
1✔
217
        dispatch({ type: ReleaseConstants.UPDATED_ARTIFACT, release }),
218
        dispatch(setSnackbar('Artifact details were updated successfully.', TIMEOUTS.fiveSeconds, '')),
219
        dispatch(getRelease(release.name)),
220
        dispatch(selectRelease(release.name))
221
      ]);
222
    });
223

224
export const removeArtifact = id => (dispatch, getState) =>
183✔
225
  GeneralApi.delete(`${deploymentsApiUrl}/artifacts/${id}`)
2✔
226
    .then(() => {
227
      const state = getState();
2✔
228
      let { release, index } = findArtifactIndexInRelease(state.releases.byId, id);
2✔
229
      const releaseArtifacts = [...release.artifacts];
2✔
230
      releaseArtifacts.splice(index, 1);
2✔
231
      if (!releaseArtifacts.length) {
2!
232
        const { releasesList } = state.releases;
2✔
233
        const releaseIds = releasesList.releaseIds.filter(id => release.name !== id);
2✔
234
        return Promise.all([
2✔
235
          dispatch({ type: ReleaseConstants.RELEASE_REMOVED, release: release.name }),
236
          dispatch(
237
            setReleasesListState({
238
              releaseIds,
239
              searchTotal: releasesList.searchTerm ? releasesList.searchTotal - 1 : releasesList.searchTotal,
2!
240
              total: releasesList.total - 1
241
            })
242
          )
243
        ]);
244
      }
245
      return Promise.all([
×
246
        dispatch(setSnackbar('Artifact was removed', TIMEOUTS.fiveSeconds, '')),
247
        dispatch({ type: ReleaseConstants.ARTIFACTS_REMOVED_ARTIFACT, release })
248
      ]);
249
    })
250
    .catch(err => commonErrorHandler(err, `Error removing artifact:`, dispatch));
×
251

252
export const removeRelease = id => (dispatch, getState) =>
183✔
253
  Promise.all(getState().releases.byId[id].artifacts.map(({ id }) => dispatch(removeArtifact(id)))).then(() => dispatch(selectRelease()));
1✔
254

255
export const selectRelease = release => dispatch => {
183✔
256
  const name = release ? release.name || release : null;
9✔
257
  let tasks = [dispatch({ type: ReleaseConstants.SELECTED_RELEASE, release: name })];
9✔
258
  if (name) {
9✔
259
    tasks.push(dispatch(getRelease(name)));
7✔
260
  }
261
  return Promise.all(tasks);
9✔
262
};
263

264
export const setReleasesListState = selectionState => (dispatch, getState) => {
183✔
265
  const currentState = getState().releases.releasesList;
24✔
266
  let nextState = {
24✔
267
    ...currentState,
268
    ...selectionState,
269
    sort: { ...currentState.sort, ...selectionState.sort }
270
  };
271
  let tasks = [];
24✔
272
  // eslint-disable-next-line no-unused-vars
273
  const { isLoading: currentLoading, ...currentRequestState } = currentState;
24✔
274
  // eslint-disable-next-line no-unused-vars
275
  const { isLoading: selectionLoading, ...selectionRequestState } = nextState;
24✔
276
  if (!deepCompare(currentRequestState, selectionRequestState)) {
24✔
277
    nextState.isLoading = true;
11✔
278
    tasks.push(dispatch(getReleases(nextState)).finally(() => dispatch(setReleasesListState({ isLoading: false }))));
11✔
279
  }
280
  tasks.push(dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: nextState }));
24✔
281
  return Promise.all(tasks);
24✔
282
};
283

284
/* Releases */
285

286
const releaseListRetrieval = config => {
183✔
287
  const { searchTerm = '', page = defaultPage, perPage = defaultPerPage, sort = {}, selectedTags = [], type = '' } = config;
29!
288
  const { key: attribute, direction } = sort;
29✔
289
  const filterQuery = formatReleases({ pageState: { searchTerm, selectedTags } });
29✔
290
  const updateType = type ? `update_type=${type}` : '';
29!
291
  const sorting = attribute ? `sort=${attribute}:${direction}`.toLowerCase() : '';
29!
292
  return GeneralApi.get(
29✔
293
    `${deploymentsApiUrlV2}/deployments/releases?${[`page=${page}`, `per_page=${perPage}`, filterQuery, updateType, sorting].filter(i => i).join('&')}`
145✔
294
  );
295
};
296

297
const deductSearchState = (receivedReleases, config, total, state) => {
183✔
298
  let releaseListState = { ...state.releasesList };
20✔
299
  const { searchTerm, searchOnly, sort = {}, selectedTags = [], type } = config;
20!
300
  const flattenedReleases = Object.values(receivedReleases).sort(customSort(sort.direction === SORTING_OPTIONS.desc, sort.key));
20✔
301
  const releaseIds = flattenedReleases.map(item => item.name);
535✔
302
  const isFiltering = !!(selectedTags.length || type || searchTerm);
20✔
303
  if (searchOnly) {
20✔
304
    releaseListState = { ...releaseListState, searchedIds: releaseIds };
6✔
305
  } else {
306
    releaseListState = {
14✔
307
      ...releaseListState,
308
      releaseIds,
309
      searchTotal: isFiltering ? total : state.releasesList.searchTotal,
14✔
310
      total: !isFiltering ? total : state.releasesList.total
14✔
311
    };
312
  }
313
  return releaseListState;
20✔
314
};
315

316
export const getReleases =
317
  (passedConfig = {}) =>
183✔
318
  (dispatch, getState) => {
29✔
319
    const config = { ...getState().releases.releasesList, ...passedConfig };
29✔
320
    return releaseListRetrieval(config)
29✔
321
      .then(({ data: receivedReleases = [], headers = {} }) => {
×
322
        const total = headers[headerNames.total] ? Number(headers[headerNames.total]) : 0;
20!
323
        const state = getState().releases;
20✔
324
        const flatReleases = reduceReceivedReleases(receivedReleases, state.byId);
20✔
325
        const combinedReleases = { ...state.byId, ...flatReleases };
20✔
326
        const releaseListState = deductSearchState(receivedReleases, config, total, state);
20✔
327
        return Promise.all([
20✔
328
          dispatch({ type: ReleaseConstants.RECEIVE_RELEASES, releases: combinedReleases }),
329
          dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: releaseListState })
330
        ]);
331
      })
332
      .catch(err => commonErrorHandler(err, `Please check your connection`, dispatch));
×
333
  };
334

335
export const getRelease = name => (dispatch, getState) =>
183✔
336
  GeneralApi.get(`${deploymentsApiUrl}/deployments/releases?name=${name}`).then(({ data: releases }) => {
9✔
337
    if (releases.length) {
7!
338
      const stateRelease = getState().releases.byId[releases[0].name] || {};
7✔
339
      return Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: flattenRelease(releases[0], stateRelease) }));
7✔
340
    }
341
    return Promise.resolve(null);
×
342
  });
343

344
export const updateReleaseInfo = (name, info) => (dispatch, getState) =>
183✔
345
  GeneralApi.patch(`${deploymentsApiUrlV2}/deployments/releases/${name}`, info)
1✔
346
    .catch(err => commonErrorHandler(err, `Release details couldn't be updated.`, dispatch))
×
347
    .then(() => {
348
      return Promise.all([
1✔
349
        dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: { ...getState().releases.byId[name], ...info } }),
350
        dispatch(setSnackbar('Release details were updated successfully.', TIMEOUTS.fiveSeconds, ''))
351
      ]);
352
    });
353

354
export const setReleaseTags =
355
  (name, tags = []) =>
183!
356
  (dispatch, getState) =>
1✔
357
    GeneralApi.put(`${deploymentsApiUrlV2}/deployments/releases/${name}/tags`, tags)
1✔
358
      .catch(err => commonErrorHandler(err, `Release tags couldn't be set.`, dispatch))
×
359
      .then(() => {
360
        return Promise.all([
1✔
361
          dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: { ...getState().releases.byId[name], tags } }),
362
          dispatch(setSnackbar('Release tags were set successfully.', TIMEOUTS.fiveSeconds, ''))
363
        ]);
364
      });
365

366
export const getExistingReleaseTags = () => dispatch =>
183✔
367
  GeneralApi.get(`${deploymentsApiUrlV2}/releases/all/tags`)
5✔
368
    .catch(err => commonErrorHandler(err, `Existing release tags couldn't be retrieved.`, dispatch))
×
369
    .then(({ data: tags }) => Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE_TAGS, tags })));
3✔
370

371
export const getUpdateTypes = () => dispatch =>
183✔
372
  GeneralApi.get(`${deploymentsApiUrlV2}/releases/all/types`)
5✔
373
    .catch(err => commonErrorHandler(err, `Existing update types couldn't be retrieved.`, dispatch))
×
374
    .then(({ data: types }) => Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE_TYPES, types })));
3✔
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