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

mendersoftware / gui / 914712491

pending completion
914712491

Pull #3798

gitlab-ci

mzedel
refac: refactored signup page to make better use of form capabilities

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3798: MEN-3530 - refactored forms

4359 of 6322 branches covered (68.95%)

92 of 99 new or added lines in 11 files covered. (92.93%)

1715 existing lines in 159 files now uncovered.

8203 of 9941 relevant lines covered (82.52%)

150.06 hits per line

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

81.39
/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, UNGROUPED_GROUP } 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
  getIdAttribute,
53
  getIsEnterprise,
54
  getOnboardingState,
55
  getReleasesById,
56
  getTenantCapabilities
57
} from '../../selectors';
58
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
59
import Confirm from '../common/confirm';
60
import { RolloutPatternSelection } from './deployment-wizard/phasesettings';
61
import { ForceDeploy, Retries, RolloutOptions } from './deployment-wizard/rolloutoptions';
62
import { ScheduleRollout } from './deployment-wizard/schedulerollout';
63
import { Devices, ReleasesWarning, Software } from './deployment-wizard/softwaredevices';
64

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

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

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

105
export const CreateDeployment = props => {
12✔
106
  const { deploymentObject = {}, onDismiss, onScheduleSubmit, open = true, setDeploymentObject } = props;
280!
107

108
  const { canRetry, canSchedule, hasFullFiltering } = useSelector(getTenantCapabilities);
279✔
109
  const { createdGroup, groups, hasDynamicGroups } = useSelector(state => {
279✔
110
    // eslint-disable-next-line no-unused-vars
111
    const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = state.devices.groups.byId;
492✔
112
    const createdGroup = Object.keys(groups).length ? Object.keys(groups)[0] : undefined;
492!
113
    const hasDynamicGroups = Object.values(groups).some(group => !!group.id);
889✔
114
    return { createdGroup, hasDynamicGroups, groups };
492✔
115
  });
116
  const { hasDelta: hasDeltaEnabled } = useSelector(state => state.deployments.config) ?? {};
492!
117
  const { total: acceptedDeviceCount } = useSelector(getAcceptedDevices);
279✔
118
  const hasDevices = !!acceptedDeviceCount;
279✔
119
  const devicesById = useSelector(getDevicesById);
279✔
120
  const docsVersion = useSelector(getDocsVersion);
279✔
121
  const { pending: hasPending } = useSelector(getDeviceCountsByStatus);
279✔
122
  const { attribute: idAttribute } = useSelector(getIdAttribute);
279✔
123
  const isEnterprise = useSelector(getIsEnterprise);
279✔
124
  const { needsDeploymentConfirmation: needsCheck, previousPhases = [], retries: previousRetries = 0 } = useSelector(getGlobalSettings);
279✔
125
  const onboardingState = useSelector(getOnboardingState) || {};
279!
126
  const { complete: isOnboardingComplete } = onboardingState;
279✔
127
  const releases = useSelector(state => state.releases.releasesList.searchedIds);
492✔
128
  const releasesById = useSelector(getReleasesById);
279✔
129
  const dispatch = useDispatch();
279✔
130

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

142
  useEffect(() => {
279✔
143
    dispatch(getReleases({ page: 1, perPage: 100, searchOnly: true }));
5✔
144
    dispatch(getDeploymentsConfig());
5✔
145
  }, []);
146

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

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

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

192
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
279✔
193

194
  const setDeploymentSettings = change => setDeploymentObject(current => ({ ...current, ...change }));
279✔
195

196
  const closeWizard = () => {
279✔
197
    cleanUpDeploymentsStatus();
1✔
198
    onDismiss();
1✔
199
  };
200

201
  const onDeltaToggle = ({ target: { checked } }) => setDeploymentSettings({ delta: checked });
279✔
202

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

244
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
279✔
245

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

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

362
export default CreateDeployment;
363

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

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