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

mendersoftware / mender-server / 14380

21 Apr 2026 07:30AM UTC coverage: 48.309%. Remained the same
14380

Pull #1667

gitlab-ci

mineralsfree
chore(gui): removed dropped property in child tenant creation endpoint

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #1667: Fix/spec alignment

4079 of 5746 branches covered (70.99%)

Branch coverage included in aggregate %.

65303 of 137875 relevant lines covered (47.36%)

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

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

1✔
23
dayjs.extend(durationDayJs);
16✔
24

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

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

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

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

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

1✔
104
  useEffect(() => {
461✔
105
    if (status === DEPLOYMENT_STATES.finished) {
34!
106
      clearInterval(timer.current);
1✔
107
    }
1✔
108
  }, [status]);
1✔
109

1✔
110
  useEffect(() => {
461✔
111
    timer.current = setInterval(() => setTime(dayjs()), TIMEOUTS.oneSecond);
134✔
112
    return () => {
34✔
113
      clearInterval(timer.current);
34✔
114
    };
1✔
115
  }, []);
1✔
116

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

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

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