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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

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

17
// material ui
18
import {
19
  Block as BlockIcon,
20
  CheckCircleOutline as CheckCircleOutlineIcon,
21
  Close as CloseIcon,
22
  Link as LinkIcon,
23
  Refresh as RefreshIcon
24
} from '@mui/icons-material';
25
import { Button, Divider, Drawer, IconButton, Tooltip } from '@mui/material';
26
import { makeStyles } from 'tss-react/mui';
27

28
import copy from 'copy-to-clipboard';
29
import moment from 'moment';
30
import momentDurationFormatSetup from 'moment-duration-format';
31

32
import { setSnackbar } from '../../actions/appActions';
33
import { getDeploymentDevices, getDeviceLog, getSingleDeployment, updateDeploymentControlMap } from '../../actions/deploymentActions';
34
import { getAuditLogs } from '../../actions/organizationActions';
35
import { getRelease } from '../../actions/releaseActions';
36
import { TIMEOUTS } from '../../constants/appConstants';
37
import { DEPLOYMENT_STATES, DEPLOYMENT_TYPES, deploymentStatesToSubstates } from '../../constants/deploymentConstants';
38
import { AUDIT_LOGS_TYPES } from '../../constants/organizationConstants';
39
import { statCollector, toggle } from '../../helpers';
40
import {
41
  getDeploymentRelease,
42
  getDevicesById,
43
  getIdAttribute,
44
  getOnboardingState,
45
  getSelectedDeploymentData,
46
  getTenantCapabilities,
47
  getUserCapabilities
48
} from '../../selectors';
49
import ConfigurationObject from '../common/configurationobject';
50
import Confirm from '../common/confirm';
51
import LogDialog from '../common/dialogs/log';
52
import LinedHeader from '../common/lined-header';
53
import BaseOnboardingTip from '../helptips/baseonboardingtip.js';
54
import { DeploymentUploadFinished } from '../helptips/onboardingtips.js';
55
import DeploymentStatus, { DeploymentPhaseNotification } from './deployment-report/deploymentstatus';
56
import DeviceList from './deployment-report/devicelist';
57
import DeploymentOverview from './deployment-report/overview';
58
import RolloutSchedule from './deployment-report/rolloutschedule';
59

60
momentDurationFormatSetup(moment);
10✔
61

62
const useStyles = makeStyles()(theme => ({
10✔
63
  divider: { marginTop: theme.spacing(2) },
64
  header: {
65
    ['&.dashboard-header span']: {
66
      backgroundColor: theme.palette.background.paper,
67
      backgroundImage: 'linear-gradient(rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.15))'
68
    }
69
  }
70
}));
71

72
export const defaultColumnDataProps = {
10✔
73
  chipLikeKey: false,
74
  style: { alignItems: 'center', alignSelf: 'flex-start', gridTemplateColumns: 'minmax(140px, 1fr) minmax(220px, 1fr)', maxWidth: '25vw' }
75
};
76

77
export const DeploymentAbortButton = ({ abort, deployment }) => {
10✔
78
  const [aborting, setAborting] = useState(false);
3✔
79

80
  const toggleAborting = () => setAborting(toggle);
3✔
81

82
  return aborting ? (
3!
UNCOV
83
    <Confirm cancel={toggleAborting} action={() => abort(deployment.id)} type="abort" />
×
84
  ) : (
85
    <Tooltip
86
      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."
87
      placement="bottom"
88
    >
89
      <Button color="secondary" startIcon={<BlockIcon fontSize="small" />} onClick={toggleAborting}>
90
        {deployment.filters?.length ? 'Stop' : 'Abort'} deployment
3!
91
      </Button>
92
    </Tooltip>
93
  );
94
};
95

96
export const DeploymentReport = ({ abort, onClose, past, retry, type }) => {
10✔
97
  const [deviceId, setDeviceId] = useState('');
3✔
98
  const rolloutSchedule = useRef();
3✔
99
  const timer = useRef();
3✔
100
  const onboardingTooltipAnchor = useRef();
3✔
101
  const { classes } = useStyles();
3✔
102
  const dispatch = useDispatch();
3✔
103
  const { deployment, selectedDevices } = useSelector(getSelectedDeploymentData);
3✔
104
  const devicesById = useSelector(getDevicesById);
3✔
105
  const { attribute: idAttribute } = useSelector(getIdAttribute);
3✔
106
  const release = useSelector(getDeploymentRelease);
3✔
107
  const tenantCapabilities = useSelector(getTenantCapabilities);
3✔
108
  const userCapabilities = useSelector(getUserCapabilities);
3✔
109
  const onboardingState = useSelector(getOnboardingState);
3✔
110
  // we can't filter by auditlog action via the api, so
111
  // - fall back to the following filter
112
  // - hope the deployment creation event is retrieved with the call to auditlogs api on report open
113
  // - otherwise no creator will be shown
114
  const { actor = {} } =
3✔
115
    useSelector(state =>
3✔
116
      state.organization.auditlog.events.find(event => event.object.id === state.deployments.selectionState.selectedId && event.action === 'create')
18!
117
    ) || {};
118
  const creator = actor.email;
3✔
119

120
  const { canAuditlog } = userCapabilities;
3✔
121
  const { hasAuditlogs } = tenantCapabilities;
3✔
122
  const { devices = {}, device_count = 0, totalDeviceCount: totalDevices, statistics = {}, type: deploymentType } = deployment;
3!
123
  const { status: stats = {} } = statistics;
3!
124
  const totalDeviceCount = totalDevices ?? device_count;
3✔
125

126
  const refreshDeployment = useCallback(() => {
3✔
127
    if (!deployment.id) {
1!
UNCOV
128
      return;
×
129
    }
130
    return dispatch(getSingleDeployment(deployment.id));
1✔
131
  }, [deployment.id, dispatch]);
132

133
  useEffect(() => {
3✔
134
    if (!deployment.id) {
2!
UNCOV
135
      return;
×
136
    }
137
    clearInterval(timer.current);
2✔
138
    const now = new Date();
2✔
139
    now.setSeconds(now.getSeconds() + TIMEOUTS.refreshDefault / TIMEOUTS.oneSecond);
2✔
140
    if (!deployment.finished || new Date(deployment.finished) > now) {
2!
141
      timer.current = past ? null : setInterval(refreshDeployment, TIMEOUTS.fiveSeconds);
2!
142
    }
143
    if ((deployment.type === DEPLOYMENT_TYPES.software || !release.device_types_compatible.length) && deployment.artifact_name) {
2!
UNCOV
144
      dispatch(getRelease(deployment.artifact_name));
×
145
    }
146
    if (hasAuditlogs && canAuditlog) {
2!
UNCOV
147
      dispatch(
×
148
        getAuditLogs({
149
          page: 1,
150
          perPage: 100,
151
          startDate: undefined,
152
          endDate: undefined,
153
          user: undefined,
UNCOV
154
          type: AUDIT_LOGS_TYPES.find(item => item.value === 'deployment'),
×
155
          detail: deployment.name
156
        })
157
      );
158
    }
159
    return () => {
2✔
160
      clearInterval(timer.current);
2✔
161
    };
162
  }, [
163
    canAuditlog,
164
    deployment.artifact_name,
165
    deployment.finished,
166
    deployment.id,
167
    deployment.name,
168
    deployment.status,
169
    deployment.type,
170
    dispatch,
171
    hasAuditlogs,
172
    past,
173
    refreshDeployment,
174
    release.device_types_compatible.length
175
  ]);
176

177
  useEffect(() => {
3✔
178
    const progressCount =
179
      statCollector(deploymentStatesToSubstates.paused, stats) +
2✔
180
      statCollector(deploymentStatesToSubstates.pending, stats) +
181
      statCollector(deploymentStatesToSubstates.inprogress, stats);
182

183
    if (!!device_count && progressCount <= 0 && timer.current) {
2!
184
      // if no more devices in "progress" statuses, deployment has finished, stop counter
UNCOV
185
      clearInterval(timer.current);
×
UNCOV
186
      timer.current = setTimeout(refreshDeployment, TIMEOUTS.oneSecond);
×
UNCOV
187
      return () => {
×
UNCOV
188
        clearTimeout(timer.current);
×
189
      };
190
    }
191
    // eslint-disable-next-line react-hooks/exhaustive-deps
192
  }, [deployment.id, device_count, JSON.stringify(stats), refreshDeployment]);
193

194
  const scrollToBottom = () => rolloutSchedule.current?.scrollIntoView({ behavior: 'smooth' });
3✔
195

196
  const viewLog = useCallback(id => dispatch(getDeviceLog(deployment.id, id)).then(() => setDeviceId(id)), [deployment.id, dispatch]);
3✔
197

198
  const copyLinkToClipboard = () => {
3✔
UNCOV
199
    const location = window.location.href.substring(0, window.location.href.indexOf('/deployments') + '/deployments'.length);
×
UNCOV
200
    copy(`${location}?open=true&id=${deployment.id}`);
×
UNCOV
201
    dispatch(setSnackbar('Link copied to clipboard'));
×
202
  };
203

204
  const { log: logData } = devices[deviceId] || {};
3✔
205
  const finished = deployment.finished || deployment.status === DEPLOYMENT_STATES.finished;
3✔
206
  const isConfigurationDeployment = deploymentType === DEPLOYMENT_TYPES.configuration;
3✔
207
  let config = {};
3✔
208
  if (isConfigurationDeployment) {
3!
UNCOV
209
    try {
×
UNCOV
210
      config = JSON.parse(atob(deployment.configuration));
×
211
    } catch (error) {
UNCOV
212
      config = {};
×
213
    }
214
  }
215

216
  const onUpdateControlChange = (updatedMap = {}) => {
3!
UNCOV
217
    const { id, update_control_map = {} } = deployment;
×
UNCOV
218
    const { states } = update_control_map;
×
UNCOV
219
    const { states: updatedStates } = updatedMap;
×
UNCOV
220
    dispatch(updateDeploymentControlMap(id, { states: { ...states, ...updatedStates } }));
×
221
  };
222

223
  const props = {
3✔
224
    deployment,
225
    getDeploymentDevices: useCallback((id, options) => dispatch(getDeploymentDevices(id, options)), [dispatch]),
2✔
226
    idAttribute,
227
    selectedDevices,
228
    userCapabilities,
229
    totalDeviceCount,
230
    viewLog
231
  };
232
  let onboardingComponent = null;
3✔
233
  if (!onboardingState.complete && onboardingTooltipAnchor.current && finished) {
3!
UNCOV
234
    const anchor = {
×
235
      left: onboardingTooltipAnchor.current.offsetLeft + (onboardingTooltipAnchor.current.offsetWidth / 100) * 50,
236
      top: onboardingTooltipAnchor.current.offsetTop + onboardingTooltipAnchor.current.offsetHeight
237
    };
UNCOV
238
    onboardingComponent = (
×
239
      <BaseOnboardingTip
240
        id={onboardingState.progress}
241
        progress={onboardingState.progress}
242
        component={<DeploymentUploadFinished></DeploymentUploadFinished>}
243
        anchor={anchor}
244
      />
245
    );
246
  }
247

248
  return (
3✔
249
    <Drawer anchor="right" open onClose={onClose} PaperProps={{ style: { minWidth: '75vw' } }}>
250
      {!!onboardingComponent && onboardingComponent}
3!
251
      <div className="flexbox margin-bottom-small space-between">
252
        <div className="flexbox">
253
          <h3>{`Deployment ${type !== DEPLOYMENT_STATES.scheduled ? 'details' : 'report'}`}</h3>
3!
254
          <h4 className="margin-left-small margin-right-small">ID: {deployment.id}</h4>
255
          <IconButton onClick={copyLinkToClipboard} style={{ alignSelf: 'center' }} size="large">
256
            <LinkIcon />
257
          </IconButton>
258
        </div>
259
        <div className="flexbox center-aligned">
260
          {!finished ? (
3!
261
            <DeploymentAbortButton abort={abort} deployment={deployment} />
262
          ) : (stats.failure || stats.aborted) && !isConfigurationDeployment ? (
×
263
            <Tooltip
264
              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."
265
              placement="bottom"
266
            >
UNCOV
267
              <Button color="secondary" startIcon={<RefreshIcon fontSize="small" />} onClick={() => retry(deployment, Object.keys(devices))}>
×
268
                Recreate deployment?
269
              </Button>
270
            </Tooltip>
271
          ) : (
272
            <div className="flexbox centered margin-right">
273
              <CheckCircleOutlineIcon fontSize="small" className="green margin-right-small" />
274
              <h3>Finished</h3>
275
            </div>
276
          )}
277
          <IconButton ref={onboardingTooltipAnchor} onClick={onClose} aria-label="close" size="large">
278
            <CloseIcon />
279
          </IconButton>
280
        </div>
281
      </div>
282
      <Divider />
283
      <div>
284
        <DeploymentPhaseNotification deployment={deployment} onReviewClick={scrollToBottom} />
285
        <DeploymentOverview
286
          creator={creator}
287
          deployment={deployment}
288
          devicesById={devicesById}
289
          onScheduleClick={scrollToBottom}
290
          tenantCapabilities={tenantCapabilities}
291
        />
292
        {isConfigurationDeployment && (
3!
293
          <>
294
            <LinedHeader className={classes.header} heading="Configuration" />
295
            <ConfigurationObject className="margin-top-small margin-bottom-large" config={config} />
296
          </>
297
        )}
298
        <LinedHeader className={classes.header} heading="Status" />
299
        <DeploymentStatus deployment={deployment} />
300
        {!!totalDeviceCount && (
6✔
301
          <>
302
            <LinedHeader className={classes.header} heading="Devices" />
303
            <DeviceList {...props} viewLog={viewLog} />
304
          </>
305
        )}
306
        <RolloutSchedule
307
          deployment={deployment}
308
          headerClass={classes.header}
309
          onUpdateControlChange={onUpdateControlChange}
310
          onAbort={abort}
311
          innerRef={rolloutSchedule}
312
        />
313
        {Boolean(deviceId.length) && (
3!
314
          <LogDialog
315
            context={{ device: deviceId, releaseName: deployment.artifact_name, date: deployment.finished }}
316
            logData={logData}
UNCOV
317
            onClose={() => setDeviceId('')}
×
318
          />
319
        )}
320
      </div>
321
      <Divider className={classes.divider} light />
322
    </Drawer>
323
  );
324
};
325
export default DeploymentReport;
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