• 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

85.88
/src/js/components/deployments/deployment-report/phaseprogress.js
1
// Copyright 2021 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, useState } from 'react';
15

16
import { CheckCircle, ErrorRounded, Pause, PlayArrow, Warning as WarningIcon } from '@mui/icons-material';
17
import { Button } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import moment from 'moment';
21
import momentDurationFormatSetup from 'moment-duration-format';
22
import pluralize from 'pluralize';
23

24
import inprogressImage from '../../../../assets/img/pending_status.png';
25
import { deploymentDisplayStates, deploymentSubstates, installationSubstatesMap, pauseMap } from '../../../constants/deploymentConstants';
26
import { getDeploymentState, groupDeploymentStats, statCollector } from '../../../helpers';
27
import Confirm from '../../common/confirm';
28
import { ProgressChartComponent } from '../progressChart';
29

30
const useStyles = makeStyles()(theme => ({
16✔
31
  active: { borderLeftColor: theme.palette.text.primary },
32
  borderColor: { borderLeftStyle: 'solid', borderLeftWidth: 1, height: '100%', zIndex: 1 },
33
  continueButton: { marginRight: theme.spacing(2) },
34
  failureIcon: { fontSize: 16, marginRight: 10 },
35
  inactive: { borderLeftColor: theme.palette.grey[500] },
36
  phaseInfo: { marginBottom: theme.spacing() },
37
  phaseIndex: { margin: theme.spacing(0.5) }
38
}));
39

40
momentDurationFormatSetup(moment);
16✔
41

42
const shortCircuitIndicators = [deploymentSubstates.alreadyInstalled, deploymentSubstates.noartifact];
16✔
43

44
const substateIconMap = {
16✔
45
  finished: { state: 'finished', icon: <CheckCircle fontSize="small" /> },
46
  inprogress: { state: 'inprogress', icon: <img src={inprogressImage} /> },
47
  failed: { state: 'failed', icon: <ErrorRounded fontSize="small" /> },
48
  paused: { state: 'paused', icon: <Pause fontSize="small" /> },
49
  pendingPause: { state: 'pendingPause', icon: <Pause fontSize="small" color="disabled" /> }
50
};
51

52
const stepTotalWidth = 100 / Object.keys(installationSubstatesMap).length;
16✔
53

54
const PhaseDelimiter = ({ classes, compact, index, phase = {} }) => {
16!
55
  const { classes: localClasses } = useStyles();
6✔
56
  const { status, substate: phaseSubstate } = phase;
6✔
57
  const isActive = status === substateIconMap.inprogress.state;
6✔
58
  const substate = phaseSubstate.done;
6✔
59

60
  const offset = `${stepTotalWidth * (index + 1) - stepTotalWidth / 2}%`;
6✔
61
  return (
6✔
62
    <div className={`${classes.phaseDelimiter} ${compact ? 'compact' : ''}`} style={{ left: offset, width: `${stepTotalWidth}%` }}>
6✔
63
      <div className={`${localClasses.borderColor} ${isActive ? localClasses.active : localClasses.inactive}`} />
6!
64
      {substateIconMap[status] ? substateIconMap[status].icon : <div />}
6✔
65
      {compact ? <div /> : <div className="capitalized slightly-smaller">{substate}</div>}
6✔
66
    </div>
67
  );
68
};
69

70
const determineSubstateStatus = (successes, failures, totalDeviceCount, pauseIndicator, hasPauseDefined) => {
16✔
71
  let status;
72
  if (successes === totalDeviceCount) {
8✔
73
    status = substateIconMap.finished.state;
2✔
74
  } else if (failures === totalDeviceCount) {
6!
75
    status = substateIconMap.failed.state;
×
76
  } else if (pauseIndicator) {
6!
77
    status = substateIconMap.paused.state;
×
78
  } else if (successes || failures) {
6!
79
    status = substateIconMap.inprogress.state;
×
80
  } else if (hasPauseDefined) {
6!
81
    status = substateIconMap.pendingPause.state;
×
82
  }
83
  return status;
8✔
84
};
85

86
const getDisplayablePhases = ({ pauseMap, deployment, stepWidth, substatesMap, totalDeviceCount }) => {
16✔
87
  const { statistics = {}, update_control_map = {} } = deployment;
2!
88
  const { status: stats = {} } = statistics;
2!
89
  const currentPauseState = Object.keys(pauseMap)
2✔
90
    .reverse()
91
    .find(key => stats[key] > 0);
6✔
92
  return Object.values(substatesMap).reduce(
2✔
93
    (accu, substate, index) => {
94
      let successes = statCollector(substate.successIndicators, stats);
8✔
95
      let failures = statCollector(substate.failureIndicators, stats);
8✔
96
      if (
8!
97
        !currentPauseState ||
8!
98
        index <= Object.keys(pauseMap).indexOf(currentPauseState) ||
99
        (index && accu.displayablePhases[index - 1].failures + accu.displayablePhases[index - 1].success === totalDeviceCount)
100
      ) {
101
        failures = accu.displayablePhases[index - 1]?.failures || failures;
8✔
102
        successes = successes + accu.shortCutSuccesses;
8✔
103
      }
104
      successes = Math.min(totalDeviceCount, successes);
8✔
105
      failures = Math.min(totalDeviceCount - successes, failures);
8✔
106
      const successWidth = (successes / totalDeviceCount) * 100 || 0;
8✔
107
      const failureWidth = (failures / totalDeviceCount) * 100 || 0;
8✔
108
      const { states = {} } = update_control_map;
8✔
109
      const hasPauseDefined = states[substate.pauseConfigurationIndicator]?.action === 'pause';
8✔
110
      const status = determineSubstateStatus(successes, failures, totalDeviceCount, !!stats[substate.pauseIndicator], hasPauseDefined);
8✔
111
      accu.displayablePhases.push({ substate, successes, failures, offset: stepWidth * index, width: stepWidth, successWidth, failureWidth, status });
8✔
112
      return accu;
8✔
113
    },
114
    { displayablePhases: [], shortCutSuccesses: statCollector(shortCircuitIndicators, stats) }
115
  ).displayablePhases;
116
};
117

118
const statusMap = {
16✔
119
  inprogress: <PlayArrow fontSize="inherit" />,
120
  paused: <Pause fontSize="inherit" />
121
};
122

123
const Header = ({ device_count, failures, totalDeviceCount, showDetails, status }) => {
16✔
124
  const { classes } = useStyles();
2✔
125
  return showDetails ? (
2✔
126
    <>
127
      <div className={`flexbox space-between ${classes.phaseInfo}`}>
128
        {statusMap[status] ? statusMap[status] : <div />}
1!
129
        <div className="flexbox center-aligned">
130
          {!!failures && <WarningIcon className={classes.failureIcon} />}
1!
131
          {`${failures} ${pluralize('failure', failures)}`}
132
        </div>
133
      </div>
134
      <div className={`muted slightly-smaller ${classes.phaseIndex}`}>Phase 1 of 1</div>
135
    </>
136
  ) : (
137
    <>
138
      Phase 1: {Math.round((device_count / totalDeviceCount || 0) * 100)}% ({device_count} {pluralize('device', device_count)})
1!
139
    </>
140
  );
141
};
142

143
export const PhaseProgressDisplay = ({ className = '', deployment, showDetails = true, status, ...remainder }) => {
16✔
144
  const { failures } = groupDeploymentStats(deployment);
2✔
145

146
  const { device_count = 0, max_devices = 0 } = deployment;
2!
147
  const totalDeviceCount = Math.max(device_count, max_devices);
2✔
148

149
  const displayablePhases = getDisplayablePhases({ deployment, pauseMap, stepWidth: stepTotalWidth, substatesMap: installationSubstatesMap, totalDeviceCount });
2✔
150

151
  return (
2✔
152
    <ProgressChartComponent
153
      className={`flexbox column stepped-progress ${showDetails ? '' : 'no-background'} ${className}`}
2✔
154
      phases={displayablePhases}
155
      PhaseDelimiter={PhaseDelimiter}
156
      Header={Header}
157
      headerProps={{ device_count, failures, showDetails, status, totalDeviceCount }}
158
      {...remainder}
159
    />
160
  );
161
};
162

163
const confirmationStyle = {
16✔
164
  justifyContent: 'flex-start',
165
  paddingLeft: 100
166
};
167

168
const PhaseLabel = ({ classes, phase }) => <div className={`capitalized progress-step-number ${classes.progressStepNumber}`}>{phase.substate.title}</div>;
16✔
169

170
export const PhaseProgress = ({ className = '', deployment = {}, onAbort, onUpdateControlChange }) => {
16!
171
  const { classes } = useStyles();
1✔
172
  const [shouldContinue, setShouldContinue] = useState(false);
1✔
173
  const [shouldAbort, setShouldAbort] = useState(false);
1✔
174
  const [isLoading, setIsLoading] = useState(false);
1✔
175

176
  const { id, statistics = {}, update_control_map = {} } = deployment;
1!
177
  const { status: stats = {} } = statistics;
1!
178
  const { states = {} } = update_control_map;
1✔
179
  const { failures: totalFailureCount, paused: totalPausedCount } = groupDeploymentStats(deployment);
1✔
180

181
  const status = getDeploymentState(deployment);
1✔
182
  const currentPauseState = Object.keys(pauseMap)
1✔
183
    .reverse()
184
    .find(key => stats[key] > 0);
3✔
185

186
  useEffect(() => {
1✔
187
    if (!isLoading) {
1!
188
      return;
1✔
189
    }
190
    setIsLoading(false);
×
191
  }, [isLoading, status]);
192

193
  const onAbortClick = () => {
1✔
194
    setShouldAbort(false);
×
195
    onAbort(id);
×
196
  };
197

198
  const onContinueClick = () => {
1✔
199
    setIsLoading(true);
×
200
    setShouldContinue(false);
×
201
    onUpdateControlChange({ states: { [pauseMap[currentPauseState].followUp]: { action: 'continue' } } });
×
202
  };
203

204
  const isPaused = status === deploymentDisplayStates.paused;
1✔
205
  const canContinue = isPaused && states[pauseMap[currentPauseState].followUp];
1!
206
  const disableContinuationButtons = isLoading || (canContinue && states[pauseMap[currentPauseState].followUp].action !== 'pause');
1!
207
  return (
1✔
208
    <div className={`flexbox column ${className}`}>
209
      <PhaseProgressDisplay compact deployment={deployment} PhaseLabel={PhaseLabel} showDetails={false} status={status} />
210
      <div className="margin-top">
211
        Deployment is <span className="uppercased">{status}</span> with {totalFailureCount} {pluralize('failure', totalFailureCount)}
212
        {isPaused && !canContinue && ` - waiting for an action on the ${pluralize('device', totalPausedCount)} to continue`}
1!
213
      </div>
214
      {canContinue && (
1!
215
        <div className="margin-top margin-bottom relative">
216
          {shouldContinue && (
×
217
            <Confirm
218
              type="deploymentContinuation"
219
              classes="confirmation-overlay"
220
              action={onContinueClick}
221
              cancel={() => setShouldContinue(false)}
×
222
              style={confirmationStyle}
223
            />
224
          )}
225
          {shouldAbort && (
×
226
            <Confirm
227
              type="deploymentAbort"
228
              classes="confirmation-overlay"
229
              action={onAbortClick}
230
              cancel={() => setShouldAbort(false)}
×
231
              style={confirmationStyle}
232
            />
233
          )}
234
          <Button color="primary" disabled={disableContinuationButtons} onClick={setShouldContinue} variant="contained" className={classes.continueButton}>
235
            Continue
236
          </Button>
237
          <Button disabled={disableContinuationButtons} onClick={setShouldAbort}>
238
            Abort
239
          </Button>
240
        </div>
241
      )}
242
    </div>
243
  );
244
};
245

246
export default PhaseProgress;
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