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

mendersoftware / gui / 908425489

pending completion
908425489

Pull #3799

gitlab-ci

mzedel
chore: aligned loader usage in devices list with deployment devices list

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3799: MEN-6553

4406 of 6423 branches covered (68.6%)

18 of 19 new or added lines in 3 files covered. (94.74%)

1777 existing lines in 167 files now uncovered.

8329 of 10123 relevant lines covered (82.28%)

144.7 hits per line

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

78.7
/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;
190✔
28

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

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

54
const findArtifactIndexInRelease = (releases, id) =>
190✔
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) => {
190✔
68
  let { release, index } = findArtifactIndexInRelease(getState().releases.byId, id);
3✔
69
  if (!release || index === -1) {
3!
UNCOV
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
  })
UNCOV
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!
UNCOV
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) =>
190✔
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!
UNCOV
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) => {
190✔
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) => {
190✔
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 }),
UNCOV
146
    GeneralApi.upload(`${deploymentsApiUrl}/artifacts/generate`, formData, e => dispatch(progress(e, uploadId)), cancelSource.signal)
×
147
  ])
148
    .then(() => {
149
      setTimeout(() => dispatch(selectRelease(meta.name)), TIMEOUTS.oneSecond);
1✔
150
      return Promise.resolve([dispatch(setSnackbar('Upload successful', 5000))]);
1✔
151
    })
152
    .catch(err => {
UNCOV
153
      if (isCancel(err)) {
×
UNCOV
154
        return dispatch(setSnackbar('The artifact generation has been cancelled', 5000));
×
155
      }
UNCOV
156
      return commonErrorHandler(err, `Artifact couldn't be generated.`, dispatch);
×
157
    })
158
    .finally(() => dispatch(cleanUpUpload(uploadId)));
1✔
159
};
160

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

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

197
export const cancelFileUpload = id => (dispatch, getState) => {
190✔
UNCOV
198
  const { [id]: current, ...remainder } = getState().app.uploadsById;
×
UNCOV
199
  current.cancelSource.abort();
×
UNCOV
200
  return Promise.resolve(dispatch({ type: UPLOAD_PROGRESS, uploads: remainder }));
×
201
};
202

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

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

246
export const removeRelease = id => (dispatch, getState) =>
190✔
247
  Promise.all(getState().releases.byId[id].Artifacts.map(({ id }) => dispatch(removeArtifact(id)))).then(() => dispatch(selectRelease()));
1✔
248

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

266
export const selectRelease = release => dispatch => {
190✔
267
  const name = release ? release.Name || release : null;
8✔
268
  let tasks = [dispatch({ type: ReleaseConstants.SELECTED_RELEASE, release: name })];
8✔
269
  if (name) {
8✔
270
    tasks.push(dispatch(getRelease(name)));
6✔
271
  }
272
  return Promise.all(tasks);
8✔
273
};
274

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

295
/* Releases */
296

297
const releaseListRetrieval = config => {
190✔
298
  const { searchTerm = '', page = defaultPage, perPage = defaultPerPage, sort = {}, selectedTags = [] } = config;
24!
299
  const { key: attribute, direction } = sort;
24✔
300

301
  const sorting = attribute ? `&sort=${attribute}:${direction}`.toLowerCase() : '';
24!
302
  const searchQuery = searchTerm ? `&name=${searchTerm}` : '';
24✔
303
  const tagQuery = selectedTags.map(tag => `&tag=${tag}`).join('');
24✔
304
  return GeneralApi.get(`${deploymentsApiUrl}/deployments/releases/list?page=${page}&per_page=${perPage}${searchQuery}${sorting}${tagQuery}`);
24✔
305
};
306

307
const deductSearchState = (receivedReleases, config, total, state) => {
190✔
308
  let releaseListState = { ...state.releasesList };
18✔
309
  const { searchTerm, searchOnly, sort = {} } = config;
18!
310
  const flattenedReleases = Object.values(receivedReleases).sort(customSort(sort.direction === SORTING_OPTIONS.desc, sort.key));
18✔
311
  const releaseIds = flattenedReleases.map(item => item.Name);
613✔
312
  if (searchOnly) {
18✔
313
    releaseListState = { ...releaseListState, searchedIds: releaseIds };
5✔
314
  } else {
315
    releaseListState = {
13✔
316
      ...releaseListState,
317
      releaseIds,
318
      searchTotal: searchTerm ? total : state.releasesList.searchTotal,
13✔
319
      total: !searchTerm ? total : state.releasesList.total
13✔
320
    };
321
  }
322
  return releaseListState;
18✔
323
};
324

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

346
export const getRelease = name => (dispatch, getState) =>
190✔
347
  GeneralApi.get(`${deploymentsApiUrl}/deployments/releases?name=${name}`).then(({ data: releases }) => {
8✔
348
    if (releases.length) {
6!
349
      const stateRelease = getState().releases.byId[releases[0].Name] || {};
6!
350
      return Promise.resolve(dispatch({ type: ReleaseConstants.RECEIVE_RELEASE, release: flattenRelease(releases[0], stateRelease) }));
6✔
351
    }
UNCOV
352
    return Promise.resolve(null);
×
353
  });
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