• 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

96.92
/src/js/components/deployments/progressChart.js
1
// Copyright 2016 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, useRef, useState } from 'react';
15

16
import { RotateLeftOutlined, Warning as WarningIcon } from '@mui/icons-material';
17
import { Tooltip } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import { mdiDotsHorizontalCircleOutline as QueuedIcon, mdiSleep as SleepIcon } from '@mdi/js';
21
import moment from 'moment';
22
import momentDurationFormatSetup from 'moment-duration-format';
23
import pluralize from 'pluralize';
24

25
import { TIMEOUTS } from '../../constants/appConstants';
26
import { groupDeploymentStats } from '../../helpers';
27
import MaterialDesignIcon from '../common/materialdesignicon';
28
import Time from '../common/time';
29

30
momentDurationFormatSetup(moment);
17✔
31

32
const statusMap = {
17✔
33
  complete: {
34
    icon: <MaterialDesignIcon path={SleepIcon} />,
35
    description: () => 'Complete, awaiting new devices'
×
36
  },
37
  queued: {
38
    icon: <MaterialDesignIcon path={QueuedIcon} />,
39
    description: () => 'Queued to start'
1,728✔
40
  },
41
  paused: { icon: <RotateLeftOutlined fontSize="inherit" />, description: window => `Paused until next window ${window}` }
×
42
};
43

44
const useStyles = makeStyles()(theme => ({
31✔
45
  container: {
46
    backgroundColor: theme.palette.grey[400],
47
    padding: '10px 20px',
48
    borderRadius: theme.spacing(0.5),
49
    justifyContent: 'center',
50
    minHeight: 70,
51
    '.chart-container': { minHeight: 70 },
52
    '.progress-chart': { minHeight: 45 },
53
    '.compact .progress-step, .detailed .progress-step': { minHeight: 45 },
54
    '.progress-step, .detailed .progress-step-total': {
55
      position: 'absolute',
56
      borderRightStyle: 'none'
57
    },
58
    '.progress-step-total .progress-bar': { backgroundColor: theme.palette.grey[50] },
59
    '.progress-step-number': { alignSelf: 'flex-start', marginTop: theme.spacing(-0.5) },
60
    '&.minimal': { padding: 'initial' },
61
    '&.no-background': { background: 'none' },
62
    '&.stepped-progress .progress-step-total': { marginLeft: '-0.25%', width: '100.5%' },
63
    '&.stepped-progress .progress-step-total .progress-bar': {
64
      backgroundColor: theme.palette.background.default,
65
      border: `1px solid ${theme.palette.grey[800]}`,
66
      borderRadius: 2,
67
      height: 12
68
    },
69
    '&.stepped-progress .detailed .progress-step': { minHeight: 20 }
70
  },
71
  dualPanel: {
72
    display: 'grid',
73
    gridTemplateColumns: '2fr 1fr',
74
    gridColumnGap: theme.spacing(2),
75
    '.progress-chart.detailed': {
76
      minHeight: 20,
77
      alignItems: 'center'
78
    }
79
  },
80
  defaultDelimiter: { borderRight: '1px dashed', zIndex: 10 },
81
  phaseDelimiter: {
82
    display: 'grid',
83
    rowGap: 4,
84
    placeItems: 'center',
85
    position: 'absolute',
86
    gridTemplateRows: `20px 1.25rem min-content`,
87
    zIndex: 2,
88
    ['&.compact']: {
89
      gridTemplateRows: '45px 1.25rem min-content'
90
    }
91
  }
92
}));
93

94
export const ProgressChartComponent = ({
17✔
95
  className,
96
  compact,
97
  Footer,
98
  footerProps,
99
  Header,
100
  headerProps,
101
  minimal,
102
  PhaseDelimiter,
103
  PhaseLabel,
104
  phases,
105
  showDelimiter,
106
  Side,
107
  sideProps,
108
  ...remainder
109
}) => {
110
  const { classes } = useStyles();
1,909✔
111
  return (
1,909✔
112
    <div className={`relative ${classes.container} ${minimal ? 'minimal' : ''} ${className}`}>
1,909✔
113
      {!minimal && Header && <Header {...headerProps} />}
2,981✔
114
      <div className={!minimal && Side ? classes.dualPanel : 'chart-container'}>
4,358✔
115
        <div className={`progress-chart relative ${compact ? 'compact' : 'detailed'}`}>
1,909✔
116
          {phases.map((phase, index) => {
117
            const commonProps = { ...remainder, compact, index, phase, classes };
1,915✔
118
            return (
1,915✔
119
              <React.Fragment key={phase.id ?? `deployment-phase-${index}`}>
1,923✔
120
                <div className="progress-step" style={{ left: `${phase.offset}%`, width: `${phase.width}%` }}>
121
                  {!minimal && PhaseLabel && <PhaseLabel {...commonProps} />}
2,473✔
122
                  {!!phase.progressWidth && <div className="progress-bar" style={{ width: `${phase.progressWidth}%`, backgroundColor: '#aaa' }} />}
3,820✔
123
                  <div style={{ display: 'contents' }}>
124
                    <div className="progress-bar green" style={{ width: `${phase.successWidth}%` }} />
125
                    <div className="progress-bar warning" style={{ left: `${phase.successWidth}%`, width: `${phase.failureWidth}%` }} />
126
                  </div>
127
                </div>
128
                {(showDelimiter || PhaseDelimiter) && index !== phases.length - 1 && (
3,844✔
129
                  <>
130
                    {PhaseDelimiter ? (
6!
131
                      <PhaseDelimiter {...commonProps} />
132
                    ) : (
133
                      <div className={`absolute ${classes.defaultDelimiter}`} style={{ left: `${phase.offset}%` }}></div>
134
                    )}
135
                  </>
136
                )}
137
              </React.Fragment>
138
            );
139
          })}
140
          <div className="progress-step relative flexbox progress-step-total">
141
            <div className="progress-bar"></div>
142
          </div>
143
        </div>
144
        {!minimal && Side && <Side compact={compact} {...remainder} {...sideProps} />}
2,979✔
145
      </div>
146
      {!minimal && Footer && <Footer {...footerProps} />}
2,979✔
147
    </div>
148
  );
149
};
150

151
// to display failures per phase we have to approximate the failure count per phase by keeping track of the failures we display in previous phases and
152
// deduct the phase failures from the remainder - so if we have a total of 5 failures reported and are in the 3rd phase, with each phase before reporting
153
// 3 successful deployments -> the 3rd phase should end up with 1 failure so far
154
export const getDisplayablePhases = ({ currentPhase, currentProgressCount, phases, totalDeviceCount, totalFailureCount, totalSuccessCount }) =>
17✔
155
  phases.reduce(
1,907✔
156
    (accu, phase, index) => {
157
      let displayablePhase = { ...phase };
1,907✔
158
      // ongoing phases might not have a device_count yet - so we calculate it
159
      let expectedDeviceCountInPhase = Math.floor((totalDeviceCount / 100) * displayablePhase.batch_size) || displayablePhase.batch_size;
1,907!
160
      // for phases with more successes than phase.device_count or more failures than phase.device_count we have to guess what phase to put them in =>
161
      // because of that we have to limit per phase success/ failure counts to the phase.device_count and split the progress between those with a bias for success,
162
      // therefore we have to track the remaining width and work with it - until we get per phase success & failure information
163
      let leftoverDevices = expectedDeviceCountInPhase;
1,907✔
164
      const possiblePhaseSuccesses = Math.max(Math.min(displayablePhase.device_count, totalSuccessCount - accu.countedSuccesses), 0);
1,907✔
165
      leftoverDevices -= possiblePhaseSuccesses;
1,907✔
166
      const possiblePhaseFailures = Math.max(Math.min(leftoverDevices, totalFailureCount - accu.countedFailures), 0);
1,907✔
167
      leftoverDevices -= possiblePhaseFailures;
1,907✔
168
      const possiblePhaseProgress = Math.max(Math.min(leftoverDevices, currentProgressCount - accu.countedProgress), 0);
1,907✔
169
      // if there are too few devices in a phase to register, fallback to occured deployments, as those have definitely happened
170
      expectedDeviceCountInPhase = Math.max(expectedDeviceCountInPhase, possiblePhaseSuccesses + possiblePhaseProgress + possiblePhaseFailures, 0);
1,907✔
171
      displayablePhase.successWidth = (possiblePhaseSuccesses / expectedDeviceCountInPhase) * 100 || 0;
1,907✔
172
      displayablePhase.failureWidth = (possiblePhaseFailures / expectedDeviceCountInPhase) * 100 || 0;
1,907✔
173
      if (displayablePhase.id === currentPhase.id || leftoverDevices > 0) {
1,907!
174
        displayablePhase.progressWidth = (possiblePhaseProgress / expectedDeviceCountInPhase) * 100;
1,907✔
175
        accu.countedProgress += possiblePhaseProgress;
1,907✔
176
      }
177
      displayablePhase.offset = accu.countedBatch;
1,907✔
178
      const remainingWidth = 100 - (100 / totalDeviceCount) * accu.countedBatch;
1,907✔
179
      displayablePhase.width = index === phases.length - 1 ? remainingWidth : displayablePhase.batch_size;
1,907!
180
      accu.countedBatch += displayablePhase.batch_size;
1,907✔
181
      accu.countedFailures += possiblePhaseFailures;
1,907✔
182
      accu.countedSuccesses += possiblePhaseSuccesses;
1,907✔
183
      accu.displayablePhases.push(displayablePhase);
1,907✔
184
      return accu;
1,907✔
185
    },
186
    { countedBatch: 0, countedFailures: 0, countedSuccesses: 0, countedProgress: 0, displayablePhases: [] }
187
  ).displayablePhases;
188

189
export const DeploymentStatusNotification = ({ status }) => (
17✔
190
  <div className="flexbox center-aligned">
1,728✔
191
    {statusMap[status].icon}
192
    <span className="margin-left-small">{statusMap[status].description()}</span>
193
  </div>
194
);
195

196
const Header = ({ status }) => (
17✔
197
  <>
530✔
198
    {statusMap[status] && (
530!
199
      <span className="flexbox center-aligned small muted">
200
        {statusMap[status].icon}
201
        <span className="margin-left-small">{statusMap[status].description()}</span>
202
      </span>
203
    )}
204
  </>
205
);
206

207
const Footer = ({ currentPhaseIndex, duration, nextPhaseStart, phasesCount }) => (
17✔
208
  <div className="flexbox space-between muted">
530✔
209
    <div>Devices in progress</div>
210
    {phasesCount > 1 && phasesCount > currentPhaseIndex + 1 ? (
1,060!
211
      <div>
212
        <span>Time until next phase: </span>
213
        <Tooltip title={<Time value={nextPhaseStart.toDate()} />} placement="top">
214
          <span>{duration}</span>
215
        </Tooltip>
216
      </div>
217
    ) : (
218
      <div>{`Current phase: ${currentPhaseIndex + 1} of ${phasesCount}`}</div>
219
    )}
220
  </div>
221
);
222

223
const Side = ({ totalFailureCount }) => (
17✔
224
  <div className={`flexbox center-aligned ${totalFailureCount ? 'warning' : 'muted'}`} style={{ justifyContent: 'flex-end' }}>
530!
225
    {!!totalFailureCount && <WarningIcon style={{ fontSize: 16, marginRight: 10 }} />}
530!
226
    {`${totalFailureCount} ${pluralize('failure', totalFailureCount)}`}
227
  </div>
228
);
229

230
export const getDeploymentPhasesInfo = deployment => {
17✔
231
  const { created, device_count = 0, id, phases: deploymentPhases = [], max_devices = 0 } = deployment;
1,907!
232
  const {
233
    inprogress: currentProgressCount,
234
    successes: totalSuccessCount,
235
    failures: totalFailureCount
236
  } = groupDeploymentStats(deployment, deploymentPhases.length < 2);
1,907✔
237
  const totalDeviceCount = Math.max(device_count, max_devices);
1,907✔
238

239
  let phases = deploymentPhases.length ? deploymentPhases : [{ id, device_count: totalSuccessCount, batch_size: 100, start_ts: created }];
1,907!
240
  return {
1,907✔
241
    currentProgressCount,
242
    phases,
243
    reversedPhases: phases.slice().reverse(),
244
    totalDeviceCount,
245
    totalFailureCount,
246
    totalSuccessCount
247
  };
248
};
249

250
export const ProgressDisplay = ({ className = '', deployment, minimal = false, status }) => {
17✔
251
  const [time, setTime] = useState(new Date());
1,899✔
252
  const timer = useRef();
1,899✔
253

254
  useEffect(() => {
1,899✔
255
    timer.current = setInterval(updateTime, TIMEOUTS.oneSecond);
23✔
256
    return () => {
23✔
257
      clearInterval(timer.current);
23✔
258
    };
259
  }, []);
260

261
  const updateTime = () => setTime(new Date());
1,899✔
262

263
  const { reversedPhases, totalFailureCount, phases, ...remainder } = getDeploymentPhasesInfo(deployment);
1,899✔
264

265
  const currentPhase = reversedPhases.find(phase => new Date(phase.start_ts) < time) || phases[0];
1,899!
266
  const currentPhaseIndex = phases.findIndex(phase => phase.id === currentPhase.id);
1,899✔
267
  const nextPhaseStart = phases.length > currentPhaseIndex + 1 ? moment(phases[currentPhaseIndex + 1].start_ts) : moment(time);
1,899!
268

269
  const displayablePhases = getDisplayablePhases({
1,899✔
270
    currentPhase,
271
    totalFailureCount,
272
    phases,
273
    ...remainder
274
  });
275

276
  const momentaryTime = moment(time);
1,899✔
277
  const duration = moment.duration(nextPhaseStart.diff(momentaryTime)).format('d [days] hh [h] mm [m] ss [s]');
1,899✔
278

279
  return (
1,899✔
280
    <ProgressChartComponent
281
      className={className}
282
      Footer={Footer}
283
      footerProps={{ currentPhaseIndex, duration, nextPhaseStart, phasesCount: phases.length }}
284
      Header={Header}
285
      headerProps={{ status }}
286
      minimal={minimal}
287
      phases={displayablePhases}
288
      Side={Side}
289
      sideProps={{ totalFailureCount }}
290
    />
291
  );
292
};
293

294
export default ProgressDisplay;
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