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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

63.91
/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 { connect } 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, removeArtifact, 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;
27✔
85
  const { selectedTags = [], searchTerm, searchTotal, tab = tabs[0].key, total } = releasesListState;
27✔
86
  const { classes } = useStyles();
27✔
87

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

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

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

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

140
export const Releases = props => {
4✔
141
  const {
142
    advanceOnboarding,
143
    artifactIncluded,
144
    demoArtifactLink,
145
    getReleases,
146
    onboardingState,
147
    features,
148
    hasReleases,
149
    releases,
150
    releasesListState,
151
    releaseTags,
152
    selectedRelease,
153
    selectRelease,
154
    setReleasesListState,
155
    setShowCreateArtifactDialog,
156
    tenantCapabilities,
157
    uploading,
158
    userCapabilities
159
  } = props;
27✔
160
  const { canUploadReleases } = userCapabilities;
27✔
161

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

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

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

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

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

211
  const onUploadClick = () => {
27✔
212
    if (releases.length) {
×
213
      advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP);
×
214
    }
215
    setShowAddArtifactDialog(true);
×
216
  };
217

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

227
  const onFileUploadClick = selectedFile => {
27✔
228
    setSelectedFile(selectedFile);
×
229
    setShowAddArtifactDialog(true);
×
230
  };
231

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

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

303
const actionCreators = {
4✔
304
  advanceOnboarding,
305
  createArtifact,
306
  getReleases,
307
  removeArtifact,
308
  selectRelease,
309
  setReleasesListState,
310
  setShowCreateArtifactDialog,
311
  setSnackbar,
312
  uploadArtifact
313
};
314

315
const mapStateToProps = state => {
4✔
316
  return {
25✔
317
    artifactIncluded: state.onboarding.artifactIncluded,
318
    demoArtifactLink: state.app.demoArtifactLink,
319
    deviceTypes: getDeviceTypes(state),
320
    features: getFeatures(state),
321
    hasReleases: !!(Object.keys(state.releases.byId).length || state.releases.releasesList.total || state.releases.releasesList.searchTotal),
25!
322
    onboardingState: getOnboardingState(state),
323
    pastCount: state.deployments.byStatus.finished.total,
324
    releases: getReleasesList(state),
325
    releasesListState: state.releases.releasesList,
326
    releaseTags: state.releases.releaseTags,
327
    selectedRelease: state.releases.byId[state.releases.selectedRelease] ?? {},
30✔
328
    showRemoveDialog: state.releases.showRemoveDialog,
329
    tenantCapabilities: getTenantCapabilities(state),
330
    uploading: state.app.uploading,
331
    userCapabilities: getUserCapabilities(state)
332
  };
333
};
334

335
export default connect(mapStateToProps, actionCreators)(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