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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

93.44
/frontend/src/js/components/deployments/Report.tsx
1
// Copyright 2015 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 { useCallback, useEffect, useRef, useState } from 'react';
2✔
15
import { useDispatch, useSelector } from 'react-redux';
2✔
16

2✔
17
// material ui
2✔
18
import { Block as BlockIcon, CheckCircleOutline as CheckCircleOutlineIcon, Refresh as RefreshIcon } from '@mui/icons-material';
2✔
19
import { Button, Divider, Drawer, Tooltip } from '@mui/material';
2✔
20
import { makeStyles } from 'tss-react/mui';
2✔
21

2✔
22
import ConfigurationObject from '@northern.tech/common-ui/ConfigurationObject';
2✔
23
import Confirm from '@northern.tech/common-ui/Confirm';
2✔
24
import { DrawerTitle } from '@northern.tech/common-ui/DrawerTitle';
2✔
25
import LinedHeader from '@northern.tech/common-ui/LinedHeader';
2✔
26
import storeActions from '@northern.tech/store/actions';
2✔
27
import { AUDIT_LOGS_TYPES, DEPLOYMENT_STATES, DEPLOYMENT_TYPES, TIMEOUTS, deploymentStatesToSubstates, onboardingSteps } from '@northern.tech/store/constants';
2✔
28
import {
2✔
29
  getDeploymentRelease,
2✔
30
  getDevicesById,
2✔
31
  getFeatures,
2✔
32
  getIdAttribute,
2✔
33
  getIsPreview,
2✔
34
  getOnboardingState,
2✔
35
  getSelectedDeploymentData,
2✔
36
  getTenantCapabilities,
2✔
37
  getUserCapabilities
2✔
38
} from '@northern.tech/store/selectors';
2✔
39
import { getAuditLogs, getDeploymentDevices, getDeviceLog, getRelease, getSingleDeployment, updateDeploymentControlMap } from '@northern.tech/store/thunks';
2✔
40
import { statCollector } from '@northern.tech/store/utils';
2✔
41
import { toggle } from '@northern.tech/utils/helpers';
2✔
42
import copy from 'copy-to-clipboard';
2✔
43

2✔
44
import { getOnboardingComponentFor } from '../../utils/onboardingManager';
2✔
45
import DeploymentStatus, { DeploymentPhaseNotification } from './deployment-report/DeploymentStatus';
2✔
46
import DeviceList from './deployment-report/DeviceList';
2✔
47
import LogDialog from './deployment-report/Log';
2✔
48
import DeploymentOverview from './deployment-report/Overview';
2✔
49
import RolloutSchedule from './deployment-report/RolloutSchedule';
2✔
50

2✔
51
const { setSnackbar } = storeActions;
7✔
52

2✔
53
const useStyles = makeStyles()(theme => ({
12✔
54
  divider: { marginTop: theme.spacing(2) },
2✔
55
  header: {
2✔
56
    ['&.dashboard-header span']: {
2✔
57
      backgroundColor: theme.palette.background.paper,
2✔
58
      backgroundImage: 'linear-gradient(rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.15))'
2✔
59
    }
2✔
60
  }
2✔
61
}));
2✔
62

2✔
63
export const DeploymentAbortButton = ({ abort, deployment }) => {
7✔
64
  const [aborting, setAborting] = useState(false);
19✔
65

2✔
66
  const toggleAborting = () => setAborting(toggle);
19✔
67

2✔
68
  return aborting ? (
19!
69
    <Confirm cancel={toggleAborting} action={() => abort(deployment.id)} type="abort" />
2✔
70
  ) : (
2✔
71
    <Tooltip
2✔
72
      title="Devices that have not yet started the deployment will not start the deployment.&#10;Devices that have already completed the deployment are not affected by the abort.&#10;Devices that are in the middle of the deployment at the time of abort will finish deployment normally, but will perform a rollback."
2✔
73
      placement="bottom"
2✔
74
    >
2✔
75
      <Button color="secondary" startIcon={<BlockIcon fontSize="small" />} onClick={toggleAborting}>
2✔
76
        {deployment.filters?.length ? 'Stop' : 'Abort'} deployment
2!
77
      </Button>
2✔
78
    </Tooltip>
2✔
79
  );
2✔
80
};
2✔
81

2✔
82
export const DeploymentReport = ({ abort, onClose, past, retry, type, open }) => {
7✔
83
  const [deviceId, setDeviceId] = useState('');
236✔
84
  const rolloutSchedule = useRef();
236✔
85
  const timer = useRef();
236✔
86
  const onboardingTooltipAnchor = useRef();
236✔
87
  const { classes } = useStyles();
236✔
88
  const dispatch = useDispatch();
236✔
89
  const { deployment, selectedDevices } = useSelector(getSelectedDeploymentData);
236✔
90
  const devicesById = useSelector(getDevicesById);
236✔
91
  const idAttribute = useSelector(getIdAttribute);
236✔
92
  const release = useSelector(getDeploymentRelease);
236✔
93
  const { hasAiEnabled, isHosted } = useSelector(getFeatures);
236✔
94
  const isPreview = useSelector(getIsPreview);
236✔
95
  const tenantCapabilities = useSelector(getTenantCapabilities);
236✔
96
  const userCapabilities = useSelector(getUserCapabilities);
236✔
97
  const onboardingState = useSelector(getOnboardingState);
236✔
98
  // we can't filter by auditlog action via the api, so
2✔
99
  // - fall back to the following filter
2✔
100
  // - hope the deployment creation event is retrieved with the call to auditlogs api on report open
2✔
101
  // - otherwise no creator will be shown
2✔
102
  const { actor = {} } =
236✔
103
    useSelector(state =>
2✔
104
      state.organization.auditlog.events.find(event => event.object.id === state.deployments.selectionState.selectedId && event.action === 'create')
1,664!
105
    ) || {};
2✔
106
  const creator = actor.email;
236✔
107

2✔
108
  const { canAuditlog } = userCapabilities;
236✔
109
  const { hasAuditlogs } = tenantCapabilities;
236✔
110
  const { devices = {}, device_count = 0, totalDeviceCount: totalDevices, statistics = {}, type: deploymentType } = deployment;
236✔
111
  const { status: stats = {} } = statistics;
236✔
112
  const totalDeviceCount = totalDevices ?? device_count;
236✔
113
  const canAi = isHosted && (isPreview || hasAiEnabled);
236!
114

2✔
115
  const refreshDeployment = useCallback(() => {
236✔
116
    if (!deployment.id) {
3!
117
      return;
2✔
118
    }
2✔
119
    return dispatch(getSingleDeployment(deployment.id));
3✔
120
  }, [deployment.id, dispatch]);
2✔
121

2✔
122
  useEffect(() => {
236✔
123
    if (!deployment.id) {
15✔
124
      return;
7✔
125
    }
2✔
126
    clearInterval(timer.current);
10✔
127
    const now = new Date();
10✔
128
    now.setSeconds(now.getSeconds() + TIMEOUTS.refreshDefault / TIMEOUTS.oneSecond);
10✔
129
    if (!deployment.finished || new Date(deployment.finished) > now) {
10!
130
      timer.current = past ? null : setInterval(refreshDeployment, TIMEOUTS.fiveSeconds);
10!
131
    }
2✔
132
    if ((deployment.type === DEPLOYMENT_TYPES.software || !release.device_types_compatible.length) && deployment.artifact_name) {
10✔
133
      dispatch(getRelease(deployment.artifact_name));
4✔
134
    }
2✔
135
    if (hasAuditlogs && canAuditlog) {
10!
136
      dispatch(
2✔
137
        getAuditLogs({
2✔
138
          page: 1,
2✔
139
          perPage: 100,
2✔
140
          startDate: undefined,
2✔
141
          endDate: undefined,
2✔
142
          user: undefined,
2✔
143
          type: AUDIT_LOGS_TYPES.find(item => item.value === 'deployment'),
2✔
144
          detail: deployment.name
2✔
145
        })
2✔
146
      );
2✔
147
    }
2✔
148
    return () => {
10✔
149
      clearInterval(timer.current);
10✔
150
    };
2✔
151
  }, [
2✔
152
    canAuditlog,
2✔
153
    deployment.artifact_name,
2✔
154
    deployment.finished,
2✔
155
    deployment.id,
2✔
156
    deployment.name,
2✔
157
    deployment.status,
2✔
158
    deployment.type,
2✔
159
    dispatch,
2✔
160
    hasAuditlogs,
2✔
161
    past,
2✔
162
    refreshDeployment,
2✔
163
    release.device_types_compatible.length
2✔
164
  ]);
2✔
165

2✔
166
  useEffect(() => {
236✔
167
    const progressCount =
15✔
168
      statCollector(deploymentStatesToSubstates.paused, stats) +
2✔
169
      statCollector(deploymentStatesToSubstates.pending, stats) +
2✔
170
      statCollector(deploymentStatesToSubstates.inprogress, stats);
2✔
171

2✔
172
    if (!!device_count && progressCount <= 0 && timer.current) {
15✔
173
      // if no more devices in "progress" statuses, deployment has finished, stop counter
2✔
174
      clearInterval(timer.current);
3✔
175
      timer.current = setTimeout(refreshDeployment, TIMEOUTS.oneSecond);
3✔
176
      return () => {
3✔
177
        clearTimeout(timer.current);
3✔
178
      };
2✔
179
    }
2✔
180
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
181
  }, [deployment.id, device_count, JSON.stringify(stats), refreshDeployment]);
2✔
182

2✔
183
  const scrollToBottom = () => rolloutSchedule.current?.scrollIntoView({ behavior: 'smooth' });
236✔
184

2✔
185
  const viewLog = useCallback(
236✔
186
    id => dispatch(getDeviceLog({ deploymentId: deployment.id, deviceId: id })).then(() => setDeviceId(id)),
6✔
187
    [deployment.id, dispatch]
2✔
188
  );
2✔
189

2✔
190
  const copyLinkToClipboard = () => {
236✔
191
    const location = window.location.href.substring(0, window.location.href.indexOf('/deployments') + '/deployments'.length);
2✔
192
    copy(`${location}?open=true&id=${deployment.id}`);
2✔
193
    dispatch(setSnackbar('Link copied to clipboard'));
2✔
194
  };
2✔
195

2✔
196
  const finished = deployment.finished || deployment.status === DEPLOYMENT_STATES.finished;
236✔
197
  const isConfigurationDeployment = deploymentType === DEPLOYMENT_TYPES.configuration;
236✔
198
  let config = {};
236✔
199
  if (isConfigurationDeployment) {
236!
200
    try {
2✔
201
      config = JSON.parse(atob(deployment.configuration));
2✔
202
    } catch {
2✔
203
      config = {};
2✔
204
    }
2✔
205
  }
2✔
206

2✔
207
  const onUpdateControlChange = (updatedMap = {}) => {
236!
208
    const { id, update_control_map = {} } = deployment;
2!
209
    const { states } = update_control_map;
2✔
210
    const { states: updatedStates } = updatedMap;
2✔
211
    dispatch(updateDeploymentControlMap({ deploymentId: id, updateControlMap: { states: { ...states, ...updatedStates } } }));
2✔
212
  };
2✔
213

2✔
214
  const props = {
236✔
215
    canAi,
2✔
216
    deployment,
2✔
217
    getDeploymentDevices: useCallback((...args) => dispatch(getDeploymentDevices(...args)), [dispatch]),
9✔
218
    idAttribute,
2✔
219
    selectedDevices,
2✔
220
    userCapabilities,
2✔
221
    totalDeviceCount,
2✔
222
    viewLog
2✔
223
  };
2✔
224
  let onboardingComponent = null;
236✔
225
  if (!onboardingState.complete && onboardingTooltipAnchor.current && finished) {
236!
226
    const anchor = {
2✔
227
      left: onboardingTooltipAnchor.current.offsetLeft + onboardingTooltipAnchor.current.offsetWidth + 55,
2✔
228
      top: onboardingTooltipAnchor.current.offsetTop + onboardingTooltipAnchor.current.offsetHeight / 2 + 15
2✔
229
    };
2✔
230
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_COMPLETED, onboardingState, { anchor });
2✔
231
  }
2✔
232

2✔
233
  return (
236✔
234
    <Drawer anchor="right" open={open} onClose={onClose} PaperProps={{ style: { minWidth: '75vw' } }}>
2✔
235
      {!!onboardingComponent && onboardingComponent}
2!
236
      <DrawerTitle
2✔
237
        title={
2✔
238
          <>
2✔
239
            Deployment {type !== DEPLOYMENT_STATES.scheduled ? 'details' : 'report'}
2!
240
            <div className="margin-left-small margin-right-small">ID: {deployment.id}</div>
2✔
241
          </>
2✔
242
        }
2✔
243
        onLinkCopy={copyLinkToClipboard}
2✔
244
        preCloser={
2✔
245
          !finished ? (
2!
246
            <DeploymentAbortButton abort={abort} deployment={deployment} />
2!
247
          ) : (stats.failure || stats.aborted) && !isConfigurationDeployment ? (
2✔
248
            <Tooltip
2✔
249
              title="This will create a new deployment with the same device group and Release.&#10;Devices with this Release already installed will be skipped, all others will be updated."
2✔
250
              placement="bottom"
2✔
251
            >
2✔
252
              <Button startIcon={<RefreshIcon fontSize="small" />} onClick={() => retry(deployment, Object.keys(devices))}>
2✔
253
                Recreate deployment?
2✔
254
              </Button>
2✔
255
            </Tooltip>
2✔
256
          ) : (
2✔
257
            <div className="flexbox centered margin-right" ref={onboardingTooltipAnchor}>
2✔
258
              <CheckCircleOutlineIcon fontSize="small" className="green margin-right-small" />
2✔
259
              <h3>Finished</h3>
2✔
260
            </div>
2✔
261
          )
2✔
262
        }
2✔
263
        onClose={onClose}
2✔
264
      />
2✔
265
      <Divider />
2✔
266
      <div>
2✔
267
        <DeploymentPhaseNotification deployment={deployment} onReviewClick={scrollToBottom} />
2✔
268
        <DeploymentOverview creator={creator} deployment={deployment} devicesById={devicesById} idAttribute={idAttribute} onScheduleClick={scrollToBottom} />
2✔
269
        {isConfigurationDeployment && (
2!
270
          <>
2✔
271
            <LinedHeader className={classes.header} heading="Configuration" />
2✔
272
            <ConfigurationObject className="margin-top-small margin-bottom-large" config={config} />
2✔
273
          </>
2✔
274
        )}
2✔
275
        <LinedHeader className={classes.header} heading="Status" />
2✔
276
        <DeploymentStatus deployment={deployment} />
2✔
277
        {!!totalDeviceCount && (
2✔
278
          <>
2✔
279
            <LinedHeader className={classes.header} heading="Devices" />
2✔
280
            <DeviceList {...props} viewLog={viewLog} />
2✔
281
          </>
2✔
282
        )}
2✔
283
        <RolloutSchedule
2✔
284
          deployment={deployment}
2✔
285
          headerClass={classes.header}
2✔
286
          onUpdateControlChange={onUpdateControlChange}
2✔
287
          onAbort={abort}
2✔
288
          innerRef={rolloutSchedule}
2✔
289
        />
2✔
UNCOV
290
        {Boolean(deviceId.length) && <LogDialog canAi={canAi} deviceId={deviceId} deployment={deployment} onClose={() => setDeviceId('')} />}
2✔
291
      </div>
2✔
292
      <Divider className={classes.divider} light />
2✔
293
    </Drawer>
2✔
294
  );
2✔
295
};
2✔
296
export default DeploymentReport;
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

© 2025 Coveralls, Inc