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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

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

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

79.64
/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 { deploymentsApiUrl } from './deploymentActions';
24
import { convertDeviceListStateToFilters, getSearchEndpoint } from './deviceActions';
25

26
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
185✔
27

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

46
const reduceReceivedReleases = (releases, stateReleasesById) =>
185✔
47
  releases.reduce((accu, release) => {
21✔
48
    const stateRelease = stateReleasesById[release.Name] || {};
753✔
49
    accu[release.Name] = flattenRelease(release, stateRelease);
753✔
50
    return accu;
753✔
51
  }, {});
52

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

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

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

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

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

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

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

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

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

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

251
export const removeRelease = id => (dispatch, getState) =>
185✔
252
  Promise.all(getState().releases.byId[id].Artifacts.map(({ id }) => dispatch(removeArtifact(id)))).then(() => dispatch(selectRelease()));
1✔
253

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

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

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

300
/* Releases */
301

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

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

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

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

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