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

mendersoftware / gui / 1315496247

03 Jun 2024 07:49AM UTC coverage: 83.437% (-16.5%) from 99.964%
1315496247

Pull #4434

gitlab-ci

mzedel
chore: aligned snapshots with updated mui version

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4434: chore: Bump the mui group with 3 updates

4476 of 6391 branches covered (70.04%)

8488 of 10173 relevant lines covered (83.44%)

140.36 hits per line

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

92.17
/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) => ({
162✔
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) {
67✔
98
    return startDate?.toISOString ? startDate.toISOString() : startDate;
28✔
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);
54✔
104
  return newStartTime.toISOString();
39✔
105
};
106

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

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

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

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

148
  useEffect(() => {
75✔
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));
5✔
157
      nextDeploymentObject.deploymentDeviceCount = acceptedDeviceCount;
5✔
158
    }
159
    if (groups[group]) {
13!
160
      dispatch(getGroupDevices(group, { perPage: 1 })).then(({ group: { total: deploymentDeviceCount } }) => setDeploymentSettings({ deploymentDeviceCount }));
×
161
    }
162
    setDeploymentSettings(nextDeploymentObject);
13✔
163
    // eslint-disable-next-line react-hooks/exhaustive-deps
164
  }, [acceptedDeviceCount, deploymentObject.group, deploymentObject.release?.name, dispatch, JSON.stringify(groups), setDeploymentSettings]);
165

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

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

187
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
75✔
188

189
  const closeWizard = () => {
75✔
190
    cleanUpDeploymentsStatus();
1✔
191
    onDismiss();
1✔
192
  };
193

194
  const onDeltaToggle = ({ target: { checked } }) => setDeploymentSettings({ delta: checked });
75✔
195

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

237
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
75✔
238

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

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

353
export default CreateDeployment;
354

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

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