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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

38.3
/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✔
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) => {
5✔
92
  // Splitting props and events to different objects
93
  const { clusterEvents, clusterProps } = Object.entries(props).reduce(
13✔
94
    (accu, [propName, prop]) => {
95
      propName.startsWith('on') ? (accu.clusterEvents[propName] = prop) : (accu.clusterProps[propName] = prop);
26!
96
      return accu;
26✔
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]) => {
13✔
103
    const clusterEvent = `cluster${eventName.substring(2).toLowerCase()}`;
×
104
    accu.on(clusterEvent, handler);
×
105
    return accu;
×
106
  }, Leaflet.markerClusterGroup(clusterProps));
107

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

114
const MarkerContext = ({ device }) => {
5✔
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) => {
5✔
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) => {
5✔
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 }) => {
5✔
143
  const [bounds, setBounds] = useState();
13✔
144

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

147
  useEffect(() => {
13✔
148
    if (debouncedBounds) {
1!
149
      onMapMoved(debouncedBounds);
×
150
    }
151
  }, [JSON.stringify(debouncedBounds)]);
152

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

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

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

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

166
  useEffect(() => {
14✔
167
    const { bounds } = mapProps;
1✔
168
    if (!bounds || !mapRef.current) {
1!
169
      return;
1✔
170
    }
171
    mapRef.current.fitBounds(bounds);
×
172
  }, [JSON.stringify(mapProps.bounds)]);
173

174
  const onMapReady = map => (mapRef.current = map.target);
14✔
175

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

212
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