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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

79.36
/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 { SET_ONBOARDING_ARTIFACT_INCLUDED } from '../constants/onboardingConstants';
22
import * as ReleaseConstants from '../constants/releaseConstants';
23
import { customSort, deepCompare, duplicateFilter, extractSoftwareItem } from '../helpers';
24
import { deploymentsApiUrl } from './deploymentActions';
25
import { convertDeviceListStateToFilters, getSearchEndpoint } from './deviceActions';
26

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

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

47
const reduceReceivedReleases = (releases, stateReleasesById) =>
187✔
48
  releases.reduce((accu, release) => {
22✔
49
    const stateRelease = stateReleasesById[release.Name] || {};
853✔
50
    accu[release.Name] = flattenRelease(release, stateRelease);
853✔
51
    return accu;
853✔
52
  }, {});
53

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

66
/* Artifacts */
67
export const getArtifactInstallCount = id => (dispatch, getState) => {
187✔
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) =>
187✔
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) => {
187✔
122
  // eslint-disable-next-line no-unused-vars
123
  const { [uploadId]: current, ...remainder } = getState().app.uploadsById;
3✔
124
  return Promise.resolve(dispatch({ type: UPLOAD_PROGRESS, uploads: remainder }));
3✔
125
};
126

127
export const createArtifact = (meta, file) => (dispatch, getState) => {
187✔
128
  let formData = Object.entries(meta).reduce((accu, [key, value]) => {
1✔
129
    if (Array.isArray(value)) {
4✔
130
      accu.append(key, value.join(','));
1✔
131
    } else if (value instanceof Object) {
3✔
132
      accu.append(key, JSON.stringify(value));
1✔
133
    } else {
134
      accu.append(key, value);
2✔
135
    }
136
    return accu;
4✔
137
  }, new FormData());
138
  formData.append('type', ReleaseConstants.ARTIFACT_GENERATION_TYPE.SINGLE_FILE);
1✔
139
  formData.append('file', file);
1✔
140
  const uploadId = uuid();
1✔
141
  const cancelSource = new AbortController();
1✔
142
  const uploads = { ...getState().app.uploadsById, [uploadId]: { name: file.name, size: file.size, uploadProgress: 0, cancelSource } };
1✔
143
  return Promise.all([
1✔
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) => {
187✔
165
  let formData = new FormData();
1✔
166
  formData.append('size', file.size);
1✔
167
  formData.append('description', meta.description);
1✔
168
  formData.append('artifact', file);
1✔
169
  const uploadId = uuid();
1✔
170
  const cancelSource = new AbortController();
1✔
171
  const uploads = { ...getState().app.uploadsById, [uploadId]: { name: file.name, size: file.size, uploadProgress: 0, cancelSource } };
1✔
172
  return Promise.all([
1✔
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())];
1✔
179
      if (meta.name) {
1!
180
        tasks.push(dispatch(selectRelease(meta.name)));
1✔
181
      }
182
      return Promise.all(tasks);
1✔
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)));
1✔
191
};
192

193
export const progress = (e, uploadId) => (dispatch, getState) => {
187✔
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) => {
187✔
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) =>
187✔
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) =>
187✔
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) =>
187✔
253
  Promise.all(getState().releases.byId[id].Artifacts.map(({ id }) => dispatch(removeArtifact(id)))).then(() => dispatch(selectRelease()));
1✔
254

255
export const selectArtifact = artifact => (dispatch, getState) => {
187✔
256
  if (!artifact) {
1!
257
    return dispatch({ type: ReleaseConstants.SELECTED_ARTIFACT, artifact });
×
258
  }
259
  const artifactName = artifact.hasOwnProperty('id') ? artifact.id : artifact;
1!
260
  const state = getState();
1✔
261
  const release = Object.values(state.releases.byId).find(item => item.Artifacts.find(releaseArtifact => releaseArtifact.id === artifactName));
1✔
262
  if (release) {
1!
263
    const selectedArtifact = release.Artifacts.find(releaseArtifact => releaseArtifact.id === artifactName);
1✔
264
    let tasks = [dispatch({ type: ReleaseConstants.SELECTED_ARTIFACT, artifact: selectedArtifact })];
1✔
265
    if (release.Name !== state.releases.selectedRelease) {
1!
266
      tasks.push(dispatch({ type: ReleaseConstants.SELECTED_RELEASE, release: release.Name }));
1✔
267
    }
268
    return Promise.all(tasks);
1✔
269
  }
270
};
271

272
export const selectRelease = release => dispatch => {
187✔
273
  const name = release ? release.Name || release : null;
9✔
274
  let tasks = [dispatch({ type: ReleaseConstants.SELECTED_RELEASE, release: name })];
9✔
275
  if (name) {
9✔
276
    tasks.push(dispatch(getRelease(name)));
7✔
277
  }
278
  return Promise.all(tasks);
9✔
279
};
280

281
export const setReleasesListState = selectionState => (dispatch, getState) => {
187✔
282
  const currentState = getState().releases.releasesList;
19✔
283
  let nextState = {
19✔
284
    ...currentState,
285
    ...selectionState,
286
    sort: { ...currentState.sort, ...selectionState.sort }
287
  };
288
  let tasks = [];
19✔
289
  // eslint-disable-next-line no-unused-vars
290
  const { isLoading: currentLoading, ...currentRequestState } = currentState;
19✔
291
  // eslint-disable-next-line no-unused-vars
292
  const { isLoading: selectionLoading, ...selectionRequestState } = nextState;
19✔
293
  if (!deepCompare(currentRequestState, selectionRequestState)) {
19✔
294
    nextState.isLoading = true;
11✔
295
    tasks.push(dispatch(getReleases(nextState)).finally(() => dispatch(setReleasesListState({ isLoading: false }))));
11✔
296
  }
297
  tasks.push(dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: nextState }));
19✔
298
  return Promise.all(tasks);
19✔
299
};
300

301
/* Releases */
302

303
const releaseListRetrieval = config => {
187✔
304
  const { searchTerm = '', page = defaultPage, perPage = defaultPerPage, sort = {}, selectedTags = [] } = config;
28!
305
  const { key: attribute, direction } = sort;
28✔
306

307
  const sorting = attribute ? `&sort=${attribute}:${direction}`.toLowerCase() : '';
28!
308
  const searchQuery = searchTerm ? `&name=${searchTerm}` : '';
28✔
309
  const tagQuery = selectedTags.map(tag => `&tag=${tag}`).join('');
28✔
310
  return GeneralApi.get(`${deploymentsApiUrl}/deployments/releases/list?page=${page}&per_page=${perPage}${searchQuery}${sorting}${tagQuery}`);
28✔
311
};
312

313
const deductSearchState = (receivedReleases, config, total, state) => {
187✔
314
  let releaseListState = { ...state.releasesList };
22✔
315
  const { searchTerm, searchOnly, sort = {} } = config;
22!
316
  const flattenedReleases = Object.values(receivedReleases).sort(customSort(sort.direction === SORTING_OPTIONS.desc, sort.key));
22✔
317
  const releaseIds = flattenedReleases.map(item => item.Name);
853✔
318
  if (searchOnly) {
22✔
319
    releaseListState = { ...releaseListState, searchedIds: releaseIds };
7✔
320
  } else {
321
    releaseListState = {
15✔
322
      ...releaseListState,
323
      releaseIds,
324
      searchTotal: searchTerm ? total : state.releasesList.searchTotal,
15✔
325
      total: !searchTerm ? total : state.releasesList.total
15✔
326
    };
327
  }
328
  return releaseListState;
22✔
329
};
330

331
export const getReleases =
332
  (passedConfig = {}) =>
187✔
333
  (dispatch, getState) => {
28✔
334
    const config = { ...getState().releases.releasesList, ...passedConfig };
28✔
335
    return releaseListRetrieval(config)
28✔
336
      .then(({ data: receivedReleases = [], headers = {} }) => {
×
337
        const total = headers[headerNames.total] ? Number(headers[headerNames.total]) : 0;
22!
338
        const state = getState().releases;
22✔
339
        const flatReleases = reduceReceivedReleases(receivedReleases, state.byId);
22✔
340
        const combinedReleases = { ...state.byId, ...flatReleases };
22✔
341
        let tasks = [dispatch({ type: ReleaseConstants.RECEIVE_RELEASES, releases: combinedReleases })];
22✔
342
        if (!getState().onboarding.complete) {
22!
343
          tasks.push(dispatch({ type: SET_ONBOARDING_ARTIFACT_INCLUDED, value: !!Object.keys(receivedReleases).length }));
22✔
344
        }
345
        const releaseListState = deductSearchState(receivedReleases, config, total, state);
22✔
346
        tasks.push(dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: releaseListState }));
22✔
347
        return Promise.all(tasks);
22✔
348
      })
349
      .catch(err => commonErrorHandler(err, `Please check your connection`, dispatch));
×
350
  };
351

352
export const getRelease = name => (dispatch, getState) =>
187✔
353
  GeneralApi.get(`${deploymentsApiUrl}/deployments/releases?name=${name}`).then(({ data: releases }) => {
9✔
354
    if (releases.length) {
7!
355
      const stateRelease = getState().releases.byId[releases[0].Name] || {};
7✔
356
      return Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: flattenRelease(releases[0], stateRelease) }));
7✔
357
    }
358
    return Promise.resolve(null);
×
359
  });
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