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

mendersoftware / gui / 963002358

pending completion
963002358

Pull #3870

gitlab-ci

mzedel
chore: cleaned up left over onboarding tooltips & aligned with updated design

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3870: MEN-5413

4348 of 6319 branches covered (68.81%)

95 of 122 new or added lines in 24 files covered. (77.87%)

1734 existing lines in 160 files now uncovered.

8174 of 9951 relevant lines covered (82.14%)

178.12 hits per line

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

13.83
/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 = {
5✔
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' };
5✔
37

38
const useStyles = makeStyles()(theme => ({
5✔
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 }) => {
5✔
UNCOV
64
  const clusterState = devices.reduce(
×
65
    (accu, device) => {
UNCOV
66
      if (device.isOffline) {
×
UNCOV
67
        return { ...accu, offline: accu.offline + 1 };
×
UNCOV
68
      } else if (device.isNew) {
×
UNCOV
69
        return { ...accu, new: accu.new + 1 };
×
70
      }
UNCOV
71
      return accu;
×
72
    },
73
    { offline: 0, new: 0 }
74
  );
UNCOV
75
  const offline = clusterState.offline && clusterState.offline >= clusterState.new;
×
UNCOV
76
  const needsContext = !!(clusterState.offline || clusterState.new);
×
UNCOV
77
  const className = getMarkerClass({ isOffline: !!clusterState.offline, isNew: clusterState.new }, classes);
×
UNCOV
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) => {
5✔
92
  // Splitting props and events to different objects
UNCOV
93
  const { clusterEvents, clusterProps } = Object.entries(props).reduce(
×
94
    (accu, [propName, prop]) => {
UNCOV
95
      propName.startsWith('on') ? (accu.clusterEvents[propName] = prop) : (accu.clusterProps[propName] = prop);
×
UNCOV
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
UNCOV
102
  const markerClusterGroup = Object.entries(clusterEvents).reduce((accu, [eventName, handler]) => {
×
UNCOV
103
    const clusterEvent = `cluster${eventName.substring(2).toLowerCase()}`;
×
UNCOV
104
    accu.on(clusterEvent, handler);
×
UNCOV
105
    return accu;
×
106
  }, Leaflet.markerClusterGroup(clusterProps));
107

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

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

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

133
const getClusterIcon = (cluster, classes) => {
5✔
UNCOV
134
  const items = cluster.getAllChildMarkers().map(marker => marker.options.item);
×
UNCOV
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 }) => {
5✔
UNCOV
143
  const [bounds, setBounds] = useState();
×
144

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

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

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

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

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

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

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

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

UNCOV
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} />
UNCOV
190
      <MarkerClusterGroup iconCreateFunction={cluster => getClusterIcon(cluster, classes)}>
×
191
        {items.map(({ device, position }) => {
UNCOV
192
          const markerClass = getMarkerClass(device, classes);
×
UNCOV
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