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

mendersoftware / gui / 1350829378

27 Jun 2024 01:46PM UTC coverage: 83.494% (-16.5%) from 99.965%
1350829378

Pull #4465

gitlab-ci

mzedel
chore: test fixes

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #4465: MEN-7169 - feat: added multi sorting capabilities to devices view

4506 of 6430 branches covered (70.08%)

81 of 100 new or added lines in 14 files covered. (81.0%)

1661 existing lines in 163 files now uncovered.

8574 of 10269 relevant lines covered (83.49%)

160.6 hits per line

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

92.24
/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 { getRelease, 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) => ({
127✔
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) {
82✔
98
    return startDate?.toISOString ? startDate.toISOString() : startDate;
33✔
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);
69✔
104
  return newStartTime.toISOString();
49✔
105
};
106

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

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

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

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

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

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

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

188
  const onSaveRetriesSetting = hasNewRetryDefault => setHasNewRetryDefault(hasNewRetryDefault);
62✔
189

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

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

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

238
  const { delta, deploymentDeviceCount, group, phases } = deploymentObject;
62✔
239

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

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

354
export default CreateDeployment;
355

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

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