Implement SignalR integration and refactor WeatherPage

Added SignalR for real-time progress updates during weather data fetch. Refactored WeatherPage to use a new reusable WeatherGrid component and SignalRHelper. Improved loading UI with a radial progress indicator.
This commit is contained in:
2025-02-09 02:00:06 +01:00
parent 72b7902b55
commit c4071786ea
7 changed files with 184 additions and 65 deletions

View File

@@ -15,9 +15,10 @@ builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
policy.WithOrigins("http://localhost:5173")
.AllowAnyHeader()
.AllowAnyMethod();
.AllowAnyMethod()
.AllowCredentials();
});
});
@@ -41,6 +42,14 @@ var summaries = new[]
app.MapGet("/weatherforecast", async () =>
{
var hub = app.Services.GetRequiredService<IHubContext<WeatherUpdateHub>>();
for (var i = 0; i < 100; i++)
{
await hub.Clients.All.SendAsync("ProgressUpdate", "None", $$"""{"percentage": "{{i}}", "message": "{{DateTime.Now.Millisecond}}"}""");
await Task.Delay(100);
}
var forecast = Enumerable.Range(1, 2500).Select(index =>
new WeatherForecast
(

View File

@@ -18,7 +18,7 @@ namespace Api.SignalR
logger.LogInformation("Background service sending message at: {Time}", DateTime.Now);
// Send a message to all connected clients.
await hubContext.Clients.User("user1234").SendAsync(
await hubContext.Clients.All.SendAsync(
"ReceiveMessage", // This is the client method name.
"Background Service", // Example sender.
$"Hello from the background task at {DateTime.Now:F}", // The message.

View File

@@ -3,7 +3,7 @@ import React from 'react';
import {BrowserRouter as Router, Route, Routes} from 'react-router-dom';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import WeatherPage from "./pages/WeatherPage.tsx";
import WeatherPage from "./pages/WeatherPage/WeatherPage.tsx";
const App: React.FC = () => {

View File

@@ -1,61 +0,0 @@
import with_main_layout from "../layouts/with_main_layout.ts.tsx";
import useLocalStorageState from 'use-local-storage-state'; // install via npm
import {Client, WeatherForecast} from "../ApiCient.ts";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faDownload, faTrash} from "@fortawesome/free-solid-svg-icons";
import {ColumnDirective, ColumnsDirective, Filter, GridComponent, Inject, Page, Reorder, Resize, Sort} from '@syncfusion/ej2-react-grids';
function WeatherPage() {
// Use the hook to persist your weather data. It will default to null.
const [weatherData, setWeatherData] = useLocalStorageState<WeatherForecast[] | null>('WeatherPage-Forecast', {
defaultValue: null,
});
const fetchWeatherData = async () => {
const apiClient = new Client();
try {
const data = await apiClient.getWeatherForecast();
setWeatherData(data);
} catch (error) {
console.error("Error fetching weather data:", error);
}
};
return (
<div className="card shadow-md card-sm w-full bg-auto-50">
<div className="card-header text-2xl ml-5 mt-1">Weather Data</div>
<div className="card-body">
<div className="row-auto flex gap-x-2 ">
<button className="btn w-48" onClick={fetchWeatherData}>
<FontAwesomeIcon className="mr-1" icon={faDownload}/>
Fetch Weather Data
</button>
<button className="btn w-48" onClick={() => setWeatherData(null)}>
<FontAwesomeIcon className="mr-1" icon={faTrash}/>
Clear Weather
</button>
</div>
<div className="mt-5">
{!weatherData ? (
<p>No weather data, please request it!</p>
) : (
<GridComponent dataSource={weatherData || []} allowPaging={true} pageSettings={{pageCount: 5}} allowReordering={true} allowSorting={true}
allowFiltering={true} allowResizing={true}>
<ColumnsDirective>
<ColumnDirective field="date" format="dd.MM.yy" headerText="Date" width="150"/>
<ColumnDirective field="temperatureC" headerText="Temp. (C)" width="60"/>
<ColumnDirective field="temperatureF" headerText="Temp. (F)" width="60"/>
<ColumnDirective field="summary" headerText="Summary"/>
</ColumnsDirective>
<Inject services={[Page, Sort, Filter, Reorder, Resize]}/>
</GridComponent>
)}
</div>
</div>
</div>
);
}
export const PageWithLayout = with_main_layout(WeatherPage);
export default PageWithLayout;

View File

@@ -0,0 +1,33 @@
import React from "react";
import {ColumnDirective, ColumnsDirective, Filter, GridComponent, Inject, Page, Reorder, Resize, Sort} from "@syncfusion/ej2-react-grids";
import {WeatherForecast} from "../../ApiCient.ts";
interface WeatherGridProps {
/**
* Represents an optional array of weather forecast data.
* Each element in the array provides detailed weather forecast information.
*/
data?: WeatherForecast[];
testing?: boolean;
}
const NavMenu: React.FC<WeatherGridProps> = ({data: weatherData}) => {
return (
<div>
<GridComponent dataSource={weatherData || []} allowPaging={true} pageSettings={{pageCount: 5}} allowReordering={true} allowSorting={true}
allowFiltering={true} allowResizing={true}>
<ColumnsDirective>
<ColumnDirective field="date" format="dd.MM.yy" headerText="Date" width="150"/>
<ColumnDirective field="temperatureC" headerText="Temp. (C)" width="60"/>
<ColumnDirective field="temperatureF" headerText="Temp. (F)" width="60"/>
<ColumnDirective field="summary" headerText="Summary"/>
</ColumnsDirective>
<Inject services={[Page, Sort, Filter, Reorder, Resize]}/>
</GridComponent>
</div>
);
};
export default NavMenu;

View File

@@ -0,0 +1,75 @@
import with_main_layout from "../../layouts/with_main_layout.ts.tsx";
import useLocalStorageState from 'use-local-storage-state'; // install via npm
import {Client, WeatherForecast} from "../../ApiCient.ts";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faDownload, faTrash} from "@fortawesome/free-solid-svg-icons";
import {useState} from "react";
import WeatherGrid from "./WeatherGrid.tsx";
import {useSignalR} from "../../signalr/SignalRHelper.ts";
function WeatherPage() {
// Use the hook to persist your weather data. It will default to null.
const [weatherData, setWeatherData] = useLocalStorageState<WeatherForecast[] | null>('WeatherPage-Forecast', {defaultValue: [],});
const [loading, setLoading] = useState(false);
const [currentState, setCurrentState] = useState<string | null>(null);
const {subscribe} = useSignalR('http://localhost:5175', 'WeatherUpdateHub');
const fetchWeatherData = async () => {
const apiClient = new Client();
try {
const progressCleanup = subscribe<string>('ProgressUpdate', (_, message) => {
setCurrentState(message);
});
setLoading(true);
const data = await apiClient.getWeatherForecast();
setWeatherData(data);
setLoading(false);
progressCleanup();
setCurrentState(null);
} catch (error) {
console.error("Error fetching weather data:", error);
}
};
return (
<div className="card shadow-md card-sm w-full bg-auto-50">
<div className="card-header text-2xl ml-5 mt-1">Weather Data</div>
<div className="card-body">
<div className="row-auto flex gap-x-2 ">
<button className="btn w-48" onClick={fetchWeatherData}>
<FontAwesomeIcon className="mr-1" icon={faDownload}/>
Fetch Weather Data
</button>
<button className="btn w-48" onClick={() => setWeatherData(null)}>
<FontAwesomeIcon className="mr-1" icon={faTrash}/>
Clear Weather
</button>
</div>
<div className="mt-5">
{loading ?
<div>
{/*<p className="m-5">Progress: {currentState ? JSON.parse(currentState).percentage : 0}% - {currentState ? JSON.parse(currentState).message : ''}</p>*/}
{(() => {
const percentage = currentState ? JSON.parse(currentState).percentage : 0;
return (
<div className="radial-progress" style={{['--value' as unknown as string]: percentage}} aria-valuenow={percentage} role="progressbar">
{percentage}%
</div>
);
})()}
</div>
:
!weatherData || weatherData.length === 0 ? (
<p>No weather data, please request it!</p>
) : (
<WeatherGrid data={weatherData}/>
)}
</div>
</div>
</div>
);
}
export const PageWithLayout = with_main_layout(WeatherPage);
export default PageWithLayout;

View File

@@ -0,0 +1,63 @@
import {useCallback, useEffect, useRef, useState} from 'react';
import {HubConnection, HubConnectionBuilder, LogLevel} from '@microsoft/signalr';
export function useSignalR(baseUrl: string, hubName: string) {
const [connection, setConnection] = useState<HubConnection | null>(null);
const connectionRef = useRef<HubConnection | null>(null);
useEffect(() => {
const newConnection: HubConnection = new HubConnectionBuilder()
.withUrl(`${baseUrl}/${hubName}`)
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build();
connectionRef.current = newConnection;
setConnection(newConnection);
newConnection
.start()
.then(() => console.log('SignalR connected.'))
.catch(error => console.error('SignalR connection error:', error));
// Cleanup on unmount: stop the connection.
return () => {
newConnection.stop();
};
}, [baseUrl, hubName]);
/**
* Subscribes to a SignalR event.
* @param eventName The event name.
* @param callback The callback invoked when the event is received.
* @returns A function to unsubscribe the event.
*/
const subscribe = useCallback(
<T>(eventName: string, callback: (...args: T[]) => void): (() => void) => {
if (connectionRef.current) {
connectionRef.current.on(eventName, callback);
return () => connectionRef.current?.off(eventName, callback);
}
return () => {
};
},
[]
);
/**
* Sends a message via the SignalR connection.
* @param methodName The hub method name.
* @param args Arguments for the hub method.
*/
const sendMessage = useCallback(async (methodName: string, ...args: any[]): Promise<void> => {
if (connectionRef.current) {
try {
await connectionRef.current.send(methodName, ...args);
} catch (error) {
console.error('Error sending message:', error);
}
}
}, []);
return {connection, subscribe, sendMessage};
}