219 lines
10 KiB
React
219 lines
10 KiB
React
import {useDashboardStore} from "@/store/useDashboardStore.jsx";
|
|||
|
|
import VdapApi from "../../hooks/useVdapApi.js";
|
||
|
|
import useBuildUrl from "../../hooks/useBuildUrl.js";
|
||
|
|
import { Footprints, Plane, Sailboat, TrendingUp, TrendingDown } from 'lucide-react';
|
||
|
|
import {Box, Collapsible, Flex, Spinner, Text, Timeline, Center} from "@chakra-ui/react";
|
||
|
|
import { useMemo, useCallback } from "react";
|
||
|
|
|
||
|
|
|
||
|
|
export default function AlertChanges({ settings, widgetID}) {
|
||
|
|
// Find this specific widgets object in the state list of active & displayed widgets
|
||
|
|
const thisWidget = useDashboardStore(useCallback((state) =>
|
||
|
|
state.widgets.find((widget) => widget.id === widgetID), [widgetID]
|
||
|
|
));
|
||
|
|
|
||
|
|
// build url, default is 7 days past
|
||
|
|
const url = useBuildUrl('/api/v2/alert_changes', settings?.startDate, settings?.endDate);
|
||
|
|
|
||
|
|
// call api hook to retrieve data
|
||
|
|
const {data: apiData, isLoading, error} = VdapApi(url);
|
||
|
|
|
||
|
|
const data = useMemo(() => {
|
||
|
|
if (apiData.length === 0) return [];
|
||
|
|
|
||
|
|
// .reduce() is a built-in array method used to transform an entire array into a single value (like an object)
|
||
|
|
// here it returns a single massive object w/ volcano names as keys
|
||
|
|
const grouped = apiData.reduce((accumulator, item) => {
|
||
|
|
if (!accumulator[item.volcano]) accumulator[item.volcano] = [];
|
||
|
|
accumulator[item.volcano].push(item);
|
||
|
|
return accumulator;
|
||
|
|
}, {});
|
||
|
|
|
||
|
|
// converts the 'grouped' object back into an array of [key, value] pairs
|
||
|
|
const group_arr = Object.entries(grouped);
|
||
|
|
|
||
|
|
// sort data newest to oldest within each volcano
|
||
|
|
const sorted = group_arr.map((item) =>
|
||
|
|
[
|
||
|
|
item[0], // volcano name
|
||
|
|
[...(item[1])].sort((a, b) =>
|
||
|
|
new Date(b.alert_start_date_local) - new Date(a.alert_start_date_local))
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
// volcano with most recent alert level change appears at top of list
|
||
|
|
const sorted_entries = [...sorted].sort((a, b) => new Date(b[1][0].alert_start_date_local) - new Date(a[1][0].alert_start_date_local));
|
||
|
|
|
||
|
|
if (thisWidget?.size === "small") {
|
||
|
|
return (sorted_entries.map((volcano) => {
|
||
|
|
const domain = volcano[1].reduce((acc, curr) => {
|
||
|
|
if (!acc[curr.alert_scheme_domain]) acc[curr.alert_scheme_domain] = [];
|
||
|
|
acc[curr.alert_scheme_domain].push(curr);
|
||
|
|
return acc;
|
||
|
|
}, {});
|
||
|
|
|
||
|
|
const domain_arr = Object.entries(domain);
|
||
|
|
return [volcano[0], domain_arr];
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
return sorted_entries;
|
||
|
|
}, [apiData, thisWidget]);
|
||
|
|
|
||
|
|
// list of alert colors used for coloring text below
|
||
|
|
const alertColors = {
|
||
|
|
Red: "red.400",
|
||
|
|
Orange: "orange.400",
|
||
|
|
Yellow: "yellow.400",
|
||
|
|
Green: "green.400",
|
||
|
|
};
|
||
|
|
|
||
|
|
if (isLoading) return (
|
||
|
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800">
|
||
|
|
<Spinner size="xl" color="blue.400" thickness="4px"/>
|
||
|
|
</Flex>
|
||
|
|
);
|
||
|
|
|
||
|
|
if (error) return (
|
||
|
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800">
|
||
|
|
<Text color="red.400">{error}</Text>
|
||
|
|
</Flex>
|
||
|
|
);
|
||
|
|
|
||
|
|
if (data.length === 0) return (
|
||
|
|
<Flex w="100%" h="100%" align="center" justify="center" borderRadius="lg" bg="gray.800">
|
||
|
|
<Text textAlign="center" color="gray.400">No flight level data available for the selected dates.</Text>
|
||
|
|
</Flex>
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Box display="flex" flexDirection="column" gap={2} p={2} w="100%" h="100%" borderRadius="lg" bg="gray.800" overflowY="auto">
|
||
|
|
{data.map((volcano, index) => {
|
||
|
|
if (thisWidget?.size === "large") return (
|
||
|
|
<Collapsible.Root key={index} w="100%">
|
||
|
|
{/* Using context to dynamically change the border radii of the triggers */}
|
||
|
|
<Collapsible.Context>
|
||
|
|
{(collapsible) => (
|
||
|
|
<>
|
||
|
|
<Collapsible.Trigger w="100%">
|
||
|
|
<Flex direction="row"
|
||
|
|
w="100%"
|
||
|
|
justify="space-between" align="center"
|
||
|
|
p={1} pl={3} pr={3}
|
||
|
|
borderTopRadius="lg"
|
||
|
|
borderBottomRadius={collapsible.open ? "0" : "lg"}
|
||
|
|
bg="gray.600"
|
||
|
|
>
|
||
|
|
<Flex direction="column" w="100%" minW={0} textAlign="left" color="gray.100">
|
||
|
|
<Text truncate >{volcano[0]}</Text> {/* volcano name */}
|
||
|
|
<Text truncate fontSize="xs" color="gray.300/90">{volcano[1][0]?.alert_start_date_local}</Text>
|
||
|
|
</Flex>
|
||
|
|
<Flex w="100%" minW={0} justify="right">
|
||
|
|
<Text truncate pl={2} pr={2} borderRadius="lg" bg="gray.800/50" color="gray.100" fontWeight="semibold">{volcano[1][0]?.alert_level_name}</Text>
|
||
|
|
</Flex>
|
||
|
|
</Flex>
|
||
|
|
</Collapsible.Trigger>
|
||
|
|
<Collapsible.Content w="100%" p={3} borderBottomRadius="lg" bg="gray.700">
|
||
|
|
<Timeline.Root size="md" variant="subtle">
|
||
|
|
{volcano[1]?.map((object, index) => (
|
||
|
|
<Timeline.Item key={index} w="100%" h="100%">
|
||
|
|
<Timeline.Connector >
|
||
|
|
{/*<Timeline.Separator borderWidth="1px" borderColor="gray.600"/>*/}
|
||
|
|
<Timeline.Indicator outline="none" bg="gray.200" color="gray.500" size="lg">
|
||
|
|
{object?.alert_scheme_domain === "terrestrial" && <Footprints size={15} />}
|
||
|
|
{object?.alert_scheme_domain === "aviation" && <Plane size={15} />}
|
||
|
|
{object?.alert_scheme_domain === "maritime" && <Sailboat size={15} />}
|
||
|
|
</Timeline.Indicator>
|
||
|
|
<Timeline.Indicator h="50%" outline="none" bg="gray.700" color="gray.100" size="lg">
|
||
|
|
{object?.change_type === "increase" ? <TrendingUp /> : <TrendingDown />}
|
||
|
|
</Timeline.Indicator>
|
||
|
|
</Timeline.Connector>
|
||
|
|
<Timeline.Content w="100%" h="100%">
|
||
|
|
{/*<Timeline.Title>{object?.volcano}</Timeline.Title>*/}
|
||
|
|
<Timeline.Description w="100%" h="100%" gap={1}>
|
||
|
|
<Flex direction="column" gap={1}>
|
||
|
|
<Text w="100%" h="100%" color="gray.200">{object?.alert_start_date_local}</Text>
|
||
|
|
<Text w="100%" h="100%"
|
||
|
|
color={alertColors[object?.alert_level_name] || "gray.200"}
|
||
|
|
fontSize="md"
|
||
|
|
fontWeight="semibold"
|
||
|
|
>
|
||
|
|
{object?.alert_level_name}
|
||
|
|
</Text>
|
||
|
|
<Flex w="100%" h="20px" direction="row" justify="left" align="center">
|
||
|
|
<Text color="gray.300">Alert Scale: </Text>
|
||
|
|
{(Array.from({ length: Number(object?.alert_scheme_highest) - Number(object?.alert_scheme_lowest) + 1}, (v, i) => Number(object?.alert_scheme_lowest) + i))
|
||
|
|
.map((num) => (
|
||
|
|
<Center
|
||
|
|
key={num}
|
||
|
|
w="20px" h="100%"
|
||
|
|
color="gray.300"
|
||
|
|
bg={num === Number(object?.alert_level_rank) ? "gray.300/20" : null} // alert level rank is a string for some reason
|
||
|
|
borderRadius={num === Number(object?.alert_level_rank) ? "xl" : null}
|
||
|
|
textAlign="center"
|
||
|
|
fontSize="sm"
|
||
|
|
fontWeight="semibold"
|
||
|
|
>
|
||
|
|
{num}
|
||
|
|
</Center>
|
||
|
|
))
|
||
|
|
}
|
||
|
|
</Flex>
|
||
|
|
</Flex>
|
||
|
|
</Timeline.Description>
|
||
|
|
</Timeline.Content>
|
||
|
|
</Timeline.Item>
|
||
|
|
))}
|
||
|
|
</Timeline.Root>
|
||
|
|
</Collapsible.Content>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</Collapsible.Context>
|
||
|
|
</Collapsible.Root>
|
||
|
|
)
|
||
|
|
else if (thisWidget?.size === "small") return (
|
||
|
|
volcano[1]?.map((domain, index) => (
|
||
|
|
<Flex
|
||
|
|
key={index}
|
||
|
|
direction="row"
|
||
|
|
w="100%"
|
||
|
|
gap={2}
|
||
|
|
justify="space-between" align="center"
|
||
|
|
p={1} pl={3} pr={3}
|
||
|
|
borderRadius="lg"
|
||
|
|
bg="gray.600"
|
||
|
|
>
|
||
|
|
<Flex direction="column" w="100%" minW={0} textAlign="left" color="gray.100">
|
||
|
|
<Flex direction="row" w="100%" gap={2} align="center">
|
||
|
|
<Flex color="gray.300">
|
||
|
|
{domain[0] === "aviation" && <Plane size={15}/>}
|
||
|
|
{domain[0] === "terrestrial" && <Footprints size={15}/>}
|
||
|
|
{domain[0] === "maritime" && <Sailboat size={15}/>}
|
||
|
|
</Flex>
|
||
|
|
<Text truncate>{volcano[0] || ""}</Text> {/* volcano name */}
|
||
|
|
</Flex>
|
||
|
|
<Text truncate w="100%" fontSize="xs" color="gray.300/90">{domain[1]?.[0]?.alert_start_date_local || ""}</Text>
|
||
|
|
</Flex>
|
||
|
|
|
||
|
|
<Flex direction="row" w="100%" minW={0} justify="right" gap={2} color="gray.200">
|
||
|
|
<Flex w="25px" justify="right">
|
||
|
|
{domain[1]?.[0]?.change_type === "increase" ? <TrendingUp/> : <TrendingDown/>}
|
||
|
|
</Flex>
|
||
|
|
<Text
|
||
|
|
truncate
|
||
|
|
pl={2} pr={2}
|
||
|
|
borderRadius="lg"
|
||
|
|
bg="gray.800/50"
|
||
|
|
color={alertColors[domain[1]?.[0]?.alert_level_name] || "gray.100"}
|
||
|
|
fontWeight="semibold"
|
||
|
|
>
|
||
|
|
{domain[1]?.[0]?.alert_level_name}
|
||
|
|
</Text>
|
||
|
|
</Flex>
|
||
|
|
</Flex>
|
||
|
|
))
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</Box>
|
||
|
|
)
|
||
|
|
}
|