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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

89.06
/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);
18✔
31

32
const statusMap = {
18✔
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'
2,066✔
40
  },
41
  paused: { icon: <RotateLeftOutlined fontSize="inherit" />, description: window => `Paused until next window ${window}` }
×
42
};
43

44
const useStyles = makeStyles()(theme => ({
74✔
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 = ({
18✔
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,085✔
111
  return (
1,085✔
112
    <div className={`relative ${classes.container} ${minimal ? 'minimal' : ''} ${className}`}>
1,085✔
113
      {!minimal && Header && <Header {...headerProps} />}
2,218✔
114
      <div className={!minimal && Side ? classes.dualPanel : 'chart-container'}>
2,739✔
115
        <div className={`progress-chart relative ${compact ? 'compact' : 'detailed'}`}>
1,085✔
116
          {phases.map((phase, index) => {
117
            const commonProps = { ...remainder, compact, index, phase, classes };
1,091✔
118
            return (
1,091✔
119
              <React.Fragment key={phase.id ?? `deployment-phase-${index}`}>
1,101✔
120
                <div className="progress-step" style={{ left: `${phase.offset}%`, width: `${phase.width}%` }}>
121
                  {!minimal && PhaseLabel && <PhaseLabel {...commonProps} />}
1,675✔
122
                  {!!phase.progressWidth && <div className="progress-bar" style={{ width: `${phase.progressWidth}%`, backgroundColor: '#aaa' }} />}
2,170✔
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 && (
2,196✔
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,216✔
145
      </div>
146
      {!minimal && Footer && <Footer {...footerProps} />}
2,216✔
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 }) =>
18✔
155
  phases.reduce(
1,083✔
156
    (accu, phase, index) => {
157
      let displayablePhase = { ...phase };
1,083✔
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,083✔
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,083✔
164
      const possiblePhaseSuccesses = Math.max(Math.min(displayablePhase.device_count, totalSuccessCount - accu.countedSuccesses), 0);
1,083✔
165
      leftoverDevices -= possiblePhaseSuccesses;
1,083✔
166
      const possiblePhaseFailures = Math.max(Math.min(leftoverDevices, totalFailureCount - accu.countedFailures), 0);
1,083✔
167
      leftoverDevices -= possiblePhaseFailures;
1,083✔
168
      const possiblePhaseProgress = Math.max(Math.min(leftoverDevices, currentProgressCount - accu.countedProgress), 0);
1,083✔
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,083✔
171
      displayablePhase.successWidth = (possiblePhaseSuccesses / expectedDeviceCountInPhase) * 100 || 0;
1,083✔
172
      displayablePhase.failureWidth = (possiblePhaseFailures / expectedDeviceCountInPhase) * 100 || 0;
1,083✔
173
      if (displayablePhase.id === currentPhase.id || leftoverDevices > 0) {
1,083!
174
        displayablePhase.progressWidth = (possiblePhaseProgress / expectedDeviceCountInPhase) * 100;
1,083✔
175
        accu.countedProgress += possiblePhaseProgress;
1,083✔
176
      }
177
      displayablePhase.offset = accu.countedBatch;
1,083✔
178
      const remainingWidth = 100 - (100 / totalDeviceCount) * accu.countedBatch;
1,083✔
179
      displayablePhase.width = index === phases.length - 1 ? remainingWidth : displayablePhase.batch_size;
1,083!
180
      accu.countedBatch += displayablePhase.batch_size;
1,083✔
181
      accu.countedFailures += possiblePhaseFailures;
1,083✔
182
      accu.countedSuccesses += possiblePhaseSuccesses;
1,083✔
183
      accu.displayablePhases.push(displayablePhase);
1,083✔
184
      return accu;
1,083✔
185
    },
186
    { countedBatch: 0, countedFailures: 0, countedSuccesses: 0, countedProgress: 0, displayablePhases: [] }
187
  ).displayablePhases;
188

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

196
const Header = ({ status }) => (
18✔
197
  <>
562✔
198
    {statusMap[status] && (
562!
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 }) => (
18✔
208
  <div className="flexbox space-between muted">
562✔
209
    <div>Devices in progress</div>
210
    {phasesCount > 1 && phasesCount > currentPhaseIndex + 1 ? (
1,124!
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 }) => (
18✔
224
  <div className={`flexbox center-aligned ${totalFailureCount ? 'warning' : 'muted'}`} style={{ justifyContent: 'flex-end' }}>
562!
225
    {!!totalFailureCount && <WarningIcon style={{ fontSize: 16, marginRight: 10 }} />}
562!
226
    {`${totalFailureCount} ${pluralize('failure', totalFailureCount)}`}
227
  </div>
228
);
229

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

239
  let phases = deploymentPhases.length ? deploymentPhases : [{ id, device_count: totalSuccessCount, batch_size: totalDeviceCount, start_ts: created }];
1,083!
240
  return {
1,083✔
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 }) => {
18✔
251
  const [time, setTime] = useState(new Date());
1,078✔
252
  const timer = useRef();
1,078✔
253

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

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

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

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

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

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

279
  return (
1,078✔
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