From 10fbed02f1a616d072f25e4abd86e7e977a18d66 Mon Sep 17 00:00:00 2001 From: "Matheisen, Alexander" Date: Mon, 20 Nov 2023 08:51:34 +0100 Subject: [PATCH 01/60] WIP: Show unavailable layers in TOC UI (#6) --- src/packages/map/api/layers/base.ts | 5 +++ src/packages/map/model/AbstractLayer.ts | 32 +++++++++++++++--- src/packages/toc/LayerList.tsx | 43 ++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/packages/map/api/layers/base.ts b/src/packages/map/api/layers/base.ts index f899340b4..261c019f7 100644 --- a/src/packages/map/api/layers/base.ts +++ b/src/packages/map/api/layers/base.ts @@ -139,6 +139,11 @@ export interface LayerConfig extends LayerBaseConfig { * Defaults to `false`. */ isBaseLayer?: boolean; + + /** + * An optional URL to check the availability of the layer. + */ + readonly healthCheckURL?: string; } /** diff --git a/src/packages/map/model/AbstractLayer.ts b/src/packages/map/model/AbstractLayer.ts index b97fe7a90..10a13c538 100644 --- a/src/packages/map/model/AbstractLayer.ts +++ b/src/packages/map/model/AbstractLayer.ts @@ -23,6 +23,7 @@ export abstract class AbstractLayer { #olLayer: OlBaseLayer; #isBaseLayer: boolean; + #healthCheckURL?: string; #visible: boolean; #loadState: LayerLoadState; @@ -32,10 +33,11 @@ export abstract class AbstractLayer super(config); this.#olLayer = config.olLayer; this.#isBaseLayer = config.isBaseLayer ?? false; + this.#healthCheckURL = config.healthCheckURL; this.#visible = config.visible ?? true; const { initial: initialState, resource: stateWatchResource } = watchLoadState( - this.#olLayer, + config, (state) => { this.#loadState = state; this.__emitChangeEvent("changed:loadState"); @@ -104,10 +106,24 @@ export abstract class AbstractLayer } } +// TODO move to a service? +function healthCheck(config: SimpleLayerConfig): LayerLoadState { + return (Math.random() < 0.5) ? "loaded" : "error"; // TODO random error for tests + + if (config.healthCheckURL) { + // TODO test request to URL in healthCheckURL + return "error"; + } else { + return "loaded"; + } +} + function watchLoadState( - olLayer: OlBaseLayer, + config: SimpleLayerConfig, onChange: (newState: LayerLoadState) => void ): { initial: LayerLoadState; resource: Resource } { + const olLayer = config.olLayer; + if (!(olLayer instanceof OlLayer)) { // Some layers don't have a source (such as group) return { @@ -121,9 +137,17 @@ function watchLoadState( } let currentSource = olLayer?.getSource() as Source | null; - let currentLoadState = mapState(currentSource?.getState()); + const currentOlLayerState = mapState(currentSource?.getState()); + const currentHealthState = healthCheck(config); + let currentLoadState: LayerLoadState = (currentOlLayerState === "error" || currentHealthState === "error") + ? "error" : currentOlLayerState; + const updateState = () => { - const nextLoadState = mapState(currentSource?.getState()); + const olLayerState = mapState(currentSource?.getState()); + const healthState = healthCheck(config); + const nextLoadState: LayerLoadState = (olLayerState === "error" || healthState === "error") + ? "error" : olLayerState; + if (currentLoadState !== nextLoadState) { currentLoadState = nextLoadState; onChange(currentLoadState); diff --git a/src/packages/toc/LayerList.tsx b/src/packages/toc/LayerList.tsx index 26b4735fc..5d80fabe9 100644 --- a/src/packages/toc/LayerList.tsx +++ b/src/packages/toc/LayerList.tsx @@ -15,6 +15,7 @@ import { PopoverHeader, PopoverTrigger, Portal, + Spacer, Text } from "@open-pioneer/chakra-integration"; import { LayerBase, MapModel, Sublayer } from "@open-pioneer/map"; @@ -22,7 +23,7 @@ import { PackageIntl } from "@open-pioneer/runtime"; import classNames from "classnames"; import { useIntl } from "open-pioneer:react-hooks"; import { useCallback, useRef, useSyncExternalStore } from "react"; -import { FiMoreVertical } from "react-icons/fi"; +import { FiAlertTriangle, FiMoreVertical } from "react-icons/fi"; /** * Lists the (top level) operational layers in the map. @@ -71,6 +72,7 @@ function LayerItem(props: { layer: LayerBase; intl: PackageIntl }): JSX.Element const title = useTitle(layer); const { isVisible, setVisible } = useVisibility(layer); const sublayers = useSublayers(layer); + const isAvailable = useLoadState(layer) !== "error"; let nestedChildren; if (sublayers?.length) { @@ -92,10 +94,17 @@ function LayerItem(props: { layer: LayerBase; intl: PackageIntl }): JSX.Element > setVisible(event.target.checked)} > {title} + {!isAvailable && ( + // TODO Tooltip? + // TODO Aria Label + + )} + {layer.description && ( )} @@ -113,11 +122,13 @@ function LayerItemDescriptor(props: { const { layer, title, intl } = props; const buttonLabel = intl.formatMessage({ id: "descriptionLabel" }); const description = useLayerDescription(layer); + const isAvailable = useLoadState(layer) !== "error"; return (