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

mendersoftware / gui / 1113439055

19 Dec 2023 09:01PM UTC coverage: 82.752% (-17.2%) from 99.964%
1113439055

Pull #4258

gitlab-ci

mender-test-bot
chore: Types update

Signed-off-by: Mender Test Bot <mender@northern.tech>
Pull Request #4258: chore: Types update

4326 of 6319 branches covered (0.0%)

8348 of 10088 relevant lines covered (82.75%)

189.39 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