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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

64.1
/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 { removeArtifact, removeRelease, selectArtifact, selectRelease } from '../../actions/releaseActions';
35
import { DEPLOYMENT_ROUTES } from '../../constants/deploymentConstants';
36
import { FileSize, customSort, formatTime, toggle } from '../../helpers';
37
import { getFeatures, getShowHelptips, getUserCapabilities } from '../../selectors';
38
import useWindowSize from '../../utils/resizehook';
39
import ChipSelect from '../common/chipselect';
40
import { RelativeTime } from '../common/time';
41
import { ExpandArtifact } from '../helptips/helptooltips';
42
import Artifact from './artifact';
43
import RemoveArtifactDialog from './dialogs/removeartifact';
44

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

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

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

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

111
export const ReleaseQuickActions = ({ actionCallbacks, innerRef, selectedRelease, userCapabilities }) => {
6✔
112
  const [showActions, setShowActions] = useState(false);
22✔
113
  const { classes } = useStyles();
22✔
114

115
  const actions = useMemo(() => {
22✔
116
    return Object.values(defaultActions).reduce((accu, action) => {
2✔
117
      if (action.isApplicable({ userCapabilities })) {
4!
118
        accu.push(action);
4✔
119
      }
120
      return accu;
4✔
121
    }, []);
122
    // eslint-disable-next-line react-hooks/exhaustive-deps
123
  }, [JSON.stringify(userCapabilities)]);
124

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

151
const ReleaseTags = ({ existingTags = [] }) => {
6!
UNCOV
152
  const [selectedTags, setSelectedTags] = useState(existingTags);
×
UNCOV
153
  const [isEditing, setIsEditing] = useState(false);
×
154

UNCOV
155
  const onToggleEdit = () => {
×
UNCOV
156
    setSelectedTags(existingTags);
×
UNCOV
157
    setIsEditing(toggle);
×
158
  };
159

UNCOV
160
  const onTagSelectionChanged = ({ selection }) => setSelectedTags(selection);
×
161

UNCOV
162
  const onSave = () => {
×
UNCOV
163
    console.log('saving tags', selectedTags);
×
164
  };
165

UNCOV
166
  const { classes } = useStyles();
×
167

UNCOV
168
  return (
×
169
    <div className="margin-bottom" style={{ maxWidth: 500 }}>
170
      <div className="flexbox center-aligned">
171
        <h4 className="margin-right">Tags</h4>
172
        {!isEditing && (
×
173
          <Button onClick={onToggleEdit} size="small" startIcon={<EditIcon />}>
174
            Edit
175
          </Button>
176
        )}
177
      </div>
178
      <ChipSelect
179
        className={classes.tagSelect}
180
        id="release-tags"
181
        label=""
182
        onChange={onTagSelectionChanged}
183
        disabled={!isEditing}
184
        key={`${isEditing}`}
185
        placeholder={isEditing ? 'Enter release tags' : 'Click edit to add release tags'}
×
186
        selection={selectedTags}
187
        options={existingTags}
188
      />
189
      <Collapse in={isEditing}>
190
        <div className="flexbox center-aligned margin-top-small" style={{ justifyContent: 'end' }}>
191
          <Button variant="contained" onClick={onSave} color="secondary" style={{ marginRight: 10 }}>
192
            Save
193
          </Button>
194
          <Button onClick={onToggleEdit}>Cancel</Button>
195
        </div>
196
      </Collapse>
197
    </div>
198
  );
199
};
200

201
const ArtifactsList = ({ artifacts, selectArtifact, selectedArtifact, setShowRemoveArtifactDialog, showHelptips }) => {
6✔
202
  const [sortCol, setSortCol] = useState('modified');
22✔
203
  const [sortDown, setSortDown] = useState(true);
22✔
204

205
  const onRowSelection = artifact => {
22✔
UNCOV
206
    if (!artifact || !selectedArtifact || selectedArtifact.id !== artifact.id) {
×
UNCOV
207
      return selectArtifact(artifact);
×
208
    }
UNCOV
209
    selectArtifact();
×
210
  };
211

212
  const sortColumn = col => {
22✔
UNCOV
213
    if (!col.sortable) {
×
UNCOV
214
      return;
×
215
    }
216
    // sort table
UNCOV
217
    setSortDown(toggle);
×
UNCOV
218
    setSortCol(col);
×
219
  };
220

221
  if (!artifacts.length) {
22✔
222
    return null;
3✔
223
  }
224

225
  const items = artifacts.sort(customSort(sortDown, sortCol));
19✔
226

227
  return (
19✔
228
    <>
229
      <h4>Artifacts in this Release:</h4>
230
      <div>
231
        <div className="release-repo-item repo-item repo-header">
232
          {columns.map(item => (
233
            <Tooltip key={item.name} className="columnHeader" title={item.title} placement="top-start" onClick={() => sortColumn(item)}>
76✔
234
              <div>
235
                {item.title}
236
                {item.sortable ? <SortIcon className={`sortIcon ${sortCol === item.name ? 'selected' : ''} ${sortDown.toString()}`} /> : null}
114✔
237
              </div>
238
            </Tooltip>
239
          ))}
240
          <div style={{ width: 48 }} />
241
        </div>
242
        {items.map((pkg, index) => {
243
          const expanded = !!(selectedArtifact && selectedArtifact.id === pkg.id);
19✔
244
          return (
19✔
245
            <Artifact
246
              key={`repository-item-${index}`}
247
              artifact={pkg}
248
              columns={columns}
249
              expanded={expanded}
250
              index={index}
UNCOV
251
              onRowSelection={() => onRowSelection(pkg)}
×
252
              // this will be run after expansion + collapse and both need some time to fully settle
253
              // otherwise the measurements are off
254
              showRemoveArtifactDialog={setShowRemoveArtifactDialog}
255
            />
256
          );
257
        })}
258
      </div>
259
      {showHelptips && (
38✔
260
        <span className="relative">
261
          <ExpandArtifact />
262
        </span>
263
      )}
264
    </>
265
  );
266
};
267

268
export const ReleaseDetails = () => {
6✔
269
  const [showRemoveDialog, setShowRemoveArtifactDialog] = useState(false);
37✔
270
  const [confirmReleaseDeletion, setConfirmReleaseDeletion] = useState(false);
37✔
271
  // eslint-disable-next-line no-unused-vars
272
  const windowSize = useWindowSize();
37✔
273
  const creationRef = useRef();
37✔
274
  const drawerRef = useRef();
37✔
275
  const navigate = useNavigate();
37✔
276
  const dispatch = useDispatch();
37✔
277
  const { hasReleaseTags } = useSelector(getFeatures);
37✔
278
  const release = useSelector(state => state.releases.byId[state.releases.selectedRelease]) ?? {};
64✔
279
  const selectedArtifact = useSelector(state => state.releases.selectedArtifact);
64✔
280
  const showHelptips = useSelector(getShowHelptips);
37✔
281
  const userCapabilities = useSelector(getUserCapabilities);
37✔
282

283
  const onRemoveArtifact = artifact => dispatch(removeArtifact(artifact.id)).finally(() => setShowRemoveArtifactDialog(false));
37✔
284

285
  const copyLinkToClipboard = () => {
37✔
UNCOV
286
    const location = window.location.href.substring(0, window.location.href.indexOf('/releases') + '/releases'.length);
×
UNCOV
287
    copy(`${location}/${release.Name}`);
×
UNCOV
288
    dispatch(setSnackbar('Link copied to clipboard'));
×
289
  };
290

291
  const onCloseClick = () => dispatch(selectRelease());
37✔
292

293
  const onCreateDeployment = () => navigate(`${DEPLOYMENT_ROUTES.active.route}?open=true&release=${encodeURIComponent(release.Name)}`);
37✔
294

295
  const onToggleReleaseDeletion = () => setConfirmReleaseDeletion(toggle);
37✔
296

297
  const onDeleteRelease = () => dispatch(removeRelease(release.Name)).then(() => setConfirmReleaseDeletion(false));
37✔
298

299
  const artifacts = release.Artifacts ?? [];
37✔
300
  return (
37✔
301
    <Drawer anchor="right" open={!!release.Name} onClose={onCloseClick} PaperProps={{ style: { minWidth: '60vw' }, ref: drawerRef }}>
302
      <div className="flexbox center-aligned space-between">
303
        <div className="flexbox center-aligned">
304
          <b>
305
            Release information for <i>{release.Name}</i>
306
          </b>
307
          <IconButton onClick={copyLinkToClipboard} size="large">
308
            <LinkIcon />
309
          </IconButton>
310
        </div>
311
        <div className="flexbox center-aligned">
312
          <div className="muted margin-right flexbox">
313
            <div className="margin-right-small">Last modified:</div>
314
            <RelativeTime updateTime={release.modified} />
315
          </div>
316
          <IconButton onClick={onCloseClick} aria-label="close" size="large">
317
            <CloseIcon />
318
          </IconButton>
319
        </div>
320
      </div>
321
      <Divider className="margin-bottom" />
322
      {hasReleaseTags && <ReleaseTags />}
37!
323
      <ArtifactsList
324
        artifacts={artifacts}
UNCOV
325
        selectArtifact={artifact => dispatch(selectArtifact(artifact))}
×
326
        selectedArtifact={selectedArtifact}
327
        setShowRemoveArtifactDialog={setShowRemoveArtifactDialog}
328
        showHelptips={showHelptips}
329
      />
330
      <RemoveArtifactDialog
331
        artifact={selectedArtifact}
332
        open={!!showRemoveDialog}
333
        onCancel={() => setShowRemoveArtifactDialog(false)}
2✔
UNCOV
334
        onRemove={() => onRemoveArtifact(selectedArtifact)}
×
335
      />
336
      <RemoveArtifactDialog open={!!confirmReleaseDeletion} onRemove={onDeleteRelease} onCancel={onToggleReleaseDeletion} release={release} />
337
      <ReleaseQuickActions
338
        actionCallbacks={{ onCreateDeployment, onDeleteRelease: onToggleReleaseDeletion }}
339
        innerRef={creationRef}
340
        selectedRelease={release}
341
        userCapabilities={userCapabilities}
342
      />
343
    </Drawer>
344
  );
345
};
346

347
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