• 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

83.53
/src/js/components/deployments/deployment-wizard/phasesettings.js
1
// Copyright 2019 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, useState } from 'react';
15

16
import { Add as AddIcon, Cancel as CancelIcon } from '@mui/icons-material';
17
import {
18
  Checkbox,
19
  Chip,
20
  Collapse,
21
  FormControl,
22
  FormControlLabel,
23
  IconButton,
24
  Input,
25
  InputAdornment,
26
  ListSubheader,
27
  MenuItem,
28
  Select,
29
  Table,
30
  TableBody,
31
  TableCell,
32
  TableHead,
33
  TableRow
34
} from '@mui/material';
35
import { makeStyles } from 'tss-react/mui';
36

37
import pluralize from 'pluralize';
38

39
import { BENEFITS } from '../../../constants/appConstants';
40
import { getPhaseDeviceCount, getRemainderPercent } from '../../../helpers';
41
import { DOCSTIPS, DocsTooltip } from '../../common/docslink';
42
import EnterpriseNotification from '../../common/enterpriseNotification';
43
import { InfoHintContainer } from '../../common/info-hint';
44
import Time from '../../common/time';
45
import { getPhaseStartTime } from '../createdeployment';
46

47
const useStyles = makeStyles()(theme => ({
11✔
48
  chip: { marginTop: theme.spacing(2) },
49
  delayInputWrapper: { display: 'grid', gridTemplateColumns: 'max-content max-content', columnGap: theme.spacing() },
50
  row: { whiteSpace: 'nowrap' },
51
  input: { minWidth: 400 },
52
  patternSelection: { maxWidth: 515, width: 'min-content' }
53
}));
54

55
const timeframes = ['minutes', 'hours', 'days'];
11✔
56
const tableHeaders = ['', 'Batch size', 'Phase begins', 'Delay before next phase', ''];
11✔
57

58
export const PhaseSettings = ({ classNames, deploymentObject, disabled, numberDevices, setDeploymentSettings }) => {
11✔
59
  const { classes } = useStyles();
43✔
60

61
  const { filter, phases = [] } = deploymentObject;
43!
62
  const updateDelay = (value, index) => {
43✔
63
    let newPhases = phases;
2✔
64
    // value must be at least 1
65
    value = Math.max(1, value);
2✔
66
    newPhases[index].delay = value;
2✔
67

68
    setDeploymentSettings({ phases: newPhases });
2✔
69
    // logic for updating time stamps should be in parent - only change delays here
70
  };
71

72
  const updateBatchSize = (value, index) => {
43✔
73
    let newPhases = [...phases];
2✔
74
    value = Math.min(100, Math.max(1, value));
2✔
75
    newPhases[index].batch_size = value;
2✔
76
    // When phase's batch size changes, check for new 'remainder'
77
    const remainder = getRemainderPercent(newPhases);
2✔
78
    // if new remainder will be 0 or negative remove phase leave last phase to get remainder
79
    if (remainder < 1) {
2!
80
      newPhases.pop();
×
81
      newPhases[newPhases.length - 1].batch_size = null;
×
82
    }
83
    setDeploymentSettings({ phases: newPhases });
2✔
84
  };
85

86
  const addPhase = () => {
43✔
87
    let newPhases = [...phases];
1✔
88
    let newPhase = {};
1✔
89

90
    // assign new batch size to *previous* last batch
91
    const remainder = getRemainderPercent(newPhases);
1✔
92
    // make it default 10, unless remainder is <=10 in which case make it half remainder
93
    let batch_size = remainder > 10 ? 10 : Math.floor(remainder / 2);
1!
94
    newPhases[newPhases.length - 1].batch_size = batch_size;
1✔
95

96
    // check for previous phase delay or set 2hr default
97
    const delay = newPhases[newPhases.length - 1].delay || 2;
1✔
98
    newPhases[newPhases.length - 1].delay = delay;
1✔
99
    const delayUnit = newPhases[newPhases.length - 1].delayUnit || 'hours';
1✔
100
    newPhases[newPhases.length - 1].delayUnit = delayUnit;
1✔
101

102
    newPhases.push(newPhase);
1✔
103
    // use function to set new phases incl start time of new phase
104
    setDeploymentSettings({ phases: newPhases });
1✔
105
  };
106

107
  const removePhase = index => {
43✔
108
    let newPhases = phases;
×
109
    newPhases.splice(index, 1);
×
110

111
    // remove batch size from new last phase, use remainder
112
    delete newPhases[newPhases.length - 1].batch_size;
×
113

114
    if (newPhases.length === 1) {
×
115
      delete newPhases[0].delay;
×
116
    }
117
    setDeploymentSettings({ phases: newPhases });
×
118
  };
119

120
  const handleDelayToggle = (value, index) => {
43✔
121
    let newPhases = phases;
2✔
122
    newPhases[index].delayUnit = value;
2✔
123
    setDeploymentSettings({ phases: newPhases });
2✔
124
  };
125

126
  const remainder = getRemainderPercent(phases);
43✔
127

128
  // disable 'add phase' button if last phase/remainder has only 1 device left
129
  const disableAdd = !filter && (remainder / 100) * numberDevices <= 1;
43✔
130
  const startTime = phases.length ? phases[0].start_ts || new Date() : new Date();
43!
131
  const mappedPhases = phases.map((phase, index) => {
43✔
132
    let max = index > 0 ? 100 - phases[index - 1].batch_size : 100;
120✔
133
    const deviceCount = getPhaseDeviceCount(numberDevices, phase.batch_size, remainder, index === phases.length - 1);
120✔
134
    return (
120✔
135
      <TableRow className={classes.row} key={index}>
136
        <TableCell component="td" scope="row">
137
          <Chip size="small" label={`Phase ${index + 1}`} />
138
        </TableCell>
139
        <TableCell>
140
          <div className="flexbox center-aligned">
141
            {phase.batch_size && phase.batch_size < 100 ? (
317✔
142
              <Input
143
                value={phase.batch_size}
144
                onChange={event => updateBatchSize(event.target.value, index)}
2✔
145
                endAdornment={
146
                  <InputAdornment className={deviceCount < 1 ? 'warning' : ''} position="end">
77✔
147
                    %
148
                  </InputAdornment>
149
                }
150
                disabled={disabled && deviceCount >= 1}
77!
151
                inputProps={{
152
                  step: 1,
153
                  min: 1,
154
                  max: max,
155
                  type: 'number'
156
                }}
157
              />
158
            ) : (
159
              phase.batch_size || remainder
86✔
160
            )}
161
            {!filter && (
240✔
162
              <span className={deviceCount < 1 ? 'warning info' : 'info'} style={{ marginLeft: '5px' }}>{`(${deviceCount} ${pluralize(
120✔
163
                'device',
164
                deviceCount
165
              )})`}</span>
166
            )}
167
          </div>
168
          {!filter && deviceCount < 1 && <div className="warning">Phases must have at least 1 device</div>}
245✔
169
        </TableCell>
170
        <TableCell>
171
          <Time value={getPhaseStartTime(phases, index, startTime)} />
172
        </TableCell>
173
        <TableCell>
174
          {phase.delay && index !== phases.length - 1 ? (
317✔
175
            <div className={classes.delayInputWrapper}>
176
              <Input
177
                value={phase.delay}
178
                onChange={event => updateDelay(event.target.value, index)}
2✔
179
                inputProps={{ step: 1, min: 1, max: 720, type: 'number' }}
180
              />
181
              <Select onChange={event => handleDelayToggle(event.target.value, index)} value={phase.delayUnit || 'hours'}>
2!
182
                {timeframes.map(value => (
183
                  <MenuItem key={value} value={value}>
231✔
184
                    <div className="capitalized-start">{value}</div>
185
                  </MenuItem>
186
                ))}
187
              </Select>
188
            </div>
189
          ) : (
190
            '-'
191
          )}
192
        </TableCell>
193
        <TableCell>
194
          {index >= 1 ? (
120✔
195
            <IconButton onClick={() => removePhase(index)} size="large">
×
196
              <CancelIcon />
197
            </IconButton>
198
          ) : null}
199
        </TableCell>
200
      </TableRow>
201
    );
202
  });
203

204
  return (
43✔
205
    <div className={classNames}>
206
      <Table size="small">
207
        <TableHead>
208
          <TableRow>
209
            {tableHeaders.map((content, index) => (
210
              <TableCell key={index}>{content}</TableCell>
215✔
211
            ))}
212
          </TableRow>
213
        </TableHead>
214
        <TableBody>{mappedPhases}</TableBody>
215
      </Table>
216

217
      {!disableAdd ? <Chip className={classes.chip} color="primary" clickable={!disableAdd} icon={<AddIcon />} label="Add a phase" onClick={addPhase} /> : null}
43!
218
    </div>
219
  );
220
};
221

222
export const RolloutPatternSelection = props => {
11✔
223
  const { setDeploymentSettings, deploymentObject = {}, disableSchedule, isEnterprise, open = false, previousPhases = [] } = props;
243!
224
  const { deploymentDeviceCount = 0, deploymentDeviceIds = [], filter, phases = [] } = deploymentObject;
243✔
225

226
  const [usesPattern, setUsesPattern] = useState(open || phases.some(i => i));
243✔
227
  const { classes } = useStyles();
243✔
228

229
  const handlePatternChange = ({ target: { value } }) => {
243✔
230
    let updatedPhases = [];
1✔
231
    // check if a start time already exists from props and if so, use it
232
    const phaseStart = phases.length ? { start_ts: phases[0].start_ts } : {};
1!
233
    // if setting new custom pattern we use default 2 phases
234
    // for small groups get minimum batch size containing at least 1 device
235
    const minBatch = deploymentDeviceCount < 10 && !filter ? Math.ceil((1 / deploymentDeviceCount) * 100) : 10;
1!
236
    switch (value) {
1!
237
      case 0:
238
        updatedPhases = [{ batch_size: 100, ...phaseStart }];
×
239
        break;
×
240
      case 1:
241
        updatedPhases = [{ batch_size: minBatch, delay: 2, delayUnit: 'hours', ...phaseStart }, {}];
1✔
242
        break;
1✔
243
      default:
244
        // have to create a deep copy of the array to prevent overwriting, due to nested objects in the array
245
        updatedPhases = JSON.parse(JSON.stringify(value));
×
246
        break;
×
247
    }
248
    setDeploymentSettings({ phases: updatedPhases });
1✔
249
  };
250

251
  const onUsesPatternClick = useCallback(() => {
243✔
252
    if (usesPattern) {
1!
253
      setDeploymentSettings({ phases: phases.slice(0, 1) });
×
254
    }
255
    setUsesPattern(!usesPattern);
1✔
256
    // eslint-disable-next-line react-hooks/exhaustive-deps
257
  }, [usesPattern, JSON.stringify(phases), setDeploymentSettings, setUsesPattern]);
258

259
  const numberDevices = deploymentDeviceCount ? deploymentDeviceCount : deploymentDeviceIds ? deploymentDeviceIds.length : 0;
243!
260
  const customPattern = phases && phases.length > 1 ? 1 : 0;
243✔
261

262
  const previousPhaseOptions =
263
    previousPhases.length > 0
243✔
264
      ? previousPhases.map((previousPhaseSetting, index) => (
265
          <MenuItem key={`previousPhaseSetting-${index}`} value={previousPhaseSetting}>
110✔
266
            {previousPhaseSetting.reduce(
267
              (accu, phase, _, source) => {
268
                const phaseDescription = phase.delay
227✔
269
                  ? `${phase.batch_size}% > ${phase.delay} ${phase.delayUnit || 'hours'} >`
117!
270
                  : `${phase.batch_size || 100 / source.length}%`;
117✔
271
                return `${accu} ${phaseDescription}`;
227✔
272
              },
273
              `${previousPhaseSetting.length} ${pluralize('phase', previousPhaseSetting.length)}:`
274
            )}
275
          </MenuItem>
276
        ))
277
      : [
278
          <MenuItem key="noPreviousPhaseSetting" disabled={true} style={{ opacity: '0.4' }}>
279
            No recent patterns
280
          </MenuItem>
281
        ];
282
  return (
243✔
283
    <>
284
      <FormControlLabel
285
        control={<Checkbox color="primary" checked={usesPattern} disabled={!isEnterprise} onChange={onUsesPatternClick} size="small" />}
286
        label={
287
          <div className="flexbox center-aligned">
288
            <b className="margin-right-small">Select a rollout pattern</b> (optional)
289
            <InfoHintContainer>
290
              <EnterpriseNotification id={BENEFITS.phasedDeployments.id} />
291
              <DocsTooltip id={DOCSTIPS.phasedDeployments.id} />
292
            </InfoHintContainer>
293
          </div>
294
        }
295
      />
296
      <Collapse in={usesPattern}>
297
        <FormControl className={classes.patternSelection}>
298
          <Select className={classes.input} onChange={handlePatternChange} value={customPattern} disabled={!isEnterprise}>
299
            <MenuItem value={0}>Single phase: 100%</MenuItem>
300
            {(numberDevices > 1 || filter) && [
486✔
301
              <MenuItem key="customPhaseSetting" divider={true} value={1}>
302
                Custom
303
              </MenuItem>,
304
              <ListSubheader key="phaseSettingsSubheader">Recent patterns</ListSubheader>,
305
              ...previousPhaseOptions
306
            ]}
307
          </Select>
308
        </FormControl>
309
      </Collapse>
310
      {customPattern ? <PhaseSettings classNames="margin-bottom-small" disabled={disableSchedule} numberDevices={numberDevices} {...props} /> : null}
243✔
311
    </>
312
  );
313
};
314

315
export default PhaseSettings;
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