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

mendersoftware / gui / 1057188406

01 Nov 2023 04:24AM UTC coverage: 82.824% (-17.1%) from 99.964%
1057188406

Pull #4134

gitlab-ci

web-flow
chore: Bump uuid from 9.0.0 to 9.0.1

Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4134: chore: Bump uuid from 9.0.0 to 9.0.1

4349 of 6284 branches covered (0.0%)

8313 of 10037 relevant lines covered (82.82%)

200.97 hits per line

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

74.07
/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 { getDeploymentRelease, getDevicesById, getIdAttribute, getTenantCapabilities, getUserCapabilities } from '../../selectors';
41
import ConfigurationObject from '../common/configurationobject';
42
import Confirm from '../common/confirm';
43
import LogDialog from '../common/dialogs/log';
44
import LinedHeader from '../common/lined-header';
45
import DeploymentStatus, { DeploymentPhaseNotification } from './deployment-report/deploymentstatus';
46
import DeviceList from './deployment-report/devicelist';
47
import DeploymentOverview from './deployment-report/overview';
48
import RolloutSchedule from './deployment-report/rolloutschedule';
49

50
momentDurationFormatSetup(moment);
11✔
51

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

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

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

70
  const toggleAborting = () => setAborting(toggle);
3✔
71

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

86
export const DeploymentReport = ({ abort, open, onClose, past, retry, type }) => {
11✔
87
  const [deviceId, setDeviceId] = useState('');
4✔
88
  const rolloutSchedule = useRef();
4✔
89
  const timer = useRef();
4✔
90
  const { classes } = useStyles();
4✔
91
  const dispatch = useDispatch();
4✔
92
  const { deployment, selectedDevices } = useSelector(state => {
4✔
93
    const deployment = state.deployments.byId[state.deployments.selectionState.selectedId] || {};
7✔
94
    const { devices = {} } = deployment;
7✔
95
    const { selectedDeviceIds } = state.deployments;
7✔
96
    return {
7✔
97
      deployment,
98
      selectedDevices: selectedDeviceIds.map(deviceId => ({ ...state.devices.byId[deviceId], ...devices[deviceId] }))
2✔
99
    };
100
  });
101
  const devicesById = useSelector(getDevicesById);
4✔
102
  const { attribute: idAttribute } = useSelector(getIdAttribute);
4✔
103
  const release = useSelector(getDeploymentRelease);
4✔
104
  const tenantCapabilities = useSelector(getTenantCapabilities);
4✔
105
  const userCapabilities = useSelector(getUserCapabilities);
4✔
106
  // we can't filter by auditlog action via the api, so
107
  // - fall back to the following filter
108
  // - hope the deployment creation event is retrieved with the call to auditlogs api on report open
109
  // - otherwise no creator will be shown
110
  const { actor = {} } =
4✔
111
    useSelector(state =>
4✔
112
      state.organization.auditlog.events.find(event => event.object.id === state.deployments.selectionState.selectedId && event.action === 'create')
21!
113
    ) || {};
114
  const creator = actor.email;
4✔
115

116
  const { canAuditlog } = userCapabilities;
4✔
117
  const { hasAuditlogs } = tenantCapabilities;
4✔
118
  const { devices = {}, device_count = 0, totalDeviceCount: totalDevices, statistics = {}, type: deploymentType } = deployment;
4!
119
  const { status: stats = {} } = statistics;
4!
120
  const totalDeviceCount = totalDevices ?? device_count;
4✔
121

122
  const refreshDeployment = useCallback(() => {
4✔
123
    if (!deployment.id) {
1!
124
      return;
×
125
    }
126
    return dispatch(getSingleDeployment(deployment.id));
1✔
127
  }, [deployment.id, dispatch]);
128

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

172
  useEffect(() => {
4✔
173
    const progressCount =
174
      statCollector(deploymentStatesToSubstates.paused, stats) +
2✔
175
      statCollector(deploymentStatesToSubstates.pending, stats) +
176
      statCollector(deploymentStatesToSubstates.inprogress, stats);
177

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

189
  const scrollToBottom = () => rolloutSchedule.current?.scrollIntoView({ behavior: 'smooth' });
4✔
190

191
  const viewLog = useCallback(id => dispatch(getDeviceLog(deployment.id, id)).then(() => setDeviceId(id)), [deployment.id, dispatch]);
4✔
192

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

199
  const { log: logData } = devices[deviceId] || {};
4✔
200
  const finished = deployment.finished || deployment.status === DEPLOYMENT_STATES.finished;
4✔
201
  const isConfigurationDeployment = deploymentType === DEPLOYMENT_TYPES.configuration;
4✔
202
  let config = {};
4✔
203
  if (isConfigurationDeployment) {
4!
204
    try {
×
205
      config = JSON.parse(atob(deployment.configuration));
×
206
    } catch (error) {
207
      config = {};
×
208
    }
209
  }
210

211
  const onUpdateControlChange = (updatedMap = {}) => {
4!
212
    const { id, update_control_map = {} } = deployment;
×
213
    const { states } = update_control_map;
×
214
    const { states: updatedStates } = updatedMap;
×
215
    dispatch(updateDeploymentControlMap(id, { states: { ...states, ...updatedStates } }));
×
216
  };
217

218
  const props = {
4✔
219
    deployment,
220
    getDeploymentDevices: useCallback((id, options) => dispatch(getDeploymentDevices(id, options)), [dispatch]),
2✔
221
    idAttribute,
222
    selectedDevices,
223
    userCapabilities,
224
    totalDeviceCount,
225
    viewLog
226
  };
227

228
  return (
4✔
229
    <Drawer anchor="right" open onClose={onClose} PaperProps={{ style: { minWidth: '75vw' } }}>
230
      <div className="flexbox margin-bottom-small space-between">
231
        <div className="flexbox">
232
          <h3>{`Deployment ${type !== DEPLOYMENT_STATES.scheduled ? 'details' : 'report'}`}</h3>
4!
233
          <h4 className="margin-left-small margin-right-small">ID: {deployment.id}</h4>
234
          <IconButton onClick={copyLinkToClipboard} style={{ alignSelf: 'center' }} size="large">
235
            <LinkIcon />
236
          </IconButton>
237
        </div>
238
        <div className="flexbox center-aligned">
239
          {!finished ? (
4!
240
            <DeploymentAbortButton abort={abort} deployment={deployment} />
241
          ) : (stats.failure || stats.aborted) && !isConfigurationDeployment ? (
×
242
            <Tooltip
243
              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."
244
              placement="bottom"
245
            >
246
              <Button color="secondary" startIcon={<RefreshIcon fontSize="small" />} onClick={() => retry(deployment, Object.keys(devices))}>
×
247
                Recreate deployment?
248
              </Button>
249
            </Tooltip>
250
          ) : (
251
            <div className="flexbox centered margin-right">
252
              <CheckCircleOutlineIcon fontSize="small" className="green margin-right-small" />
253
              <h3>Finished</h3>
254
            </div>
255
          )}
256
          <IconButton onClick={onClose} aria-label="close" size="large">
257
            <CloseIcon />
258
          </IconButton>
259
        </div>
260
      </div>
261
      <Divider />
262
      <div>
263
        <DeploymentPhaseNotification deployment={deployment} onReviewClick={scrollToBottom} />
264
        <DeploymentOverview
265
          creator={creator}
266
          deployment={deployment}
267
          devicesById={devicesById}
268
          onScheduleClick={scrollToBottom}
269
          tenantCapabilities={tenantCapabilities}
270
        />
271
        {isConfigurationDeployment && (
4!
272
          <>
273
            <LinedHeader className={classes.header} heading="Configuration" />
274
            <ConfigurationObject className="margin-top-small margin-bottom-large" config={config} />
275
          </>
276
        )}
277
        <LinedHeader className={classes.header} heading="Status" />
278
        <DeploymentStatus deployment={deployment} />
279
        {!!totalDeviceCount && (
8✔
280
          <>
281
            <LinedHeader className={classes.header} heading="Devices" />
282
            <DeviceList {...props} viewLog={viewLog} />
283
          </>
284
        )}
285
        <RolloutSchedule
286
          deployment={deployment}
287
          headerClass={classes.header}
288
          onUpdateControlChange={onUpdateControlChange}
289
          onAbort={abort}
290
          innerRef={rolloutSchedule}
291
        />
292
        {Boolean(deviceId.length) && <LogDialog logData={logData} onClose={() => setDeviceId('')} />}
×
293
      </div>
294
      <Divider className={classes.divider} light />
295
    </Drawer>
296
  );
297
};
298
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