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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

67.19
/src/js/components/releases/releaseslist.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 React, { useCallback, useMemo, useRef, useState } from 'react';
15
import Dropzone from 'react-dropzone';
16
import { useDispatch, useSelector } from 'react-redux';
17

18
import { makeStyles } from 'tss-react/mui';
19

20
import { setSnackbar } from '../../actions/appActions';
21
import { removeRelease, selectRelease, setReleasesListState } from '../../actions/releaseActions.js';
22
import { SORTING_OPTIONS, canAccess as canShow } from '../../constants/appConstants';
23
import { DEVICE_LIST_DEFAULTS } from '../../constants/deviceConstants';
24
import { getFeatures, getHasReleases, getReleaseListState, getReleasesList, getUserCapabilities } from '../../selectors';
25
import DetailsTable from '../common/detailstable';
26
import Loader from '../common/loader';
27
import Pagination from '../common/pagination';
28
import { RelativeTime } from '../common/time';
29
import AddTagsDialog from './dialogs/addTags.js';
30
import { DeleteReleasesConfirmationDialog, ReleaseQuickActions } from './releasedetails.js';
31

32
const columns = [
4✔
33
  {
34
    key: 'name',
35
    title: 'Name',
36
    render: ({ name }) => name,
418✔
37
    sortable: true,
38
    defaultSortDirection: SORTING_OPTIONS.asc,
39
    canShow
40
  },
41
  {
42
    key: 'artifacts-count',
43
    title: 'Number of artifacts',
44
    render: ({ artifacts = [] }) => artifacts.length,
418!
45
    canShow
46
  },
47
  {
48
    key: 'tags',
49
    title: 'Tags',
50
    render: ({ tags = [] }) => tags.join(', ') || '-',
418✔
51
    defaultSortDirection: SORTING_OPTIONS.asc,
52
    sortable: true,
53
    canShow
54
  },
55
  {
56
    key: 'modified',
57
    title: 'Last modified',
58
    render: ({ modified }) => <RelativeTime updateTime={modified} />,
418✔
59
    defaultSortDirection: SORTING_OPTIONS.desc,
60
    sortable: true,
61
    canShow
62
  }
63
];
64

65
const useStyles = makeStyles()(() => ({
4✔
66
  empty: { margin: '8vh auto' }
67
}));
68

69
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
4✔
70

71
const EmptyState = ({ canUpload, className = '', dropzoneRef, uploading, onDrop, onUpload }) => (
4!
UNCOV
72
  <div className={`dashboard-placeholder fadeIn ${className}`} ref={dropzoneRef}>
×
73
    <Dropzone activeClassName="active" disabled={uploading} multiple={false} noClick={true} onDrop={onDrop} rejectClassName="active">
74
      {({ getRootProps, getInputProps }) => (
UNCOV
75
        <div {...getRootProps({ className: uploading ? 'dropzone disabled muted' : 'dropzone' })} onClick={() => onUpload()}>
×
76
          <input {...getInputProps()} disabled={uploading} />
77
          <p>
78
            There are no Releases yet.{' '}
79
            {canUpload && (
×
80
              <>
81
                <a>Upload an Artifact</a> to create a new Release
82
              </>
83
            )}
84
          </p>
85
        </div>
86
      )}
87
    </Dropzone>
88
  </div>
89
);
90

91
export const ReleasesList = ({ className = '', onFileUploadClick }) => {
4✔
92
  const repoRef = useRef();
39✔
93
  const dropzoneRef = useRef();
39✔
94
  const uploading = useSelector(state => state.app.uploading);
123✔
95
  const releasesListState = useSelector(getReleaseListState);
39✔
96
  const { selection: selectedRows } = releasesListState;
39✔
97
  const { isLoading, page = defaultPage, perPage = defaultPerPage, searchTerm, sort = {}, searchTotal, selectedTags = [], total, type } = releasesListState;
39!
98
  const hasReleases = useSelector(getHasReleases);
39✔
99
  const features = useSelector(getFeatures);
39✔
100
  const releases = useSelector(getReleasesList);
39✔
101
  const userCapabilities = useSelector(getUserCapabilities);
39✔
102
  const dispatch = useDispatch();
39✔
103
  const { classes } = useStyles();
39✔
104
  const [addTagsDialog, setAddTagsDialog] = useState(false);
39✔
105
  const [deleteDialogConfirmation, setDeleteDialogConfirmation] = useState(false);
39✔
106
  const [selectedReleases, setSelectedReleases] = useState([]);
39✔
107

108
  const { canUploadReleases } = userCapabilities;
39✔
109

110
  const onSelect = useCallback(id => dispatch(selectRelease(id)), [dispatch]);
39✔
111

112
  const onChangeSorting = useCallback(sortOption => dispatch(setReleasesListState({ page: 1, sort: sortOption })), [dispatch]); // release retrieval only supports a single sort criterium so we can't pass a list of sortOptions
39✔
113

114
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setReleasesListState({ page, perPage: currentPerPage }));
39!
115

116
  const onDrop = (acceptedFiles, rejectedFiles) => {
39✔
UNCOV
117
    if (acceptedFiles.length) {
×
UNCOV
118
      onFileUploadClick(acceptedFiles[0]);
×
119
    }
UNCOV
120
    if (rejectedFiles.length) {
×
UNCOV
121
      dispatch(setSnackbar(`File '${rejectedFiles[0].name}' was rejected. File should be of type .mender`, null));
×
122
    }
123
  };
124

125
  const applicableColumns = useMemo(
39✔
126
    () =>
127
      columns.reduce((accu, column) => {
4✔
128
        if (column.canShow({ features })) {
16!
129
          accu.push(column);
16✔
130
        }
131
        return accu;
16✔
132
      }, []),
133
    // eslint-disable-next-line react-hooks/exhaustive-deps
134
    [JSON.stringify(features)]
135
  );
136

137
  const onDeleteRelease = releases => {
39✔
UNCOV
138
    setSelectedReleases(releases);
×
UNCOV
139
    setDeleteDialogConfirmation(true);
×
140
  };
141

142
  const deleteReleases = () => {
39✔
UNCOV
143
    setDeleteDialogConfirmation(false);
×
UNCOV
144
    dispatch(setReleasesListState({ loading: true }))
×
145
      .then(() => {
UNCOV
146
        const deleteRequests = selectedReleases.reduce((accu, release) => {
×
UNCOV
147
          accu.push(dispatch(removeRelease(release.name)));
×
UNCOV
148
          return accu;
×
149
        }, []);
UNCOV
150
        return Promise.all(deleteRequests);
×
151
      })
UNCOV
152
      .then(() => onSelectionChange([]));
×
153
  };
154

155
  const onTagRelease = releases => {
39✔
UNCOV
156
    setSelectedReleases(releases);
×
UNCOV
157
    setAddTagsDialog(true);
×
158
  };
159

160
  const actionCallbacks = {
39✔
161
    onDeleteRelease,
162
    onTagRelease
163
  };
164

165
  const isFiltering = !!(selectedTags.length || type || searchTerm);
39✔
166
  const potentialTotal = isFiltering ? searchTotal : total;
39✔
167
  if (!hasReleases) {
39!
UNCOV
168
    return (
×
169
      <EmptyState
170
        canUpload={canUploadReleases}
171
        className={classes.empty}
172
        dropzoneRef={dropzoneRef}
173
        uploading={uploading}
174
        onDrop={onDrop}
175
        onUpload={onFileUploadClick}
176
      />
177
    );
178
  }
179

180
  const onSelectionChange = (selection = []) => dispatch(setReleasesListState({ selection }));
39!
181

182
  return (
39✔
183
    <div className={className}>
184
      {isLoading === undefined ? (
39!
185
        <Loader show />
186
      ) : !potentialTotal ? (
39✔
187
        <p className="margin-top muted align-center margin-right">There are no Releases {isFiltering ? 'for the filter selection' : 'yet'}</p>
1!
188
      ) : (
189
        <>
190
          <DetailsTable
191
            columns={applicableColumns}
192
            items={releases}
193
            onItemClick={onSelect}
194
            sort={[sort]}
195
            onChangeSorting={onChangeSorting}
196
            tableRef={repoRef}
197
            onRowSelected={onSelectionChange}
198
            selectedRows={selectedRows}
199
          />
200
          <div className="flexbox">
201
            <Pagination
202
              className="margin-top-none"
203
              count={potentialTotal}
204
              rowsPerPage={perPage}
205
              onChangePage={onChangePagination}
UNCOV
206
              onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
×
207
              page={page}
208
            />
209
            <Loader show={isLoading} small />
210
          </div>
211
          {selectedRows?.length > 0 && <ReleaseQuickActions actionCallbacks={actionCallbacks} userCapabilities={userCapabilities} releases={releases} />}
38!
UNCOV
212
          {addTagsDialog && <AddTagsDialog selectedReleases={selectedReleases} onClose={() => setAddTagsDialog(false)}></AddTagsDialog>}
×
213
          {deleteDialogConfirmation && (
38!
UNCOV
214
            <DeleteReleasesConfirmationDialog onClose={() => setDeleteDialogConfirmation(false)} onSubmit={deleteReleases}></DeleteReleasesConfirmationDialog>
×
215
          )}
216
        </>
217
      )}
218
    </div>
219
  );
220
};
221

222
export default ReleasesList;
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