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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

85.07
/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;
184✔
28

29
const sortingDefaults = { direction: SORTING_OPTIONS.desc, key: 'modified' };
184✔
30

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

49
const reduceReceivedReleases = (releases, stateReleasesById) =>
184✔
50
  releases.reduce((accu, release) => {
40✔
51
    const stateRelease = stateReleasesById[release.name] || {};
882✔
52
    accu[release.name] = flattenRelease(release, stateRelease);
882✔
53
    return accu;
882✔
54
  }, {});
55

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

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

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

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

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

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

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

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

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

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

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

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

266
export const setReleasesListState = selectionState => (dispatch, getState) => {
184✔
267
  const currentState = getState().releases.releasesList;
49✔
268

269
  let nextSort = selectionState.sort ?? {};
49✔
270
  if (nextSort.key === currentState.sort.key && nextSort.disabled) {
49!
NEW
271
    nextSort = { direction: SORTING_OPTIONS.desc, key: 'modified' };
×
272
  } else if (nextSort.disabled && nextSort.key !== currentState.sort.key) {
49✔
273
    nextSort = currentState.sort;
15✔
274
  }
275

276
  let nextState = {
49✔
277
    ...currentState,
278
    ...selectionState,
279
    sort: { ...currentState.sort, ...nextSort }
280
  };
281
  let tasks = [];
49✔
282
  // eslint-disable-next-line no-unused-vars
283
  const { isLoading: currentLoading, ...currentRequestState } = currentState;
49✔
284
  // eslint-disable-next-line no-unused-vars
285
  const { isLoading: selectionLoading, ...selectionRequestState } = nextState;
49✔
286
  if (!deepCompare(currentRequestState, selectionRequestState)) {
49✔
287
    nextState.isLoading = true;
13✔
288
    tasks.push(dispatch(getReleases(nextState)).finally(() => dispatch(setReleasesListState({ isLoading: false }))));
13✔
289
  }
290
  tasks.push(dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: nextState }));
49✔
291
  return Promise.all(tasks);
49✔
292
};
293

294
/* Releases */
295

296
const releaseListRetrieval = config => {
184✔
297
  const { searchTerm = '', page = defaultPage, perPage = defaultPerPage, sort = sortingDefaults, selectedTags = [], type = '' } = config;
50!
298
  const { key: attribute, direction } = sort;
50✔
299
  const filterQuery = formatReleases({ pageState: { searchTerm, selectedTags } });
50✔
300
  const updateType = type ? `update_type=${type}` : '';
50!
301
  const sorting = attribute ? `sort=${attribute}:${direction}`.toLowerCase() : '';
50!
302
  return GeneralApi.get(
50✔
303
    `${deploymentsApiUrlV2}/deployments/releases?${[`page=${page}`, `per_page=${perPage}`, filterQuery, updateType, sorting].filter(i => i).join('&')}`
250✔
304
  );
305
};
306

307
const deductSearchState = (receivedReleases, config, total, state) => {
184✔
308
  let releaseListState = { ...state.releasesList };
40✔
309
  const { searchTerm, searchOnly, sort = {}, selectedTags = [], type } = config;
40!
310
  const flattenedReleases = Object.values(receivedReleases).sort(customSort(sort.direction === SORTING_OPTIONS.desc, sort.key));
40✔
311
  const releaseIds = flattenedReleases.map(item => item.name);
882✔
312
  const isFiltering = !!(selectedTags.length || type || searchTerm);
40✔
313
  if (searchOnly) {
40✔
314
    releaseListState = { ...releaseListState, searchedIds: releaseIds };
8✔
315
  } else {
316
    releaseListState = {
32✔
317
      ...releaseListState,
318
      releaseIds,
319
      searchTotal: isFiltering ? total : state.releasesList.searchTotal,
32✔
320
      total: !isFiltering ? total : state.releasesList.total
32✔
321
    };
322
  }
323
  return releaseListState;
40✔
324
};
325

326
export const getReleases =
327
  (passedConfig = {}) =>
184✔
328
  (dispatch, getState) => {
40✔
329
    const config = { ...getState().releases.releasesList, ...passedConfig };
40✔
330
    return releaseListRetrieval(config)
40✔
331
      .then(({ data: receivedReleases = [], headers = {} }) => {
×
332
        const total = headers[headerNames.total] ? Number(headers[headerNames.total]) : 0;
40!
333
        const state = getState().releases;
40✔
334
        const flatReleases = reduceReceivedReleases(receivedReleases, state.byId);
40✔
335
        const combinedReleases = { ...state.byId, ...flatReleases };
40✔
336
        const releaseListState = deductSearchState(receivedReleases, config, total, state);
40✔
337
        return Promise.all([
40✔
338
          dispatch({ type: ReleaseConstants.RECEIVE_RELEASES, releases: combinedReleases }),
339
          dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: releaseListState })
340
        ]);
341
      })
UNCOV
342
      .catch(err => commonErrorHandler(err, `Please check your connection`, dispatch));
×
343
  };
344

345
export const getRelease = name => (dispatch, getState) =>
184✔
346
  releaseListRetrieval({ searchTerm: name, page: 1, perPage: 1 }).then(({ data: releases }) => {
10✔
347
    if (releases.length) {
10!
348
      const stateRelease = getState().releases.byId[releases[0].name] || {};
10!
349
      return Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: flattenRelease(releases[0], stateRelease) }));
10✔
350
    }
UNCOV
351
    return Promise.resolve(null);
×
352
  });
353

354
export const updateReleaseInfo = (name, info) => (dispatch, getState) =>
184✔
355
  GeneralApi.patch(`${deploymentsApiUrlV2}/deployments/releases/${name}`, info)
1✔
UNCOV
356
    .catch(err => commonErrorHandler(err, `Release details couldn't be updated.`, dispatch))
×
357
    .then(() => {
358
      return Promise.all([
1✔
359
        dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: { ...getState().releases.byId[name], ...info } }),
360
        dispatch(setSnackbar('Release details were updated successfully.', TIMEOUTS.fiveSeconds, ''))
361
      ]);
362
    });
363

364
export const setReleaseTags =
365
  (name, tags = []) =>
184!
366
  (dispatch, getState) =>
1✔
367
    GeneralApi.put(`${deploymentsApiUrlV2}/deployments/releases/${name}/tags`, tags)
1✔
UNCOV
368
      .catch(err => commonErrorHandler(err, `Release tags couldn't be set.`, dispatch))
×
369
      .then(() => {
370
        return Promise.all([
1✔
371
          dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: { ...getState().releases.byId[name], tags } }),
372
          dispatch(setSnackbar('Release tags were set successfully.', TIMEOUTS.fiveSeconds, ''))
373
        ]);
374
      });
375

376
export const getExistingReleaseTags = () => dispatch =>
184✔
377
  GeneralApi.get(`${deploymentsApiUrlV2}/releases/all/tags`)
6✔
UNCOV
378
    .catch(err => commonErrorHandler(err, `Existing release tags couldn't be retrieved.`, dispatch))
×
379
    .then(({ data: tags }) => Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE_TAGS, tags })));
6✔
380

381
export const getUpdateTypes = () => dispatch =>
184✔
382
  GeneralApi.get(`${deploymentsApiUrlV2}/releases/all/types`)
4✔
UNCOV
383
    .catch(err => commonErrorHandler(err, `Existing update types couldn't be retrieved.`, dispatch))
×
384
    .then(({ data: types }) => Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE_TYPES, types })));
4✔
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