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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

81.67
/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, { useEffect, useRef, useState } from 'react';
15
import { useSelector } from 'react-redux';
16

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

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

32
import GatewayIcon from '../../../../assets/img/gateway.svg';
33
import { TIMEOUTS } from '../../../constants/appConstants';
34
import { DEVICE_STATES, UNGROUPED_GROUP } from '../../../constants/deviceConstants';
35
import { onboardingSteps } from '../../../constants/onboardingConstants';
36
import { stringToBoolean, toggle } from '../../../helpers';
37
import { getDeviceById, getFeatures, getMappedDevicesList, getOnboardingState, getTenantCapabilities, getUserCapabilities } from '../../../selectors';
38
import { getOnboardingComponentFor } from '../../../utils/onboardingmanager';
39
import MaterialDesignIcon from '../../common/materialdesignicon';
40

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

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

129
export const DeviceQuickActions = ({ actionCallbacks, deviceId, selectedGroup }) => {
9✔
130
  const [showActions, setShowActions] = useState(false);
19✔
131
  const features = useSelector(getFeatures);
19✔
132
  const tenantCapabilities = useSelector(getTenantCapabilities);
19✔
133
  const userCapabilities = useSelector(getUserCapabilities);
19✔
134
  const { selection: selectedRows } = useSelector(state => state.devices.deviceList);
35✔
135
  const singleDevice = useSelector(state => getDeviceById(state, deviceId));
35✔
136
  const devices = useSelector(state => getMappedDevicesList(state, 'deviceList'));
35✔
137
  const { classes } = useStyles();
19✔
138
  const deployActionRef = useRef();
19✔
139
  const onboardingState = useSelector(getOnboardingState);
19✔
140
  const [isInitialized, setIsInitialized] = useState(false);
19✔
141
  const timer = useRef();
19✔
142

143
  const handleShowActions = () => {
19✔
144
    setShowActions(!showActions);
×
145
  };
146

147
  const handleClickAway = () => {
19✔
148
    setShowActions(false);
6✔
149
  };
150

151
  useEffect(() => {
19✔
152
    clearTimeout(timer.current);
3✔
153
    timer.current = setTimeout(() => setIsInitialized(toggle), TIMEOUTS.debounceDefault);
3✔
154
    return () => {
3✔
155
      clearTimeout(timer.current);
3✔
156
    };
157
  }, []);
158

159
  const selectedDevices = deviceId ? [singleDevice] : selectedRows.map(row => devices[row]);
35✔
160
  const actions = Object.values(defaultActions).reduce((accu, action) => {
19✔
161
    if (selectedDevices.every(device => device && action.checkRelevance({ device, features, selectedGroup, tenantCapabilities, userCapabilities }))) {
220✔
162
      accu.push(action);
57✔
163
    }
164
    return accu;
152✔
165
  }, []);
166

167
  const pluralized = pluralize('devices', selectedDevices.length);
19✔
168

169
  let onboardingComponent;
170
  if (deployActionRef.current && isInitialized) {
19✔
171
    const anchor = {
2✔
172
      left: deployActionRef.current.firstElementChild.offsetLeft - 15,
173
      top: deployActionRef.current.offsetTop + deployActionRef.current.firstElementChild.offsetTop + deployActionRef.current.firstElementChild.offsetHeight / 2
174
    };
175
    onboardingComponent = getOnboardingComponentFor(
2✔
176
      onboardingSteps.DEVICES_DEPLOY_RELEASE_ONBOARDING,
177
      onboardingState,
178
      { anchor, place: 'left' },
179
      onboardingComponent
180
    );
181
  }
182
  return (
19✔
183
    <div className={classes.container}>
184
      <div className="relative">
185
        <div className={classes.innerContainer} ref={deployActionRef}>
186
          <div className={classes.label}>{deviceId ? 'Device actions' : `${selectedDevices.length} ${pluralized} selected`}</div>
19✔
187
          <ClickAwayListener onClickAway={handleClickAway}>
188
            <SpeedDial className={classes.fab} ariaLabel="device-actions" icon={<SpeedDialIcon />} onClick={handleShowActions} open={Boolean(showActions)}>
189
              {actions.map(action => (
190
                <SpeedDialAction
57✔
191
                  key={action.key}
192
                  aria-label={action.key}
193
                  icon={action.icon}
194
                  tooltipTitle={action.title(pluralized, selectedDevices.length)}
195
                  tooltipOpen
196
                  onClick={() => action.action({ ...actionCallbacks, selection: selectedDevices })}
×
197
                />
198
              ))}
199
            </SpeedDial>
200
          </ClickAwayListener>
201
        </div>
202
        {onboardingComponent}
203
      </div>
204
    </div>
205
  );
206
};
207

208
export default DeviceQuickActions;
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