Skip to content

Commit a5a6f06

Browse files
committed
feat: Add loading indicator when features are being fetched
Refs: LIIK-482
1 parent d55c742 commit a5a6f06

File tree

11 files changed

+117
-9
lines changed

11 files changed

+117
-9
lines changed

map-view/src/App.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import LayerSwitcher from "./components/LayerSwitcher";
1111
import FeatureInfo from "./components/FeatureInfo";
1212
import Map from "./common/Map";
1313
import { Feature, MapConfig } from "./models";
14+
import OngoingFetchInfo from "./components/OngoingFetchInfo";
1415

1516
const drawWidth = "400px";
1617

@@ -36,6 +37,7 @@ interface AppState {
3637
open: boolean;
3738
mapConfig: MapConfig | null;
3839
features: Feature[];
40+
ongoingFeatureFetches: Set<string>;
3941
}
4042

4143
class App extends React.Component<AppProps, AppState> {
@@ -47,6 +49,7 @@ class App extends React.Component<AppProps, AppState> {
4749
open: false,
4850
mapConfig: null,
4951
features: [],
52+
ongoingFeatureFetches: new Set<string>(),
5053
};
5154
}
5255

@@ -57,12 +60,15 @@ class App extends React.Component<AppProps, AppState> {
5760
});
5861
Map.initialize(this.mapId, mapConfig);
5962
Map.registerFeatureInfoCallback((features: Feature[]) => this.setState({ features }));
63+
Map.registerOngoingFeatureFetchesCallback((fetches: Set<string>) =>
64+
this.setState({ ongoingFeatureFetches: fetches }),
65+
);
6066
});
6167
}
6268

6369
render() {
6470
const { classes } = this.props;
65-
const { open, mapConfig, features } = this.state;
71+
const { open, mapConfig, features, ongoingFeatureFetches } = this.state;
6672
return (
6773
<React.StrictMode>
6874
<div className="App">
@@ -80,6 +86,9 @@ class App extends React.Component<AppProps, AppState> {
8086
}}
8187
/>
8288
)}
89+
{ongoingFeatureFetches.size > 0 && (
90+
<OngoingFetchInfo layerIdentifiers={ongoingFeatureFetches}></OngoingFetchInfo>
91+
)}
8392
<Fab
8493
size="medium"
8594
color="primary"

map-view/src/common/Map.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,10 @@ class Map {
467467
this.featureInfoCallback = fn;
468468
}
469469

470+
registerOngoingFeatureFetchesCallback(fn: (fetches: Set<string>) => void) {
471+
this.ongoingFeatureFetchesCallback = fn;
472+
}
473+
470474
setVisibleBasemap(basemap: string) {
471475
// there can be only one visible base
472476
this.basemapLayers[this.visibleBasemap].setVisible(false);
@@ -491,7 +495,7 @@ class Map {
491495
getWfsUrl(overlayIdentifier: string, filterId?: string, filterValue?: string, ignoreBbox?: boolean) {
492496
const urlBuildResult = buildWFSQuery(
493497
overlayIdentifier,
494-
ignoreBbox ? undefined :this.fetchedAreaPolygons[overlayIdentifier],
498+
ignoreBbox ? undefined : this.fetchedAreaPolygons[overlayIdentifier],
495499
filterId,
496500
filterValue,
497501
ignoreBbox ? undefined : this.getCurrentBoundingBox(),
@@ -509,13 +513,16 @@ class Map {
509513
// nothing to fetch
510514
return;
511515
}
516+
512517
const tempSource = isClustered ? this.createClusterSource(wfsUrl) : this.createNonClusteredSource(wfsUrl);
513518
const tempVectorSource = isClustered ? (tempSource as Cluster).getSource() : tempSource;
514519
if (!tempVectorSource) {
515520
console.log("No vector source, skipping getting new features");
516521
return;
517522
}
518523

524+
this.ongoingFeatureFetches.add(overlayIdentifier);
525+
this.ongoingFeatureFetchesCallback(this.ongoingFeatureFetches);
519526
// create a temporary layer and add it to the map
520527
// to initialize data fetch
521528
const tempLayer = new VectorLayer({
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.ongoingFetchInfoContainer {
2+
position: fixed;
3+
top: 16px;
4+
left: 50%;
5+
transform: translateX(-50%);
6+
z-index: 1000; /* Ensure it stays on top */
7+
8+
background-color: #fff; /* Using a neutral white, similar to Material-UI's background.paper */
9+
padding: 16px; /* Equivalent to MUI padding: 2 (16px) */
10+
border-radius: 8px; /* Equivalent to MUI borderRadius: 2 */
11+
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.2); /* A common Material-UI shadow (boxShadow: 3) */
12+
border: 1px solid #e0e0e0; /* Light grey border */
13+
min-width: 200px;
14+
15+
display: flex;
16+
flex-direction: column;
17+
align-items: flex-start;
18+
}
19+
20+
.centeredSpinner {
21+
display: flex;
22+
justify-content: center; /* Centers horizontally */
23+
align-items: center; /* Centers vertically */
24+
width: 100%;
25+
min-height: 50px; /* Optional: Adjust height as needed to provide space */
26+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from "react";
2+
import { Box, Typography, List, ListItem, CircularProgress } from "@mui/material";
3+
import styles from "./OngoingFetchInfo.module.css";
4+
import { useTranslation } from "react-i18next";
5+
6+
/**
7+
* A React functional component that displays strings from a Set in a
8+
* top-left corner box using Material-UI components.
9+
*
10+
* @param {object} props - The component's properties.
11+
* @param {Set<string>} props.stringSet - The Set of strings to display.
12+
*/
13+
const OngoingFetchInfo = ({ layerIdentifiers = new Set<string>() }) => {
14+
const { t } = useTranslation();
15+
return (
16+
// The main container for the display box, using Material-UI's Box component.
17+
// The 'sx' prop is used for styling, similar to inline styles but with theming support.
18+
<Box className={styles.ongoingFetchInfoContainer}>
19+
<Box sx={{ mb: 1 }}>
20+
<Typography variant="h6" component="h2">
21+
{t("Active data fetches")}:
22+
</Typography>
23+
</Box>
24+
{layerIdentifiers.size > 0 ? (
25+
<>
26+
<Box className={styles.centeredSpinner}>
27+
<CircularProgress size={24} sx={{ color: "primary.main" }} />
28+
</Box>
29+
<List dense>
30+
{/* dense makes the list items smaller */}
31+
{/* Iterate over the Set and render each string as a Material-UI ListItem. */}
32+
{Array.from(layerIdentifiers).map((str) => (
33+
<ListItem key={str} disablePadding>
34+
<Typography variant="body2">{str}</Typography>
35+
</ListItem>
36+
))}
37+
</List>
38+
</>
39+
) : (
40+
<Typography variant="body2" sx={{ fontStyle: "italic", color: "text.secondary" }}>
41+
{t("No active data fetches")}:
42+
</Typography>
43+
)}
44+
</Box>
45+
);
46+
};
47+
48+
export default OngoingFetchInfo;

map-view/src/custom.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.css" {
2+
const styles: { [key: string]: string };
3+
export default styles;
4+
}

map-view/src/i18n.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ i18n
1515
sv,
1616
},
1717
detection: {
18-
order: ["path"],
18+
order: ["path", "navigator"],
1919
lookupFromPathIndex: 0,
2020
},
2121
fallbackLng: "en",

map-view/src/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { createRoot } from "react-dom/client";
33
import "./index.css";
44
import App from "./App";
55
import { StyledEngineProvider } from "@mui/material/styles";
6-
7-
import "./i18n"; // needs to be bundled
6+
import { I18nextProvider } from "react-i18next";
7+
import i18n from "./i18n";
88

99
const container = document.getElementById("root");
1010
const root = createRoot(container!);
1111
root.render(
1212
<StyledEngineProvider injectFirst>
13-
<App />
13+
<I18nextProvider i18n={i18n}>
14+
<App />
15+
</I18nextProvider>
1416
</StyledEngineProvider>,
1517
);

map-view/src/locale/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"Layers": "Layers",
1212
"Settings": "Settings",
1313
"Display Plan/Real difference": "Display Plan/Real difference",
14-
"Filter by Project": "Filter by Project"
14+
"Filter by Project": "Filter by Project",
15+
"Active data fetches": "Active data fetches",
16+
"No active data fetches": "No active data fetches"
1517
}
1618
}

map-view/src/locale/fi.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"Layers": "Tasot",
1212
"Settings": "Asetukset",
1313
"Display Plan/Real difference": "Näytä suunnitelman ja toteuman etäisyys",
14-
"Filter by Project": "Suodata projektin perusteella"
14+
"Filter by Project": "Suodata projektin perusteella",
15+
"Active data fetches": "Aktiiviset tietojen haut",
16+
"No active data fetches.": "Ei aktiivisia tietojen hakuja"
1517
}
1618
}

map-view/src/locale/sv.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"Layers": "Skikt",
1212
"Settings": "Inställningar",
1313
"Display Plan/Real difference": "Visa plan/implementering skillnad",
14-
"Filter by Project": "Filtrera efter projekt"
14+
"Filter by Project": "Filtrera efter projekt",
15+
"Active data fetches": "Aktiva datahämtningar",
16+
"No active data fetches.": "Inga aktiva datahämtningar."
1517
}
1618
}

0 commit comments

Comments
 (0)