• 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

62.22
/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 { useDispatch, useSelector } 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 { removeArtifact, removeRelease, selectArtifact, selectRelease } 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 { HELPTOOLTIPS, MenderHelpTooltip } 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(', ');
25✔
50
  return (
25✔
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
    tooltip: <MenderHelpTooltip id={HELPTOOLTIPS.expandArtifact.id} className="margin-left-small" />
64
  },
65
  {
66
    title: 'Type',
67
    name: 'type',
68
    sortable: false,
69
    render: ({ artifact }) => <div style={{ maxWidth: '100vw' }}>{artifact.updates.reduce((accu, item) => (accu ? accu : item.type_info.type), '')}</div>
25!
70
  },
71
  { title: 'Size', name: 'size', sortable: true, render: ({ artifact }) => <FileSize fileSize={artifact.size} /> },
25✔
72
  { title: 'Last modified', name: 'modified', sortable: true, render: ({ artifact }) => <RelativeTime updateTime={formatTime(artifact.modified)} /> }
25✔
73
];
74

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

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

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

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

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

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

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

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

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

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

UNCOV
184
  const { classes } = useStyles();
×
185

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

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

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

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

239
  if (!artifacts.length) {
26✔
240
    return null;
2✔
241
  }
242

243
  const items = artifacts.sort(customSort(sortDown, sortCol));
24✔
244

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

282
export const ReleaseDetails = () => {
6✔
283
  const [showRemoveDialog, setShowRemoveArtifactDialog] = useState(false);
40✔
284
  const [confirmReleaseDeletion, setConfirmReleaseDeletion] = useState(false);
39✔
285
  // eslint-disable-next-line no-unused-vars
286
  const windowSize = useWindowSize();
39✔
287
  const creationRef = useRef();
39✔
288
  const drawerRef = useRef();
39✔
289
  const navigate = useNavigate();
39✔
290
  const dispatch = useDispatch();
39✔
291
  const { hasReleaseTags } = useSelector(getFeatures);
39✔
292
  const onboardingState = useSelector(getOnboardingState);
39✔
293
  const pastDeploymentsCount = useSelector(state => state.deployments.byStatus.finished.total);
72✔
294
  const release = useSelector(state => state.releases.byId[state.releases.selectedRelease]) ?? {};
72✔
295
  const selectedArtifact = useSelector(state => state.releases.selectedArtifact);
72✔
296
  const userCapabilities = useSelector(getUserCapabilities);
39✔
297

298
  const onRemoveArtifact = artifact => dispatch(removeArtifact(artifact.id)).finally(() => setShowRemoveArtifactDialog(false));
39✔
299

300
  const copyLinkToClipboard = () => {
39✔
UNCOV
301
    const location = window.location.href.substring(0, window.location.href.indexOf('/releases') + '/releases'.length);
×
UNCOV
302
    copy(`${location}/${release.Name}`);
×
UNCOV
303
    dispatch(setSnackbar('Link copied to clipboard'));
×
304
  };
305

306
  const onCloseClick = () => dispatch(selectRelease());
39✔
307

308
  const onCreateDeployment = () => {
39✔
UNCOV
309
    if (!onboardingState.complete) {
×
UNCOV
310
      dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_INCLUDED_DEPLOY_ONBOARDING));
×
UNCOV
311
      if (pastDeploymentsCount === 1) {
×
UNCOV
312
        dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_MODIFIED_ONBOARDING));
×
313
      }
314
    }
UNCOV
315
    navigate(`${DEPLOYMENT_ROUTES.active.route}?open=true&release=${encodeURIComponent(release.Name)}`);
×
316
  };
317

318
  const onToggleReleaseDeletion = () => setConfirmReleaseDeletion(toggle);
39✔
319

320
  const onDeleteRelease = () => dispatch(removeRelease(release.Name)).then(() => setConfirmReleaseDeletion(false));
39✔
321

322
  const artifacts = release.Artifacts ?? [];
39✔
323
  return (
39✔
324
    <Drawer anchor="right" open={!!release.Name} onClose={onCloseClick} PaperProps={{ style: { minWidth: '60vw' }, ref: drawerRef }}>
325
      <div className="flexbox center-aligned space-between">
326
        <div className="flexbox center-aligned">
327
          <b>
328
            Release information for <i>{release.Name}</i>
329
          </b>
330
          <IconButton onClick={copyLinkToClipboard} size="large">
331
            <LinkIcon />
332
          </IconButton>
333
        </div>
334
        <div className="flexbox center-aligned">
335
          <div className="muted margin-right flexbox">
336
            <div className="margin-right-small">Last modified:</div>
337
            <RelativeTime updateTime={release.modified} />
338
          </div>
339
          <IconButton onClick={onCloseClick} aria-label="close" size="large">
340
            <CloseIcon />
341
          </IconButton>
342
        </div>
343
      </div>
344
      <Divider className="margin-bottom" />
345
      {hasReleaseTags && <ReleaseTags />}
39!
346
      <ArtifactsList
347
        artifacts={artifacts}
UNCOV
348
        selectArtifact={artifact => dispatch(selectArtifact(artifact))}
×
349
        selectedArtifact={selectedArtifact}
350
        setShowRemoveArtifactDialog={setShowRemoveArtifactDialog}
351
      />
352
      <OnboardingComponent creationRef={creationRef} drawerRef={drawerRef} onboardingState={onboardingState} />
353
      <RemoveArtifactDialog
354
        artifact={selectedArtifact}
355
        open={!!showRemoveDialog}
356
        onCancel={() => setShowRemoveArtifactDialog(false)}
2✔
UNCOV
357
        onRemove={() => onRemoveArtifact(selectedArtifact)}
×
358
      />
359
      <RemoveArtifactDialog open={!!confirmReleaseDeletion} onRemove={onDeleteRelease} onCancel={onToggleReleaseDeletion} release={release} />
360
      <ReleaseQuickActions
361
        actionCallbacks={{ onCreateDeployment, onDeleteRelease: onToggleReleaseDeletion }}
362
        innerRef={creationRef}
363
        selectedRelease={release}
364
        userCapabilities={userCapabilities}
365
      />
366
    </Drawer>
367
  );
368
};
369

370
export default 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