• 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

20.97
/src/js/components/dashboard/widgets/map.js
1
// Copyright 2023 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 { renderToStaticMarkup } from 'react-dom/server';
16
import { AttributionControl, MapContainer, Marker, TileLayer, Tooltip, ZoomControl, useMapEvent } from 'react-leaflet';
17

18
import { WifiOffOutlined } from '@mui/icons-material';
19
import { Avatar, Chip, avatarClasses, chipClasses } from '@mui/material';
20
import { makeStyles } from 'tss-react/mui';
21

22
import { createPathComponent } from '@react-leaflet/core';
23
import Leaflet from 'leaflet';
24
import 'leaflet.markercluster';
25
import 'leaflet/dist/leaflet.css';
26

27
import { TIMEOUTS } from '../../../constants/appConstants';
28
import { useDebounce } from '../../../utils/debouncehook';
29
import PinIcon from './pin.svg';
30

31
const tileLayer = {
4✔
32
  attribution: '',
33
  url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
34
};
35

36
const sharedStyle = { border: 'none', boxShadow: 'none', borderRadius: 12, lineHeight: '8px' };
4✔
37

38
const useStyles = makeStyles()(theme => ({
4✔
39
  avatar: {
40
    [`&.${avatarClasses.root}`]: {
41
      backgroundColor: theme.palette.primary.main
42
    },
43
    marginBottom: 4
44
  },
45
  new: {
46
    [`&.${chipClasses.root}`]: { backgroundColor: theme.palette.grey[900], color: theme.palette.common.white },
47
    '&.leaflet-tooltip': { ...sharedStyle, backgroundColor: theme.palette.grey[900], color: theme.palette.common.white },
48
    '&.leaflet-tooltip:before': { visibility: 'collapse' },
49
    'svg g, svg path': { stroke: theme.palette.primary.main }
50
  },
51
  offline: {
52
    [`&.${chipClasses.root}`]: { backgroundColor: theme.palette.error.main, color: theme.palette.common.white, cursor: 'pointer' },
53
    [`&.${chipClasses.root} .${chipClasses.deleteIcon}, &.${chipClasses.root} .${chipClasses.deleteIcon}:hover`]: {
54
      backgroundColor: theme.palette.error.main,
55
      color: theme.palette.common.white
56
    },
57
    '&.leaflet-tooltip': { ...sharedStyle, backgroundColor: theme.palette.error.main, color: theme.palette.common.white },
58
    '&.leaflet-tooltip:before': { visibility: 'collapse' },
59
    'svg g': { stroke: theme.palette.error.main }
60
  }
61
}));
62

63
const DeviceClusterIcon = ({ classes, devices }) => {
4✔
64
  const clusterState = devices.reduce(
×
65
    (accu, device) => {
66
      if (device.isOffline) {
×
67
        return { ...accu, offline: accu.offline + 1 };
×
68
      } else if (device.isNew) {
×
69
        return { ...accu, new: accu.new + 1 };
×
70
      }
71
      return accu;
×
72
    },
73
    { offline: 0, new: 0 }
74
  );
75
  const offline = clusterState.offline && clusterState.offline >= clusterState.new;
×
76
  const needsContext = !!(clusterState.offline || clusterState.new);
×
77
  const className = getMarkerClass({ isOffline: !!clusterState.offline, isNew: clusterState.new }, classes);
×
78
  return (
×
79
    <div className="flexbox column centered">
80
      <Avatar className={`${classes.avatar} small`}>{devices.length}</Avatar>
81
      {needsContext &&
×
82
        (offline ? (
×
83
          <Chip size="small" className={className} label={clusterState.offline} deleteIcon={<WifiOffOutlined />} onDelete={() => {}} />
84
        ) : (
85
          <Chip size="small" label={`${clusterState.new} new`} />
86
        ))}
87
    </div>
88
  );
89
};
90

91
const MarkerClusterGroup = createPathComponent((props, context) => {
4✔
92
  // Splitting props and events to different objects
93
  const { clusterEvents, clusterProps } = Object.entries(props).reduce(
×
94
    (accu, [propName, prop]) => {
95
      propName.startsWith('on') ? (accu.clusterEvents[propName] = prop) : (accu.clusterProps[propName] = prop);
×
96
      return accu;
×
97
    },
98
    { clusterProps: {}, clusterEvents: {} }
99
  );
100

101
  // use event listener names that we selected above starting with `on` + map them to their handler
102
  const markerClusterGroup = Object.entries(clusterEvents).reduce((accu, [eventName, handler]) => {
×
103
    const clusterEvent = `cluster${eventName.substring(2).toLowerCase()}`;
×
104
    accu.on(clusterEvent, handler);
×
105
    return accu;
×
106
  }, Leaflet.markerClusterGroup(clusterProps));
107

108
  return {
×
109
    instance: markerClusterGroup,
110
    context: { ...context, layerContainer: markerClusterGroup }
111
  };
112
});
113

114
const MarkerContext = ({ device }) => {
4✔
115
  if (device.isOffline) {
×
116
    return <WifiOffOutlined />;
×
117
  }
118
  if (device.isNew) {
×
119
    return <div>new</div>;
×
120
  }
121
  return null;
×
122
};
123

124
const getMarkerClass = (device, classes) => {
4✔
125
  if (device.isOffline) {
×
126
    return classes.offline;
×
127
  } else if (device.isNew) {
×
128
    return classes.new;
×
129
  }
130
  return '';
×
131
};
132

133
const getClusterIcon = (cluster, classes) => {
4✔
134
  const items = cluster.getAllChildMarkers().map(marker => marker.options.item);
×
135
  return Leaflet.divIcon({
×
136
    html: renderToStaticMarkup(<DeviceClusterIcon classes={classes} devices={items} />),
137
    className: '',
138
    iconSize: Leaflet.point(44, 44)
139
  });
140
};
141

142
const EventsLayer = ({ onMapMoved }) => {
4✔
143
  const [bounds, setBounds] = useState();
×
144

145
  const debouncedBounds = useDebounce(bounds, TIMEOUTS.oneSecond);
×
146

147
  useEffect(() => {
×
148
    if (debouncedBounds) {
×
149
      onMapMoved(debouncedBounds);
×
150
    }
151
    // eslint-disable-next-line react-hooks/exhaustive-deps
152
  }, [JSON.stringify(debouncedBounds), onMapMoved]);
153

154
  useMapEvent('moveend', ({ target }) => setBounds(target.getBounds()));
×
155
  return null;
×
156
};
157

158
const pinIcon = renderToStaticMarkup(<PinIcon style={{ transform: 'scale(3)' }} />);
4✔
159

160
const iconSize = [8, 8];
4✔
161
const tooltipOffset = [0, 20];
4✔
162

163
const Map = ({ items = [], mapProps = {}, onMapMoved }) => {
4!
164
  const mapRef = useRef();
×
165
  const { classes } = useStyles();
×
166

167
  useEffect(() => {
×
168
    const { bounds } = mapProps;
×
169
    if (!bounds || !mapRef.current) {
×
170
      return;
×
171
    }
172
    mapRef.current.fitBounds(bounds);
×
173
    // eslint-disable-next-line react-hooks/exhaustive-deps
174
  }, [JSON.stringify(mapProps.bounds)]);
175

176
  const onMapReady = map => (mapRef.current = map.target);
×
177

178
  return (
×
179
    <MapContainer
180
      className="markercluster-map"
181
      style={{ width: 600, height: 400 }}
182
      zoomControl={false}
183
      attributionControl={false}
184
      whenReady={onMapReady}
185
      {...mapProps}
186
    >
187
      <AttributionControl prefix="" />
188
      <ZoomControl position="bottomright" />
189
      <TileLayer {...tileLayer} />
190
      <MarkerClusterGroup iconCreateFunction={cluster => getClusterIcon(cluster, classes)}>
×
191
        {items.map(({ device, position }) => {
192
          const markerClass = getMarkerClass(device, classes);
×
193
          return (
×
194
            <Marker
195
              key={device.id}
196
              icon={Leaflet.divIcon({ className: markerClass, html: `<div title=${device.id}>${pinIcon}</div>`, iconSize })}
197
              item={device}
198
              position={position}
199
            >
200
              {!!markerClass && (
×
201
                <Tooltip className={markerClass} direction="bottom" offset={tooltipOffset} permanent>
202
                  <MarkerContext device={device} />
203
                </Tooltip>
204
              )}
205
            </Marker>
206
          );
207
        })}
208
        <EventsLayer onMapMoved={onMapMoved} />
209
      </MarkerClusterGroup>
210
    </MapContainer>
211
  );
212
};
213

214
export default Map;
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