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

mendersoftware / gui / 1315496247

03 Jun 2024 07:49AM UTC coverage: 83.437% (-16.5%) from 99.964%
1315496247

Pull #4434

gitlab-ci

mzedel
chore: aligned snapshots with updated mui version

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4434: chore: Bump the mui group with 3 updates

4476 of 6391 branches covered (70.04%)

8488 of 10173 relevant lines covered (83.44%)

140.36 hits per line

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

85.2
/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')) || [];
884!
33
  const { artifacts, deviceTypes, modified } = updatedArtifacts.reduce(
884✔
34
    (accu, item) => {
35
      accu.deviceTypes.push(...item.device_types_compatible);
884✔
36
      const stateArtifact = stateRelease.artifacts?.find(releaseArtifact => releaseArtifact.id === item.id) || {};
884✔
37
      accu.modified = accu.modified ? accu.modified : item.modified;
884!
38
      accu.artifacts.push({
884✔
39
        ...stateArtifact,
40
        ...item
41
      });
42
      return accu;
884✔
43
    },
44
    { artifacts: [], deviceTypes: [], modified: undefined }
45
  );
46
  return { ...stateRelease, ...release, artifacts, device_types_compatible: deviceTypes.filter(duplicateFilter), modified };
884✔
47
};
48

49
const reduceReceivedReleases = (releases, stateReleasesById) =>
184✔
50
  releases.reduce((accu, release) => {
30✔
51
    const stateRelease = stateReleasesById[release.name] || {};
876✔
52
    accu[release.name] = flattenRelease(release, stateRelease);
876✔
53
    return accu;
876✔
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!
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
  })
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!
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!
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;
5✔
126
  return Promise.resolve(dispatch({ type: UPLOAD_PROGRESS, uploads: remainder }));
5✔
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 }),
148
    GeneralApi.upload(`${deploymentsApiUrl}/artifacts/generate`, formData, e => dispatch(progress(e, uploadId)), cancelSource.signal)
×
149
  ])
150
    .then(() => {
151
      setTimeout(() => {
2✔
152
        dispatch(getReleases());
1✔
153
        dispatch(selectRelease(meta.name));
1✔
154
      }, TIMEOUTS.oneSecond);
155
      return Promise.resolve(dispatch(setSnackbar('Upload successful', TIMEOUTS.fiveSeconds)));
2✔
156
    })
157
    .catch(err => {
158
      if (isCancel(err)) {
×
159
        return dispatch(setSnackbar('The artifact generation has been cancelled', TIMEOUTS.fiveSeconds));
×
160
      }
161
      return commonErrorHandler(err, `Artifact couldn't be generated.`, dispatch);
×
162
    })
163
    .finally(() => dispatch(cleanUpUpload(uploadId)));
2✔
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 }),
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 => {
187
      if (isCancel(err)) {
×
188
        return dispatch(setSnackbar('The upload has been cancelled', TIMEOUTS.fiveSeconds));
×
189
      }
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✔
196
  let uploadProgress = (e.loaded / e.total) * 100;
×
197
  uploadProgress = uploadProgress < 50 ? Math.ceil(uploadProgress) : Math.round(uploadProgress);
×
198
  const uploads = { ...getState().app.uploadsById, [uploadId]: { ...getState().app.uploadsById[uploadId], uploadProgress } };
×
199
  return dispatch({ type: UPLOAD_PROGRESS, uploads });
×
200
};
201

202
export const cancelFileUpload = id => (dispatch, getState) => {
184✔
203
  const { [id]: current, ...remainder } = getState().app.uploadsById;
×
204
  current.cancelSource.abort();
×
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✔
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!
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
      }
247
      return Promise.all([
×
248
        dispatch(setSnackbar('Artifact was removed', TIMEOUTS.fiveSeconds, '')),
249
        dispatch({ type: ReleaseConstants.ARTIFACTS_REMOVED_ARTIFACT, release })
250
      ]);
251
    })
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;
25✔
268
  let nextState = {
25✔
269
    ...currentState,
270
    ...selectionState,
271
    sort: { ...currentState.sort, ...selectionState.sort }
272
  };
273
  let tasks = [];
25✔
274
  // eslint-disable-next-line no-unused-vars
275
  const { isLoading: currentLoading, ...currentRequestState } = currentState;
25✔
276
  // eslint-disable-next-line no-unused-vars
277
  const { isLoading: selectionLoading, ...selectionRequestState } = nextState;
25✔
278
  if (!deepCompare(currentRequestState, selectionRequestState)) {
25✔
279
    nextState.isLoading = true;
9✔
280
    tasks.push(dispatch(getReleases(nextState)).finally(() => dispatch(setReleasesListState({ isLoading: false }))));
9✔
281
  }
282
  tasks.push(dispatch({ type: ReleaseConstants.SET_RELEASES_LIST_STATE, value: nextState }));
25✔
283
  return Promise.all(tasks);
25✔
284
};
285

286
/* Releases */
287

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

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

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

337
export const getRelease = name => (dispatch, getState) =>
184✔
338
  releaseListRetrieval({ searchTerm: name, page: 1, perPage: 1 }).then(({ data: releases }) => {
8✔
339
    if (releases.length) {
8!
340
      const stateRelease = getState().releases.byId[releases[0].name] || {};
8!
341
      return Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: flattenRelease(releases[0], stateRelease) }));
8✔
342
    }
343
    return Promise.resolve(null);
×
344
  });
345

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

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

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

373
export const getUpdateTypes = () => dispatch =>
184✔
374
  GeneralApi.get(`${deploymentsApiUrlV2}/releases/all/types`)
4✔
375
    .catch(err => commonErrorHandler(err, `Existing update types couldn't be retrieved.`, dispatch))
×
376
    .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