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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

67.14
/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, { useEffect, useRef, useState } from 'react';
15
import { connect } 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 { getDeviceAuth, getDeviceById } from '../../actions/deviceActions';
35
import { getAuditLogs } from '../../actions/organizationActions';
36
import { getRelease } from '../../actions/releaseActions';
37
import { TIMEOUTS } from '../../constants/appConstants';
38
import { DEPLOYMENT_STATES, DEPLOYMENT_TYPES, deploymentStatesToSubstates } from '../../constants/deploymentConstants';
39
import { AUDIT_LOGS_TYPES } from '../../constants/organizationConstants';
40
import { statCollector, toggle } from '../../helpers';
41
import { getIdAttribute, getTenantCapabilities, getUserCapabilities } from '../../selectors';
42
import ConfigurationObject from '../common/configurationobject';
43
import Confirm from '../common/confirm';
44
import LogDialog from '../common/dialogs/log';
45
import LinedHeader from '../common/lined-header';
46
import DeploymentStatus, { DeploymentPhaseNotification } from './deployment-report/deploymentstatus';
47
import DeviceList from './deployment-report/devicelist';
48
import DeploymentOverview from './deployment-report/overview';
49
import RolloutSchedule from './deployment-report/rolloutschedule';
50

51
momentDurationFormatSetup(moment);
11✔
52

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

63
export const defaultColumnDataProps = {
11✔
64
  chipLikeKey: false,
65
  style: { alignItems: 'center', alignSelf: 'flex-start', gridTemplateColumns: 'minmax(140px, 1fr) minmax(220px, 1fr)', maxWidth: '25vw' }
66
};
67

68
export const DeploymentAbortButton = ({ abort, deployment }) => {
11✔
69
  const [aborting, setAborting] = useState(false);
4✔
70

71
  const toggleAborting = () => setAborting(toggle);
4✔
72

73
  return aborting ? (
4!
74
    <Confirm cancel={toggleAborting} action={() => abort(deployment.id)} type="abort" />
×
75
  ) : (
76
    <Tooltip
77
      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."
78
      placement="bottom"
79
    >
80
      <Button color="secondary" startIcon={<BlockIcon fontSize="small" />} onClick={toggleAborting}>
81
        {deployment.filters?.length ? 'Stop' : 'Abort'} deployment
4!
82
      </Button>
83
    </Tooltip>
84
  );
85
};
86

87
export const DeploymentReport = props => {
11✔
88
  const {
89
    abort,
90
    creator,
91
    deployment,
92
    devicesById,
93
    getAuditLogs,
94
    getDeviceLog,
95
    getRelease,
96
    getSingleDeployment,
97
    idAttribute,
98
    open,
99
    onClose,
100
    past,
101
    retry,
102
    release,
103
    tenantCapabilities,
104
    type,
105
    updateDeploymentControlMap,
106
    userCapabilities
107
  } = props;
274✔
108
  const { canAuditlog } = userCapabilities;
274✔
109
  const { hasAuditlogs } = tenantCapabilities;
274✔
110
  const { devices = {}, device_count, statistics = {}, type: deploymentType } = deployment;
274✔
111
  const { status: stats = {} } = statistics;
274✔
112
  const { classes } = useStyles();
274✔
113
  const [deviceId, setDeviceId] = useState('');
274✔
114
  const rolloutSchedule = useRef();
274✔
115
  const timer = useRef();
274✔
116

117
  useEffect(() => {
274✔
118
    if (!open) {
8✔
119
      return;
6✔
120
    }
121
    clearInterval(timer.current);
2✔
122
    if (!(deployment.finished || deployment.status === DEPLOYMENT_STATES.finished)) {
2!
123
      timer.current = past ? null : setInterval(refreshDeployment, TIMEOUTS.fiveSeconds);
2!
124
    }
125
    if ((deployment.type === DEPLOYMENT_TYPES.software || !release.device_types_compatible.length) && deployment.artifact_name) {
2!
126
      getRelease(deployment.artifact_name);
×
127
    }
128
    if (hasAuditlogs && canAuditlog) {
2!
129
      getAuditLogs({
×
130
        page: 1,
131
        perPage: 100,
132
        startDate: undefined,
133
        endDate: undefined,
134
        user: undefined,
135
        type: AUDIT_LOGS_TYPES.find(item => item.value === 'deployment'),
×
136
        detail: deployment.name
137
      });
138
    }
139
    return () => {
2✔
140
      clearInterval(timer.current);
2✔
141
    };
142
  }, [deployment.id, open]);
143

144
  useEffect(() => {
274✔
145
    const progressCount =
146
      statCollector(deploymentStatesToSubstates.paused, stats) +
8✔
147
      statCollector(deploymentStatesToSubstates.pending, stats) +
148
      statCollector(deploymentStatesToSubstates.inprogress, stats);
149

150
    if (!!device_count && progressCount <= 0 && timer.current) {
8!
151
      // if no more devices in "progress" statuses, deployment has finished, stop counter
152
      clearInterval(timer.current);
×
153
      timer.current = setTimeout(refreshDeployment, TIMEOUTS.oneSecond);
×
154
      return () => {
×
155
        clearTimeout(timer.current);
×
156
      };
157
    }
158
  }, [deployment.id, device_count, JSON.stringify(stats)]);
159

160
  const scrollToBottom = () => {
274✔
161
    rolloutSchedule.current?.scrollIntoView({ behavior: 'smooth' });
×
162
  };
163

164
  const refreshDeployment = () => {
274✔
165
    if (!deployment.id) {
1!
166
      return;
×
167
    }
168
    return getSingleDeployment(deployment.id);
1✔
169
  };
170

171
  const viewLog = id => getDeviceLog(deployment.id, id).then(() => setDeviceId(id));
274✔
172

173
  const copyLinkToClipboard = () => {
274✔
174
    const location = window.location.href.substring(0, window.location.href.indexOf('/deployments') + '/deployments'.length);
×
175
    copy(`${location}?open=true&id=${deployment.id}`);
×
176
    setSnackbar('Link copied to clipboard');
×
177
  };
178

179
  const { log: logData } = devices[deviceId] || {};
274✔
180
  const finished = deployment.finished || deployment.status === DEPLOYMENT_STATES.finished;
274✔
181
  const isConfigurationDeployment = deploymentType === DEPLOYMENT_TYPES.configuration;
274✔
182
  let config = {};
274✔
183
  if (isConfigurationDeployment) {
274!
184
    try {
×
185
      config = JSON.parse(atob(deployment.configuration));
×
186
    } catch (error) {
187
      config = {};
×
188
    }
189
  }
190

191
  const onUpdateControlChange = (updatedMap = {}) => {
274!
192
    const { id, update_control_map = {} } = deployment;
×
193
    const { states } = update_control_map;
×
194
    const { states: updatedStates } = updatedMap;
×
195
    updateDeploymentControlMap(id, { states: { ...states, ...updatedStates } });
×
196
  };
197

198
  return (
274✔
199
    <Drawer className={`${open ? 'fadeIn' : 'fadeOut'}`} anchor="right" open={open} onClose={onClose} PaperProps={{ style: { minWidth: '75vw' } }}>
274✔
200
      <div className="flexbox margin-bottom-small space-between">
201
        <div className="flexbox">
202
          <h3>{`Deployment ${type !== DEPLOYMENT_STATES.scheduled ? 'details' : 'report'}`}</h3>
274!
203
          <h4 className="margin-left-small margin-right-small">ID: {deployment.id}</h4>
204
          <IconButton onClick={copyLinkToClipboard} style={{ alignSelf: 'center' }} size="large">
205
            <LinkIcon />
206
          </IconButton>
207
        </div>
208
        <div className="flexbox center-aligned">
209
          {!finished ? (
274!
210
            <DeploymentAbortButton abort={abort} deployment={deployment} />
211
          ) : (stats.failure || stats.aborted) && !isConfigurationDeployment ? (
×
212
            <Tooltip
213
              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."
214
              placement="bottom"
215
            >
216
              <Button color="secondary" startIcon={<RefreshIcon fontSize="small" />} onClick={() => retry(deployment, Object.keys(devices))}>
×
217
                Recreate deployment?
218
              </Button>
219
            </Tooltip>
220
          ) : (
221
            <div className="flexbox centered margin-right">
222
              <CheckCircleOutlineIcon fontSize="small" className="green margin-right-small" />
223
              <h3>Finished</h3>
224
            </div>
225
          )}
226
          <IconButton onClick={onClose} aria-label="close" size="large">
227
            <CloseIcon />
228
          </IconButton>
229
        </div>
230
      </div>
231
      <Divider />
232
      <div>
233
        <DeploymentPhaseNotification deployment={deployment} onReviewClick={scrollToBottom} />
234
        <DeploymentOverview
235
          creator={creator}
236
          deployment={deployment}
237
          devicesById={devicesById}
238
          idAttribute={idAttribute}
239
          onScheduleClick={scrollToBottom}
240
          tenantCapabilities={tenantCapabilities}
241
        />
242
        {isConfigurationDeployment && (
274!
243
          <>
244
            <LinedHeader className={classes.header} heading="Configuration" />
245
            <ConfigurationObject className="margin-top-small margin-bottom-large" config={config} />
246
          </>
247
        )}
248
        <LinedHeader className={classes.header} heading="Status" />
249
        <DeploymentStatus deployment={deployment} />
250
        <LinedHeader className={classes.header} heading="Devices" />
251
        <DeviceList {...props} viewLog={viewLog} />
252
        <RolloutSchedule
253
          deployment={deployment}
254
          headerClass={classes.header}
255
          onUpdateControlChange={onUpdateControlChange}
256
          onAbort={abort}
257
          innerRef={rolloutSchedule}
258
        />
259
        {Boolean(deviceId.length) && <LogDialog logData={logData} onClose={() => setDeviceId('')} />}
×
260
      </div>
261
      <Divider className={classes.divider} light />
262
    </Drawer>
263
  );
264
};
265

266
const actionCreators = {
11✔
267
  getAuditLogs,
268
  getDeploymentDevices,
269
  getDeviceAuth,
270
  getDeviceById,
271
  getDeviceLog,
272
  getRelease,
273
  getSingleDeployment,
274
  setSnackbar,
275
  updateDeploymentControlMap
276
};
277

278
const mapStateToProps = state => {
11✔
279
  const { devices = {} } = state.deployments.byId[state.deployments.selectionState.selectedId] || {};
209✔
280
  const selectedDevices = state.deployments.selectedDeviceIds.map(deviceId => ({ ...state.devices.byId[deviceId], ...devices[deviceId] }));
209✔
281
  const deployment = state.deployments.byId[state.deployments.selectionState.selectedId] || {};
209✔
282
  // we can't filter by auditlog action via the api, so
283
  // - fall back to the following filter
284
  // - hope the deployment creation event is retrieved with the call to auditlogs api on report open
285
  // - otherwise no creator will be shown
286
  const { actor = {} } =
209✔
287
    state.organization.auditlog.events.find(event => event.object.id === state.deployments.selectionState.selectedId && event.action === 'create') || {};
627!
288
  return {
209✔
289
    acceptedDevicesCount: state.devices.byStatus.accepted.total,
290
    creator: actor.email,
291
    deployment,
292
    devicesById: state.devices.byId,
293
    idAttribute: getIdAttribute(state).attribute,
294
    isHosted: state.app.features.isHosted,
295
    release:
296
      deployment.artifact_name && state.releases.byId[deployment.artifact_name]
421✔
297
        ? state.releases.byId[deployment.artifact_name]
298
        : { device_types_compatible: [] },
299
    selectedDeviceIds: state.deployments.selectedDeviceIds,
300
    selectedDevices,
301
    tenantCapabilities: getTenantCapabilities(state),
302
    userCapabilities: getUserCapabilities(state)
303
  };
304
};
305

306
export default connect(mapStateToProps, actionCreators)(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