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

mendersoftware / mender-server / 12626

20 Feb 2026 03:03PM UTC coverage: 54.034% (+0.1%) from 53.908%
12626

push

gitlab-ci

web-flow
Merge pull request #1374 from mzedel/men-8459

MEN-8459 - deployments page design alignment (+ dashboard deployment widget and deployment report related changes)

4027 of 5631 branches covered (71.51%)

Branch coverage included in aggregate %.

194 of 244 new or added lines in 29 files covered. (79.51%)

7 existing lines in 2 files now uncovered.

67405 of 126567 relevant lines covered (53.26%)

6.14 hits per line

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

98.77
/frontend/src/js/components/deployments/progress/usePhaseProgress.ts
1
// Copyright 2026 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useEffect, useRef, useState } from 'react';
2✔
15

2✔
16
import { DEPLOYMENT_STATES, TIMEOUTS } from '@northern.tech/store/constants';
2✔
17
import { Deployment } from '@northern.tech/store/deploymentsSlice';
2✔
18
import { groupDeploymentStats } from '@northern.tech/store/utils';
2✔
19
import { DeploymentPhase } from '@northern.tech/types/MenderTypes';
2✔
20
import dayjs from 'dayjs';
2✔
21
import durationDayJs from 'dayjs/plugin/duration';
2✔
22

2✔
23
dayjs.extend(durationDayJs);
17✔
24

2✔
25
interface RolloutPhasesParams {
2✔
26
  currentPhase: { id: string };
2✔
27
  currentProgressCount: number;
2✔
28
  phases: DeploymentPhase[];
2✔
29
  totalDeviceCount: number;
2✔
30
  totalFailureCount: number;
2✔
31
  totalSuccessCount: number;
2✔
32
}
2✔
33

2✔
34
// 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
2✔
35
// 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
2✔
36
// 3 successful deployments -> the 3rd phase should end up with 1 failure so far
2✔
37
export const getDisplayableRolloutPhases = ({
17✔
38
  currentPhase,
2✔
39
  currentProgressCount,
2✔
40
  phases,
2✔
41
  totalDeviceCount,
2✔
42
  totalFailureCount,
2✔
43
  totalSuccessCount
2✔
44
}: RolloutPhasesParams) =>
2✔
45
  phases.reduce(
457✔
46
    (accu, phase, index) => {
2✔
47
      const displayablePhase = { ...phase };
1,309✔
48
      // ongoing phases might not have a device_count yet - so we calculate it
2✔
49
      let expectedDeviceCountInPhase = Math.floor((totalDeviceCount / 100) * displayablePhase.batch_size) || displayablePhase.batch_size;
1,309✔
50
      // 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 =>
2✔
51
      // 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,
2✔
52
      // therefore we have to track the remaining width and work with it - until we get per phase success & failure information
2✔
53
      let leftoverDevices = expectedDeviceCountInPhase;
1,309✔
54
      const possiblePhaseSuccesses = Math.max(Math.min(displayablePhase.device_count, totalSuccessCount - accu.countedSuccesses), 0);
1,309✔
55
      leftoverDevices -= possiblePhaseSuccesses;
1,309✔
56
      const possiblePhaseFailures = Math.max(Math.min(leftoverDevices, totalFailureCount - accu.countedFailures), 0);
1,309✔
57
      leftoverDevices -= possiblePhaseFailures;
1,309✔
58
      const possiblePhaseProgress = Math.max(Math.min(leftoverDevices, currentProgressCount - accu.countedProgress), 0);
1,309✔
59
      // if there are too few devices in a phase to register, fallback to occured deployments, as those have definitely happened
2✔
60
      expectedDeviceCountInPhase = Math.max(expectedDeviceCountInPhase, possiblePhaseSuccesses + possiblePhaseProgress + possiblePhaseFailures, 0);
1,309✔
61
      displayablePhase.successWidth = (possiblePhaseSuccesses / expectedDeviceCountInPhase) * 100 || 0;
1,309✔
62
      displayablePhase.failureWidth = (possiblePhaseFailures / expectedDeviceCountInPhase) * 100 || 0;
1,309✔
63
      if (displayablePhase.id === currentPhase.id || leftoverDevices > 0) {
1,309!
64
        displayablePhase.progressWidth = (possiblePhaseProgress / expectedDeviceCountInPhase) * 100;
1,309✔
65
        accu.countedProgress += possiblePhaseProgress;
1,309✔
66
      }
2✔
67
      displayablePhase.offset = accu.countedBatch;
1,309✔
68
      const remainingWidth = 100 - accu.countedBatch; // countedBatch should be the summarized percentages of the phases so far
1,309✔
69
      displayablePhase.width = index === phases.length - 1 ? remainingWidth : displayablePhase.batch_size;
1,309✔
70
      accu.countedBatch += displayablePhase.batch_size;
1,309✔
71
      accu.countedFailures += possiblePhaseFailures;
1,309✔
72
      accu.countedSuccesses += possiblePhaseSuccesses;
1,309✔
73
      accu.displayablePhases.push(displayablePhase);
1,309✔
74
      return accu;
1,309✔
75
    },
2✔
76
    { countedBatch: 0, countedFailures: 0, countedSuccesses: 0, countedProgress: 0, displayablePhases: [] }
2✔
77
  ).displayablePhases;
2✔
78

2✔
79
export const getDeploymentPhasesInfo = (deployment: Deployment) => {
17✔
80
  const { created, device_count = 0, id, phases: deploymentPhases = [], max_devices = 0 } = deployment;
475✔
81
  const {
2✔
82
    inprogress: currentProgressCount,
2✔
83
    successes: totalSuccessCount,
2✔
84
    failures: totalFailureCount
2✔
85
  } = groupDeploymentStats(deployment, deploymentPhases.length < 2);
475✔
86
  const totalDeviceCount = Math.max(device_count, max_devices);
475✔
87

2✔
88
  const phases = deploymentPhases.length ? deploymentPhases : [{ id, device_count: totalSuccessCount, batch_size: 100, start_ts: created }];
475✔
89
  return {
475✔
90
    currentProgressCount,
2✔
91
    phases,
2✔
92
    reversedPhases: phases.slice().reverse(),
2✔
93
    totalDeviceCount,
2✔
94
    totalFailureCount,
2✔
95
    totalSuccessCount
2✔
96
  };
2✔
97
};
2✔
98

2✔
99
export const usePhaseProgress = (deployment: Deployment) => {
17✔
100
  const [time, setTime] = useState(dayjs());
457✔
101
  const timer = useRef<ReturnType<typeof setInterval>>();
457✔
102
  const { status } = deployment;
457✔
103

2✔
104
  useEffect(() => {
457✔
105
    if (status === DEPLOYMENT_STATES.finished) {
35!
NEW
106
      clearInterval(timer.current);
2✔
107
    }
2✔
108
  }, [status]);
2✔
109

2✔
110
  useEffect(() => {
457✔
111
    timer.current = setInterval(() => setTime(dayjs()), TIMEOUTS.oneSecond);
135✔
112
    return () => {
35✔
113
      clearInterval(timer.current);
35✔
114
    };
2✔
115
  }, []);
2✔
116

2✔
117
  const { reversedPhases, totalFailureCount, phases, ...remainder } = getDeploymentPhasesInfo(deployment);
457✔
118
  const currentPhase = reversedPhases.find(phase => dayjs(phase.start_ts) < time) || phases[0];
1,309✔
119
  const currentPhaseIndex = phases.findIndex(phase => phase.id === currentPhase.id);
457✔
120
  const nextPhaseStart = phases.length > currentPhaseIndex + 1 ? dayjs(phases[currentPhaseIndex + 1].start_ts) : dayjs(time);
457✔
121
  const duration = dayjs.duration(nextPhaseStart.diff(time)).format('DD [days] HH [h] mm [m] ss [s]');
457✔
122

2✔
123
  const displayablePhases = getDisplayableRolloutPhases({
457✔
124
    currentPhase,
2✔
125
    totalFailureCount,
2✔
126
    phases,
2✔
127
    ...remainder
2✔
128
  });
2✔
129

2✔
130
  return {
457✔
131
    currentPhase,
2✔
132
    currentPhaseIndex,
2✔
133
    displayablePhases,
2✔
134
    duration,
2✔
135
    nextPhaseStart,
2✔
136
    phases,
2✔
137
    totalFailureCount
2✔
138
  };
2✔
139
};
2✔
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

© 2026 Coveralls, Inc