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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

81.19
/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 { connect } 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, UNGROUPED_GROUP } from '../../constants/deviceConstants';
44
import { onboardingSteps } from '../../constants/onboardingConstants';
45
import { toggle, validatePhases } from '../../helpers';
46
import { getDocsVersion, getIdAttribute, getIsEnterprise, getOnboardingState, getTenantCapabilities } from '../../selectors';
47
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
48
import Confirm from '../common/confirm';
49
import { RolloutPatternSelection } from './deployment-wizard/phasesettings';
50
import { ForceDeploy, Retries, RolloutOptions } from './deployment-wizard/rolloutoptions';
51
import { ScheduleRollout } from './deployment-wizard/schedulerollout';
52
import { Devices, ReleasesWarning, Software } from './deployment-wizard/softwaredevices';
53

54
const useStyles = makeStyles()(theme => ({
12✔
55
  accordion: {
56
    backgroundColor: theme.palette.grey[400],
57
    marginTop: theme.spacing(4),
58
    '&:before': {
59
      display: 'none'
60
    },
61
    [`&.${accordionClasses.expanded}`]: {
62
      margin: 'auto',
63
      marginTop: theme.spacing(4)
64
    }
65
  },
66
  columns: {
67
    alignItems: 'start',
68
    columnGap: 30,
69
    display: 'grid',
70
    gridTemplateColumns: 'max-content 1fr',
71
    '&>p': {
72
      marginTop: theme.spacing(3)
73
    }
74
  },
75
  disabled: { color: theme.palette.text.disabled }
76
}));
77

78
const getAnchor = (element, heightAdjustment = 3) => ({
542✔
79
  top: element.offsetTop + element.offsetHeight / heightAdjustment,
80
  left: element.offsetLeft + element.offsetWidth
81
});
82

83
export const getPhaseStartTime = (phases, index, startDate) => {
12✔
84
  if (index < 1) {
136✔
85
    return startDate?.toISOString ? startDate.toISOString() : startDate;
53✔
86
  }
87
  // since we don't want to get stale phase start times when the creation dialog is open for a long time
88
  // we have to ensure start times are based on delay from previous phases
89
  // since there likely won't be 1000s of phases this should still be fine to recalculate
90
  const newStartTime = phases.slice(0, index).reduce((accu, phase) => moment(accu).add(phase.delay, phase.delayUnit), startDate);
120✔
91
  return newStartTime.toISOString();
83✔
92
};
93

94
export const CreateDeployment = props => {
12✔
95
  const {
96
    acceptedDeviceCount,
97
    advanceOnboarding,
98
    canRetry,
99
    createdGroup,
100
    createDeployment,
101
    deploymentObject = {},
×
102
    devicesById,
103
    getDeploymentsConfig,
104
    getGroupDevices,
105
    getReleases,
106
    groups,
107
    hasDeltaEnabled,
108
    hasDevices,
109
    isOnboardingComplete,
110
    onboardingState,
111
    onDismiss,
112
    onScheduleSubmit,
113
    open = true,
1✔
114
    needsCheck,
115
    releases,
116
    releasesById,
117
    setDeploymentObject
118
  } = props;
277✔
119

120
  const isCreating = useRef(false);
276✔
121
  const [releaseSelectionLocked, setReleaseSelectionLocked] = useState(Boolean(deploymentObject.release));
276✔
122
  const [hasNewRetryDefault, setHasNewRetryDefault] = useState(false);
276✔
123
  const [isChecking, setIsChecking] = useState(false);
276✔
124
  const [isExpanded, setIsExpanded] = useState(false);
276✔
125
  const navigate = useNavigate();
276✔
126
  const releaseRef = useRef();
276✔
127
  const groupRef = useRef();
276✔
128
  const deploymentAnchor = useRef();
276✔
129
  const { classes } = useStyles();
276✔
130

131
  useEffect(() => {
276✔
132
    getReleases({ page: 1, perPage: 100, searchOnly: true });
5✔
133
    getDeploymentsConfig();
5✔
134
  }, []);
135

136
  useEffect(() => {
276✔
137
    const { devices = [], group, release } = deploymentObject;
11✔
138
    if (release) {
11✔
139
      advanceOnboarding(onboardingSteps.SCHEDULING_ARTIFACT_SELECTION);
3✔
140
      setReleaseSelectionLocked(Boolean(deploymentObject.release));
3✔
141
    }
142
    if (!group) {
11✔
143
      setDeploymentObject({ ...deploymentObject, deploymentDeviceCount: devices.length ? devices.length : 0 });
8!
144
      return;
8✔
145
    }
146
    advanceOnboarding(onboardingSteps.SCHEDULING_GROUP_SELECTION);
3✔
147
    if (group === ALL_DEVICES) {
3!
148
      advanceOnboarding(onboardingSteps.SCHEDULING_ALL_DEVICES_SELECTION);
3✔
149
      setDeploymentObject({ ...deploymentObject, deploymentDeviceCount: acceptedDeviceCount });
3✔
150
      return;
3✔
151
    }
152
    if (!groups[group]) {
×
153
      setDeploymentObject({ ...deploymentObject, deploymentDeviceCount: devices.length ? devices.length : 0 });
×
154
      return;
×
155
    }
156
    getGroupDevices(group, { perPage: 1 }).then(({ group: { total: deploymentDeviceCount } }) =>
×
157
      setDeploymentObject(deploymentObject => ({ ...deploymentObject, deploymentDeviceCount }))
×
158
    );
159
  }, [deploymentObject.group, deploymentObject.release]);
160

161
  useEffect(() => {
276✔
162
    let { deploymentDeviceCount: deviceCount, deploymentDeviceIds: deviceIds = [], devices = [] } = deploymentObject;
46✔
163
    if (devices.length) {
46!
164
      deviceIds = devices.map(({ id }) => id);
×
165
      deviceCount = deviceIds.length;
×
166
      devices = devices.map(({ id }) => ({ id, ...(devicesById[id] ?? {}) }));
×
167
    } else if (deploymentObject.group === ALL_DEVICES) {
46✔
168
      deviceCount = acceptedDeviceCount;
17✔
169
    }
170
    setDeploymentObject({ ...deploymentObject, deploymentDeviceIds: deviceIds, deploymentDeviceCount: deviceCount, devices });
46✔
171
  }, [JSON.stringify(deploymentObject), devicesById]);
172

173
  const cleanUpDeploymentsStatus = () => {
276✔
174
    if (!window.location.search) {
3!
175
      return;
3✔
176
    }
177
    const location = window.location.pathname.slice('/ui'.length);
×
178
    navigate(location); // lgtm [js/client-side-unvalidated-url-redirection]
×
179
  };
180

181
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
276✔
182

183
  const setDeploymentSettings = change => setDeploymentObject(current => ({ ...current, ...change }));
276✔
184

185
  const closeWizard = () => {
276✔
186
    cleanUpDeploymentsStatus();
1✔
187
    onDismiss();
1✔
188
  };
189

190
  const onDeltaToggle = ({ target: { checked } }) => setDeploymentSettings({ delta: checked });
276✔
191

192
  const onScheduleSubmitClick = settings => {
276✔
193
    if (needsCheck && !isChecking) {
2!
194
      return setIsChecking(true);
×
195
    }
196
    isCreating.current = true;
2✔
197
    const { delta, deploymentDeviceIds, devices, filterId, forceDeploy = false, group, phases, release, retries, update_control_map } = settings;
2!
198
    const startTime = phases?.length ? phases[0].start_ts : undefined;
2✔
199
    const retrySetting = canRetry && retries ? { retries } : {};
2✔
200
    const newDeployment = {
2✔
201
      artifact_name: release.Name,
202
      autogenerate_delta: delta,
203
      devices: (filterId || group) && !devices.length ? undefined : deploymentDeviceIds,
8!
204
      filter_id: filterId,
205
      all_devices: !filterId && group === ALL_DEVICES,
4✔
206
      group: group === ALL_DEVICES || devices.length ? undefined : group,
4!
207
      name: devices[0]?.id || (group ? decodeURIComponent(group) : ALL_DEVICES),
6!
208
      phases: phases
2✔
209
        ? phases.map((phase, i, origPhases) => {
210
            phase.start_ts = getPhaseStartTime(origPhases, i, startTime);
3✔
211
            return phase;
3✔
212
          })
213
        : phases,
214
      ...retrySetting,
215
      force_installation: forceDeploy,
216
      update_control_map
217
    };
218
    if (!isOnboardingComplete) {
2!
219
      advanceOnboarding(onboardingSteps.SCHEDULING_RELEASE_TO_DEVICES);
2✔
220
    }
221
    return createDeployment(newDeployment, hasNewRetryDefault)
2✔
222
      .then(() => {
223
        // successfully retrieved new deployment
224
        cleanUpDeploymentsStatus();
2✔
225
        onScheduleSubmit();
2✔
226
      })
227
      .finally(() => {
228
        isCreating.current = false;
2✔
229
        setIsChecking(false);
2✔
230
      });
231
  };
232

233
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
276✔
234

235
  const deploymentSettings = {
276✔
236
    ...deploymentObject,
237
    filterId: groups[group] ? groups[group].id : undefined
276!
238
  };
239
  const disabled =
240
    isCreating.current ||
276✔
241
    !(deploymentSettings.release && (deploymentSettings.deploymentDeviceCount || deploymentSettings.filterId || deploymentSettings.group)) ||
271✔
242
    !validatePhases(phases, deploymentSettings.deploymentDeviceCount, deploymentSettings.filterId);
243

244
  const sharedProps = {
276✔
245
    ...props,
246
    commonClasses: classes,
247
    deploymentObject: deploymentSettings,
248
    hasNewRetryDefault,
249
    onSaveRetriesSetting,
250
    open: false,
251
    releaseSelectionLocked,
252
    setDeploymentSettings
253
  };
254
  const hasReleases = !!Object.keys(releasesById).length;
276✔
255
  return (
276✔
256
    <Drawer anchor="right" open={open} onClose={closeWizard} PaperProps={{ style: { minWidth: '50vw' } }}>
257
      <div className="flexbox space-between center-aligned">
258
        <h3>Create a deployment</h3>
259
        <IconButton onClick={closeWizard} aria-label="close" size="large">
260
          <CloseIcon />
261
        </IconButton>
262
      </div>
263
      <Divider className="margin-bottom" />
264
      <FormGroup>
265
        {!hasReleases ? (
276!
266
          <ReleasesWarning />
267
        ) : (
268
          <>
269
            <Devices {...sharedProps} groupRef={groupRef} />
270
            <Software {...sharedProps} releaseRef={releaseRef} />
271
          </>
272
        )}
273
        <ScheduleRollout {...sharedProps} />
274
        <Accordion className={classes.accordion} square expanded={isExpanded} onChange={() => setIsExpanded(toggle)}>
1✔
275
          <AccordionSummary expandIcon={<ExpandMore />}>
276
            <Typography className={classes.disabled} variant="subtitle2">
277
              {isExpanded ? 'Hide' : 'Show'} advanced options
276✔
278
            </Typography>
279
          </AccordionSummary>
280
          <AccordionDetails>
281
            <RolloutPatternSelection {...sharedProps} />
282
            <RolloutOptions {...sharedProps} />
283
            <Retries {...sharedProps} />
284
            <ForceDeploy {...sharedProps} />
285
            {hasDeltaEnabled && (
457✔
286
              <FormControlLabel
287
                control={<Checkbox color="primary" checked={delta} onChange={onDeltaToggle} size="small" />}
288
                label={
289
                  <>
290
                    Generate and deploy Delta Artifacts (where available) <DeltaIcon />
291
                  </>
292
                }
293
              />
294
            )}
295
          </AccordionDetails>
296
        </Accordion>
297
      </FormGroup>
298
      <div className="margin-top">
299
        {isChecking && (
276!
300
          <Confirm
301
            classes="confirmation-overlay"
302
            cancel={() => setIsChecking(false)}
×
303
            action={() => onScheduleSubmitClick(deploymentSettings)}
×
304
            message={`This will deploy ${deploymentSettings.release?.Name} to ${deploymentDeviceCount} ${pluralize(
305
              'device',
306
              deploymentDeviceCount
307
            )}. Are you sure?`}
308
            style={{ paddingLeft: 12, justifyContent: 'flex-start', maxHeight: 44 }}
309
          />
310
        )}
311
        <Button onClick={closeWizard} style={{ marginRight: 10 }}>
312
          Cancel
313
        </Button>
314
        <Button variant="contained" color="primary" ref={deploymentAnchor} disabled={disabled} onClick={() => onScheduleSubmitClick(deploymentSettings)}>
2✔
315
          Create deployment
316
        </Button>
317
      </div>
318
      <OnboardingComponent
319
        releaseRef={releaseRef}
320
        groupRef={groupRef}
321
        deploymentObject={deploymentObject}
322
        deploymentAnchor={deploymentAnchor}
323
        onboardingState={onboardingState}
324
        createdGroup={createdGroup}
325
        releasesById={releasesById}
326
        releases={releases}
327
        hasDevices={hasDevices}
328
      />
329
    </Drawer>
330
  );
331
};
332

333
const actionCreators = { advanceOnboarding, createDeployment, getDeploymentsConfig, getGroupDevices, getReleases, getSystemDevices };
12✔
334

335
export const mapStateToProps = state => {
12✔
336
  const { canRetry, canSchedule, hasFullFiltering } = getTenantCapabilities(state);
209✔
337
  // eslint-disable-next-line no-unused-vars
338
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = state.devices.groups.byId;
209✔
339
  const { hasDelta } = state.deployments.config ?? {};
209!
340
  return {
209✔
341
    acceptedDeviceCount: state.devices.byStatus.accepted.total,
342
    canRetry,
343
    canSchedule,
344
    createdGroup: Object.keys(groups).length ? Object.keys(groups)[0] : undefined,
209!
345
    devicesById: state.devices.byId,
346
    docsVersion: getDocsVersion(state),
347
    groups,
348
    hasDevices: state.devices.byStatus.accepted.total || state.devices.byStatus.accepted.deviceIds.length > 0,
209!
349
    hasDeltaEnabled: hasDelta,
350
    hasDynamicGroups: Object.values(groups).some(group => !!group.id),
371✔
351
    hasFullFiltering,
352
    hasPending: state.devices.byStatus.pending.total,
353
    idAttribute: getIdAttribute(state).attribute,
354
    isEnterprise: getIsEnterprise(state),
355
    isHosted: state.app.features.isHosted,
356
    isOnboardingComplete: state.onboarding.complete,
357
    needsCheck: state.users.globalSettings.needsDeploymentConfirmation,
358
    onboardingState: getOnboardingState(state),
359
    previousPhases: state.users.globalSettings.previousPhases || [],
354✔
360
    previousRetries: state.users.globalSettings.retries || 0,
410✔
361
    releases: state.releases.releasesList.searchedIds,
362
    releasesById: state.releases.byId
363
  };
364
};
365

366
export default connect(mapStateToProps, actionCreators)(CreateDeployment);
367

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

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