• 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.47
/src/js/components/deployments/createdeployment.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, { useEffect, useRef, useState } from 'react';
15
import { useDispatch, useSelector } from 'react-redux';
16
import { useNavigate } from 'react-router-dom';
17

18
import { Close as CloseIcon, ExpandMore } from '@mui/icons-material';
19
import {
20
  Accordion,
21
  AccordionDetails,
22
  AccordionSummary,
23
  Button,
24
  Checkbox,
25
  Divider,
26
  Drawer,
27
  FormControlLabel,
28
  FormGroup,
29
  IconButton,
30
  Typography,
31
  accordionClasses
32
} from '@mui/material';
33
import { makeStyles } from 'tss-react/mui';
34

35
import moment from 'moment';
36
import pluralize from 'pluralize';
37

38
import DeltaIcon from '../../../assets/img/deltaicon.svg';
39
import { createDeployment, getDeploymentsConfig } from '../../actions/deploymentActions';
40
import { getGroupDevices, getSystemDevices } from '../../actions/deviceActions';
41
import { advanceOnboarding } from '../../actions/onboardingActions';
42
import { getReleases } from '../../actions/releaseActions';
43
import { ALL_DEVICES } from '../../constants/deviceConstants';
44
import { onboardingSteps } from '../../constants/onboardingConstants';
45
import { toggle, validatePhases } from '../../helpers';
46
import {
47
  getAcceptedDevices,
48
  getDeviceCountsByStatus,
49
  getDevicesById,
50
  getDocsVersion,
51
  getGlobalSettings,
52
  getGroupNames,
53
  getGroupsByIdWithoutUngrouped,
54
  getIdAttribute,
55
  getIsEnterprise,
56
  getOnboardingState,
57
  getReleasesById,
58
  getTenantCapabilities
59
} from '../../selectors';
60
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
61
import Confirm from '../common/confirm';
62
import { RolloutPatternSelection } from './deployment-wizard/phasesettings';
63
import { ForceDeploy, Retries, RolloutOptions } from './deployment-wizard/rolloutoptions';
64
import { ScheduleRollout } from './deployment-wizard/schedulerollout';
65
import { Devices, ReleasesWarning, Software } from './deployment-wizard/softwaredevices';
66

67
const useStyles = makeStyles()(theme => ({
12✔
68
  accordion: {
69
    backgroundColor: theme.palette.grey[400],
70
    marginTop: theme.spacing(4),
71
    '&:before': {
72
      display: 'none'
73
    },
74
    [`&.${accordionClasses.expanded}`]: {
75
      margin: 'auto',
76
      marginTop: theme.spacing(4)
77
    }
78
  },
79
  columns: {
80
    alignItems: 'start',
81
    columnGap: 30,
82
    display: 'grid',
83
    gridTemplateColumns: 'max-content 1fr',
84
    '&>p': {
85
      marginTop: theme.spacing(3)
86
    }
87
  },
88
  disabled: { color: theme.palette.text.disabled }
89
}));
90

91
const getAnchor = (element, heightAdjustment = 3) => ({
530✔
92
  top: element.offsetTop + element.offsetHeight / heightAdjustment,
93
  left: element.offsetLeft + element.offsetWidth
94
});
95

96
export const getPhaseStartTime = (phases, index, startDate) => {
12✔
97
  if (index < 1) {
136✔
98
    return startDate?.toISOString ? startDate.toISOString() : startDate;
53✔
99
  }
100
  // since we don't want to get stale phase start times when the creation dialog is open for a long time
101
  // we have to ensure start times are based on delay from previous phases
102
  // since there likely won't be 1000s of phases this should still be fine to recalculate
103
  const newStartTime = phases.slice(0, index).reduce((accu, phase) => moment(accu).add(phase.delay, phase.delayUnit), startDate);
120✔
104
  return newStartTime.toISOString();
83✔
105
};
106

107
export const CreateDeployment = props => {
12✔
108
  const { deploymentObject = {}, onDismiss, onScheduleSubmit, open = true, setDeploymentObject } = props;
358!
109

110
  const { canRetry, canSchedule, hasFullFiltering } = useSelector(getTenantCapabilities);
357✔
111
  const { createdGroup, groups, hasDynamicGroups } = useSelector(state => {
357✔
112
    const groups = getGroupsByIdWithoutUngrouped(state);
647✔
113
    const createdGroup = Object.keys(groups).length ? Object.keys(groups)[0] : undefined;
647!
114
    const hasDynamicGroups = Object.values(groups).some(group => !!group.id);
1,142✔
115
    return { createdGroup, hasDynamicGroups, groups };
647✔
116
  });
117
  const { hasDelta: hasDeltaEnabled } = useSelector(state => state.deployments.config) ?? {};
647!
118
  const { total: acceptedDeviceCount } = useSelector(getAcceptedDevices);
357✔
119
  const hasDevices = !!acceptedDeviceCount;
357✔
120
  const devicesById = useSelector(getDevicesById);
357✔
121
  const docsVersion = useSelector(getDocsVersion);
357✔
122
  const { pending: hasPending } = useSelector(getDeviceCountsByStatus);
357✔
123
  const { attribute: idAttribute } = useSelector(getIdAttribute);
357✔
124
  const isEnterprise = useSelector(getIsEnterprise);
357✔
125
  const { needsDeploymentConfirmation: needsCheck, previousPhases = [], retries: previousRetries = 0 } = useSelector(getGlobalSettings);
357✔
126
  const onboardingState = useSelector(getOnboardingState) || {};
357!
127
  const { complete: isOnboardingComplete } = onboardingState;
357✔
128
  const releases = useSelector(state => state.releases.releasesList.searchedIds);
647✔
129
  const releasesById = useSelector(getReleasesById);
357✔
130
  const groupNames = useSelector(getGroupNames);
357✔
131
  const dispatch = useDispatch();
357✔
132

133
  const isCreating = useRef(false);
357✔
134
  const [releaseSelectionLocked, setReleaseSelectionLocked] = useState(Boolean(deploymentObject.release));
357✔
135
  const [hasNewRetryDefault, setHasNewRetryDefault] = useState(false);
357✔
136
  const [isChecking, setIsChecking] = useState(false);
357✔
137
  const [isExpanded, setIsExpanded] = useState(false);
357✔
138
  const navigate = useNavigate();
357✔
139
  const releaseRef = useRef();
357✔
140
  const groupRef = useRef();
357✔
141
  const deploymentAnchor = useRef();
357✔
142
  const { classes } = useStyles();
357✔
143

144
  useEffect(() => {
357✔
145
    dispatch(getReleases({ page: 1, perPage: 100, searchOnly: true }));
7✔
146
    dispatch(getDeploymentsConfig());
7✔
147
  }, []);
148

149
  useEffect(() => {
357✔
150
    const { devices = [], group, release } = deploymentObject;
13✔
151
    if (release) {
13✔
152
      setReleaseSelectionLocked(Boolean(deploymentObject.release));
3✔
153
      dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_ARTIFACT_SELECTION));
3✔
154
    }
155
    if (!group) {
13✔
156
      setDeploymentObject({ ...deploymentObject, deploymentDeviceCount: devices.length ? devices.length : 0 });
10!
157
      return;
10✔
158
    }
159
    dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_GROUP_SELECTION));
3✔
160
    if (group === ALL_DEVICES) {
3!
161
      dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_ALL_DEVICES_SELECTION));
3✔
162
      setDeploymentObject({ ...deploymentObject, deploymentDeviceCount: acceptedDeviceCount });
3✔
163
      return;
3✔
164
    }
165
    if (!groups[group]) {
×
166
      setDeploymentObject({ ...deploymentObject, deploymentDeviceCount: devices.length ? devices.length : 0 });
×
167
      return;
×
168
    }
169
    dispatch(getGroupDevices(group, { perPage: 1 })).then(({ group: { total: deploymentDeviceCount } }) =>
×
170
      setDeploymentObject(deploymentObject => ({ ...deploymentObject, deploymentDeviceCount }))
×
171
    );
172
  }, [deploymentObject.group, deploymentObject.release]);
173

174
  useEffect(() => {
357✔
175
    let { deploymentDeviceCount: deviceCount, deploymentDeviceIds: deviceIds = [], devices = [] } = deploymentObject;
56✔
176
    if (devices.length) {
56!
177
      deviceIds = devices.map(({ id }) => id);
×
178
      deviceCount = deviceIds.length;
×
179
      devices = devices.map(({ id }) => ({ id, ...(devicesById[id] ?? {}) }));
×
180
    } else if (deploymentObject.group === ALL_DEVICES) {
56✔
181
      deviceCount = acceptedDeviceCount;
17✔
182
    }
183
    setDeploymentObject({ ...deploymentObject, deploymentDeviceIds: deviceIds, deploymentDeviceCount: deviceCount, devices });
56✔
184
  }, [JSON.stringify(deploymentObject), devicesById]);
185

186
  const cleanUpDeploymentsStatus = () => {
357✔
187
    if (!window.location.search) {
3!
188
      return;
3✔
189
    }
190
    const location = window.location.pathname.slice('/ui'.length);
×
191
    navigate(location); // lgtm [js/client-side-unvalidated-url-redirection]
×
192
  };
193

194
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
357✔
195

196
  const setDeploymentSettings = change => setDeploymentObject(current => ({ ...current, ...change }));
357✔
197

198
  const closeWizard = () => {
357✔
199
    cleanUpDeploymentsStatus();
1✔
200
    onDismiss();
1✔
201
  };
202

203
  const onDeltaToggle = ({ target: { checked } }) => setDeploymentSettings({ delta: checked });
357✔
204

205
  const onScheduleSubmitClick = settings => {
357✔
206
    if (needsCheck && !isChecking) {
2!
207
      return setIsChecking(true);
×
208
    }
209
    isCreating.current = true;
2✔
210
    const { delta, deploymentDeviceIds, devices, filterId, forceDeploy = false, group, phases, release, retries, update_control_map } = settings;
2!
211
    const startTime = phases?.length ? phases[0].start_ts : undefined;
2✔
212
    const retrySetting = canRetry && retries ? { retries } : {};
2✔
213
    const newDeployment = {
2✔
214
      artifact_name: release.Name,
215
      autogenerate_delta: delta,
216
      devices: (filterId || group) && !devices.length ? undefined : deploymentDeviceIds,
8!
217
      filter_id: filterId,
218
      all_devices: !filterId && group === ALL_DEVICES,
4✔
219
      group: group === ALL_DEVICES || devices.length ? undefined : group,
4!
220
      name: devices[0]?.id || (group ? decodeURIComponent(group) : ALL_DEVICES),
6!
221
      phases: phases
2✔
222
        ? phases.map((phase, i, origPhases) => {
223
            phase.start_ts = getPhaseStartTime(origPhases, i, startTime);
3✔
224
            return phase;
3✔
225
          })
226
        : phases,
227
      ...retrySetting,
228
      force_installation: forceDeploy,
229
      update_control_map
230
    };
231
    if (!isOnboardingComplete) {
2!
232
      dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_RELEASE_TO_DEVICES));
2✔
233
    }
234
    return dispatch(createDeployment(newDeployment, hasNewRetryDefault))
2✔
235
      .then(() => {
236
        // successfully retrieved new deployment
237
        cleanUpDeploymentsStatus();
2✔
238
        onScheduleSubmit();
2✔
239
      })
240
      .finally(() => {
241
        isCreating.current = false;
2✔
242
        setIsChecking(false);
2✔
243
      });
244
  };
245

246
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
357✔
247

248
  const deploymentSettings = {
357✔
249
    ...deploymentObject,
250
    filterId: groups[group] ? groups[group].id : undefined
357!
251
  };
252
  const disabled =
253
    isCreating.current ||
357✔
254
    !(deploymentSettings.release && (deploymentSettings.deploymentDeviceCount || deploymentSettings.filterId || deploymentSettings.group)) ||
352✔
255
    !validatePhases(phases, deploymentSettings.deploymentDeviceCount, deploymentSettings.filterId);
256

257
  const sharedProps = {
357✔
258
    ...props,
259
    getSystemDevices: (...args) => dispatch(getSystemDevices(...args)),
×
260
    canRetry,
261
    canSchedule,
262
    docsVersion,
263
    groupNames,
264
    getReleases: (...args) => dispatch(getReleases(...args)),
×
265
    groupRef,
266
    groups,
267
    hasDevices,
268
    hasDynamicGroups,
269
    hasFullFiltering,
270
    hasPending,
271
    idAttribute,
272
    isEnterprise,
273
    previousPhases,
274
    previousRetries,
275
    releaseRef,
276
    releases,
277
    releasesById,
278
    commonClasses: classes,
279
    deploymentObject: deploymentSettings,
280
    hasNewRetryDefault,
281
    onSaveRetriesSetting,
282
    open: false,
283
    releaseSelectionLocked,
284
    setDeploymentSettings
285
  };
286
  const hasReleases = !!Object.keys(releasesById).length;
357✔
287
  return (
357✔
288
    <Drawer anchor="right" open={open} onClose={closeWizard} PaperProps={{ style: { minWidth: '50vw' } }}>
289
      <div className="flexbox space-between center-aligned">
290
        <h3>Create a deployment</h3>
291
        <IconButton onClick={closeWizard} aria-label="close" size="large">
292
          <CloseIcon />
293
        </IconButton>
294
      </div>
295
      <Divider className="margin-bottom" />
296
      <FormGroup>
297
        {!hasReleases ? (
357!
298
          <ReleasesWarning />
299
        ) : (
300
          <>
301
            <Devices {...sharedProps} groupRef={groupRef} />
302
            <Software {...sharedProps} releaseRef={releaseRef} />
303
          </>
304
        )}
305
        <ScheduleRollout {...sharedProps} />
306
        <Accordion className={classes.accordion} square expanded={isExpanded} onChange={() => setIsExpanded(toggle)}>
1✔
307
          <AccordionSummary expandIcon={<ExpandMore />}>
308
            <Typography className={classes.disabled} variant="subtitle2">
309
              {isExpanded ? 'Hide' : 'Show'} advanced options
357✔
310
            </Typography>
311
          </AccordionSummary>
312
          <AccordionDetails>
313
            <RolloutPatternSelection {...sharedProps} />
314
            <RolloutOptions {...sharedProps} />
315
            <Retries {...sharedProps} />
316
            <ForceDeploy {...sharedProps} />
317
            {hasDeltaEnabled && (
566✔
318
              <FormControlLabel
319
                control={<Checkbox color="primary" checked={delta} onChange={onDeltaToggle} size="small" />}
320
                label={
321
                  <>
322
                    Generate and deploy Delta Artifacts (where available) <DeltaIcon />
323
                  </>
324
                }
325
              />
326
            )}
327
          </AccordionDetails>
328
        </Accordion>
329
      </FormGroup>
330
      <div className="margin-top">
331
        {isChecking && (
357!
332
          <Confirm
333
            classes="confirmation-overlay"
334
            cancel={() => setIsChecking(false)}
×
335
            action={() => onScheduleSubmitClick(deploymentSettings)}
×
336
            message={`This will deploy ${deploymentSettings.release?.Name} to ${deploymentDeviceCount} ${pluralize(
337
              'device',
338
              deploymentDeviceCount
339
            )}. Are you sure?`}
340
            style={{ paddingLeft: 12, justifyContent: 'flex-start', maxHeight: 44 }}
341
          />
342
        )}
343
        <Button onClick={closeWizard} style={{ marginRight: 10 }}>
344
          Cancel
345
        </Button>
346
        <Button variant="contained" color="primary" ref={deploymentAnchor} disabled={disabled} onClick={() => onScheduleSubmitClick(deploymentSettings)}>
2✔
347
          Create deployment
348
        </Button>
349
      </div>
350
      <OnboardingComponent
351
        releaseRef={releaseRef}
352
        groupRef={groupRef}
353
        deploymentObject={deploymentObject}
354
        deploymentAnchor={deploymentAnchor}
355
        onboardingState={onboardingState}
356
        createdGroup={createdGroup}
357
        releasesById={releasesById}
358
        releases={releases}
359
        hasDevices={hasDevices}
360
      />
361
    </Drawer>
362
  );
363
};
364

365
export default CreateDeployment;
366

367
const OnboardingComponent = ({
12✔
368
  releaseRef,
369
  groupRef,
370
  deploymentAnchor,
371
  deploymentObject,
372
  onboardingState,
373
  createdGroup,
374
  releasesById,
375
  releases,
376
  hasDevices
377
}) => {
378
  const { deploymentDeviceCount, devices, group, release: deploymentRelease = null } = deploymentObject;
239✔
379

380
  let onboardingComponent = null;
239✔
381
  if (releaseRef.current && groupRef.current && deploymentAnchor.current) {
239✔
382
    const anchor = getAnchor(releaseRef.current);
235✔
383
    const groupAnchor = getAnchor(groupRef.current);
235✔
384
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.SCHEDULING_ALL_DEVICES_SELECTION, onboardingState, { anchor: groupAnchor, place: 'right' });
235✔
385
    if (createdGroup) {
235!
386
      onboardingComponent = getOnboardingComponentFor(
235✔
387
        onboardingSteps.SCHEDULING_GROUP_SELECTION,
388
        { ...onboardingState, createdGroup },
389
        { anchor: groupAnchor, place: 'right' },
390
        onboardingComponent
391
      );
392
    }
393
    if (deploymentDeviceCount && !deploymentRelease) {
235✔
394
      onboardingComponent = getOnboardingComponentFor(
4✔
395
        onboardingSteps.SCHEDULING_ARTIFACT_SELECTION,
396
        { ...onboardingState, selectedRelease: releasesById[releases[0]] || {} },
4!
397
        { anchor, place: 'right' },
398
        onboardingComponent
399
      );
400
    }
401
    if (hasDevices && (deploymentDeviceCount || devices?.length) && deploymentRelease) {
235✔
402
      const buttonAnchor = getAnchor(deploymentAnchor.current, 2);
60✔
403
      onboardingComponent = getOnboardingComponentFor(
60✔
404
        onboardingSteps.SCHEDULING_RELEASE_TO_DEVICES,
405
        { ...onboardingState, selectedDevice: devices.length ? devices[0] : undefined, selectedGroup: group, selectedRelease: deploymentRelease },
60!
406
        { anchor: buttonAnchor, place: 'right' },
407
        onboardingComponent
408
      );
409
    }
410
  }
411
  return onboardingComponent;
239✔
412
};
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