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

mendersoftware / gui / 901187442

pending completion
901187442

Pull #3795

gitlab-ci

mzedel
feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

Ticket: None
Changelog: None
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3795: feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

4389 of 6365 branches covered (68.96%)

5 of 5 new or added lines in 1 file covered. (100.0%)

1729 existing lines in 165 files now uncovered.

8274 of 10019 relevant lines covered (82.58%)

144.86 hits per line

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

75.64
/src/js/components/devices/widgets/devicequickactions.js
1
// Copyright 2021 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, { forwardRef, memo, useMemo, useState } from 'react';
15

16
import {
17
  AddCircle as AddCircleIcon,
18
  CheckCircle as CheckCircleIcon,
19
  HeightOutlined as HeightOutlinedIcon,
20
  HighlightOffOutlined as HighlightOffOutlinedIcon,
21
  RemoveCircleOutline as RemoveCircleOutlineIcon,
22
  Replay as ReplayIcon
23
} from '@mui/icons-material';
24
import { SpeedDial, SpeedDialAction, SpeedDialIcon } from '@mui/material';
25
import { speedDialActionClasses } from '@mui/material/SpeedDialAction';
26
import { makeStyles } from 'tss-react/mui';
27

28
import { mdiTrashCanOutline as TrashCan } from '@mdi/js';
29
import pluralize from 'pluralize';
30

31
import GatewayIcon from '../../../../assets/img/gateway.svg';
32
import { DEVICE_STATES, UNGROUPED_GROUP } from '../../../constants/deviceConstants';
33
import { deepCompare, stringToBoolean } from '../../../helpers';
34
import MaterialDesignIcon from '../../common/materialdesignicon';
35

36
const defaultActions = {
10✔
37
  accept: {
38
    icon: <CheckCircleIcon className="green" />,
39
    key: 'accept',
40
    title: pluralized => `Accept ${pluralized}`,
1✔
UNCOV
41
    action: ({ onAuthorizationChange, selection }) => onAuthorizationChange(selection, DEVICE_STATES.accepted),
×
42
    checkRelevance: ({ device, userCapabilities: { canWriteDevices } }) =>
43
      canWriteDevices && [DEVICE_STATES.pending, DEVICE_STATES.rejected].includes(device.status)
2✔
44
  },
45
  dismiss: {
46
    icon: <RemoveCircleOutlineIcon className="red" />,
47
    key: 'dismiss',
48
    title: pluralized => `Dismiss ${pluralized}`,
2✔
UNCOV
49
    action: ({ onDeviceDismiss, selection }) => onDeviceDismiss(selection),
×
50
    checkRelevance: ({ device, userCapabilities: { canWriteDevices } }) =>
51
      canWriteDevices && [DEVICE_STATES.accepted, DEVICE_STATES.pending, DEVICE_STATES.preauth, DEVICE_STATES.rejected, 'noauth'].includes(device.status)
2✔
52
  },
53
  reject: {
54
    icon: <HighlightOffOutlinedIcon className="red" />,
55
    key: 'reject',
56
    title: pluralized => `Reject ${pluralized}`,
2✔
UNCOV
57
    action: ({ onAuthorizationChange, selection }) => onAuthorizationChange(selection, DEVICE_STATES.rejected),
×
58
    checkRelevance: ({ device, userCapabilities: { canWriteDevices } }) =>
59
      canWriteDevices && [DEVICE_STATES.accepted, DEVICE_STATES.pending].includes(device.status)
2✔
60
  },
61
  addToGroup: {
62
    icon: <AddCircleIcon className="green" />,
63
    key: 'group-add',
64
    title: pluralized => `Add selected ${pluralized} to a group`,
1✔
UNCOV
65
    action: ({ onAddDevicesToGroup, selection }) => onAddDevicesToGroup(selection),
×
66
    checkRelevance: ({ selectedGroup, userCapabilities: { canWriteDevices } }) => canWriteDevices && !selectedGroup
2✔
67
  },
68
  moveToGroup: {
69
    icon: <HeightOutlinedIcon className="rotated ninety" />,
70
    key: 'group-change',
71
    title: pluralized => `Move selected ${pluralized} to another group`,
1✔
UNCOV
72
    action: ({ onAddDevicesToGroup, selection }) => onAddDevicesToGroup(selection),
×
73
    checkRelevance: ({ selectedGroup, userCapabilities: { canWriteDevices } }) => canWriteDevices && !!selectedGroup
2✔
74
  },
75
  removeFromGroup: {
76
    icon: <MaterialDesignIcon path={TrashCan} />,
77
    key: 'group-remove',
78
    title: pluralized => `Remove selected ${pluralized} from this group`,
1✔
UNCOV
79
    action: ({ onRemoveDevicesFromGroup, selection }) => onRemoveDevicesFromGroup(selection),
×
80
    checkRelevance: ({ selectedGroup, userCapabilities: { canWriteDevices } }) => canWriteDevices && selectedGroup && selectedGroup !== UNGROUPED_GROUP.id
2✔
81
  },
82
  promoteToGateway: {
83
    icon: <GatewayIcon style={{ width: 20 }} />,
84
    key: 'promote-to-gateway',
UNCOV
85
    title: () => 'Promote to gateway',
×
UNCOV
86
    action: ({ onPromoteGateway, selection }) => onPromoteGateway(selection),
×
87
    checkRelevance: ({ device, features, tenantCapabilities: { isEnterprise } }) =>
88
      features.isHosted && isEnterprise && !stringToBoolean(device.attributes?.mender_is_gateway) && device.status === DEVICE_STATES.accepted
2✔
89
  },
90
  createDeployment: {
91
    icon: <ReplayIcon />,
92
    key: 'create-deployment',
93
    title: (pluralized, count) => `Create deployment for ${pluralize('this', count)} ${pluralized}`,
1✔
UNCOV
94
    action: ({ onCreateDeployment, selection }) => onCreateDeployment(selection),
×
95
    checkRelevance: ({ device, userCapabilities: { canDeploy, canReadReleases } }) =>
96
      canDeploy && canReadReleases && device && device.status === DEVICE_STATES.accepted
2✔
97
  }
98
};
99

100
const useStyles = makeStyles()(theme => ({
10✔
101
  container: {
102
    display: 'flex',
103
    position: 'fixed',
104
    bottom: theme.spacing(6.5),
105
    right: theme.spacing(6.5),
106
    zIndex: 10,
107
    minWidth: 400,
108
    alignItems: 'flex-end',
109
    justifyContent: 'flex-end',
110
    pointerEvents: 'none',
111
    [`.${speedDialActionClasses.staticTooltipLabel}`]: {
112
      minWidth: 'max-content'
113
    }
114
  },
115
  fab: { margin: theme.spacing(2) },
116
  label: {
117
    marginRight: theme.spacing(2),
118
    marginBottom: theme.spacing(4)
119
  }
120
}));
121

122
export const DeviceQuickActions = (
10✔
123
  { actionCallbacks, devices, features, isSingleDevice = false, selectedGroup, selectedRows, tenantCapabilities, userCapabilities },
1✔
124
  ref
125
) => {
126
  const [showActions, setShowActions] = useState(false);
2✔
127
  const { classes } = useStyles();
2✔
128

129
  const { actions, selectedDevices } = useMemo(() => {
2✔
130
    const selectedDevices = selectedRows.map(row => devices[row]);
2✔
131
    const actions = Object.values(defaultActions).reduce((accu, action) => {
2✔
132
      if (selectedDevices.every(device => device && action.checkRelevance({ device, features, selectedGroup, tenantCapabilities, userCapabilities }))) {
16✔
133
        accu.push(action);
9✔
134
      }
135
      return accu;
16✔
136
    }, []);
137
    return { actions, selectedDevices };
2✔
138
  }, [devices, isSingleDevice, selectedRows, selectedGroup, JSON.stringify(tenantCapabilities), JSON.stringify(userCapabilities)]);
139

140
  const pluralized = pluralize('devices', selectedDevices.length);
2✔
141
  return (
2✔
142
    <div className={classes.container} ref={ref}>
143
      <div className={classes.label}>{isSingleDevice ? 'Device actions' : `${selectedDevices.length} ${pluralized} selected`}</div>
2✔
144
      <SpeedDial
145
        className={classes.fab}
146
        ariaLabel="device-actions"
147
        icon={<SpeedDialIcon />}
UNCOV
148
        onClose={() => setShowActions(false)}
×
149
        onOpen={setShowActions}
150
        open={Boolean(showActions)}
151
      >
152
        {actions.map(action => (
153
          <SpeedDialAction
9✔
154
            key={action.key}
155
            aria-label={action.key}
156
            icon={action.icon}
157
            tooltipTitle={action.title(pluralized, selectedDevices.length)}
158
            tooltipOpen
UNCOV
159
            onClick={() => action.action({ ...actionCallbacks, selection: selectedDevices })}
×
160
          />
161
        ))}
162
      </SpeedDial>
163
    </div>
164
  );
165
};
166

167
const areEqual = (prevProps, nextProps) => {
10✔
UNCOV
168
  if (prevProps.selectedGroup != nextProps.selectedGroup) {
×
UNCOV
169
    return false;
×
170
  }
UNCOV
171
  return (
×
172
    deepCompare(prevProps.tenantCapabilities, nextProps.tenantCapabilities) &&
×
173
    deepCompare(prevProps.userCapabilities, nextProps.userCapabilities) &&
174
    deepCompare(prevProps.selectedRows, nextProps.selectedRows)
175
  );
176
};
177

178
export default memo(forwardRef(DeviceQuickActions), areEqual);
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