diff --git a/Api/Program.cs b/Api/Program.cs index dd420e0..a455e45 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -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>(); + 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 ( diff --git a/Api/SignalR/SignalRSendService.cs b/Api/SignalR/SignalRSendService.cs index 351ab0c..962c874 100644 --- a/Api/SignalR/SignalRSendService.cs +++ b/Api/SignalR/SignalRSendService.cs @@ -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. diff --git a/react-vite-client/src/App.tsx b/react-vite-client/src/App.tsx index 918c988..df5c3e2 100644 --- a/react-vite-client/src/App.tsx +++ b/react-vite-client/src/App.tsx @@ -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 = () => { diff --git a/react-vite-client/src/pages/WeatherPage.tsx b/react-vite-client/src/pages/WeatherPage.tsx deleted file mode 100644 index 8969ba9..0000000 --- a/react-vite-client/src/pages/WeatherPage.tsx +++ /dev/null @@ -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('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 ( -
-
Weather Data
-
-
- - -
-
- {!weatherData ? ( -

No weather data, please request it!

- ) : ( - - - - - - - - - - )} -
-
-
- ); -} - -export const PageWithLayout = with_main_layout(WeatherPage); -export default PageWithLayout; diff --git a/react-vite-client/src/pages/WeatherPage/WeatherGrid.tsx b/react-vite-client/src/pages/WeatherPage/WeatherGrid.tsx new file mode 100644 index 0000000..3a716a1 --- /dev/null +++ b/react-vite-client/src/pages/WeatherPage/WeatherGrid.tsx @@ -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 = ({data: weatherData}) => { + return ( +
+ + + + + + + + + +
+ ); +}; + +export default NavMenu; \ No newline at end of file diff --git a/react-vite-client/src/pages/WeatherPage/WeatherPage.tsx b/react-vite-client/src/pages/WeatherPage/WeatherPage.tsx new file mode 100644 index 0000000..4ec57c1 --- /dev/null +++ b/react-vite-client/src/pages/WeatherPage/WeatherPage.tsx @@ -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('WeatherPage-Forecast', {defaultValue: [],}); + const [loading, setLoading] = useState(false); + const [currentState, setCurrentState] = useState(null); + const {subscribe} = useSignalR('http://localhost:5175', 'WeatherUpdateHub'); + + + const fetchWeatherData = async () => { + const apiClient = new Client(); + try { + const progressCleanup = subscribe('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 ( +
+
Weather Data
+
+
+ + +
+
+ {loading ? +
+ {/*

Progress: {currentState ? JSON.parse(currentState).percentage : 0}% - {currentState ? JSON.parse(currentState).message : ''}

*/} + {(() => { + const percentage = currentState ? JSON.parse(currentState).percentage : 0; + return ( +
+ {percentage}% +
+ ); + })()} +
+ : + !weatherData || weatherData.length === 0 ? ( +

No weather data, please request it!

+ ) : ( + + )} +
+
+
+ ); +} + +export const PageWithLayout = with_main_layout(WeatherPage); +export default PageWithLayout; diff --git a/react-vite-client/src/signalr/SignalRHelper.ts b/react-vite-client/src/signalr/SignalRHelper.ts new file mode 100644 index 0000000..fed7050 --- /dev/null +++ b/react-vite-client/src/signalr/SignalRHelper.ts @@ -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(null); + const connectionRef = useRef(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( + (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 => { + if (connectionRef.current) { + try { + await connectionRef.current.send(methodName, ...args); + } catch (error) { + console.error('Error sending message:', error); + } + } + }, []); + + return {connection, subscribe, sendMessage}; +}