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

mendersoftware / mender-server / 1593965839

18 Dec 2024 10:58AM UTC coverage: 73.514% (+0.7%) from 72.829%
1593965839

Pull #253

gitlab-ci

mineralsfree
chore(gui): aligned tests with edit billing profile

Ticket: MEN-7466
Changelog: None

Signed-off-by: Mikita Pilinka <mikita.pilinka@northern.tech>
Pull Request #253: MEN-7466-feat: updated billing section in My Organization settings

4257 of 6185 branches covered (68.83%)

Branch coverage included in aggregate %.

53 of 87 new or added lines in 11 files covered. (60.92%)

43 existing lines in 11 files now uncovered.

40083 of 54130 relevant lines covered (74.05%)

22.98 hits per line

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

76.92
/frontend/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, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
import { FormProvider, useForm } from 'react-hook-form';
16
import { useDispatch, useSelector } from 'react-redux';
17
import { useNavigate } from 'react-router-dom';
18

19
// material ui
20
import {
21
  HighlightOffOutlined as HighlightOffOutlinedIcon,
22
  LabelOutlined as LabelOutlinedIcon,
23
  Replay as ReplayIcon,
24
  Sort as SortIcon
25
} from '@mui/icons-material';
26
import {
27
  Button,
28
  ClickAwayListener,
29
  Dialog,
30
  DialogActions,
31
  DialogContent,
32
  DialogTitle,
33
  Divider,
34
  Drawer,
35
  SpeedDial,
36
  SpeedDialAction,
37
  SpeedDialIcon,
38
  Tooltip
39
} from '@mui/material';
40
import { speedDialActionClasses } from '@mui/material/SpeedDialAction';
41
import { makeStyles } from 'tss-react/mui';
42

43
import { DrawerTitle } from '@northern.tech/common-ui/DrawerTitle';
44
import ChipSelect from '@northern.tech/common-ui/chipselect';
45
import { ConfirmationButtons, EditButton } from '@northern.tech/common-ui/confirm';
46
import { EditableLongText } from '@northern.tech/common-ui/editablelongtext';
47
import FileSize from '@northern.tech/common-ui/filesize';
48
import { RelativeTime } from '@northern.tech/common-ui/time';
49
import { HELPTOOLTIPS, MenderHelpTooltip } from '@northern.tech/helptips/helptooltips';
50
import storeActions from '@northern.tech/store/actions';
51
import { DEPLOYMENT_ROUTES } from '@northern.tech/store/constants';
52
import { getReleaseListState, getReleaseTags, getSelectedRelease, getUserCapabilities } from '@northern.tech/store/selectors';
53
import { removeArtifact, removeRelease, selectRelease, setReleaseTags, updateReleaseInfo } from '@northern.tech/store/thunks';
54
import { customSort, formatTime, toggle } from '@northern.tech/utils/helpers';
55
import { generateReleasesPath } from '@northern.tech/utils/locationutils';
56
import useWindowSize from '@northern.tech/utils/resizehook';
57
import copy from 'copy-to-clipboard';
58
import pluralize from 'pluralize';
59

60
import Artifact from './artifact';
61
import RemoveArtifactDialog from './dialogs/removeartifact';
62

63
const { setSnackbar } = storeActions;
6✔
64

65
const DeviceTypeCompatibility = ({ artifact }) => {
6✔
66
  const compatible = artifact.artifact_depends ? artifact.artifact_depends.device_type.join(', ') : artifact.device_types_compatible.join(', ');
9✔
67
  return (
9✔
68
    <Tooltip title={compatible} placement="top-start">
69
      <div className="text-overflow">{compatible}</div>
70
    </Tooltip>
71
  );
72
};
73

74
export const columns = [
6✔
75
  {
76
    title: 'Device type compatibility',
77
    name: 'device_types',
78
    sortable: false,
79
    render: DeviceTypeCompatibility,
80
    tooltip: <MenderHelpTooltip id={HELPTOOLTIPS.expandArtifact.id} className="margin-left-small" />
81
  },
82
  {
83
    title: 'Type',
84
    name: 'type',
85
    sortable: false,
86
    render: ({ artifact }) => <div style={{ maxWidth: '100vw' }}>{artifact.updates.reduce((accu, item) => (accu ? accu : item.type_info.type), '')}</div>
9!
87
  },
88
  { title: 'Size', name: 'size', sortable: true, render: ({ artifact }) => <FileSize fileSize={artifact.size} /> },
9✔
89
  { title: 'Last modified', name: 'modified', sortable: true, render: ({ artifact }) => <RelativeTime updateTime={formatTime(artifact.modified)} /> }
9✔
90
];
91

92
const defaultActions = [
6✔
93
  {
94
    action: ({ onCreateDeployment, selection }) => onCreateDeployment(selection),
×
95
    icon: <ReplayIcon />,
96
    isApplicable: ({ userCapabilities: { canDeploy }, selectedSingleRelease }) => canDeploy && selectedSingleRelease,
3✔
97
    key: 'deploy',
98
    title: () => 'Create a deployment for this release'
13✔
99
  },
100
  {
101
    action: ({ onTagRelease, selectedReleases }) => onTagRelease(selectedReleases),
×
102
    icon: <LabelOutlinedIcon />,
103
    isApplicable: ({ userCapabilities: { canManageReleases } }) => canManageReleases,
3✔
104
    key: 'tag',
105
    title: pluralized => `Tag ${pluralized}`
13✔
106
  },
107
  {
108
    action: ({ onDeleteRelease, selection, selectedReleases }) => onDeleteRelease(selection || selectedReleases),
×
109
    icon: <HighlightOffOutlinedIcon className="red" />,
110
    isApplicable: ({ userCapabilities: { canManageReleases } }) => canManageReleases,
3✔
111
    key: 'delete',
112
    title: pluralized => `Delete ${pluralized}`
13✔
113
  }
114
];
115

116
const useStyles = makeStyles()(theme => ({
6✔
117
  container: {
118
    display: 'flex',
119
    position: 'fixed',
120
    bottom: theme.spacing(6.5),
121
    right: theme.spacing(6.5),
122
    zIndex: 10,
123
    minWidth: 'max-content',
124
    alignItems: 'flex-end',
125
    justifyContent: 'flex-end',
126
    pointerEvents: 'none',
127
    [`.${speedDialActionClasses.staticTooltipLabel}`]: {
128
      minWidth: 'max-content'
129
    }
130
  },
131
  fab: { margin: theme.spacing(2) },
132
  tagSelect: { marginRight: theme.spacing(2), maxWidth: 350 },
133
  label: {
134
    marginRight: theme.spacing(2),
135
    marginBottom: theme.spacing(4)
136
  }
137
}));
138

139
export const ReleaseQuickActions = ({ actionCallbacks, innerRef, selectedRelease, userCapabilities, releases }) => {
6✔
140
  const [showActions, setShowActions] = useState(false);
13✔
141
  const [selectedReleases, setSelectedReleases] = useState([]);
13✔
142
  const { classes } = useStyles();
13✔
143
  const { selection: selectedRows } = useSelector(getReleaseListState);
13✔
144

145
  useEffect(() => {
13✔
146
    if (releases) {
1!
UNCOV
147
      setSelectedReleases(selectedRows.map(row => releases[row]));
×
148
    }
149
  }, [releases, selectedRows, setSelectedReleases]);
150

151
  const actions = useMemo(() => {
13✔
152
    return Object.values(defaultActions).reduce((accu, action) => {
3✔
153
      if (action.isApplicable({ userCapabilities, selectedSingleRelease: !!selectedRelease })) {
9!
154
        accu.push(action);
9✔
155
      }
156
      return accu;
9✔
157
    }, []);
158
    // eslint-disable-next-line react-hooks/exhaustive-deps
159
  }, [JSON.stringify(userCapabilities), selectedRelease]);
160

161
  const handleShowActions = () => {
13✔
UNCOV
162
    setShowActions(!showActions);
×
163
  };
164

165
  const handleClickAway = () => {
13✔
166
    setShowActions(false);
5✔
167
  };
168

169
  const pluralized = pluralize('releases', selectedRows.length);
13✔
170

171
  return (
13✔
172
    <div className={classes.container} ref={innerRef}>
173
      <div className={classes.label}>{selectedRelease ? 'Release actions' : `${selectedRows.length} ${pluralized} selected`}</div>
13!
174
      <ClickAwayListener onClickAway={handleClickAway}>
175
        <SpeedDial className={classes.fab} ariaLabel="device-actions" icon={<SpeedDialIcon />} onClick={handleShowActions} open={Boolean(showActions)}>
176
          {actions.map(action => (
177
            <SpeedDialAction
39✔
178
              key={action.key}
179
              aria-label={action.key}
180
              icon={action.icon}
181
              tooltipTitle={action.title(pluralized)}
182
              tooltipOpen
UNCOV
183
              onClick={() => action.action({ ...actionCallbacks, selection: selectedRelease, selectedReleases })}
×
184
            />
185
          ))}
186
        </SpeedDial>
187
      </ClickAwayListener>
188
    </div>
189
  );
190
};
191

192
const ReleaseNotes = ({ onChange, release: { notes = '' } }) => (
6✔
193
  <>
12✔
194
    <h4>Release notes</h4>
195
    <EditableLongText contentFallback="Add release notes here" original={notes} onChange={onChange} placeholder="Release notes" />
196
  </>
197
);
198

199
const ReleaseTags = ({ existingTags = [], release: { tags = [] }, onChange }) => {
6!
200
  const [isEditing, setIsEditing] = useState(false);
12✔
201
  const [initialValues] = useState({ tags });
12✔
202
  const { classes } = useStyles();
12✔
203

204
  const methods = useForm({ mode: 'onChange', defaultValues: initialValues });
12✔
205
  const { setValue, getValues } = methods;
12✔
206

207
  useEffect(() => {
12✔
208
    if (!initialValues.tags.length) {
12!
209
      setValue('tags', tags);
12✔
210
    }
211
  }, [initialValues.tags, setValue, tags]);
212

213
  const onToggleEdit = useCallback(() => {
12✔
UNCOV
214
    setValue('tags', tags);
×
215
    setIsEditing(toggle);
×
216
  }, [setValue, tags]);
217

218
  const onSave = () => {
12✔
UNCOV
219
    onChange(getValues('tags'));
×
220
    setIsEditing(false);
×
221
  };
222

223
  return (
12✔
224
    <div className="margin-bottom margin-top" style={{ maxWidth: 500 }}>
225
      <div className="flexbox center-aligned">
226
        <h4 className="margin-right">Tags</h4>
227
        {!isEditing && <EditButton onClick={onToggleEdit} />}
24✔
228
      </div>
229
      <div className="flexbox" style={{ alignItems: 'end' }}>
230
        <FormProvider {...methods}>
231
          <form noValidate>
232
            <ChipSelect
233
              className={classes.tagSelect}
234
              disabled={!isEditing}
235
              label=""
236
              name="tags"
237
              options={existingTags}
238
              placeholder={isEditing ? 'Enter release tags' : 'Click edit to add release tags'}
12!
239
            />
240
          </form>
241
        </FormProvider>
242
        {isEditing && <ConfirmationButtons onConfirm={onSave} onCancel={onToggleEdit} />}
12!
243
      </div>
244
    </div>
245
  );
246
};
247

248
const ArtifactsList = ({ artifacts, selectedArtifact, setSelectedArtifact, setShowRemoveArtifactDialog }) => {
6✔
249
  const [sortCol, setSortCol] = useState('modified');
12✔
250
  const [sortDown, setSortDown] = useState(true);
12✔
251

252
  const onRowSelection = artifact => {
12✔
253
    if (artifact?.id === selectedArtifact?.id) {
1!
UNCOV
254
      return setSelectedArtifact();
×
255
    }
256
    setSelectedArtifact(artifact);
1✔
257
  };
258

259
  const sortColumn = col => {
12✔
UNCOV
260
    if (!col.sortable) {
×
261
      return;
×
262
    }
263
    // sort table
UNCOV
264
    setSortDown(toggle);
×
265
    setSortCol(col);
×
266
  };
267

268
  if (!artifacts.length) {
12✔
269
    return null;
4✔
270
  }
271

272
  const items = artifacts.sort(customSort(sortDown, sortCol));
8✔
273

274
  return (
8✔
275
    <>
276
      <h4>Artifacts in this Release:</h4>
277
      <div>
278
        <div className="release-repo-item repo-item repo-header">
279
          {columns.map(item => (
280
            <div className="columnHeader" key={item.name} onClick={() => sortColumn(item)}>
32✔
281
              <Tooltip title={item.title} placement="top-start">
282
                <>{item.title}</>
283
              </Tooltip>
284
              {item.sortable ? <SortIcon className={`sortIcon ${sortCol === item.name ? 'selected' : ''} ${sortDown.toString()}`} /> : null}
48✔
285
              {item.tooltip}
286
            </div>
287
          ))}
288
          <div style={{ width: 48 }} />
289
        </div>
290
        {items.map((artifact, index) => {
291
          const expanded = selectedArtifact?.id === artifact.id;
8✔
292
          return (
8✔
293
            <Artifact
294
              key={`repository-item-${index}`}
295
              artifact={artifact}
296
              columns={columns}
297
              expanded={expanded}
298
              index={index}
299
              onRowSelection={() => onRowSelection(artifact)}
1✔
300
              // this will be run after expansion + collapse and both need some time to fully settle
301
              // otherwise the measurements are off
302
              showRemoveArtifactDialog={setShowRemoveArtifactDialog}
303
            />
304
          );
305
        })}
306
      </div>
307
    </>
308
  );
309
};
310

311
export const ReleaseDetails = () => {
6✔
312
  const [showRemoveDialog, setShowRemoveArtifactDialog] = useState(false);
43✔
313
  const [confirmReleaseDeletion, setConfirmReleaseDeletion] = useState(false);
42✔
314
  const [selectedArtifact, setSelectedArtifact] = useState();
42✔
315

316
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
317
  const windowSize = useWindowSize();
42✔
318
  const creationRef = useRef();
42✔
319
  const drawerRef = useRef();
42✔
320
  const navigate = useNavigate();
42✔
321
  const dispatch = useDispatch();
42✔
322
  const release = useSelector(getSelectedRelease);
42✔
323
  const existingTags = useSelector(getReleaseTags);
42✔
324
  const userCapabilities = useSelector(getUserCapabilities);
42✔
325

326
  const { name: releaseName, artifacts = [] } = release;
42✔
327

328
  const onRemoveArtifact = artifact => dispatch(removeArtifact(artifact.id)).finally(() => setShowRemoveArtifactDialog(false));
42✔
329

330
  const copyLinkToClipboard = () => {
42✔
UNCOV
331
    const location = window.location.href.substring(0, window.location.href.indexOf('/releases'));
×
332
    copy(`${location}${generateReleasesPath({ pageState: { selectedRelease: releaseName } })}`);
×
333
    dispatch(setSnackbar('Link copied to clipboard'));
×
334
  };
335

336
  const onCloseClick = () => dispatch(selectRelease());
42✔
337

338
  const onCreateDeployment = () => navigate(`${DEPLOYMENT_ROUTES.active.route}?open=true&release=${encodeURIComponent(releaseName)}`);
42✔
339

340
  const onToggleReleaseDeletion = () => setConfirmReleaseDeletion(toggle);
42✔
341

342
  const onDeleteRelease = () => dispatch(removeRelease(releaseName)).then(() => setConfirmReleaseDeletion(false));
42✔
343

344
  const onReleaseNotesChanged = useCallback(notes => dispatch(updateReleaseInfo({ name: releaseName, info: { notes } })), [dispatch, releaseName]);
42✔
345

346
  const onTagSelectionChanged = useCallback(tags => dispatch(setReleaseTags({ name: releaseName, tags })), [dispatch, releaseName]);
42✔
347

348
  return (
42✔
349
    <Drawer anchor="right" open={!!releaseName} onClose={onCloseClick} PaperProps={{ style: { minWidth: '60vw' }, ref: drawerRef }}>
350
      <DrawerTitle
351
        title={
352
          <>
353
            Release information for <h4 className="margin-none margin-left-small margin-right-small">{releaseName}</h4>
354
          </>
355
        }
356
        onLinkCopy={copyLinkToClipboard}
357
        preCloser={
358
          <div className="muted margin-right flexbox">
359
            <div className="margin-right-small">Last modified:</div>
360
            <RelativeTime updateTime={release.modified} />
361
          </div>
362
        }
363
        onClose={onCloseClick}
364
      />
365
      <Divider className="margin-bottom" />
366
      <ReleaseNotes onChange={onReleaseNotesChanged} release={release} />
367
      <ReleaseTags existingTags={existingTags} onChange={onTagSelectionChanged} release={release} />
368
      <ArtifactsList
369
        artifacts={artifacts}
370
        selectedArtifact={selectedArtifact}
371
        setSelectedArtifact={setSelectedArtifact}
372
        setShowRemoveArtifactDialog={setShowRemoveArtifactDialog}
373
      />
374
      <RemoveArtifactDialog
375
        artifact={selectedArtifact}
376
        open={!!showRemoveDialog}
377
        onCancel={() => setShowRemoveArtifactDialog(false)}
1✔
UNCOV
378
        onRemove={() => onRemoveArtifact(selectedArtifact)}
×
379
      />
380
      <RemoveArtifactDialog open={!!confirmReleaseDeletion} onRemove={onDeleteRelease} onCancel={onToggleReleaseDeletion} release={release} />
381
      <ReleaseQuickActions
382
        actionCallbacks={{ onCreateDeployment, onDeleteRelease: onToggleReleaseDeletion }}
383
        innerRef={creationRef}
384
        selectedRelease={release}
385
        userCapabilities={userCapabilities}
386
      />
387
    </Drawer>
388
  );
389
};
390

391
export default ReleaseDetails;
392

393
export const DeleteReleasesConfirmationDialog = ({ onClose, onSubmit }) => (
6✔
394
  <Dialog open={true}>
×
395
    <DialogTitle>Delete releases?</DialogTitle>
396
    <DialogContent style={{ overflow: 'hidden' }}>All releases artifacts will be deleted. Are you sure you want to delete these releases ?</DialogContent>
397
    <DialogActions>
398
      <Button style={{ marginRight: 10 }} onClick={onClose}>
399
        Cancel
400
      </Button>
401
      <Button variant="contained" color="primary" onClick={onSubmit}>
402
        Delete
403
      </Button>
404
    </DialogActions>
405
  </Dialog>
406
);
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