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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

62.69
/src/js/components/releases/releasedetails.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, { useMemo, useRef, useState } from 'react';
15
import { connect } from 'react-redux';
16
import { useNavigate } from 'react-router-dom';
17

18
// material ui
19
import {
20
  Close as CloseIcon,
21
  Edit as EditIcon,
22
  HighlightOffOutlined as HighlightOffOutlinedIcon,
23
  Link as LinkIcon,
24
  Replay as ReplayIcon,
25
  Sort as SortIcon
26
} from '@mui/icons-material';
27
import { Button, Collapse, Divider, Drawer, IconButton, SpeedDial, SpeedDialAction, SpeedDialIcon, Tooltip } from '@mui/material';
28
import { speedDialActionClasses } from '@mui/material/SpeedDialAction';
29
import { makeStyles } from 'tss-react/mui';
30

31
import copy from 'copy-to-clipboard';
32

33
import { setSnackbar } from '../../actions/appActions';
34
import { advanceOnboarding } from '../../actions/onboardingActions';
35
import { editArtifact, removeArtifact, removeRelease, selectArtifact, selectRelease, uploadArtifact } from '../../actions/releaseActions';
36
import { DEPLOYMENT_ROUTES } from '../../constants/deploymentConstants';
37
import { onboardingSteps } from '../../constants/onboardingConstants';
38
import { FileSize, customSort, formatTime, toggle } from '../../helpers';
39
import { getFeatures, getOnboardingState, getUserCapabilities } from '../../selectors';
40
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
41
import useWindowSize from '../../utils/resizehook';
42
import ChipSelect from '../common/chipselect';
43
import { RelativeTime } from '../common/time';
44
import { ExpandArtifact } from '../helptips/helptooltips';
45
import Artifact from './artifact';
46
import RemoveArtifactDialog from './dialogs/removeartifact';
47

48
const DeviceTypeCompatibility = ({ artifact }) => {
6✔
49
  const compatible = artifact.artifact_depends ? artifact.artifact_depends.device_type.join(', ') : artifact.device_types_compatible.join(', ');
12✔
50
  return (
12✔
51
    <Tooltip title={compatible} placement="top-start">
52
      <div className="text-overflow">{compatible}</div>
53
    </Tooltip>
54
  );
55
};
56

57
export const columns = [
6✔
58
  {
59
    title: 'Device type compatibility',
60
    name: 'device_types',
61
    sortable: false,
62
    render: DeviceTypeCompatibility
63
  },
64
  {
65
    title: 'Type',
66
    name: 'type',
67
    sortable: false,
68
    render: ({ artifact }) => <div style={{ maxWidth: '100vw' }}>{artifact.updates.reduce((accu, item) => (accu ? accu : item.type_info.type), '')}</div>
12!
69
  },
70
  { title: 'Size', name: 'size', sortable: true, render: ({ artifact }) => <FileSize fileSize={artifact.size} /> },
12✔
71
  { title: 'Last modified', name: 'modified', sortable: true, render: ({ artifact }) => <RelativeTime updateTime={formatTime(artifact.modified)} /> }
12✔
72
];
73

74
const defaultActions = [
6✔
75
  {
76
    action: ({ onCreateDeployment, selection }) => onCreateDeployment(selection),
×
77
    icon: <ReplayIcon />,
78
    isApplicable: ({ userCapabilities: { canDeploy } }) => canDeploy,
2✔
79
    key: 'deploy',
80
    title: 'Create a deployment for this release'
81
  },
82
  {
83
    action: ({ onDeleteRelease, selection }) => onDeleteRelease(selection),
×
84
    icon: <HighlightOffOutlinedIcon className="red" />,
85
    isApplicable: ({ userCapabilities: { canManageReleases } }) => canManageReleases,
2✔
86
    key: 'delete',
87
    title: 'Delete release'
88
  }
89
];
90

91
const useStyles = makeStyles()(theme => ({
6✔
92
  container: {
93
    display: 'flex',
94
    position: 'fixed',
95
    bottom: theme.spacing(6.5),
96
    right: theme.spacing(6.5),
97
    zIndex: 10,
98
    minWidth: 'max-content',
99
    alignItems: 'flex-end',
100
    justifyContent: 'flex-end',
101
    pointerEvents: 'none',
102
    [`.${speedDialActionClasses.staticTooltipLabel}`]: {
103
      minWidth: 'max-content'
104
    }
105
  },
106
  fab: { margin: theme.spacing(2) },
107
  tagSelect: { maxWidth: 350 },
108
  label: {
109
    marginRight: theme.spacing(2),
110
    marginBottom: theme.spacing(4)
111
  }
112
}));
113

114
export const ReleaseQuickActions = ({ actionCallbacks, innerRef, selectedRelease, userCapabilities }) => {
6✔
115
  const [showActions, setShowActions] = useState(false);
12✔
116
  const { classes } = useStyles();
12✔
117

118
  const actions = useMemo(() => {
12✔
119
    return Object.values(defaultActions).reduce((accu, action) => {
2✔
120
      if (action.isApplicable({ userCapabilities })) {
4!
121
        accu.push(action);
4✔
122
      }
123
      return accu;
4✔
124
    }, []);
125
  }, [JSON.stringify(userCapabilities)]);
126

127
  return (
12✔
128
    <div className={classes.container} ref={innerRef}>
129
      <div className={classes.label}>Release actions</div>
130
      <SpeedDial
131
        className={classes.fab}
132
        ariaLabel="device-actions"
133
        icon={<SpeedDialIcon />}
134
        onClose={() => setShowActions(false)}
×
135
        onOpen={setShowActions}
136
        open={Boolean(showActions)}
137
      >
138
        {actions.map(action => (
139
          <SpeedDialAction
24✔
140
            key={action.key}
141
            aria-label={action.key}
142
            icon={action.icon}
143
            tooltipTitle={action.title}
144
            tooltipOpen
145
            onClick={() => action.action({ ...actionCallbacks, selection: selectedRelease })}
×
146
          />
147
        ))}
148
      </SpeedDial>
149
    </div>
150
  );
151
};
152

153
const OnboardingComponent = ({ creationRef, drawerRef, onboardingState }) => {
6✔
154
  if (!(creationRef.current && drawerRef.current)) {
12✔
155
    return null;
2✔
156
  }
157
  const anchor = {
10✔
158
    anchor: {
159
      left: creationRef.current.offsetLeft - drawerRef.current.offsetLeft - 48,
160
      top: creationRef.current.offsetTop + creationRef.current.offsetHeight - 48
161
    },
162
    place: 'left'
163
  };
164
  let onboardingComponent = getOnboardingComponentFor(onboardingSteps.ARTIFACT_INCLUDED_DEPLOY_ONBOARDING, onboardingState, anchor);
10✔
165
  return getOnboardingComponentFor(onboardingSteps.ARTIFACT_MODIFIED_ONBOARDING, onboardingState, anchor, onboardingComponent);
10✔
166
};
167

168
const ReleaseTags = ({ existingTags = [] }) => {
6!
169
  const [selectedTags, setSelectedTags] = useState(existingTags);
×
170
  const [isEditing, setIsEditing] = useState(false);
×
171

172
  const onToggleEdit = () => {
×
173
    setSelectedTags(existingTags);
×
174
    setIsEditing(toggle);
×
175
  };
176

177
  const onTagSelectionChanged = ({ selection }) => setSelectedTags(selection);
×
178

179
  const onSave = () => {
×
180
    console.log('saving tags', selectedTags);
×
181
  };
182

183
  const { classes } = useStyles();
×
184

185
  return (
×
186
    <div className="margin-bottom" style={{ maxWidth: 500 }}>
187
      <div className="flexbox center-aligned">
188
        <h4 className="margin-right">Tags</h4>
189
        {!isEditing && (
×
190
          <Button onClick={onToggleEdit} size="small" startIcon={<EditIcon />}>
191
            Edit
192
          </Button>
193
        )}
194
      </div>
195
      <ChipSelect
196
        className={classes.tagSelect}
197
        id="release-tags"
198
        label=""
199
        onChange={onTagSelectionChanged}
200
        disabled={!isEditing}
201
        key={`${isEditing}`}
202
        placeholder={isEditing ? 'Enter release tags' : 'Click edit to add release tags'}
×
203
        selection={selectedTags}
204
        options={existingTags}
205
      />
206
      <Collapse in={isEditing}>
207
        <div className="flexbox center-aligned margin-top-small" style={{ justifyContent: 'end' }}>
208
          <Button variant="contained" onClick={onSave} color="secondary" style={{ marginRight: 10 }}>
209
            Save
210
          </Button>
211
          <Button onClick={onToggleEdit}>Cancel</Button>
212
        </div>
213
      </Collapse>
214
    </div>
215
  );
216
};
217

218
const ArtifactsList = ({ artifacts, editArtifact, selectArtifact, selectedArtifact, setShowRemoveArtifactDialog, showHelptips }) => {
6✔
219
  const [sortCol, setSortCol] = useState('modified');
12✔
220
  const [sortDown, setSortDown] = useState(true);
12✔
221

222
  const onRowSelection = artifact => {
12✔
223
    if (!artifact || !selectedArtifact || selectedArtifact.id !== artifact.id) {
×
224
      return selectArtifact(artifact);
×
225
    }
226
    selectArtifact();
×
227
  };
228

229
  const sortColumn = col => {
12✔
230
    if (!col.sortable) {
×
231
      return;
×
232
    }
233
    // sort table
234
    setSortDown(toggle);
×
235
    setSortCol(col);
×
236
  };
237

238
  if (!artifacts.length) {
12✔
239
    return null;
1✔
240
  }
241

242
  const items = artifacts.sort(customSort(sortDown, sortCol));
11✔
243

244
  return (
11✔
245
    <>
246
      <h4>Artifacts in this Release:</h4>
247
      <div>
248
        <div className="release-repo-item repo-item repo-header">
249
          {columns.map(item => (
250
            <Tooltip key={item.name} className="columnHeader" title={item.title} placement="top-start" onClick={() => sortColumn(item)}>
44✔
251
              <div>
252
                {item.title}
253
                {item.sortable ? <SortIcon className={`sortIcon ${sortCol === item.name ? 'selected' : ''} ${sortDown.toString()}`} /> : null}
66✔
254
              </div>
255
            </Tooltip>
256
          ))}
257
          <div style={{ width: 48 }} />
258
        </div>
259
        {items.map((pkg, index) => {
260
          const expanded = !!(selectedArtifact && selectedArtifact.id === pkg.id);
11✔
261
          return (
11✔
262
            <Artifact
263
              key={`repository-item-${index}`}
264
              artifact={pkg}
265
              columns={columns}
266
              expanded={expanded}
267
              index={index}
268
              onEdit={editArtifact}
269
              onRowSelection={() => onRowSelection(pkg)}
×
270
              // this will be run after expansion + collapse and both need some time to fully settle
271
              // otherwise the measurements are off
272
              showRemoveArtifactDialog={setShowRemoveArtifactDialog}
273
            />
274
          );
275
        })}
276
      </div>
277
      {showHelptips && (
22✔
278
        <span className="relative">
279
          <ExpandArtifact />
280
        </span>
281
      )}
282
    </>
283
  );
284
};
285

286
export const ReleaseDetails = ({
6✔
287
  advanceOnboarding,
288
  editArtifact,
289
  features,
290
  onboardingState,
291
  pastDeploymentsCount,
292
  release,
293
  removeArtifact,
294
  removeRelease,
295
  selectArtifact,
296
  selectedArtifact,
297
  selectRelease,
298
  setSnackbar,
299
  showHelptips,
300
  userCapabilities
301
}) => {
302
  const [showRemoveDialog, setShowRemoveArtifactDialog] = useState(false);
21✔
303
  const [confirmReleaseDeletion, setConfirmReleaseDeletion] = useState(false);
21✔
304
  // eslint-disable-next-line no-unused-vars
305
  const windowSize = useWindowSize();
21✔
306
  const creationRef = useRef();
21✔
307
  const drawerRef = useRef();
21✔
308
  const navigate = useNavigate();
21✔
309

310
  const { hasReleaseTags } = features;
21✔
311

312
  const editArtifactData = (id, description) => editArtifact(id, { description });
21✔
313

314
  const onRemoveArtifact = artifact => removeArtifact(artifact.id).finally(() => setShowRemoveArtifactDialog(false));
21✔
315

316
  const copyLinkToClipboard = () => {
21✔
317
    const location = window.location.href.substring(0, window.location.href.indexOf('/releases') + '/releases'.length);
×
318
    copy(`${location}/${release.Name}`);
×
319
    setSnackbar('Link copied to clipboard');
×
320
  };
321

322
  const onCloseClick = () => selectRelease();
21✔
323

324
  const onCreateDeployment = () => {
21✔
325
    if (!onboardingState.complete) {
×
326
      advanceOnboarding(onboardingSteps.ARTIFACT_INCLUDED_DEPLOY_ONBOARDING);
×
327
      if (pastDeploymentsCount === 1) {
×
328
        advanceOnboarding(onboardingSteps.ARTIFACT_MODIFIED_ONBOARDING);
×
329
      }
330
    }
331
    navigate(`${DEPLOYMENT_ROUTES.active.route}?open=true&release=${encodeURIComponent(release.Name)}`);
×
332
  };
333

334
  const onToggleReleaseDeletion = () => setConfirmReleaseDeletion(toggle);
21✔
335

336
  const onDeleteRelease = () => removeRelease(release.Name).then(() => setConfirmReleaseDeletion(false));
21✔
337

338
  const artifacts = release.Artifacts ?? [];
21✔
339
  return (
21✔
340
    <Drawer anchor="right" open={!!release.Name} onClose={onCloseClick} PaperProps={{ style: { minWidth: '60vw' }, ref: drawerRef }}>
341
      <div className="flexbox center-aligned space-between">
342
        <div className="flexbox center-aligned">
343
          <b>
344
            Release information for <i>{release.Name}</i>
345
          </b>
346
          <IconButton onClick={copyLinkToClipboard} size="large">
347
            <LinkIcon />
348
          </IconButton>
349
        </div>
350
        <div className="flexbox center-aligned">
351
          <div className="muted margin-right flexbox">
352
            <div className="margin-right-small">Last modified:</div>
353
            <RelativeTime updateTime={release.modified} />
354
          </div>
355
          <IconButton onClick={onCloseClick} aria-label="close" size="large">
356
            <CloseIcon />
357
          </IconButton>
358
        </div>
359
      </div>
360
      <Divider className="margin-bottom" />
361
      {hasReleaseTags && <ReleaseTags />}
21!
362
      <ArtifactsList
363
        artifacts={artifacts}
364
        editArtifact={editArtifactData}
365
        selectArtifact={selectArtifact}
366
        selectedArtifact={selectedArtifact}
367
        setShowRemoveArtifactDialog={setShowRemoveArtifactDialog}
368
        showHelptips={showHelptips}
369
      />
370
      <OnboardingComponent creationRef={creationRef} drawerRef={drawerRef} onboardingState={onboardingState} />
371
      <RemoveArtifactDialog
372
        artifact={selectedArtifact}
373
        open={!!showRemoveDialog}
374
        onCancel={() => setShowRemoveArtifactDialog(false)}
2✔
375
        onRemove={() => onRemoveArtifact(selectedArtifact)}
×
376
      />
377
      <RemoveArtifactDialog open={!!confirmReleaseDeletion} onRemove={onDeleteRelease} onCancel={onToggleReleaseDeletion} release={release} />
378
      <ReleaseQuickActions
379
        actionCallbacks={{ onCreateDeployment, onDeleteRelease: onToggleReleaseDeletion }}
380
        innerRef={creationRef}
381
        selectedRelease={release}
382
        userCapabilities={userCapabilities}
383
      />
384
    </Drawer>
385
  );
386
};
387

388
const actionCreators = { advanceOnboarding, editArtifact, removeArtifact, removeRelease, selectArtifact, selectRelease, setSnackbar, uploadArtifact };
6✔
389

390
const mapStateToProps = state => {
6✔
391
  return {
27✔
392
    features: getFeatures(state),
393
    onboardingState: getOnboardingState(state),
394
    pastDeploymentsCount: state.deployments.byStatus.finished.total,
395
    release: state.releases.byId[state.releases.selectedRelease] ?? {},
34✔
396
    selectedArtifact: state.releases.selectedArtifact,
397
    showHelptips: state.users.showHelptips,
398
    userCapabilities: getUserCapabilities(state)
399
  };
400
};
401

402
export default connect(mapStateToProps, actionCreators)(ReleaseDetails);
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