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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

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

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

21
import pluralize from 'pluralize';
22

23
import { getExistingReleaseTags, getReleases, getUpdateTypes, selectRelease, setReleasesListState } from '../../actions/releaseActions';
24
import { BENEFITS, SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
25
import {
26
  getHasReleases,
27
  getIsEnterprise,
28
  getReleaseListState,
29
  getReleaseTags,
30
  getReleasesList,
31
  getSelectedRelease,
32
  getUpdateTypes as getUpdateTypesSelector,
33
  getUserCapabilities
34
} from '../../selectors';
35
import { useDebounce } from '../../utils/debouncehook';
36
import { useLocationParams } from '../../utils/liststatehook';
37
import ChipSelect from '../common/chipselect';
38
import EnterpriseNotification, { DefaultUpgradeNotification } from '../common/enterpriseNotification';
39
import { ControlledAutoComplete } from '../common/forms/autocomplete';
40
import { Filters } from '../common/forms/filters';
41
import { ControlledSearch } from '../common/search';
42
import { HELPTOOLTIPS, MenderHelpTooltip } from '../helptips/helptooltips';
43
import AddArtifactDialog from './dialogs/addartifact';
44
import ReleaseDetails from './releasedetails';
45
import ReleasesList from './releaseslist';
46

47
const refreshArtifactsLength = 60000;
3✔
48

49
const DeltaProgress = ({ className = '' }) => {
3!
50
  const isEnterprise = useSelector(getIsEnterprise);
×
51
  return (
×
52
    <div className={`dashboard-placeholder ${className}`} style={{ display: 'grid', placeContent: 'center' }}>
53
      {isEnterprise ? 'There is no automatic delta artifacts generation running.' : <DefaultUpgradeNotification />}
×
54
    </div>
55
  );
56
};
57

58
const DeltaTitle = () => (
3✔
59
  <div className="flexbox center-aligned">
42✔
60
    <div>Delta Artifacts generation</div>
61
    <EnterpriseNotification className="margin-left-small" id={BENEFITS.deltaGeneration.id} />
62
  </div>
63
);
64

65
const tabs = [
3✔
66
  { key: 'releases', Title: () => 'Releases', component: ReleasesList },
42✔
67
  { key: 'delta', Title: DeltaTitle, component: DeltaProgress }
68
];
69

70
const useStyles = makeStyles()(theme => ({
8✔
71
  container: { maxWidth: 1600 },
72
  searchNote: { minHeight: '1.8rem' },
73
  tabContainer: { alignSelf: 'flex-start' },
74
  uploadButton: { minWidth: 164, marginRight: theme.spacing(2) }
75
}));
76

77
const Header = ({ canUpload, releasesListState, setReleasesListState, onUploadClick }) => {
3✔
78
  const { selectedTags = [], searchTerm = '', searchTotal, tab = tabs[0].key, total, type } = releasesListState;
42!
79
  const { classes } = useStyles();
42✔
80
  const hasReleases = useSelector(getHasReleases);
42✔
81
  const existingTags = useSelector(getReleaseTags);
42✔
82
  const updateTypes = useSelector(getUpdateTypesSelector);
42✔
83

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

86
  const onTabChanged = (e, tab) => setReleasesListState({ tab });
42✔
87

88
  const onFiltersChange = useCallback(({ name, tags, type }) => setReleasesListState({ selectedTags: tags, searchTerm: name, type }), [setReleasesListState]);
42✔
89

90
  return (
42✔
91
    <div>
92
      <div className="flexbox space-between center-aligned">
93
        <Tabs className={classes.tabContainer} value={tab} onChange={onTabChanged} textColor="primary">
94
          {tabs.map(({ key, Title }) => (
95
            <Tab key={key} label={<Title />} value={key} />
84✔
96
          ))}
97
        </Tabs>
98
        {canUpload && (
84✔
99
          <div className="flexbox center-aligned">
100
            <Button color="secondary" className={classes.uploadButton} onClick={onUploadClick} startIcon={<CloudUpload fontSize="small" />} variant="contained">
101
              Upload
102
            </Button>
103
            <MenderHelpTooltip id={HELPTOOLTIPS.artifactUpload.id} style={{ marginTop: 8 }} />
104
          </div>
105
        )}
106
      </div>
107
      {hasReleases && tab === tabs[0].key && (
126✔
108
        <Filters
109
          className={classes.container}
110
          onChange={onFiltersChange}
111
          initialValues={{ name: searchTerm, tags: selectedTags, type }}
112
          defaultValues={{ name: '', tags: [], type: '' }}
113
          filters={[
114
            {
115
              key: 'name',
116
              title: 'Release name',
117
              Component: ControlledSearch,
118
              componentProps: {
119
                onSearch: searchUpdated,
120
                placeholder: 'Starts with'
121
              }
122
            },
123
            {
124
              key: 'tags',
125
              title: 'Tags',
126
              Component: ChipSelect,
127
              componentProps: {
128
                options: existingTags,
129
                placeholder: 'Select tags',
130
                selection: selectedTags
131
              }
132
            },
133
            {
134
              key: 'type',
135
              title: 'Contains Artifact type',
136
              Component: ControlledAutoComplete,
137
              componentProps: {
138
                autoHighlight: true,
139
                autoSelect: true,
140
                filterSelectedOptions: true,
141
                freeSolo: true,
142
                handleHomeEndKeys: true,
143
                options: updateTypes,
144
                renderInput: params => <TextField {...params} placeholder="Any" InputProps={{ ...params.InputProps }} />
48✔
145
              }
146
            }
147
          ]}
148
        />
149
      )}
150
      <p className={`muted ${classes.searchNote}`}>{searchTerm && searchTotal !== total ? `Filtered from ${total} ${pluralize('Release', total)}` : ''}</p>
85✔
151
    </div>
152
  );
153
};
154

155
export const Releases = () => {
3✔
156
  const releasesListState = useSelector(getReleaseListState);
42✔
157
  const { searchTerm, sort = {}, page, perPage, tab = tabs[0].key, selectedTags, type } = releasesListState;
42!
158
  const releases = useSelector(getReleasesList);
42✔
159
  const selectedRelease = useSelector(getSelectedRelease);
42✔
160
  const { canUploadReleases } = useSelector(getUserCapabilities);
42✔
161
  const dispatch = useDispatch();
42✔
162
  const { classes } = useStyles();
42✔
163

164
  const [selectedFile, setSelectedFile] = useState();
42✔
165
  const [showAddArtifactDialog, setShowAddArtifactDialog] = useState(false);
42✔
166
  const artifactTimer = useRef();
42✔
167
  const [locationParams, setLocationParams] = useLocationParams('releases', { defaults: { direction: SORTING_OPTIONS.desc, key: 'modified' } });
42✔
168
  const debouncedSearchTerm = useDebounce(searchTerm, TIMEOUTS.debounceDefault);
42✔
169
  const debouncedTypeFilter = useDebounce(type, TIMEOUTS.debounceDefault);
42✔
170

171
  useEffect(() => {
42✔
172
    if (!artifactTimer.current) {
12✔
173
      return;
4✔
174
    }
175
    setLocationParams({ pageState: { ...releasesListState, selectedRelease: selectedRelease.name } });
8✔
176
    // eslint-disable-next-line react-hooks/exhaustive-deps
177
  }, [
178
    debouncedSearchTerm,
179
    debouncedTypeFilter,
180
    // eslint-disable-next-line react-hooks/exhaustive-deps
181
    JSON.stringify(sort),
182
    page,
183
    perPage,
184
    selectedRelease.name,
185
    setLocationParams,
186
    tab,
187
    // eslint-disable-next-line react-hooks/exhaustive-deps
188
    JSON.stringify(selectedTags)
189
  ]);
190

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

205
  useEffect(() => {
42✔
206
    dispatch(getReleases({ searchTerm: '', searchOnly: true, page: 1, perPage: 1, selectedTags: [], type: '' }));
4✔
207
    dispatch(getExistingReleaseTags());
4✔
208
    dispatch(getUpdateTypes());
4✔
209
  }, [dispatch]);
210

211
  const onUploadClick = () => setShowAddArtifactDialog(true);
42✔
212

213
  const onFileUploadClick = selectedFile => {
42✔
214
    setSelectedFile(selectedFile);
×
215
    setShowAddArtifactDialog(true);
×
216
  };
217

218
  const onHideAddArtifactDialog = () => setShowAddArtifactDialog(false);
42✔
219

220
  const onSetReleasesListState = useCallback(state => dispatch(setReleasesListState(state)), [dispatch]);
42✔
221

222
  const ContentComponent = useMemo(() => tabs.find(({ key }) => key === tab).component, [tab]);
42✔
223
  return (
42✔
224
    <div className="margin">
225
      <div>
226
        <Header
227
          canUpload={canUploadReleases}
228
          onUploadClick={onUploadClick}
229
          releasesListState={releasesListState}
230
          setReleasesListState={onSetReleasesListState}
231
        />
232
        <ContentComponent className={classes.container} onFileUploadClick={onFileUploadClick} />
233
      </div>
234
      <ReleaseDetails />
235
      {showAddArtifactDialog && (
42!
236
        <AddArtifactDialog releases={releases} onCancel={onHideAddArtifactDialog} onUploadStarted={onHideAddArtifactDialog} selectedFile={selectedFile} />
237
      )}
238
    </div>
239
  );
240
};
241

242
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