diff --git a/apps/nowcasting-app/components/API-Routes.ts b/apps/nowcasting-app/components/API-Routes.ts new file mode 100644 index 00000000..f78069bc --- /dev/null +++ b/apps/nowcasting-app/components/API-Routes.ts @@ -0,0 +1,12 @@ +const API_BASE_URL = process.env.NEXT_PUBLIC_API_PREFIX || ""; +const IS_FAKE = process.env.NEXT_PUBLIC_IS_FAKE === "1"; // Placeholder for fake data env variable. Please advise on how to implement this? + +const paths = { + allForecasts: `${API_BASE_URL}/v0/solar/GB/gsp/forecast/all/?isFake=${IS_FAKE}`, + allPVLive: `${API_BASE_URL}/v0/solar/GB/gsp/pvlive/all/?isFake=${IS_FAKE}`, + forecastByGSPId: (gspId: number) => `${API_BASE_URL}/v0/solar/GB/gsp/${gspId}/forecast/?isFake=${IS_FAKE}`, + PVLiveByGSPId: (gspId: number) => `${API_BASE_URL}/v0/solar/GB/gsp/${gspId}/pvlive/?isFake=${IS_FAKE}`, + nationalPVLive: `${API_BASE_URL}/v0/solar/GB/national/pvlive/?isFake=${IS_FAKE}` +}; + +export { paths, IS_FAKE }; \ No newline at end of file diff --git a/apps/nowcasting-app/components/fake-API-calls.ts b/apps/nowcasting-app/components/fake-API-calls.ts new file mode 100644 index 00000000..6a773805 --- /dev/null +++ b/apps/nowcasting-app/components/fake-API-calls.ts @@ -0,0 +1,49 @@ +import { paths, IS_FAKE } from "./API-Routes" + +const FakeResponsehandler = async (response: Response) => { + if (!response.ok) { + if (IS_FAKE) { + return { fake : true, data: [] }; // Return empty array if no fake data returned + } + throw new Error(`HTTP Error status code: ${response.status}`); + } + return response.json(); +}; + +// Fetch all forecasts +const fetchAllForecasts = async () => { + const response = await fetch(paths.allForecasts); + return FakeResponsehandler(response); +}; + +// Fetch forecasts by GSP ID +const fetchforecastByGSPID = async (gspId: number) => { + const response = await fetch(paths.forecastByGSPId(gspId)); + return FakeResponsehandler(response); +}; + +// Fetch all PVLive forecasts +const fetchallPVLive = async () => { + const response = await fetch(paths.allPVLive); + return FakeResponsehandler(response); +}; + +// Fetch PVLive by GSP ID +const fetchPVLive = async (gspId: number) => { + const response = await fetch(paths.PVLiveByGSPId(gspId)); + return FakeResponsehandler(response); +}; + +// Fetch fake PVLive National forecasts +const nationalPVLive = async () => { + const response = await fetch(paths.nationalPVLive); + return FakeResponsehandler(response); +}; + +export { + fetchAllForecasts, + fetchforecastByGSPID, + fetchallPVLive, + fetchPVLive, + nationalPVLive +}; \ No newline at end of file diff --git a/apps/nowcasting-app/components/fake-forecast-generation.tsx b/apps/nowcasting-app/components/fake-forecast-generation.tsx new file mode 100644 index 00000000..c0d05b0a --- /dev/null +++ b/apps/nowcasting-app/components/fake-forecast-generation.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from "react"; +import { IS_FAKE } from "./API-Routes"; +import { fetchAllForecasts, + fetchforecastByGSPID, + fetchallPVLive, + fetchPVLive, + nationalPVLive +} from "./fake-API-calls"; +import Tooltip from "./tooltip"; +import Toggle from "./Toggle"; +import ButtonGroup from "./button-group"; + +// Define available route keys +type RouteKey = + | "allForecasts" + | "forecastByGspId" + | "allPVLive" + | "PVLiveByGspId" + | "nationalPVLive"; + +interface GSPIds { + itemId?: number; +} + +const ForecastsComponent: React.FC = ({ itemId }) => { + const [selectedRoutes, setSelectedRoutes] = useState(["allForecasts"]); // Set default route to 'allForecasts' + const [results, setResults] = useState>([]); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const loadForecasts = async () => { + setIsLoading(true); + try { + // Map selected routes to their corresponding FastAPI calls + const promises = selectedRoutes.map(async (route) => { + try { + let data; + switch (route) { + case "allForecasts": + data = await fetchAllForecasts(); + break; + case "forecastByGspId": + if (!itemId && !IS_FAKE) throw new Error("Please enter GSP ID for this route"); + data = await fetchforecastByGSPID(itemId || 1); + break; + case "allPVLive": + data = await fetchallPVLive(); + break; + case "PVLiveByGspId": + if (!itemId && !IS_FAKE) throw new Error("Please enter GSP ID for this route"); + data = await fetchPVLive(itemId || 1); + break; + case "nationalPVLive": + data = await nationalPVLive(); + break; + default: + throw new Error(`Unknown route: ${route}`); + } + return { route, data }; + } catch (error) { + if (IS_FAKE) { + return { route, data: { fake: true, data: [] } }; + } + throw error; + } + }); + + // Wait for all requests to complete + const results = await Promise.all(promises); + setResults(results); + setError(null); + } catch (error) { + setError(error instanceof Error ? error.message : "Unable to fetch data"); + } finally { + setIsLoading(false); + } + }; + + loadForecasts(); + }, [setSelectedRoutes, setResults, setError, setIsLoading, itemId]); + + const toggleRoute = (route: RouteKey) => { + setSelectedRoutes(prev => + prev.includes(route) + ? prev.filter(r => r !== route) + : [...prev, route] + ); + }; + + // Define Route Configuration + const routeConfig: Array<{ key: RouteKey; label: string; tip: string }> = [ + { key: "allForecasts", label: "All available forecasts", tip: "Fetch all available forecasts" }, + { key: "forecastByGspId", label: "Forecast by GSP ID", tip: "Fetch forecast by GSP ID" }, + { key: "allPVLive", label: "All PV Live", tip: "Fetch all PV live data" }, + { key: "PVLiveByGspId", label: "PV Live by GSP ID", tip: "Fetch PV live data by GSP ID" }, + { key: "nationalPVLive", label: "National PV Live", tip: "Fetch national PV live data" } + ]; + + return ( +
+ {IS_FAKE && ( +
+ Running in Fake Data Mode +
+ )} + +
+ {routeConfig.map(({ key, label, tip }) => ( + + toggleRoute(key)} + visible={selectedRoutes.includes(key)} + /> + {label} + + ))} +
+ + {isLoading &&
Loading Fake Forecasts...
} + {error &&
{error}
} +
{JSON.stringify(results, null, 2)}
+ + +
+ ); +}; + +export default ForecastsComponent; \ No newline at end of file diff --git a/apps/nowcasting-app/pages/fake-forecasts.tsx b/apps/nowcasting-app/pages/fake-forecasts.tsx new file mode 100644 index 00000000..2b951c45 --- /dev/null +++ b/apps/nowcasting-app/pages/fake-forecasts.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from "react"; +import ForecastsComponent from "../components/fake-forecast-generation"; +import Layout from "../components/layout/layout"; +import useGlobalState, { get30MinNow } from '../components/helpers/globalState'; +import { AGGREGATION_LEVELS } from "../constant"; + +export default function ForecastsPage() { + const [gspId, setGspId] = useState(1); + + // Use global state for necessary views and states + const [, setView] = useGlobalState("aggregationLevel"); + const [clickedGspId, setClickedGspId] = useGlobalState("clickedGspId"); + const [, setSelectedISOTime] = useGlobalState("selectedISOTime"); + + useEffect(() => { + setView(AGGREGATION_LEVELS.GSP); + + setSelectedISOTime(get30MinNow()); + + if (clickedGspId !== undefined && typeof clickedGspId === "number") { + setClickedGspId(clickedGspId); + } + + return () => { + setView(AGGREGATION_LEVELS.GSP); + + // Handle National view + if (clickedGspId == 0) { + setView(AGGREGATION_LEVELS.NATIONAL); + } + }; + + }, [setView, setSelectedISOTime, clickedGspId]); + + const handleGspIdChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const newGspId = value === '' ? 1 : parseInt(value, 10) + setGspId(newGspId); + + setClickedGspId(newGspId); + }; + + return ( + +
+

Fake Forecasts

+ +
+ +

+ Grid Supply Point (GSP) ID for targeted forecast data +

+
+ + +
+
+ ); +}; \ No newline at end of file