• 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

75.73
/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 { getReleases, selectRelease, setReleasesListState } from '../../actions/releaseActions';
26
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
27
import { getFeatures, getReleasesList, getTenantCapabilities, getUserCapabilities } from '../../selectors';
28
import { useDebounce } from '../../utils/debouncehook';
29
import { useLocationParams } from '../../utils/liststatehook';
30
import ChipSelect from '../common/chipselect';
31
import EnterpriseNotification from '../common/enterpriseNotification';
32
import InfoHint from '../common/info-hint';
33
import Search from '../common/search';
34
import AddArtifactDialog from './dialogs/addartifact';
35
import ReleaseDetails from './releasedetails';
36
import ReleasesList from './releaseslist';
37

38
const refreshArtifactsLength = 60000;
4✔
39

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

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

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

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

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

85
  const searchUpdated = useCallback(searchTerm => setReleasesListState({ searchTerm }), [setReleasesListState]);
29✔
86

87
  const onTabChanged = (e, tab) => setReleasesListState({ tab });
29✔
88

89
  const onTagSelectionChanged = ({ selection }) => setReleasesListState({ selectedTags: selection });
29✔
90

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

136
export const Releases = () => {
4✔
137
  const features = useSelector(getFeatures);
29✔
138
  const hasReleases = useSelector(
29✔
139
    state => !!(Object.keys(state.releases.byId).length || state.releases.releasesList.total || state.releases.releasesList.searchTotal)
55!
140
  );
141
  const releases = useSelector(getReleasesList);
29✔
142
  const releasesListState = useSelector(state => state.releases.releasesList);
55✔
143
  const releaseTags = useSelector(state => state.releases.releaseTags);
55✔
144
  const selectedRelease = useSelector(state => state.releases.byId[state.releases.selectedRelease]) ?? {};
55✔
145
  const tenantCapabilities = useSelector(getTenantCapabilities);
29✔
146
  const uploading = useSelector(state => state.app.uploading);
55✔
147
  const userCapabilities = useSelector(getUserCapabilities);
29✔
148
  const { canUploadReleases } = userCapabilities;
29✔
149
  const dispatch = useDispatch();
29✔
150

151
  const [selectedFile, setSelectedFile] = useState();
29✔
152
  const [showAddArtifactDialog, setShowAddArtifactDialog] = useState(false);
29✔
153
  const dropzoneRef = useRef();
29✔
154
  const artifactTimer = useRef();
29✔
155
  const [locationParams, setLocationParams] = useLocationParams('releases', { defaults: { direction: SORTING_OPTIONS.desc, key: 'modified' } });
29✔
156
  const { searchTerm, sort = {}, page, perPage, tab = tabs[0].key, selectedTags } = releasesListState;
29!
157
  const debouncedSearchTerm = useDebounce(searchTerm, TIMEOUTS.debounceDefault);
29✔
158
  const { classes } = useStyles();
29✔
159

160
  useEffect(() => {
29✔
161
    if (!artifactTimer.current) {
14✔
162
      return;
4✔
163
    }
164
    setLocationParams({ pageState: { ...releasesListState, selectedRelease: selectedRelease.Name } });
10✔
165
    // eslint-disable-next-line react-hooks/exhaustive-deps
166
  }, [debouncedSearchTerm, JSON.stringify(sort), page, perPage, selectedRelease.Name, setLocationParams, tab, JSON.stringify(selectedTags)]);
167

168
  useEffect(() => {
29✔
169
    const { selectedRelease, tags, ...remainder } = locationParams;
9✔
170
    if (selectedRelease) {
9✔
171
      dispatch(selectRelease(selectedRelease));
2✔
172
    }
173
    dispatch(setReleasesListState({ ...remainder, selectedTags: tags }));
9✔
174
    clearInterval(artifactTimer.current);
9✔
175
    artifactTimer.current = setInterval(() => dispatch(getReleases()), refreshArtifactsLength);
9✔
176
    return () => {
9✔
177
      clearInterval(artifactTimer.current);
9✔
178
    };
179
    // eslint-disable-next-line react-hooks/exhaustive-deps
180
  }, [dispatch, JSON.stringify(locationParams)]);
181

182
  const onUploadClick = () => setShowAddArtifactDialog(true);
29✔
183

184
  const onDrop = (acceptedFiles, rejectedFiles) => {
29✔
UNCOV
185
    if (acceptedFiles.length) {
×
UNCOV
186
      onFileUploadClick(acceptedFiles[0]);
×
187
    }
UNCOV
188
    if (rejectedFiles.length) {
×
UNCOV
189
      dispatch(setSnackbar(`File '${rejectedFiles[0].name}' was rejected. File should be of type .mender`, null));
×
190
    }
191
  };
192

193
  const onFileUploadClick = selectedFile => {
29✔
UNCOV
194
    setSelectedFile(selectedFile);
×
UNCOV
195
    setShowAddArtifactDialog(true);
×
196
  };
197

198
  const onHideAddArtifactDialog = () => setShowAddArtifactDialog(false);
29✔
199

200
  const onSetReleasesListState = useCallback(state => dispatch(setReleasesListState(state)), [dispatch]);
29✔
201
  const onReleaseSelect = useCallback(id => dispatch(selectRelease(id)), [dispatch]);
29✔
202

203
  const ContentComponent = useMemo(() => tabs.find(({ key }) => key === tab).component, [tab]);
29✔
204
  return (
29✔
205
    <div className="margin">
206
      <div>
207
        <Header
208
          canUpload={canUploadReleases}
209
          existingTags={releaseTags}
210
          features={features}
211
          hasReleases={hasReleases}
212
          onUploadClick={onUploadClick}
213
          releasesListState={releasesListState}
214
          setReleasesListState={onSetReleasesListState}
215
        />
216
        {hasReleases ? (
29!
217
          <ContentComponent
218
            features={features}
219
            onSelect={onReleaseSelect}
220
            releases={releases}
221
            releasesListState={releasesListState}
222
            setReleasesListState={onSetReleasesListState}
223
            tenantCapabilities={tenantCapabilities}
224
          />
225
        ) : (
226
          <EmptyState
227
            canUpload={canUploadReleases}
228
            className={classes.empty}
229
            dropzoneRef={dropzoneRef}
230
            uploading={uploading}
231
            onDrop={onDrop}
232
            onUpload={onFileUploadClick}
233
          />
234
        )}
235
      </div>
236
      <ReleaseDetails />
237
      {showAddArtifactDialog && (
29!
238
        <AddArtifactDialog releases={releases} onCancel={onHideAddArtifactDialog} onUploadStarted={onHideAddArtifactDialog} selectedFile={selectedFile} />
239
      )}
240
    </div>
241
  );
242
};
243

244
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