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

mendersoftware / gui / 947088195

pending completion
947088195

Pull #2661

gitlab-ci

mzedel
chore: improved device filter scrolling behaviour

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #2661: chore: added lint rules for hooks usage

4411 of 6415 branches covered (68.76%)

297 of 440 new or added lines in 62 files covered. (67.5%)

1617 existing lines in 163 files now uncovered.

8311 of 10087 relevant lines covered (82.39%)

192.12 hits per line

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

70.5
/src/js/components/releases/releases.js
1
// Copyright 2015 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, useEffect, useMemo, useRef, useState } from 'react';
15
import Dropzone from 'react-dropzone';
16
import { useDispatch, useSelector } from 'react-redux';
17

18
import { CloudUpload } from '@mui/icons-material';
19
import { Button, Tab, Tabs } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import pluralize from 'pluralize';
23

24
import { setSnackbar } from '../../actions/appActions';
25
import { advanceOnboarding, setShowCreateArtifactDialog } from '../../actions/onboardingActions';
26
import { getReleases, selectRelease, setReleasesListState } from '../../actions/releaseActions';
27
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
28
import { onboardingSteps } from '../../constants/onboardingConstants';
29
import { getFeatures, getOnboardingState, getReleasesList, getTenantCapabilities, getUserCapabilities } from '../../selectors';
30
import { useDebounce } from '../../utils/debouncehook';
31
import { useLocationParams } from '../../utils/liststatehook';
32
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
33
import ChipSelect from '../common/chipselect';
34
import EnterpriseNotification from '../common/enterpriseNotification';
35
import InfoHint from '../common/info-hint';
36
import Search from '../common/search';
37
import AddArtifactDialog from './dialogs/addartifact';
38
import ReleaseDetails from './releasedetails';
39
import ReleasesList from './releaseslist';
40

41
const refreshArtifactsLength = 60000;
4✔
42

43
const DeltaProgressPlaceholder = ({ tenantCapabilities: { canDelta } }) => (
4✔
UNCOV
44
  <div className="dashboard-placeholder" style={{ display: 'grid', placeContent: 'center' }}>
×
45
    There is no automatic delta artifacts generation running.
46
    <EnterpriseNotification isEnterprise={canDelta} benefit="automatic artifact generation to reduce bandwidth consumption during deployments" />
47
  </div>
48
);
49

50
const tabs = [
4✔
51
  { key: 'releases', title: 'Releases', component: ReleasesList },
52
  { key: 'delta', title: 'Delta Artifacts generation', component: DeltaProgressPlaceholder }
53
];
54

55
const useStyles = makeStyles()(theme => ({
8✔
56
  empty: { margin: '8vh auto' },
57
  filters: { maxWidth: 400, alignItems: 'end', columnGap: 50 },
58
  searchNote: { minHeight: '1.8rem' },
59
  tabContainer: { alignSelf: 'flex-start' },
60
  uploadButton: { marginTop: theme.spacing(1.5), minWidth: 164 }
61
}));
62

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

83
const Header = ({ canUpload, existingTags = [], features, hasReleases, releasesListState, setReleasesListState, onUploadClick, uploadButtonRef }) => {
4!
84
  const { hasReleaseTags } = features;
30✔
85
  const { selectedTags = [], searchTerm, searchTotal, tab = tabs[0].key, total } = releasesListState;
30✔
86
  const { classes } = useStyles();
30✔
87

88
  const searchUpdated = useCallback(searchTerm => setReleasesListState({ searchTerm }), [setReleasesListState]);
30✔
89

90
  const onTabChanged = (e, tab) => setReleasesListState({ tab });
30✔
91

92
  const onTagSelectionChanged = ({ selection }) => setReleasesListState({ selectedTags: selection });
30✔
93

94
  return (
30✔
95
    <div>
96
      <div className="flexbox space-between">
97
        <Tabs className={classes.tabContainer} value={tab} onChange={onTabChanged} textColor="primary">
98
          {tabs.map(({ key, title }) => (
99
            <Tab key={key} label={title} value={key} />
60✔
100
          ))}
101
        </Tabs>
102
        <div>
103
          {canUpload && (
60✔
104
            <>
105
              <Button
106
                ref={uploadButtonRef}
107
                color="secondary"
108
                className={classes.uploadButton}
109
                onClick={onUploadClick}
110
                startIcon={<CloudUpload fontSize="small" />}
111
                variant="contained"
112
              >
113
                Upload
114
              </Button>
115
              <InfoHint content="Upload an Artifact to an existing or new Release" />
116
            </>
117
          )}
118
        </div>
119
      </div>
120
      {hasReleases && tab === tabs[0].key && (
90✔
121
        <div className={`two-columns ${classes.filters}`}>
122
          <Search onSearch={searchUpdated} searchTerm={searchTerm} placeholder="Search releases by name" />
123
          {hasReleaseTags && (
30!
124
            <ChipSelect
125
              id="release-tag-selection"
126
              label="Filter by tag"
127
              onChange={onTagSelectionChanged}
128
              placeholder="Filter by tag"
129
              selection={selectedTags}
130
              options={existingTags}
131
            />
132
          )}
133
        </div>
134
      )}
135
      <p className={`muted ${classes.searchNote}`}>{searchTerm && searchTotal !== total ? `Filtered from ${total} ${pluralize('Release', total)}` : ''}</p>
61✔
136
    </div>
137
  );
138
};
139

140
export const Releases = () => {
4✔
141
  const demoArtifactLink = useSelector(state => state.app.demoArtifactLink);
57✔
142
  const features = useSelector(getFeatures);
30✔
143
  const hasReleases = useSelector(
30✔
144
    state => !!(Object.keys(state.releases.byId).length || state.releases.releasesList.total || state.releases.releasesList.searchTotal)
57!
145
  );
146
  const onboardingState = useSelector(getOnboardingState);
30✔
147
  const { artifactIncluded } = onboardingState;
30✔
148
  const releases = useSelector(getReleasesList);
30✔
149
  const releasesListState = useSelector(state => state.releases.releasesList);
57✔
150
  const releaseTags = useSelector(state => state.releases.releaseTags);
57✔
151
  const selectedRelease = useSelector(state => state.releases.byId[state.releases.selectedRelease]) ?? {};
57✔
152
  const tenantCapabilities = useSelector(getTenantCapabilities);
30✔
153
  const uploading = useSelector(state => state.app.uploading);
57✔
154
  const userCapabilities = useSelector(getUserCapabilities);
30✔
155
  const { canUploadReleases } = userCapabilities;
30✔
156
  const dispatch = useDispatch();
30✔
157

158
  const [selectedFile, setSelectedFile] = useState();
30✔
159
  const [showAddArtifactDialog, setShowAddArtifactDialog] = useState(false);
30✔
160
  const dropzoneRef = useRef();
30✔
161
  const uploadButtonRef = useRef();
30✔
162
  const artifactTimer = useRef();
30✔
163
  const [locationParams, setLocationParams] = useLocationParams('releases', { defaults: { direction: SORTING_OPTIONS.desc, key: 'modified' } });
30✔
164
  const { searchTerm, sort = {}, page, perPage, tab = tabs[0].key, selectedTags } = releasesListState;
30!
165
  const debouncedSearchTerm = useDebounce(searchTerm, TIMEOUTS.debounceDefault);
30✔
166
  const { classes } = useStyles();
30✔
167

168
  useEffect(() => {
30✔
169
    if (!artifactTimer.current) {
14✔
170
      return;
4✔
171
    }
172
    setLocationParams({ pageState: { ...releasesListState, selectedRelease: selectedRelease.Name } });
10✔
173
    // eslint-disable-next-line react-hooks/exhaustive-deps
174
  }, [debouncedSearchTerm, JSON.stringify(sort), page, perPage, selectedRelease.Name, setLocationParams, tab, JSON.stringify(selectedTags)]);
175

176
  useEffect(() => {
30✔
177
    if (!onboardingState.progress || onboardingState.complete) {
6!
178
      return;
6✔
179
    }
UNCOV
180
    if (releases.length === 1) {
×
UNCOV
181
      dispatch(advanceOnboarding(onboardingSteps.UPLOAD_PREPARED_ARTIFACT_TIP));
×
182
    }
UNCOV
183
    if (selectedRelease.Name) {
×
UNCOV
184
      dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_INCLUDED_ONBOARDING));
×
185
    }
186
  }, [dispatch, onboardingState.complete, onboardingState.progress, releases.length, selectedRelease.Name]);
187

188
  useEffect(() => {
30✔
189
    const { selectedRelease, tags, ...remainder } = locationParams;
9✔
190
    if (selectedRelease) {
9✔
191
      dispatch(selectRelease(selectedRelease));
2✔
192
    }
193
    dispatch(setReleasesListState({ ...remainder, selectedTags: tags }));
9✔
194
    clearInterval(artifactTimer.current);
9✔
195
    artifactTimer.current = setInterval(() => dispatch(getReleases()), refreshArtifactsLength);
9✔
196
    return () => {
9✔
197
      clearInterval(artifactTimer.current);
9✔
198
    };
199
    // eslint-disable-next-line react-hooks/exhaustive-deps
200
  }, [dispatch, JSON.stringify(locationParams)]);
201

202
  const onUploadClick = () => {
30✔
UNCOV
203
    if (releases.length) {
×
UNCOV
204
      dispatch(advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP));
×
205
    }
UNCOV
206
    setShowAddArtifactDialog(true);
×
207
  };
208

209
  const onDrop = (acceptedFiles, rejectedFiles) => {
30✔
UNCOV
210
    if (acceptedFiles.length) {
×
UNCOV
211
      onFileUploadClick(acceptedFiles[0]);
×
212
    }
UNCOV
213
    if (rejectedFiles.length) {
×
UNCOV
214
      dispatch(setSnackbar(`File '${rejectedFiles[0].name}' was rejected. File should be of type .mender`, null));
×
215
    }
216
  };
217

218
  const onFileUploadClick = selectedFile => {
30✔
UNCOV
219
    setSelectedFile(selectedFile);
×
UNCOV
220
    setShowAddArtifactDialog(true);
×
221
  };
222

223
  const onHideAddArtifactDialog = () => setShowAddArtifactDialog(false);
30✔
224

225
  const onSetReleasesListState = useCallback(state => dispatch(setReleasesListState(state)), [dispatch]);
30✔
226
  const onReleaseSelect = useCallback(id => dispatch(selectRelease(id)), [dispatch]);
30✔
227

228
  let uploadArtifactOnboardingComponent = null;
30✔
229
  if (!onboardingState.complete && uploadButtonRef.current) {
30✔
230
    const anchor = {
26✔
231
      anchor: {
232
        left: uploadButtonRef.current.offsetLeft - 15,
233
        top: uploadButtonRef.current.offsetTop + uploadButtonRef.current.offsetHeight / 2
234
      },
235
      place: 'left'
236
    };
237
    uploadArtifactOnboardingComponent = getOnboardingComponentFor(
26✔
238
      onboardingSteps.UPLOAD_PREPARED_ARTIFACT_TIP,
239
      { ...onboardingState, demoArtifactLink },
240
      anchor
241
    );
242
    uploadArtifactOnboardingComponent = getOnboardingComponentFor(
26✔
243
      onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP,
UNCOV
244
      { ...onboardingState, setShowCreateArtifactDialog: state => dispatch(setShowCreateArtifactDialog(state)) },
×
245
      anchor,
246
      uploadArtifactOnboardingComponent
247
    );
248
  }
249

250
  const ContentComponent = useMemo(() => tabs.find(({ key }) => key === tab).component, [tab]);
30✔
251
  return (
30✔
252
    <div className="margin">
253
      <div>
254
        <Header
255
          canUpload={canUploadReleases}
256
          existingTags={releaseTags}
257
          features={features}
258
          hasReleases={hasReleases}
259
          onUploadClick={onUploadClick}
260
          releasesListState={releasesListState}
261
          setReleasesListState={onSetReleasesListState}
262
          uploadButtonRef={uploadButtonRef}
263
        />
264
        {hasReleases ? (
30!
265
          <ContentComponent
266
            artifactIncluded={artifactIncluded}
267
            features={features}
268
            onboardingState={onboardingState}
269
            onSelect={onReleaseSelect}
270
            releases={releases}
271
            releasesListState={releasesListState}
272
            setReleasesListState={onSetReleasesListState}
273
            tenantCapabilities={tenantCapabilities}
274
          />
275
        ) : (
276
          <EmptyState
277
            canUpload={canUploadReleases}
278
            className={classes.empty}
279
            dropzoneRef={dropzoneRef}
280
            uploading={uploading}
281
            onDrop={onDrop}
282
            onUpload={onFileUploadClick}
283
          />
284
        )}
285
      </div>
286
      <ReleaseDetails />
287
      {!showAddArtifactDialog && uploadArtifactOnboardingComponent}
60✔
288
      {showAddArtifactDialog && (
30!
289
        <AddArtifactDialog releases={releases} onCancel={onHideAddArtifactDialog} onUploadStarted={onHideAddArtifactDialog} selectedFile={selectedFile} />
290
      )}
291
    </div>
292
  );
293
};
294

295
export default Releases;
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