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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

64.71
/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, { 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 { createArtifact, getReleases, selectRelease, setReleasesListState, uploadArtifact } from '../../actions/releaseActions';
27
import { SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
28
import { onboardingSteps } from '../../constants/onboardingConstants';
29
import { getDeviceTypes, 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✔
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!
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 }) => (
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;
29✔
85
  const { selectedTags = [], searchTerm, searchTotal, tab = tabs[0].key, total } = releasesListState;
29✔
86
  const { classes } = useStyles();
29✔
87

88
  const searchUpdated = searchTerm => setReleasesListState({ searchTerm });
29✔
89

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

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

94
  return (
29✔
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} />
58✔
100
          ))}
101
        </Tabs>
102
        <div>
103
          {canUpload && (
58✔
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 && (
87✔
121
        <div className={`two-columns ${classes.filters}`}>
122
          <Search onSearch={searchUpdated} searchTerm={searchTerm} placeholder="Search releases by name" />
123
          {hasReleaseTags && (
29!
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>
59✔
136
    </div>
137
  );
138
};
139

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

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

170
  useEffect(() => {
29✔
171
    if (!artifactTimer.current) {
9✔
172
      return;
4✔
173
    }
174
    setLocationParams({ pageState: { ...releasesListState, selectedRelease: selectedRelease.Name } });
5✔
175
  }, [debouncedSearchTerm, JSON.stringify(sort), page, perPage, selectedRelease.Name, tab, JSON.stringify(selectedTags)]);
176

177
  useEffect(() => {
29✔
178
    dispatch(setReleasesListState({ selectedTags: locationParams.tags }));
6✔
179
    if (locationParams.selectedRelease) {
6✔
180
      dispatch(selectRelease(locationParams.selectedRelease));
2✔
181
    }
182
  }, [JSON.stringify(locationParams.tags), locationParams.selectedRelease]);
183

184
  useEffect(() => {
29✔
185
    if (!onboardingState.progress || onboardingState.complete) {
6!
186
      return;
6✔
187
    }
188
    if (releases.length === 1) {
×
189
      dispatch(advanceOnboarding(onboardingSteps.UPLOAD_PREPARED_ARTIFACT_TIP));
×
190
    }
191
    if (selectedRelease.Name) {
×
192
      dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_INCLUDED_ONBOARDING));
×
193
    }
194
  }, [onboardingState.complete, onboardingState.progress, releases.length, selectedRelease.Name]);
195

196
  useEffect(() => {
29✔
197
    const { selectedRelease, tags, ...remainder } = locationParams;
4✔
198
    if (selectedRelease) {
4!
199
      dispatch(selectRelease(selectedRelease));
×
200
    }
201
    dispatch(setReleasesListState({ ...remainder, selectedTags: tags }));
4✔
202
    clearInterval(artifactTimer.current);
4✔
203
    artifactTimer.current = setInterval(() => dispatch(getReleases()), refreshArtifactsLength);
4✔
204
    return () => {
4✔
205
      clearInterval(artifactTimer.current);
4✔
206
    };
207
  }, []);
208

209
  const onUploadClick = () => {
29✔
210
    if (releases.length) {
×
211
      dispatch(advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP));
×
212
    }
213
    setShowAddArtifactDialog(true);
×
214
  };
215

216
  const onDrop = (acceptedFiles, rejectedFiles) => {
29✔
217
    if (acceptedFiles.length) {
×
218
      onFileUploadClick(acceptedFiles[0]);
×
219
    }
220
    if (rejectedFiles.length) {
×
221
      dispatch(setSnackbar(`File '${rejectedFiles[0].name}' was rejected. File should be of type .mender`, null));
×
222
    }
223
  };
224

225
  const onFileUploadClick = selectedFile => {
29✔
226
    setSelectedFile(selectedFile);
×
227
    setShowAddArtifactDialog(true);
×
228
  };
229

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

252
  const ContentComponent = useMemo(() => tabs.find(({ key }) => key === tab).component, [tab]);
29✔
253
  return (
29✔
254
    <div className="margin">
255
      <div>
256
        <Header
257
          canUpload={canUploadReleases}
258
          existingTags={releaseTags}
259
          features={features}
260
          hasReleases={hasReleases}
261
          onUploadClick={onUploadClick}
262
          releasesListState={releasesListState}
263
          setReleasesListState={state => dispatch(setReleasesListState(state))}
1✔
264
        />
265
        {hasReleases ? (
29!
266
          <ContentComponent
267
            artifactIncluded={artifactIncluded}
268
            features={features}
269
            onboardingState={onboardingState}
270
            onSelect={id => dispatch(selectRelease(id))}
1✔
271
            releases={releases}
272
            releasesListState={releasesListState}
273
            setReleasesListState={state => dispatch(setReleasesListState(state))}
×
274
            tenantCapabilities={tenantCapabilities}
275
          />
276
        ) : (
277
          <EmptyState
278
            canUpload={canUploadReleases}
279
            className={classes.empty}
280
            dropzoneRef={dropzoneRef}
281
            uploading={uploading}
282
            onDrop={onDrop}
283
            onUpload={onFileUploadClick}
284
          />
285
        )}
286
      </div>
287
      <ReleaseDetails />
288
      {!showAddArtifactDialog && uploadArtifactOnboardingComponent}
58✔
289
      {showAddArtifactDialog && (
29!
290
        <AddArtifactDialog
291
          advanceOnboarding={step => dispatch(advanceOnboarding(step))}
×
292
          createArtifact={(meta, file) => dispatch(createArtifact(meta, file))}
×
293
          deviceTypes={deviceTypes}
294
          onboardingState={onboardingState}
295
          pastCount={pastCount}
296
          releases={releases}
297
          setSnackbar={(...args) => dispatch(setSnackbar(...args))}
×
298
          uploadArtifact={(meta, file) => dispatch(uploadArtifact(meta, file))}
×
299
          onCancel={() => setShowAddArtifactDialog(false)}
×
300
          onUploadStarted={() => setShowAddArtifactDialog(false)}
×
301
          selectedFile={selectedFile}
302
        />
303
      )}
304
    </div>
305
  );
306
};
307

308
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