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

mendersoftware / gui / 944676341

pending completion
944676341

Pull #3875

gitlab-ci

mzedel
chore: aligned snapshots with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3875: MEN-5414

4469 of 6446 branches covered (69.33%)

230 of 266 new or added lines in 43 files covered. (86.47%)

1712 existing lines in 161 files now uncovered.

8406 of 10170 relevant lines covered (82.65%)

196.7 hits per line

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

69.92
/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 { useDispatch, useSelector } from 'react-redux';
16

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

21
import pluralize from 'pluralize';
22

23
import { setSnackbar } from '../../actions/appActions';
24
import { advanceOnboarding, setShowCreateArtifactDialog } from '../../actions/onboardingActions';
25
import { createArtifact, getReleases, selectRelease, setReleasesListState, uploadArtifact } from '../../actions/releaseActions';
26
import { BENEFITS, SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
27
import { onboardingSteps } from '../../constants/onboardingConstants';
28
import { getDeviceTypes, getFeatures, getIsEnterprise, getOnboardingState, getReleasesList, getUserCapabilities } from '../../selectors';
29
import { useDebounce } from '../../utils/debouncehook';
30
import { useLocationParams } from '../../utils/liststatehook';
31
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
32
import ChipSelect from '../common/chipselect';
33
import EnterpriseNotification, { DefaultUpgradeNotification } from '../common/enterpriseNotification';
34
import Search from '../common/search';
35
import { HELPTOOLTIPS, MenderHelpTooltip } from '../helptips/helptooltips';
36
import AddArtifactDialog from './dialogs/addartifact';
37
import ReleaseDetails from './releasedetails';
38
import ReleasesList from './releaseslist';
39

40
const refreshArtifactsLength = 60000;
4✔
41

42
const DeltaProgress = () => {
4✔
NEW
43
  const isEnterprise = useSelector(getIsEnterprise);
×
NEW
44
  return (
×
45
    <div className="dashboard-placeholder" style={{ display: 'grid', placeContent: 'center' }}>
46
      {isEnterprise ? 'There is no automatic delta artifacts generation running.' : <DefaultUpgradeNotification />}
×
47
    </div>
48
  );
49
};
50

51
const DeltaTitle = () => (
4✔
52
  <div className="flexbox center-aligned">
31✔
53
    <div>Delta Artifacts generation</div>
54
    <EnterpriseNotification className="margin-left-small" id={BENEFITS.deltaGeneration.id} />
55
  </div>
56
);
57

58
const tabs = [
4✔
59
  { key: 'releases', Title: () => 'Releases', component: ReleasesList },
31✔
60
  { key: 'delta', Title: DeltaTitle, component: DeltaProgress }
61
];
62

63
const useStyles = makeStyles()(theme => ({
4✔
64
  filters: { maxWidth: 400, alignItems: 'end', columnGap: 50 },
65
  searchNote: { minHeight: '1.8rem' },
66
  tabContainer: { alignSelf: 'flex-start' },
67
  uploadButton: { minWidth: 164, marginRight: theme.spacing(2) }
68
}));
69

70
const Header = ({ canUpload, existingTags = [], features, hasReleases, releasesListState, setReleasesListState, onUploadClick, uploadButtonRef }) => {
4!
71
  const { hasReleaseTags } = features;
31✔
72
  const { selectedTags = [], searchTerm, searchTotal, tab = tabs[0].key, total } = releasesListState;
31✔
73
  const { classes } = useStyles();
31✔
74

75
  const searchUpdated = searchTerm => setReleasesListState({ searchTerm });
31✔
76

77
  const onTabChanged = (e, tab) => setReleasesListState({ tab });
31✔
78

79
  const onTagSelectionChanged = ({ selection }) => setReleasesListState({ selectedTags: selection });
31✔
80

81
  return (
31✔
82
    <div>
83
      <div className="flexbox space-between center-aligned">
84
        <Tabs className={classes.tabContainer} value={tab} onChange={onTabChanged} textColor="primary">
85
          {tabs.map(({ key, Title }) => (
86
            <Tab key={key} label={<Title />} value={key} />
62✔
87
          ))}
88
        </Tabs>
89
        {canUpload && (
62✔
90
          <div className="flexbox center-aligned">
91
            <Button
92
              ref={uploadButtonRef}
93
              color="secondary"
94
              className={classes.uploadButton}
95
              onClick={onUploadClick}
96
              startIcon={<CloudUpload fontSize="small" />}
97
              variant="contained"
98
            >
99
              Upload
100
            </Button>
101
            <MenderHelpTooltip id={HELPTOOLTIPS.artifactUpload.id} style={{ marginTop: 8 }} />
102
          </div>
103
        )}
104
      </div>
105
      {hasReleases && tab === tabs[0].key && (
93✔
106
        <div className={`two-columns ${classes.filters}`}>
107
          <Search onSearch={searchUpdated} searchTerm={searchTerm} placeholder="Search releases by name" />
108
          {hasReleaseTags && (
31!
109
            <ChipSelect
110
              id="release-tag-selection"
111
              label="Filter by tag"
112
              onChange={onTagSelectionChanged}
113
              placeholder="Filter by tag"
114
              selection={selectedTags}
115
              options={existingTags}
116
            />
117
          )}
118
        </div>
119
      )}
120
      <p className={`muted ${classes.searchNote}`}>{searchTerm && searchTotal !== total ? `Filtered from ${total} ${pluralize('Release', total)}` : ''}</p>
63✔
121
    </div>
122
  );
123
};
124

125
export const Releases = () => {
4✔
126
  const demoArtifactLink = useSelector(state => state.app.demoArtifactLink);
63✔
127
  const deviceTypes = useSelector(getDeviceTypes);
31✔
128
  const features = useSelector(getFeatures);
31✔
129
  const hasReleases = useSelector(
31✔
130
    state => !!(Object.keys(state.releases.byId).length || state.releases.releasesList.total || state.releases.releasesList.searchTotal)
63!
131
  );
132
  const onboardingState = useSelector(getOnboardingState);
31✔
133
  const pastCount = useSelector(state => state.deployments.byStatus.finished.total);
63✔
134
  const releases = useSelector(getReleasesList);
31✔
135
  const releasesListState = useSelector(state => state.releases.releasesList);
63✔
136
  const releaseTags = useSelector(state => state.releases.releaseTags);
63✔
137
  const selectedRelease = useSelector(state => state.releases.byId[state.releases.selectedRelease]) ?? {};
63✔
138
  const userCapabilities = useSelector(getUserCapabilities);
31✔
139
  const { canUploadReleases } = userCapabilities;
31✔
140
  const dispatch = useDispatch();
31✔
141

142
  const [selectedFile, setSelectedFile] = useState();
31✔
143
  const [showAddArtifactDialog, setShowAddArtifactDialog] = useState(false);
31✔
144
  const uploadButtonRef = useRef();
31✔
145
  const artifactTimer = useRef();
31✔
146
  const [locationParams, setLocationParams] = useLocationParams('releases', { defaults: { direction: SORTING_OPTIONS.desc, key: 'modified' } });
31✔
147
  const { searchTerm, sort = {}, page, perPage, tab = tabs[0].key, selectedTags } = releasesListState;
31!
148
  const debouncedSearchTerm = useDebounce(searchTerm, TIMEOUTS.debounceDefault);
31✔
149

150
  useEffect(() => {
31✔
151
    if (!artifactTimer.current) {
9✔
152
      return;
4✔
153
    }
154
    setLocationParams({ pageState: { ...releasesListState, selectedRelease: selectedRelease.Name } });
5✔
155
  }, [debouncedSearchTerm, JSON.stringify(sort), page, perPage, selectedRelease.Name, tab, JSON.stringify(selectedTags)]);
156

157
  useEffect(() => {
31✔
158
    dispatch(setReleasesListState({ selectedTags: locationParams.tags }));
6✔
159
    if (locationParams.selectedRelease) {
6✔
160
      dispatch(selectRelease(locationParams.selectedRelease));
2✔
161
    }
162
  }, [JSON.stringify(locationParams.tags), locationParams.selectedRelease]);
163

164
  useEffect(() => {
31✔
165
    if (!onboardingState.progress || onboardingState.complete) {
6!
166
      return;
6✔
167
    }
UNCOV
168
    if (releases.length === 1) {
×
UNCOV
169
      dispatch(advanceOnboarding(onboardingSteps.UPLOAD_PREPARED_ARTIFACT_TIP));
×
170
    }
UNCOV
171
    if (selectedRelease.Name) {
×
UNCOV
172
      dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_INCLUDED_ONBOARDING));
×
173
    }
174
  }, [onboardingState.complete, onboardingState.progress, releases.length, selectedRelease.Name]);
175

176
  useEffect(() => {
31✔
177
    const { selectedRelease, tags, ...remainder } = locationParams;
4✔
178
    if (selectedRelease) {
4!
UNCOV
179
      dispatch(selectRelease(selectedRelease));
×
180
    }
181
    dispatch(setReleasesListState({ ...remainder, selectedTags: tags }));
4✔
182
    clearInterval(artifactTimer.current);
4✔
183
    artifactTimer.current = setInterval(() => dispatch(getReleases()), refreshArtifactsLength);
4✔
184
    return () => {
4✔
185
      clearInterval(artifactTimer.current);
4✔
186
    };
187
  }, []);
188

189
  const onUploadClick = () => {
31✔
UNCOV
190
    if (releases.length) {
×
UNCOV
191
      dispatch(advanceOnboarding(onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP));
×
192
    }
UNCOV
193
    setShowAddArtifactDialog(true);
×
194
  };
195

196
  const onFileUploadClick = selectedFile => {
31✔
UNCOV
197
    setSelectedFile(selectedFile);
×
UNCOV
198
    setShowAddArtifactDialog(true);
×
199
  };
200

201
  let uploadArtifactOnboardingComponent = null;
31✔
202
  if (!onboardingState.complete && uploadButtonRef.current) {
31!
UNCOV
203
    const anchor = {
×
204
      anchor: {
205
        left: uploadButtonRef.current.offsetLeft - 15,
206
        top: uploadButtonRef.current.offsetTop + uploadButtonRef.current.offsetHeight / 2
207
      },
208
      place: 'left'
209
    };
UNCOV
210
    uploadArtifactOnboardingComponent = getOnboardingComponentFor(
×
211
      onboardingSteps.UPLOAD_PREPARED_ARTIFACT_TIP,
212
      { ...onboardingState, demoArtifactLink },
213
      anchor
214
    );
UNCOV
215
    uploadArtifactOnboardingComponent = getOnboardingComponentFor(
×
216
      onboardingSteps.UPLOAD_NEW_ARTIFACT_TIP,
UNCOV
217
      { ...onboardingState, setShowCreateArtifactDialog: state => dispatch(setShowCreateArtifactDialog(state)) },
×
218
      anchor,
219
      uploadArtifactOnboardingComponent
220
    );
221
  }
222

223
  const ContentComponent = useMemo(() => tabs.find(({ key }) => key === tab).component, [tab]);
31✔
224
  return (
31✔
225
    <div className="margin">
226
      <div>
227
        <Header
228
          canUpload={canUploadReleases}
229
          existingTags={releaseTags}
230
          features={features}
231
          hasReleases={hasReleases}
232
          onUploadClick={onUploadClick}
233
          releasesListState={releasesListState}
234
          setReleasesListState={state => dispatch(setReleasesListState(state))}
1✔
235
        />
236
        <ContentComponent onFileUploadClick={onFileUploadClick} />
237
      </div>
238
      <ReleaseDetails />
239
      {!showAddArtifactDialog && uploadArtifactOnboardingComponent}
62✔
240
      {showAddArtifactDialog && (
31!
241
        <AddArtifactDialog
UNCOV
242
          advanceOnboarding={step => dispatch(advanceOnboarding(step))}
×
UNCOV
243
          createArtifact={(meta, file) => dispatch(createArtifact(meta, file))}
×
244
          deviceTypes={deviceTypes}
245
          onboardingState={onboardingState}
246
          pastCount={pastCount}
247
          releases={releases}
UNCOV
248
          setSnackbar={(...args) => dispatch(setSnackbar(...args))}
×
UNCOV
249
          uploadArtifact={(meta, file) => dispatch(uploadArtifact(meta, file))}
×
UNCOV
250
          onCancel={() => setShowAddArtifactDialog(false)}
×
UNCOV
251
          onUploadStarted={() => setShowAddArtifactDialog(false)}
×
252
          selectedFile={selectedFile}
253
        />
254
      )}
255
    </div>
256
  );
257
};
258

259
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