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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

81.29
/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 { getPhaseDeviceCount, getRemainderPercent } from '../../../helpers';
40
import EnterpriseNotification from '../../common/enterpriseNotification';
41
import Time from '../../common/time';
42
import { getPhaseStartTime } from '../createdeployment';
43

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

52
const timeframes = ['minutes', 'hours', 'days'];
12✔
53
const tableHeaders = ['', 'Batch size', 'Phase begins', 'Delay before next phase', ''];
12✔
54

55
export const PhaseSettings = ({ classNames, deploymentObject, disabled, numberDevices, setDeploymentSettings }) => {
12✔
56
  const { classes } = useStyles();
45✔
57

58
  const { filterId, phases = [] } = deploymentObject;
45!
59
  const updateDelay = (value, index) => {
45✔
60
    let newPhases = phases;
2✔
61
    // value must be at least 1
62
    value = Math.max(1, value);
2✔
63
    newPhases[index].delay = value;
2✔
64

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

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

83
  const addPhase = () => {
45✔
84
    let newPhases = [...phases];
1✔
85
    let newPhase = {};
1✔
86

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

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

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

104
  const removePhase = index => {
45✔
105
    let newPhases = phases;
×
106
    newPhases.splice(index, 1);
×
107

108
    // remove batch size from new last phase, use remainder
109
    delete newPhases[newPhases.length - 1].batch_size;
×
110

111
    if (newPhases.length === 1) {
×
112
      delete newPhases[0].delay;
×
113
    }
114
    setDeploymentSettings({ phases: newPhases });
×
115
  };
116

117
  const handleDelayToggle = (value, index) => {
45✔
118
    let newPhases = phases;
2✔
119
    newPhases[index].delayUnit = value;
2✔
120
    setDeploymentSettings({ phases: newPhases });
2✔
121
  };
122

123
  const remainder = getRemainderPercent(phases);
45✔
124

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

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

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

219
export const RolloutPatternSelection = props => {
12✔
220
  const { setDeploymentSettings, deploymentObject = {}, disableSchedule, isEnterprise, open = false, previousPhases = [] } = props;
242!
221
  const { deploymentDeviceCount = 0, deploymentDeviceIds = [], filterId, phases = [] } = deploymentObject;
242✔
222

223
  const [usesPattern, setUsesPattern] = useState(open || phases.some(i => i));
242✔
224
  const { classes } = useStyles();
242✔
225

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

248
  const onUsesPatternClick = useCallback(() => {
242✔
249
    if (usesPattern) {
1!
250
      setDeploymentSettings({ phases: phases.slice(0, 1) });
×
251
    }
252
    setUsesPattern(!usesPattern);
1✔
253
  }, [usesPattern, JSON.stringify(phases), setDeploymentSettings, setUsesPattern]);
254

255
  const numberDevices = deploymentDeviceCount ? deploymentDeviceCount : deploymentDeviceIds ? deploymentDeviceIds.length : 0;
242!
256
  const customPattern = phases && phases.length > 1 ? 1 : 0;
242✔
257

258
  const previousPhaseOptions =
259
    previousPhases.length > 0
242✔
260
      ? previousPhases.map((previousPhaseSetting, index) => (
261
          <MenuItem key={`previousPhaseSetting-${index}`} value={previousPhaseSetting}>
109✔
262
            {previousPhaseSetting.reduce((accu, phase, _, source) => {
263
              const phaseDescription = phase.delay
230✔
264
                ? `${phase.batch_size}% > ${phase.delay} ${phase.delayUnit || 'hours'} >`
121!
265
                : `${phase.batch_size || 100 / source.length}%`;
121✔
266
              return `${accu} ${phaseDescription}`;
230✔
267
            }, `${previousPhaseSetting.length} ${pluralize('phase', previousPhaseSetting.length)}:`)}
268
          </MenuItem>
269
        ))
270
      : [
271
          <MenuItem key="noPreviousPhaseSetting" disabled={true} style={{ opacity: '0.4' }}>
272
            No recent patterns
273
          </MenuItem>
274
        ];
275
  return (
242✔
276
    <>
277
      <FormControlLabel
278
        control={<Checkbox color="primary" checked={usesPattern} disabled={!isEnterprise} onChange={onUsesPatternClick} size="small" />}
279
        label={
280
          <>
281
            <b>Select a rollout pattern</b> (optional)
282
          </>
283
        }
284
      />
285
      <Collapse in={usesPattern}>
286
        <FormControl className={classes.patternSelection}>
287
          <Select className={classes.input} onChange={handlePatternChange} value={customPattern} disabled={!isEnterprise}>
288
            <MenuItem value={0}>Single phase: 100%</MenuItem>
289
            {(numberDevices > 1 || filterId) && [
484✔
290
              <MenuItem key="customPhaseSetting" divider={true} value={1}>
291
                Custom
292
              </MenuItem>,
293
              <ListSubheader key="phaseSettingsSubheader">Recent patterns</ListSubheader>,
294
              ...previousPhaseOptions
295
            ]}
296
          </Select>
297
        </FormControl>
298
      </Collapse>
299
      <EnterpriseNotification isEnterprise={isEnterprise} benefit="choose to roll out deployments in multiple phases" />
300
      {customPattern ? <PhaseSettings classNames="margin-bottom-small" disabled={disableSchedule} numberDevices={numberDevices} {...props} /> : null}
242✔
301
    </>
302
  );
303
};
304

305
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