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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

71.79
/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, getSelectedDeploymentData, 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);
10✔
51

52
const useStyles = makeStyles()(theme => ({
10✔
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 = {
10✔
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 }) => {
10✔
68
  const [aborting, setAborting] = useState(false);
7✔
69

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

72
  return aborting ? (
7!
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
7!
81
      </Button>
82
    </Tooltip>
83
  );
84
};
85

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

108
  const { canAuditlog } = userCapabilities;
8✔
109
  const { hasAuditlogs } = tenantCapabilities;
8✔
110
  const { devices = {}, device_count = 0, totalDeviceCount: totalDevices, statistics = {}, type: deploymentType } = deployment;
8!
111
  const { status: stats = {} } = statistics;
8!
112
  const totalDeviceCount = totalDevices ?? device_count;
8✔
113

114
  const refreshDeployment = useCallback(() => {
8✔
115
    if (!deployment.id) {
4!
116
      return;
×
117
    }
118
    return dispatch(getSingleDeployment(deployment.id));
4✔
119
  }, [deployment.id, dispatch]);
120

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

165
  useEffect(() => {
8✔
166
    const progressCount =
167
      statCollector(deploymentStatesToSubstates.paused, stats) +
5✔
168
      statCollector(deploymentStatesToSubstates.pending, stats) +
169
      statCollector(deploymentStatesToSubstates.inprogress, stats);
170

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

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

184
  const viewLog = useCallback(id => dispatch(getDeviceLog(deployment.id, id)).then(() => setDeviceId(id)), [deployment.id, dispatch]);
8✔
185

186
  const copyLinkToClipboard = () => {
8✔
187
    const location = window.location.href.substring(0, window.location.href.indexOf('/deployments') + '/deployments'.length);
×
188
    copy(`${location}?open=true&id=${deployment.id}`);
×
189
    dispatch(setSnackbar('Link copied to clipboard'));
×
190
  };
191

192
  const { log: logData } = devices[deviceId] || {};
8✔
193
  const finished = deployment.finished || deployment.status === DEPLOYMENT_STATES.finished;
8✔
194
  const isConfigurationDeployment = deploymentType === DEPLOYMENT_TYPES.configuration;
8✔
195
  let config = {};
8✔
196
  if (isConfigurationDeployment) {
8!
197
    try {
×
198
      config = JSON.parse(atob(deployment.configuration));
×
199
    } catch (error) {
200
      config = {};
×
201
    }
202
  }
203

204
  const onUpdateControlChange = (updatedMap = {}) => {
8!
205
    const { id, update_control_map = {} } = deployment;
×
206
    const { states } = update_control_map;
×
207
    const { states: updatedStates } = updatedMap;
×
208
    dispatch(updateDeploymentControlMap(id, { states: { ...states, ...updatedStates } }));
×
209
  };
210

211
  const props = {
8✔
212
    deployment,
213
    getDeploymentDevices: useCallback((id, options) => dispatch(getDeploymentDevices(id, options)), [dispatch]),
5✔
214
    idAttribute,
215
    selectedDevices,
216
    userCapabilities,
217
    totalDeviceCount,
218
    viewLog
219
  };
220

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