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

mendersoftware / mender-server / 14955

04 May 2026 02:42PM UTC coverage: 48.68% (+0.03%) from 48.651%
14955

push

gitlab-ci

web-flow
Merge pull request #1757 from mineralsfree/MEN-9552

MEN-9552: common Link component

4201 of 5922 branches covered (70.94%)

Branch coverage included in aggregate %.

21 of 23 new or added lines in 11 files covered. (91.3%)

1591 existing lines in 176 files now uncovered.

66471 of 139254 relevant lines covered (47.73%)

5.94 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 type { Deployment } from '@northern.tech/store/deploymentsSlice';
2✔
18
import { groupDeploymentStats } from '@northern.tech/store/utils';
2✔
19
import type { 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(
462✔
46
    (accu, phase, index) => {
2✔
47
      const displayablePhase = { ...phase };
1,322✔
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,322✔
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,322✔
54
      const possiblePhaseSuccesses = Math.max(Math.min(displayablePhase.device_count, totalSuccessCount - accu.countedSuccesses), 0);
1,322✔
55
      leftoverDevices -= possiblePhaseSuccesses;
1,322✔
56
      const possiblePhaseFailures = Math.max(Math.min(leftoverDevices, totalFailureCount - accu.countedFailures), 0);
1,322✔
57
      leftoverDevices -= possiblePhaseFailures;
1,322✔
58
      const possiblePhaseProgress = Math.max(Math.min(leftoverDevices, currentProgressCount - accu.countedProgress), 0);
1,322✔
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,322✔
61
      displayablePhase.successWidth = (possiblePhaseSuccesses / expectedDeviceCountInPhase) * 100 || 0;
1,322✔
62
      displayablePhase.failureWidth = (possiblePhaseFailures / expectedDeviceCountInPhase) * 100 || 0;
1,322✔
63
      if (displayablePhase.id === currentPhase.id || leftoverDevices > 0) {
1,322!
64
        displayablePhase.progressWidth = (possiblePhaseProgress / expectedDeviceCountInPhase) * 100;
1,322✔
65
        accu.countedProgress += possiblePhaseProgress;
1,322✔
66
      }
2✔
67
      displayablePhase.offset = accu.countedBatch;
1,322✔
68
      const remainingWidth = 100 - accu.countedBatch; // countedBatch should be the summarized percentages of the phases so far
1,322✔
69
      displayablePhase.width = index === phases.length - 1 ? remainingWidth : displayablePhase.batch_size;
1,322✔
70
      accu.countedBatch += displayablePhase.batch_size;
1,322✔
71
      accu.countedFailures += possiblePhaseFailures;
1,322✔
72
      accu.countedSuccesses += possiblePhaseSuccesses;
1,322✔
73
      accu.displayablePhases.push(displayablePhase);
1,322✔
74
      return accu;
1,322✔
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;
481✔
81
  const {
2✔
82
    inprogress: currentProgressCount,
2✔
83
    successes: totalSuccessCount,
2✔
84
    failures: totalFailureCount
481✔
85
  } = groupDeploymentStats(deployment, deploymentPhases.length < 2);
2✔
86
  const totalDeviceCount = Math.max(device_count, max_devices);
481✔
87

2✔
88
  const phases = deploymentPhases.length ? deploymentPhases : [{ id, device_count: totalSuccessCount, batch_size: 100, start_ts: created }];
481✔
89
  return {
481✔
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());
462✔
101
  const timer = useRef<ReturnType<typeof setInterval>>();
462✔
102
  const { status } = deployment;
462✔
103

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

2✔
110
  useEffect(() => {
462✔
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);
462✔
118
  const currentPhase = reversedPhases.find(phase => dayjs(phase.start_ts) < time) || phases[0];
1,322✔
119
  const currentPhaseIndex = phases.findIndex(phase => phase.id === currentPhase.id);
462✔
120
  const nextPhaseStart = phases.length > currentPhaseIndex + 1 ? dayjs(phases[currentPhaseIndex + 1].start_ts) : dayjs(time);
462✔
121
  const duration = dayjs.duration(nextPhaseStart.diff(time)).format('DD [days] HH [h] mm [m] ss [s]');
462✔
122

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

2✔
130
  return {
462✔
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