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

mendersoftware / gui / 944676341

pending completion
944676341

Pull #3875

gitlab-ci

mzedel
chore: aligned snapshots with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3875: MEN-5414

4469 of 6446 branches covered (69.33%)

230 of 266 new or added lines in 43 files covered. (86.47%)

1712 existing lines in 161 files now uncovered.

8406 of 10170 relevant lines covered (82.65%)

196.7 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
    columnGap: 30,
81
    display: 'grid',
82
    gridTemplateColumns: 'max-content max-content',
83
    '&>p': {
84
      marginTop: theme.spacing(3)
85
    }
86
  },
87
  disabled: { color: theme.palette.text.disabled }
88
}));
89

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

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

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

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

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

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

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

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

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

193
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
342✔
194

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

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

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

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

245
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
342✔
246

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

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

364
export default CreateDeployment;
365

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

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