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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

92.43
/frontend/src/js/components/releases/ReleasesList.tsx
1
// Copyright 2019 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useCallback, useMemo, useRef, useState } from 'react';
2✔
15
import Dropzone from 'react-dropzone';
2✔
16
import { useDispatch, useSelector } from 'react-redux';
2✔
17
import { useNavigate } from 'react-router-dom';
2✔
18

2✔
19
import { makeStyles } from 'tss-react/mui';
2✔
20

2✔
21
import DetailsTable from '@northern.tech/common-ui/DetailsTable';
2✔
22
import Loader from '@northern.tech/common-ui/Loader';
2✔
23
import Pagination from '@northern.tech/common-ui/Pagination';
2✔
24
import { RelativeTime } from '@northern.tech/common-ui/Time';
2✔
25
import storeActions from '@northern.tech/store/actions';
2✔
26
import { DEPLOYMENT_ROUTES, DEVICE_LIST_DEFAULTS, SORTING_OPTIONS, canAccess as canShow } from '@northern.tech/store/constants';
2✔
27
import {
2✔
28
  getFeatures,
2✔
29
  getHasReleases,
2✔
30
  getIsUploading,
2✔
31
  getReleaseListState,
2✔
32
  getReleasesList,
2✔
33
  getSelectedReleases,
2✔
34
  getUserCapabilities
2✔
35
} from '@northern.tech/store/selectors';
2✔
36
import { removeReleases, selectRelease, setReleasesListState } from '@northern.tech/store/thunks';
2✔
37

2✔
38
import { DeleteReleasesConfirmationDialog, ReleaseQuickActions } from './ReleaseDetails';
2✔
39
import AddTagsDialog from './dialogs/AddTags';
2✔
40

2✔
41
const { setSnackbar } = storeActions;
7✔
42

2✔
43
const columns = [
7✔
44
  {
2✔
45
    key: 'name',
2✔
46
    title: 'Name',
2✔
47
    render: ({ name }) => name,
708✔
48
    sortable: true,
2✔
49
    defaultSortDirection: SORTING_OPTIONS.asc,
2✔
50
    canShow
2✔
51
  },
2✔
52
  {
2✔
53
    key: 'artifacts-count',
2✔
54
    title: 'Number of artifacts',
2✔
55
    render: ({ artifacts = [] }) => artifacts.length,
708✔
56
    canShow
2✔
57
  },
2✔
58
  {
2✔
59
    key: 'tags',
2✔
60
    title: 'Tags',
2✔
61
    render: ({ tags = [] }) => tags.join(', ') || '-',
708✔
62
    defaultSortDirection: SORTING_OPTIONS.asc,
2✔
63
    sortable: true,
2✔
64
    canShow
2✔
65
  },
2✔
66
  {
2✔
67
    key: 'modified',
2✔
68
    title: 'Last modified',
2✔
69
    render: ({ modified }) => <RelativeTime updateTime={modified} />,
708✔
70
    defaultSortDirection: SORTING_OPTIONS.desc,
2✔
71
    sortable: true,
2✔
72
    canShow
2✔
73
  }
2✔
74
];
2✔
75

2✔
76
const useStyles = makeStyles()(() => ({
7✔
77
  empty: { margin: '8vh auto' }
2✔
78
}));
2✔
79

2✔
80
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
7✔
81

2✔
82
const EmptyState = ({ canUpload, className = '', dropzoneRef, uploading, onDrop, onUpload }) => (
7!
83
  <div className={`dashboard-placeholder fadeIn ${className}`} ref={dropzoneRef}>
2✔
84
    <Dropzone activeClassName="active" disabled={uploading} multiple={false} noClick={true} onDrop={onDrop} rejectClassName="active">
2✔
85
      {({ getRootProps, getInputProps }) => (
2✔
86
        <div {...getRootProps({ className: uploading ? 'dropzone disabled muted' : 'dropzone' })} onClick={() => onUpload()}>
2!
87
          <input {...getInputProps()} disabled={uploading} />
2✔
88
          <p>
2✔
89
            There are no Releases yet.{' '}
2✔
90
            {canUpload && (
2!
91
              <>
2✔
92
                <a>Upload an Artifact</a> to create a new Release
2✔
93
              </>
2✔
94
            )}
2✔
95
          </p>
2✔
96
        </div>
2✔
97
      )}
2✔
98
    </Dropzone>
2✔
99
  </div>
2✔
100
);
2✔
101

2✔
102
export const ReleasesList = ({ className = '', onFileUploadClick }) => {
7✔
103
  const repoRef = useRef();
65✔
104
  const dropzoneRef = useRef();
65✔
105
  const uploading = useSelector(getIsUploading);
65✔
106
  const releasesListState = useSelector(getReleaseListState);
65✔
107
  const {
2✔
108
    isLoading,
2✔
109
    page = defaultPage,
2✔
110
    perPage = defaultPerPage,
2✔
111
    searchTerm,
2✔
112
    sort = {},
2✔
113
    searchTotal,
2✔
114
    selection: selectedRows,
2✔
115
    selectedTags = [],
2✔
116
    total,
2✔
117
    type
2✔
118
  } = releasesListState;
65✔
119
  const hasReleases = useSelector(getHasReleases);
65✔
120
  const features = useSelector(getFeatures);
65✔
121
  const releases = useSelector(getReleasesList);
65✔
122
  const userCapabilities = useSelector(getUserCapabilities);
65✔
123
  const selectedReleases = useSelector(getSelectedReleases);
65✔
124
  const dispatch = useDispatch();
65✔
125
  const { classes } = useStyles();
65✔
126
  const [addTagsDialog, setAddTagsDialog] = useState(false);
65✔
127
  const [deleteDialogConfirmation, setDeleteDialogConfirmation] = useState(false);
65✔
128
  const navigate = useNavigate();
65✔
129

2✔
130
  const { canUploadReleases } = userCapabilities;
65✔
131
  const { key: attribute, direction } = sort;
65✔
132

2✔
133
  const onSelect = useCallback(id => dispatch(selectRelease(id)), [dispatch]);
65✔
134

2✔
135
  const onChangeSorting = sortKey => {
65✔
136
    let sort = { key: sortKey, direction: direction === SORTING_OPTIONS.asc ? SORTING_OPTIONS.desc : SORTING_OPTIONS.asc };
2!
137
    if (sortKey !== attribute) {
2!
138
      sort = { ...sort, direction: columns.find(({ key }) => key === sortKey)?.defaultSortDirection ?? SORTING_OPTIONS.desc };
2!
139
    }
2✔
140
    dispatch(setReleasesListState({ page: 1, sort }));
2✔
141
  };
2✔
142

2✔
143
  const onChangePagination = (page, currentPerPage = perPage) => dispatch(setReleasesListState({ page, perPage: currentPerPage }));
65!
144

2✔
145
  const onDrop = (acceptedFiles, rejectedFiles) => {
65✔
146
    if (acceptedFiles.length) {
2!
147
      onFileUploadClick(acceptedFiles[0]);
2✔
148
    }
2✔
149
    if (rejectedFiles.length) {
2!
150
      dispatch(setSnackbar(`File '${rejectedFiles[0].name}' was rejected. File should be of type .mender`));
2✔
151
    }
2✔
152
  };
2✔
153

2✔
154
  const applicableColumns = useMemo(
65✔
155
    () =>
2✔
156
      columns.reduce((accu, column) => {
7✔
157
        if (column.canShow({ features })) {
22!
158
          accu.push(column);
22✔
159
        }
2✔
160
        return accu;
22✔
161
      }, []),
2✔
162
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
163
    [JSON.stringify(features)]
2✔
164
  );
2✔
165

2✔
166
  const onDeleteRelease = releases => {
65✔
167
    onSelectionChange(releases);
3✔
168
    setDeleteDialogConfirmation(true);
3✔
169
  };
2✔
170

2✔
171
  const onSelectionChange = useCallback((selection: number[] = []) => dispatch(setReleasesListState({ selection })), [dispatch]);
65✔
172

2✔
173
  const deleteReleases = useCallback(() => {
65✔
174
    dispatch(removeReleases(selectedReleases.map(({ name }) => name))).then(() => {
3✔
175
      setDeleteDialogConfirmation(false);
3✔
176
      onSelectionChange([]);
3✔
177
    });
2✔
178
  }, [dispatch, onSelectionChange, selectedReleases]);
2✔
179

2✔
180
  const onTagRelease = releases => {
65✔
181
    onSelectionChange(releases);
2✔
182
    setAddTagsDialog(true);
2✔
183
  };
2✔
184

2✔
185
  const onCreateDeployment = useCallback(
65✔
186
    selection => {
2✔
187
      if (selection.length !== 1) {
2!
188
        return;
2✔
189
      }
2✔
190
      const { name: releaseName } = selectedReleases[0];
2✔
191
      navigate(`${DEPLOYMENT_ROUTES.active.route}?open=true&release=${encodeURIComponent(releaseName)}`);
2✔
192
    },
2✔
193
    [navigate, selectedReleases]
2✔
194
  );
2✔
195

2✔
196
  const actionCallbacks = {
65✔
197
    onCreateDeployment,
2✔
198
    onDeleteRelease,
2✔
199
    onTagRelease
2✔
200
  };
2✔
201

2✔
202
  const isFiltering = !!(selectedTags.length || type || searchTerm);
65✔
203
  const potentialTotal = isFiltering ? searchTotal : total;
65✔
204
  if (!hasReleases) {
65!
205
    return (
2✔
206
      <EmptyState
2✔
207
        canUpload={canUploadReleases}
2✔
208
        className={classes.empty}
2✔
209
        dropzoneRef={dropzoneRef}
2✔
210
        uploading={uploading}
2✔
211
        onDrop={onDrop}
2✔
212
        onUpload={onFileUploadClick}
2✔
213
      />
2✔
214
    );
2✔
215
  }
2✔
216

2✔
217
  return (
65✔
218
    <div className={className}>
2✔
219
      {isLoading === undefined ? (
2!
220
        <Loader show />
2✔
221
      ) : !potentialTotal ? (
2✔
222
        <p className="margin-top muted align-center margin-right">There are no Releases {isFiltering ? 'for the filter selection' : 'yet'}</p>
2!
223
      ) : (
2✔
224
        <>
2✔
225
          <DetailsTable
2✔
226
            columns={applicableColumns}
2✔
227
            items={releases}
2✔
228
            onItemClick={onSelect}
2✔
229
            sort={sort}
2✔
230
            onChangeSorting={onChangeSorting}
2✔
231
            tableRef={repoRef}
2✔
232
            onRowSelected={onSelectionChange}
2✔
233
            selectedRows={selectedRows}
2✔
234
          />
2✔
235
          <div className="flexbox">
2✔
236
            <Pagination
2✔
237
              className="margin-top-none"
2✔
238
              count={potentialTotal}
2✔
239
              rowsPerPage={perPage}
2✔
240
              onChangePage={onChangePagination}
2✔
UNCOV
241
              onChangeRowsPerPage={newPerPage => onChangePagination(1, newPerPage)}
2✔
242
              page={page}
2✔
243
            />
2✔
244
            <Loader show={isLoading} small />
2✔
245
          </div>
2✔
246
          {selectedReleases?.length > 0 && <ReleaseQuickActions actionCallbacks={actionCallbacks} />}
2✔
UNCOV
247
          {addTagsDialog && <AddTagsDialog selectedReleases={selectedReleases} onClose={() => setAddTagsDialog(false)} />}
2!
UNCOV
248
          {deleteDialogConfirmation && <DeleteReleasesConfirmationDialog onClose={() => setDeleteDialogConfirmation(false)} onSubmit={deleteReleases} />}
2✔
249
        </>
2✔
250
      )}
2✔
251
    </div>
2✔
252
  );
2✔
253
};
2✔
254

2✔
255
export default ReleasesList;
2✔
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