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

mendersoftware / gui / 947088195

pending completion
947088195

Pull #2661

gitlab-ci

mzedel
chore: improved device filter scrolling behaviour

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #2661: chore: added lint rules for hooks usage

4411 of 6415 branches covered (68.76%)

297 of 440 new or added lines in 62 files covered. (67.5%)

1617 existing lines in 163 files now uncovered.

8311 of 10087 relevant lines covered (82.39%)

192.12 hits per line

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

63.04
/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, getShowHelptips, 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(', ');
21✔
50
  return (
21✔
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>
21!
69
  },
70
  { title: 'Size', name: 'size', sortable: true, render: ({ artifact }) => <FileSize fileSize={artifact.size} /> },
21✔
71
  { title: 'Last modified', name: 'modified', sortable: true, render: ({ artifact }) => <RelativeTime updateTime={formatTime(artifact.modified)} /> }
21✔
72
];
73

74
const defaultActions = [
6✔
75
  {
UNCOV
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
  {
UNCOV
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);
23✔
116
  const { classes } = useStyles();
23✔
117

118
  const actions = useMemo(() => {
23✔
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
    // eslint-disable-next-line react-hooks/exhaustive-deps
126
  }, [JSON.stringify(userCapabilities)]);
127

128
  return (
23✔
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
46✔
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)) {
23✔
156
    return null;
2✔
157
  }
158
  const anchor = {
21✔
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);
21✔
166
  return getOnboardingComponentFor(onboardingSteps.ARTIFACT_MODIFIED_ONBOARDING, onboardingState, anchor, onboardingComponent);
21✔
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, showHelptips }) => {
6✔
220
  const [sortCol, setSortCol] = useState('modified');
23✔
221
  const [sortDown, setSortDown] = useState(true);
23✔
222

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

230
  const sortColumn = col => {
23✔
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) {
23✔
240
    return null;
3✔
241
  }
242

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

245
  return (
20✔
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
            <Tooltip key={item.name} className="columnHeader" title={item.title} placement="top-start" onClick={() => sortColumn(item)}>
80✔
252
              <div>
253
                {item.title}
254
                {item.sortable ? <SortIcon className={`sortIcon ${sortCol === item.name ? 'selected' : ''} ${sortDown.toString()}`} /> : null}
120✔
255
              </div>
256
            </Tooltip>
257
          ))}
258
          <div style={{ width: 48 }} />
259
        </div>
260
        {items.map((pkg, index) => {
261
          const expanded = !!(selectedArtifact && selectedArtifact.id === pkg.id);
20✔
262
          return (
20✔
263
            <Artifact
264
              key={`repository-item-${index}`}
265
              artifact={pkg}
266
              columns={columns}
267
              expanded={expanded}
268
              index={index}
UNCOV
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 && (
40✔
278
        <span className="relative">
279
          <ExpandArtifact />
280
        </span>
281
      )}
282
    </>
283
  );
284
};
285

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

303
  const onRemoveArtifact = artifact => dispatch(removeArtifact(artifact.id)).finally(() => setShowRemoveArtifactDialog(false));
38✔
304

305
  const copyLinkToClipboard = () => {
38✔
UNCOV
306
    const location = window.location.href.substring(0, window.location.href.indexOf('/releases') + '/releases'.length);
×
UNCOV
307
    copy(`${location}/${release.Name}`);
×
UNCOV
308
    dispatch(setSnackbar('Link copied to clipboard'));
×
309
  };
310

311
  const onCloseClick = () => dispatch(selectRelease());
38✔
312

313
  const onCreateDeployment = () => {
38✔
UNCOV
314
    if (!onboardingState.complete) {
×
UNCOV
315
      dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_INCLUDED_DEPLOY_ONBOARDING));
×
UNCOV
316
      if (pastDeploymentsCount === 1) {
×
UNCOV
317
        dispatch(advanceOnboarding(onboardingSteps.ARTIFACT_MODIFIED_ONBOARDING));
×
318
      }
319
    }
UNCOV
320
    navigate(`${DEPLOYMENT_ROUTES.active.route}?open=true&release=${encodeURIComponent(release.Name)}`);
×
321
  };
322

323
  const onToggleReleaseDeletion = () => setConfirmReleaseDeletion(toggle);
38✔
324

325
  const onDeleteRelease = () => dispatch(removeRelease(release.Name)).then(() => setConfirmReleaseDeletion(false));
38✔
326

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

376
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