• 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

84.62
/src/js/components/deployments/deployments.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

15
/* eslint-disable react-hooks/exhaustive-deps */
16
import React, { useCallback, useEffect, useRef, useState } from 'react';
17
import { useDispatch, useSelector } from 'react-redux';
18
import { Link, useNavigate } from 'react-router-dom';
19

20
import { Button, Tab, Tabs } from '@mui/material';
21

22
import { setSnackbar } from '../../actions/appActions';
23
import { abortDeployment, setDeploymentsState } from '../../actions/deploymentActions';
24
import { getDynamicGroups, getGroups } from '../../actions/deviceActions';
25
import { advanceOnboarding } from '../../actions/onboardingActions';
26
import { DEPLOYMENT_ROUTES, DEPLOYMENT_STATES, listDefaultsByState } from '../../constants/deploymentConstants';
27
import { ALL_DEVICES } from '../../constants/deviceConstants';
28
import { onboardingSteps } from '../../constants/onboardingConstants';
29
import { getISOStringBoundaries } from '../../helpers';
30
import { getDevicesById, getGroupsByIdWithoutUngrouped, getIsEnterprise, getOnboardingState, getReleasesById, getUserCapabilities } from '../../selectors';
31
import { useLocationParams } from '../../utils/liststatehook';
32
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
33
import useWindowSize from '../../utils/resizehook';
34
import CreateDeployment from './createdeployment';
35
import Progress from './inprogressdeployments';
36
import Past from './pastdeployments';
37
import Report from './report';
38
import Scheduled from './scheduleddeployments';
39

40
const routes = {
6✔
41
  [DEPLOYMENT_ROUTES.active.key]: {
42
    ...DEPLOYMENT_ROUTES.active,
43
    component: Progress
44
  },
45
  [DEPLOYMENT_ROUTES.scheduled.key]: {
46
    ...DEPLOYMENT_ROUTES.scheduled,
47
    component: Scheduled
48
  },
49
  [DEPLOYMENT_ROUTES.finished.key]: {
50
    ...DEPLOYMENT_ROUTES.finished,
51
    component: Past
52
  }
53
};
54

55
export const defaultRefreshDeploymentsLength = 30000;
6✔
56

57
export const Deployments = () => {
6✔
58
  const groupsById = useSelector(getGroupsByIdWithoutUngrouped);
197✔
59
  const devicesById = useSelector(getDevicesById);
196✔
60
  const isEnterprise = useSelector(getIsEnterprise);
196✔
61
  const onboardingState = useSelector(getOnboardingState);
196✔
62
  const pastCount = useSelector(state => state.deployments.byStatus.finished.total);
491✔
63
  const releases = useSelector(getReleasesById);
196✔
64
  const selectionState = useSelector(state => state.deployments.selectionState);
491✔
65
  const userCapabilities = useSelector(getUserCapabilities);
196✔
66
  const dispatch = useDispatch();
196✔
67

68
  const [deploymentObject, setDeploymentObject] = useState({});
196✔
69
  // eslint-disable-next-line no-unused-vars
70
  const size = useWindowSize();
196✔
71
  const tabsRef = useRef();
196✔
72
  const isInitialized = useRef(false);
196✔
73
  const navigate = useNavigate();
196✔
74
  const { reportType, showCreationDialog: createDialog, showReportDialog: reportDialog, state } = selectionState.general;
196✔
75
  const { canDeploy, canReadReleases } = userCapabilities;
196✔
76

77
  const [date] = useState(getISOStringBoundaries(new Date()));
196✔
78
  const { start: today, end: tonight } = date;
196✔
79

80
  const [locationParams, setLocationParams] = useLocationParams('deployments', { today, tonight, defaults: listDefaultsByState });
196✔
81

82
  useEffect(() => {
196✔
83
    if (!isInitialized.current) {
54✔
84
      return;
5✔
85
    }
86
    setLocationParams({ deploymentObject, pageState: selectionState });
49✔
87
  }, [
88
    JSON.stringify(deploymentObject),
89
    selectionState.selectedId,
90
    selectionState.general.state,
91
    selectionState.general.showCreationDialog,
92
    selectionState.general.showReportDialog,
93
    selectionState.general.reportType,
94
    selectionState[DEPLOYMENT_STATES.finished].endDate,
95
    selectionState[DEPLOYMENT_STATES.finished].search,
96
    selectionState[DEPLOYMENT_STATES.finished].startDate,
97
    selectionState[DEPLOYMENT_STATES.finished].page,
98
    selectionState[DEPLOYMENT_STATES.finished].perPage,
99
    selectionState[DEPLOYMENT_STATES.finished].type,
100
    selectionState[DEPLOYMENT_STATES.inprogress].page,
101
    selectionState[DEPLOYMENT_STATES.inprogress].perPage,
102
    selectionState[DEPLOYMENT_STATES.pending].page,
103
    selectionState[DEPLOYMENT_STATES.pending].perPage,
104
    setLocationParams
105
  ]);
106

107
  useEffect(() => {
196✔
108
    dispatch(getGroups());
5✔
109
    if (isEnterprise) {
5✔
110
      dispatch(getDynamicGroups());
2✔
111
    }
112
    const { deploymentObject = {}, id: selectedId = [], ...remainder } = locationParams;
5!
113
    const { devices: selectedDevices = [], release: releaseName } = deploymentObject;
5✔
114
    const release = releaseName ? { ...(releases[releaseName] ?? { name: releaseName }) } : undefined;
5!
115
    const devices = selectedDevices.length ? selectedDevices.map(device => ({ ...device, ...devicesById[device.id] })) : [];
5!
116
    setDeploymentObject({ devices, release, releaseSelectionLocked: !!release });
5✔
117
    dispatch(setDeploymentsState({ selectedId: selectedId[0], ...remainder }));
5✔
118
    isInitialized.current = true;
5✔
119
  }, [dispatch, isEnterprise]);
120

121
  const retryDeployment = (deployment, deploymentDeviceIds) => {
196✔
122
    const { artifact_name, name, update_control_map = {} } = deployment;
×
123
    const release = releases[artifact_name];
×
124
    const enterpriseSettings = isEnterprise
×
125
      ? {
126
          phases: [{ batch_size: 100, start_ts: undefined, delay: 0 }],
127
          update_control_map: { states: update_control_map.states || {} }
×
128
        }
129
      : {};
130
    const targetDevicesConfig = name === ALL_DEVICES || groupsById[name] ? { group: name } : { devices: [devicesById[name]] };
×
131
    const deploymentObject = {
×
132
      deploymentDeviceIds,
133
      release,
134
      deploymentDeviceCount: deploymentDeviceIds.length,
135
      ...targetDevicesConfig,
136
      ...enterpriseSettings
137
    };
138
    setDeploymentObject(deploymentObject);
×
139
    dispatch(setDeploymentsState({ general: { showCreationDialog: true, showReportDialog: false } }));
×
140
  };
141

142
  const onScheduleSubmit = () => {
196✔
143
    dispatch(setDeploymentsState({ general: { showCreationDialog: false, showReportDialog: false } }));
2✔
144
    setDeploymentObject({});
2✔
145
    // successfully retrieved new deployment
146
    if (routes.active.key !== state) {
2✔
147
      navigate(routes.active.route);
1✔
148
      changeTab(undefined, routes.active.key);
1✔
149
    }
150
  };
151

152
  const onAbortDeployment = id =>
196✔
153
    dispatch(abortDeployment(id)).then(() => {
×
154
      dispatch(setDeploymentsState({ general: { showCreationDialog: false, showReportDialog: false } }));
×
155
      return Promise.resolve();
×
156
    });
157

158
  const changeTab = (_, tabIndex) => {
196✔
159
    dispatch(setDeploymentsState({ general: { state: tabIndex } }));
5✔
160
    dispatch(setSnackbar(''));
5✔
161
    if (pastCount && !onboardingState.complete) {
5!
162
      dispatch(advanceOnboarding(onboardingSteps.DEPLOYMENTS_PAST));
×
163
    }
164
  };
165

166
  const showReport = (reportType, selectedId) => {
196✔
167
    if (!onboardingState.complete) {
1!
168
      dispatch(advanceOnboarding(onboardingSteps.DEPLOYMENTS_INPROGRESS));
×
169
    }
170
    dispatch(setDeploymentsState({ general: { reportType, showCreationDialog: false, showReportDialog: true }, selectedId }));
1✔
171
  };
172

173
  const closeReport = () => dispatch(setDeploymentsState({ general: { reportType: undefined, showReportDialog: false }, selectedId: undefined }));
196✔
174

175
  const onCreationDismiss = () => {
196✔
176
    dispatch(setDeploymentsState({ general: { showCreationDialog: false } }));
1✔
177
    setDeploymentObject({});
1✔
178
  };
179

180
  const onCreationShow = () => dispatch(setDeploymentsState({ general: { showCreationDialog: true } }));
196✔
181

182
  const setDeploymentSettings = useCallback(change => setDeploymentObject(current => ({ ...current, ...change })), []);
196✔
183

184
  let onboardingComponent = null;
196✔
185
  // the pastCount prop is needed to trigger the rerender as the change in past deployments would otherwise not be noticed on this view
186
  if (pastCount && tabsRef.current && !reportDialog) {
196✔
187
    const tabs = tabsRef.current.getElementsByClassName('MuiTab-root');
156✔
188
    const finishedTab = tabs[tabs.length - 1];
156✔
189
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST, onboardingState, {
156✔
190
      anchor: {
191
        left: tabsRef.current.offsetLeft + tabsRef.current.offsetWidth - finishedTab.offsetWidth / 2,
192
        top: tabsRef.current.parentElement.offsetTop + finishedTab.offsetHeight
193
      }
194
    });
195
  }
196

197
  const ComponentToShow = routes[state].component;
196✔
198
  return (
196✔
199
    <>
200
      <div className="margin-left-small margin-top" style={{ maxWidth: '80vw' }}>
201
        <div className="flexbox space-between">
202
          <Tabs value={state} onChange={changeTab} ref={tabsRef}>
203
            {Object.values(routes).map(route => (
204
              <Tab component={Link} key={route.route} label={route.title} to={route.route} value={route.key} />
588✔
205
            ))}
206
          </Tabs>
207
          {canDeploy && canReadReleases && (
588✔
208
            <Button color="secondary" variant="contained" onClick={onCreationShow} style={{ height: '100%' }}>
209
              Create a deployment
210
            </Button>
211
          )}
212
        </div>
213
        <ComponentToShow abort={onAbortDeployment} createClick={onCreationShow} openReport={showReport} isShowingDetails={reportDialog} />
214
      </div>
215
      {reportDialog && <Report abort={onAbortDeployment} onClose={closeReport} retry={retryDeployment} type={reportType} />}
198✔
216
      {createDialog && (
327✔
217
        <CreateDeployment
218
          onDismiss={onCreationDismiss}
219
          deploymentObject={deploymentObject}
220
          onScheduleSubmit={onScheduleSubmit}
221
          setDeploymentSettings={setDeploymentSettings}
222
        />
223
      )}
224
      {!reportDialog && onboardingComponent}
390✔
225
    </>
226
  );
227
};
228

229
export default Deployments;
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