• 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

81.94
/src/js/components/deployments/pastdeployments.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, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16

17
// material ui
18
import { TextField } from '@mui/material';
19

20
import historyImage from '../../../assets/img/history.png';
21
import { setSnackbar } from '../../actions/appActions';
22
import { getDeploymentsByStatus, setDeploymentsState } from '../../actions/deploymentActions';
23
import { advanceOnboarding } from '../../actions/onboardingActions';
24
import { BEGINNING_OF_TIME, SORTING_OPTIONS } from '../../constants/appConstants';
25
import { DEPLOYMENT_STATES, DEPLOYMENT_TYPES } from '../../constants/deploymentConstants';
26
import { onboardingSteps } from '../../constants/onboardingConstants';
27
import { getISOStringBoundaries } from '../../helpers';
28
import {
29
  getDeploymentsSelectionState,
30
  getDevicesById,
31
  getGroupNames,
32
  getIdAttribute,
33
  getMappedDeploymentSelection,
34
  getOnboardingState,
35
  getUserCapabilities
36
} from '../../selectors';
37
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
38
import useWindowSize from '../../utils/resizehook';
39
import { clearAllRetryTimers, clearRetryTimer, setRetryTimer } from '../../utils/retrytimer';
40
import { ControlledAutoComplete } from '../common/forms/autocomplete';
41
import Filters from '../common/forms/filters';
42
import TimeframePicker from '../common/forms/timeframe-picker';
43
import { DeploymentSize, DeploymentStatus } from './deploymentitem';
44
import { defaultRefreshDeploymentsLength as refreshDeploymentsLength } from './deployments';
45
import DeploymentsList, { defaultHeaders } from './deploymentslist';
46

47
const headers = [
6✔
48
  ...defaultHeaders.slice(0, defaultHeaders.length - 1),
49
  { title: 'Status', renderer: DeploymentStatus },
50
  { title: 'Data downloaded', renderer: DeploymentSize }
51
];
52

53
const type = DEPLOYMENT_STATES.finished;
6✔
54

55
export const Past = props => {
6✔
56
  const { createClick, isShowingDetails } = props;
66✔
57
  // eslint-disable-next-line no-unused-vars
58
  const size = useWindowSize();
65✔
59
  const [tonight] = useState(getISOStringBoundaries(new Date()).end);
65✔
60
  const [loading, setLoading] = useState(false);
65✔
61
  const deploymentsRef = useRef();
65✔
62
  const timer = useRef();
65✔
63

64
  const dispatch = useDispatch();
65✔
65
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
65✔
66

67
  const { finished: pastSelectionState } = useSelector(getDeploymentsSelectionState);
65✔
68
  const past = useSelector(state => getMappedDeploymentSelection(state, type));
123✔
69
  const { canConfigure, canDeploy } = useSelector(getUserCapabilities);
65✔
70
  const { attribute: idAttribute } = useSelector(getIdAttribute);
65✔
71
  const onboardingState = useSelector(getOnboardingState);
65✔
72
  const devices = useSelector(getDevicesById);
65✔
73
  const groupNames = useSelector(getGroupNames);
65✔
74

75
  const { endDate, page, perPage, search: deviceGroup, startDate, total: count, type: deploymentType } = pastSelectionState;
65✔
76

77
  /*
78
  / refresh only finished deployments
79
  /
80
  */
81
  const refreshPast = useCallback(
65✔
82
    (
83
      currentPage = page,
×
84
      currentPerPage = perPage,
×
85
      currentStartDate = startDate,
×
86
      currentEndDate = endDate,
×
87
      currentDeviceGroup = deviceGroup,
×
88
      currentType = deploymentType
×
89
    ) => {
90
      const roundedStartDate = Math.round(Date.parse(currentStartDate) / 1000);
×
91
      const roundedEndDate = Math.round(Date.parse(currentEndDate) / 1000);
×
92
      setLoading(true);
×
93
      return dispatch(getDeploymentsByStatus(type, currentPage, currentPerPage, roundedStartDate, roundedEndDate, currentDeviceGroup, currentType))
×
94
        .then(deploymentsAction => {
95
          setLoading(false);
×
96
          clearRetryTimer(type, dispatchedSetSnackbar);
×
97
          const { total, deploymentIds } = deploymentsAction[deploymentsAction.length - 1];
×
98
          if (total && !deploymentIds.length) {
×
99
            return refreshPast(currentPage, currentPerPage, currentStartDate, currentEndDate, currentDeviceGroup);
×
100
          }
101
        })
102
        .catch(err => setRetryTimer(err, 'deployments', `Couldn't load deployments.`, refreshDeploymentsLength, dispatchedSetSnackbar));
×
103
    },
104
    [deploymentType, deviceGroup, dispatch, dispatchedSetSnackbar, endDate, page, perPage, startDate]
105
  );
106

107
  useEffect(() => {
65✔
108
    const roundedStartDate = Math.round(Date.parse(startDate || BEGINNING_OF_TIME) / 1000);
4✔
109
    const roundedEndDate = Math.round(Date.parse(endDate) / 1000);
4✔
110
    setLoading(true);
4✔
111
    dispatch(getDeploymentsByStatus(type, page, perPage, roundedStartDate, roundedEndDate, deviceGroup, deploymentType, true, SORTING_OPTIONS.desc))
4✔
112
      .then(deploymentsAction => {
113
        const deploymentsList = deploymentsAction ? Object.values(deploymentsAction[0].deployments) : [];
3!
114
        if (deploymentsList.length) {
3!
115
          let newStartDate = new Date(deploymentsList[deploymentsList.length - 1].created);
3✔
116
          const { start } = getISOStringBoundaries(newStartDate);
3✔
117
          dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { startDate: startDate || start } }));
3✔
118
        }
119
      })
120
      .finally(() => setLoading(false));
3✔
121
    return () => {
4✔
122
      clearAllRetryTimers(dispatchedSetSnackbar);
4✔
123
    };
124
  }, [deploymentType, deviceGroup, dispatch, dispatchedSetSnackbar, endDate, page, perPage, startDate]);
125

126
  useEffect(() => {
65✔
127
    clearInterval(timer.current);
4✔
128
    timer.current = setInterval(refreshPast, refreshDeploymentsLength);
4✔
129
    // refreshPast();
130
    return () => {
4✔
131
      clearInterval(timer.current);
4✔
132
    };
133
  }, [page, perPage, startDate, endDate, deviceGroup, deploymentType, refreshPast]);
134

135
  useEffect(() => {
65✔
136
    if (!past.length || onboardingState.complete) {
28✔
137
      return;
22✔
138
    }
139
    const pastDeploymentsFailed = past.reduce(
6✔
140
      (accu, item) =>
141
        item.status === 'failed' ||
8✔
142
        (item.statistics?.status &&
143
          item.statistics.status.noartifact + item.statistics.status.failure + item.statistics.status['already-installed'] + item.statistics.status.aborted >
144
            0) ||
145
        accu,
146
      false
147
    );
148
    let onboardingStep = onboardingSteps.DEPLOYMENTS_PAST;
6✔
149
    if (pastDeploymentsFailed) {
6!
150
      onboardingStep = onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE;
×
151
    }
152
    dispatch(advanceOnboarding(onboardingStep));
6✔
153
  }, [dispatch, onboardingState.complete, past]);
154

155
  let onboardingComponent = null;
65✔
156
  if (deploymentsRef.current) {
65✔
157
    const detailsButtons = deploymentsRef.current.getElementsByClassName('MuiButton-contained');
37✔
158
    const left = detailsButtons.length
37!
159
      ? deploymentsRef.current.offsetLeft + detailsButtons[0].offsetLeft + detailsButtons[0].offsetWidth / 2 + 15
160
      : deploymentsRef.current.offsetWidth;
161
    let anchor = { left: deploymentsRef.current.offsetWidth / 2, top: deploymentsRef.current.offsetTop };
37✔
162
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED, onboardingState, {
37✔
163
      anchor,
164
      setSnackbar: dispatchedSetSnackbar
165
    });
166
    onboardingComponent = getOnboardingComponentFor(
37✔
167
      onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE,
168
      onboardingState,
169
      { anchor: { left, top: detailsButtons[0].parentElement.offsetTop + detailsButtons[0].parentElement.offsetHeight } },
170
      onboardingComponent
171
    );
172
  }
173

174
  const onFiltersChange = useCallback(
65✔
175
    ({ endDate, group, startDate, type }) =>
176
      dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, search: group, type, startDate, endDate } })),
4✔
177
    [dispatch]
178
  );
179

180
  const autoCompleteProps = { autoHighlight: true, autoSelect: true, filterSelectedOptions: true, freeSolo: true, handleHomeEndKeys: true };
65✔
181
  return (
65✔
182
    <div className="fadeIn margin-left margin-top-large">
183
      <Filters
184
        initialValues={{ startDate, endDate, group: deviceGroup, type: deploymentType }}
185
        defaultValues={{ startDate: '', endDate: tonight, group: '', type: '' }}
186
        filters={[
187
          {
188
            key: 'group',
189
            title: 'Device group',
190
            Component: ControlledAutoComplete,
191
            componentProps: {
192
              ...autoCompleteProps,
193
              options: groupNames,
194
              renderInput: params => <TextField {...params} label="Target devices" placeholder="Select a group" InputProps={{ ...params.InputProps }} />
66✔
195
            }
196
          },
197
          {
198
            key: 'type',
199
            title: 'Contains Artifact type',
200
            Component: ControlledAutoComplete,
201
            componentProps: {
202
              ...autoCompleteProps,
203
              options: Object.keys(DEPLOYMENT_TYPES),
204
              renderInput: params => <TextField {...params} label="Deployment type" placeholder="Select a type" InputProps={{ ...params.InputProps }} />
66✔
205
            }
206
          },
207
          {
208
            key: 'timeframe',
209
            title: 'Start time',
210
            Component: TimeframePicker,
211
            componentProps: {
212
              tonight
213
            }
214
          }
215
        ]}
216
        onChange={onFiltersChange}
217
      />
218
      <div className="deploy-table-contain">
219
        {/* TODO: fix status retrieval for past deployments to decide what to show here - */}
220
        {!loading && !!past.length && !!onboardingComponent && !isShowingDetails && onboardingComponent}
110!
221
        {!!past.length && (
105✔
222
          <DeploymentsList
223
            {...props}
224
            canConfigure={canConfigure}
225
            canDeploy={canDeploy}
226
            componentClass="margin-left-small"
227
            count={count}
228
            devices={devices}
229
            headers={headers}
230
            idAttribute={idAttribute}
231
            items={past}
232
            loading={loading}
233
            onChangePage={page => dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page } }))}
×
234
            onChangeRowsPerPage={perPage => dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, perPage } }))}
×
235
            page={page}
236
            pageSize={perPage}
237
            rootRef={deploymentsRef}
238
            showPagination
239
            type={type}
240
          />
241
        )}
242
        {!(loading || past.length) && (
154✔
243
          <div className="dashboard-placeholder">
244
            <p>No finished deployments were found.</p>
245
            <p>
246
              Try adjusting the filters, or <a onClick={createClick}>Create a new deployment</a> to get started
247
            </p>
248
            <img src={historyImage} alt="Past" />
249
          </div>
250
        )}
251
      </div>
252
    </div>
253
  );
254
};
255

256
export default Past;
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