Skip to content

Commit

Permalink
Rewrote initial loading to async/await, added new Error page:
Browse files Browse the repository at this point in the history
- I got rid of the nested promise callbacks, now it's all in one try/catch
- A new error page is added. It's (hopefully) nice and clean. It also features a reload button that will attempt to reset the application's state by redirecting to '/'. Should this not work, there are more things to try out (parsing the documnent.location and removing searchParams manually).
  • Loading branch information
jacobwod committed Aug 18, 2023
1 parent a08f81e commit f2201fc
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 143 deletions.
5 changes: 3 additions & 2 deletions new-client/public/appConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
"showExperimentalLanguageSwitcher": false,
"lang": "en",
"noLayerSwitcherMessage": "This map has no layer switcher, which indicates that you are not allowed to use this map!",
"networkErrorMessage": "Nätverksfel. Försök att ladda om sidan genom att trycka på F5.",
"parseErrorMessage": "Konfigurationsfel. Försök att ladda om sidan genom att trycka på F5.",
"loadErrorTitle": "Ett fel har inträffat",
"loadErrorMessage": "Fel när applikationen skulle läsas in. Du kan försöka att återställa applikationen genom att trycka på knappen nedan. Om felet kvarstår kan du kontakta systemansvarig.",
"loadErrorReloadButtonText": "Återställ applikationen",
"announcements": [
{
"id": 1,
Expand Down
13 changes: 0 additions & 13 deletions new-client/public/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,6 @@ body {
line-height: 5rem;
}

/* Used only for displaying error if nothing loaded (and we don't have any MUI yet) */
.start-error {
color: red;
background: rgb(255, 200, 200);
border: 1px solid red;
border-radius: 5px;
padding: 15px;
margin: 0 auto;
width: 50%;
text-align: center;
margin-top: 5%;
}

.introjs-tooltip {
min-width: 280px !important;
}
Expand Down
29 changes: 29 additions & 0 deletions new-client/src/components/Errors/StartupError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from "react";
import { Alert, AlertTitle, Box, Button } from "@mui/material";

export default function Error({
loadErrorMessage,
loadErrorTitle,
loadErrorReloadButtonText,
}) {
return (
<Box
sx={{
display: "flex",
p: 10,
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
minHeight: "100vh",
}}
>
<Alert severity="error">
<AlertTitle>{loadErrorTitle}</AlertTitle>
{loadErrorMessage}
</Alert>
<Button href="/" variant="contained" sx={{ mt: 3 }}>
{loadErrorReloadButtonText}
</Button>
</Box>
);
}
241 changes: 113 additions & 128 deletions new-client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,142 +26,127 @@ import "./custom-ol.css";
import React from "react";
import { createRoot } from "react-dom/client";
import buildConfig from "./buildConfig.json";
import ErrorIcon from "@mui/icons-material/Error";
import StartupError from "./components/Errors/StartupError";
import HajkThemeProvider from "./components/HajkThemeProvider";
import { initHFetch, hfetch, initFetchWrapper } from "./utils/FetchWrapper";
import LocalStorageHelper from "./utils/LocalStorageHelper";
import { getMergedSearchAndHashParams } from "./utils/getMergedSearchAndHashParams";

initHFetch();

let networkErrorMessage =
"Nätverksfel. Prova att ladda om applikationen genom att trycka på F5 på ditt tangentbord.";
let parseErrorMessage =
"Fel när applikationen skulle läsas in. Detta beror troligtvis på ett konfigurationsfel. Försök igen senare.";

const domRoot = createRoot(document.getElementById("root"));

const renderError = (message, err) => {
console.error(err);
domRoot.render(
<div className="start-error">
<div>
<ErrorIcon />
</div>
<div>{message}</div>
</div>
);
};

/**
* Entry point to Hajk.
* We start with a fetching appConfig.json, that is expected
* to be located in the same directory as index.js.
* We start with initializing HFetch (that provides some custom fetch options
* to all requests throughout the application). Next, we fetch appConfig.json.
*
* appConfig.json includes URL to the backend application (called MapService),
* appConfig.json includes URL to Hajk's backend application,
* as well as the default preferred map configuration's file name.
*/
hfetch("appConfig.json", { cacheBuster: true })
.then((appConfigResponse) => {
appConfigResponse.json().then((appConfig) => {
// Update hfetch with loaded config.
initFetchWrapper(appConfig);
// See if we have site-specific error messages
if (appConfig.networkErrorMessage)
networkErrorMessage = appConfig.networkErrorMessage;
if (appConfig.parseErrorMessage)
parseErrorMessage = appConfig.parseErrorMessage;

// Grab URL params and save for later use
const initialURLParams = getMergedSearchAndHashParams();

// If m param is supplied, try loading a map with that name
// or else, fall back to default from appConfig.json
let activeMap = initialURLParams.has("m")
? initialURLParams.get("m")
: appConfig.defaultMap;

// Check if mapserviceBase is set in appConfig. If it is not, we will
// fall back on the simple map and layer configurations found in /public.
const useBackend = appConfig.mapserviceBase?.trim().length > 0;

const fetchConfig = async () => {
if (useBackend === false) {
// No backend specified, let's return static config
return await hfetch("simpleMapAndLayersConfig.json", {
cacheBuster: true,
});
} else {
// Prepare the URL config string
const configUrl = `${appConfig.proxy}${appConfig.mapserviceBase}/config`;
try {
// Try to fetch user-specified config. Return it if OK.
return await hfetch(`${configUrl}/${activeMap}`);
} catch {
// If the previous attempt fails reset "activeMap" to hard-coded value…
activeMap = appConfig.defaultMap;
// …and try to fetch again.
return await hfetch(`${configUrl}/${activeMap}`);
}
}
};

Promise.all([
fetchConfig(),
hfetch("customTheme.json", { cacheBuster: true }),
])
.then(([mapConfigResponse, customThemeResponse]) => {
Promise.all([mapConfigResponse.json(), customThemeResponse.json()])
.then(([mapConfig, customTheme]) => {
const config = {
activeMap: useBackend ? activeMap : "simpleMapConfig", // If we are not utilizing mapService, we know that the active map must be "simpleMapConfig".
appConfig: appConfig,
layersConfig: mapConfig.layersConfig,
mapConfig: mapConfig.mapConfig,
userDetails: mapConfig.userDetails,
userSpecificMaps: mapConfig.userSpecificMaps,
initialURLParams,
};

// TODO: Watchout - this can be a controversial introduction!
// Before we merge, ensure that we really want this!
// Why am I adding it? The examples/embedded.html shows Hajk running
// in an IFRAME and allows it to be controlled by changing the SRC
// attribute of the IFRAME. In that file, there are two buttons (one
// to increase and another one to decrease the zoom level). However,
// we don't want to zoom past map's zoom limits. At first I used some
// hard-coded values for min/max zoom, but these will vary depending on
// map config. So I figured out that we could expose some of Hajk's settings
// on the global object. That way, the IFRAME's parent document can read
// those values and use to check that we don't allow zooming past limits.
//
// We can of course add more things that can be "nice to have" for an
// embedded solution. In addition to parameters, we could expose some API
// that would control the map itself! But it should be carefully crafted.
//
// For the sake of this example, I'm committing this basic object:
window.hajkPublicApi = {
maxZoom: config.mapConfig.map.maxZoom,
minZoom: config.mapConfig.map.minZoom,
};

// At this stage, we know for sure what activeMap is, so we can initiate the LocalStorageHelper
LocalStorageHelper.setKeyName(config.activeMap);

// Invoke React's renderer. Render Theme. Theme will render App.
domRoot.render(
<HajkThemeProvider
activeTools={buildConfig.activeTools}
config={config}
customTheme={customTheme}
/>
);
})
.catch((err) => renderError(parseErrorMessage, err));
})
.catch((err) => renderError(networkErrorMessage, err));
});
})
.catch((err) => {
renderError(networkErrorMessage, err);
initHFetch();

// We must define appConfig outside of the try so it's available in catch
// statement. If we fail at some point, we may still be able to use the
// error message from appConfig (depending on if we manage to load it or not).
let appConfig;

try {
const appConfigResponse = await hfetch("appConfig.json", {
cacheBuster: true,
});
appConfig = await appConfigResponse.json();

// Update hfetch with loaded config.
initFetchWrapper(appConfig);

// Grab URL params and save for later use
const initialURLParams = getMergedSearchAndHashParams();

// If m param is supplied, try loading a map with that name
// or else, fall back to default from appConfig.json
let activeMap = initialURLParams.has("m")
? initialURLParams.get("m")
: appConfig.defaultMap;

// Check if mapserviceBase is set in appConfig. If it is not, we will
// fall back on the simple map and layer configurations found in /public.
const useBackend = appConfig.mapserviceBase?.trim().length > 0;

// Prepare a helper function that fetches config files
const fetchConfig = async () => {
if (useBackend === false) {
// No backend specified, let's return static config
return await hfetch("simpleMapAndLayersConfig.json", {
cacheBuster: true,
});
} else {
// Prepare the URL config string
const configUrl = `${appConfig.proxy}${appConfig.mapserviceBase}/config`;
try {
// Try to fetch user-specified config. Return it if OK.
return await hfetch(`${configUrl}/${activeMap}`);
} catch {
// If the previous attempt fails reset "activeMap" to hard-coded value…
activeMap = appConfig.defaultMap;
// …and try to fetch again.
return await hfetch(`${configUrl}/${activeMap}`);
}
}
};

const [mapConfigResponse, customThemeResponse] = await Promise.all([
fetchConfig(),
hfetch("customTheme.json", { cacheBuster: true }),
]);

const mapConfig = await mapConfigResponse.json();
const customTheme = await customThemeResponse.json();

const config = {
activeMap: useBackend ? activeMap : "simpleMapConfig", // If we are not utilizing mapService, we know that the active map must be "simpleMapConfig".
appConfig: appConfig,
layersConfig: mapConfig.layersConfig,
mapConfig: mapConfig.mapConfig,
userDetails: mapConfig.userDetails,
userSpecificMaps: mapConfig.userSpecificMaps,
initialURLParams,
};

// For the sake of this example, I'm committing this basic object:
// Expose a couple of useful properties as part of Hajk's public API. For
// usage, see examples/embedded.html, which reads these values to control
// which zoom levels are available in the embedded (outer) application.
// This can be extended with more properties or even functions when the
// need comes up.
window.hajkPublicApi = {
maxZoom: config.mapConfig.map.maxZoom,
minZoom: config.mapConfig.map.minZoom,
};

// At this stage, we know for sure what activeMap is, so we can initiate the LocalStorageHelper
LocalStorageHelper.setKeyName(config.activeMap);

// Invoke React's renderer. Render the theme. Theme will render the App.
createRoot(document.getElementById("root")).render(
<HajkThemeProvider
activeTools={buildConfig.activeTools}
config={config}
customTheme={customTheme}
/>
);
} catch (error) {
console.error(error);

// Attempt to grab the custom error texts from appConfig, fall back to default.
const loadErrorTitle = appConfig?.loadErrorTitle || "Ett fel har inträffat";
const loadErrorMessage =
appConfig?.loadErrorMessage ||
"Fel när applikationen skulle läsas in. Du kan försöka att återställa applikationen genom att trycka på knappen nedan. Om felet kvarstår kan du kontakta systemansvarig.";
const loadErrorReloadButtonText =
appConfig?.loadErrorReloadButtonText || "Återställ applikationen";

createRoot(document.getElementById("root")).render(
<StartupError
loadErrorMessage={loadErrorMessage}
loadErrorTitle={loadErrorTitle}
loadErrorReloadButtonText={loadErrorReloadButtonText}
/>
);
}

0 comments on commit f2201fc

Please sign in to comment.