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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 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')) || [];
543✔
31
  const { artifacts, deviceTypes, modified } = updatedArtifacts.reduce(
543✔
32
    (accu, item) => {
33
      accu.deviceTypes.push(...item.device_types_compatible);
542✔
34
      const stateArtifact = stateRelease.artifacts?.find(releaseArtifact => releaseArtifact.id === item.id) || {};
542✔
35
      accu.modified = accu.modified ? accu.modified : item.modified;
542!
36
      accu.artifacts.push({
542✔
37
        ...stateArtifact,
38
        ...item
39
      });
40
      return accu;
542✔
41
    },
42
    { artifacts: [], deviceTypes: [], modified: undefined }
43
  );
44
  return { ...stateRelease, ...release, artifacts, device_types_compatible: deviceTypes.filter(duplicateFilter), modified };
543✔
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);
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) => {
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;
10✔
257
  let tasks = [dispatch({ type: ReleaseConstants.SELECTED_RELEASE, release: name })];
10✔
258
  if (name) {
10✔
259
    tasks.push(dispatch(getRelease(name)));
8✔
260
  }
261
  return Promise.all(tasks);
10✔
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 }) => {
10✔
337
    if (releases.length) {
8!
338
      const stateRelease = getState().releases.byId[releases[0].name] || {};
8✔
339
      return Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: flattenRelease(releases[0], stateRelease) }));
8✔
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