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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 hits per line

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

91.38
/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 } 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
  getReleaseListState,
58
  getReleasesById,
59
  getTenantCapabilities
60
} from '../../selectors';
61
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
62
import Confirm from '../common/confirm';
63
import { RolloutPatternSelection } from './deployment-wizard/phasesettings';
64
import { ForceDeploy, Retries, RolloutOptions } from './deployment-wizard/rolloutoptions';
65
import { ScheduleRollout } from './deployment-wizard/schedulerollout';
66
import { Devices, ReleasesWarning, Software } from './deployment-wizard/softwaredevices';
67

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

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

96
export const getPhaseStartTime = (phases, index, startDate) => {
11✔
97
  if (index < 1) {
128✔
98
    return startDate?.toISOString ? startDate.toISOString() : startDate;
49✔
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);
114✔
104
  return newStartTime.toISOString();
79✔
105
};
106

107
export const CreateDeployment = props => {
11✔
108
  const { deploymentObject = {}, onDismiss, onScheduleSubmit, setDeploymentSettings } = props;
245!
109

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

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

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

148
  useEffect(() => {
244✔
149
    const { devices = [], group, release } = deploymentObject;
13✔
150
    if (release) {
13✔
151
      dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_ARTIFACT_SELECTION));
3✔
152
    }
153
    dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_GROUP_SELECTION));
13✔
154
    let nextDeploymentObject = { deploymentDeviceCount: devices.length ? devices.length : 0 };
13!
155
    if (group === ALL_DEVICES) {
13✔
156
      dispatch(advanceOnboarding(onboardingSteps.SCHEDULING_ALL_DEVICES_SELECTION));
3✔
157
      nextDeploymentObject.deploymentDeviceCount = acceptedDeviceCount;
3✔
158
    }
159
    if (groups[group]) {
13!
160
      return dispatch(getGroupDevices(group, { perPage: 1 })).then(({ group: { total: deploymentDeviceCount } }) =>
×
161
        setDeploymentSettings({ deploymentDeviceCount })
×
162
      );
163
    }
164
    setDeploymentSettings(nextDeploymentObject);
13✔
165
    // eslint-disable-next-line react-hooks/exhaustive-deps
166
  }, [acceptedDeviceCount, deploymentObject.group, deploymentObject.release?.name, dispatch, JSON.stringify(groups), setDeploymentSettings]);
167

168
  useEffect(() => {
244✔
169
    let { deploymentDeviceCount: deviceCount, deploymentDeviceIds: deviceIds = [], devices = [] } = deploymentObject;
34✔
170
    if (devices.length) {
34!
171
      deviceIds = devices.map(({ id }) => id);
×
172
      deviceCount = deviceIds.length;
×
173
      devices = devices.map(({ id }) => ({ id, ...(devicesById[id] ?? {}) }));
×
174
    } else if (deploymentObject.group === ALL_DEVICES) {
34✔
175
      deviceCount = acceptedDeviceCount;
17✔
176
    }
177
    setDeploymentSettings({ deploymentDeviceIds: deviceIds, deploymentDeviceCount: deviceCount, devices });
34✔
178
    // eslint-disable-next-line react-hooks/exhaustive-deps
179
  }, [acceptedDeviceCount, JSON.stringify(deploymentObject), JSON.stringify(devicesById), setDeploymentSettings]);
180

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

189
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
244✔
190

191
  const closeWizard = () => {
244✔
192
    cleanUpDeploymentsStatus();
1✔
193
    onDismiss();
1✔
194
  };
195

196
  const onDeltaToggle = ({ target: { checked } }) => setDeploymentSettings({ delta: checked });
244✔
197

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

239
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
244✔
240

241
  const deploymentSettings = {
244✔
242
    ...deploymentObject,
243
    filter: groups[group]?.id ? groups[group] : undefined
244!
244
  };
245
  const disabled =
246
    isCreating.current ||
244✔
247
    !(deploymentSettings.release && (deploymentSettings.deploymentDeviceCount || !!deploymentSettings.filter || deploymentSettings.group)) ||
253✔
248
    !validatePhases(phases, deploymentSettings.deploymentDeviceCount, !!deploymentSettings.filter);
249

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

355
export default CreateDeployment;
356

357
const OnboardingComponent = ({
11✔
358
  releaseRef,
359
  groupRef,
360
  deploymentAnchor,
361
  deploymentObject,
362
  onboardingState,
363
  createdGroup,
364
  releasesById,
365
  releases,
366
  hasDevices
367
}) => {
368
  const { deploymentDeviceCount, devices, group, release: deploymentRelease = null } = deploymentObject;
240✔
369

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