Move alerts and progress into context

This commit is contained in:
Corey 2024-05-07 07:41:06 +00:00
parent ddf74e8343
commit 5f215050b7
12 changed files with 129 additions and 89 deletions

View File

@ -12,10 +12,6 @@ import NotFound from './pages/NotFound';
import ServerDetails from './pages/ServerDetails'; import ServerDetails from './pages/ServerDetails';
export function App() { export function App() {
const [alertInfo, setAlertInfo] = useState<{
message: string;
severity: 'error' | 'warning' | 'info' | 'success';
} | null>(null);
const [servers, setServers] = useState<ServerData[]>([]); const [servers, setServers] = useState<ServerData[]>([]);
const [searchState, setSearchState] = const [searchState, setSearchState] =
@ -24,13 +20,12 @@ export function App() {
return ( return (
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
<Header <Header
setAlertInfo={setAlertInfo}
servers={servers} servers={servers}
setServers={setServers} setServers={setServers}
searchState={searchState} searchState={searchState}
setSearchState={setSearchState} setSearchState={setSearchState}
/> />
<AlertComponent alertInfo={alertInfo} setAlertInfo={setAlertInfo} /> <AlertComponent />
<Container className="grow p-0"> <Container className="grow p-0">
<Box className="m-2"> <Box className="m-2">
<Routes> <Routes>
@ -38,10 +33,7 @@ export function App() {
path="/" path="/"
element={<Home servers={servers} searchState={searchState} />} element={<Home servers={servers} searchState={searchState} />}
/> />
<Route <Route path="/server/:url" element={<ServerDetails />} />
path="/server/:url"
element={<ServerDetails setAlertInfo={setAlertInfo} />}
/>
<Route path="/about" element={<About />} /> <Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>

View File

@ -12,8 +12,8 @@ import {
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { postData } from '../apiUtil'; import { postData } from '../apiUtil';
import { useLoadingContext } from '../context/LoadingContext';
import { ServerData } from '../data/ServerData'; import { ServerData } from '../data/ServerData';
import { AlertResponse } from './Alert';
const AddServerInput = styled('div')(({ theme }) => ({ const AddServerInput = styled('div')(({ theme }) => ({
position: 'relative', position: 'relative',
@ -43,15 +43,13 @@ const StyledInputBase = styled(InputBase)(({ theme }) => ({
}, },
})); }));
export default function AddServer({ type AddServerProps = {
setAlertInfo,
servers,
setServers,
}: {
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
servers: ServerData[]; servers: ServerData[];
setServers: React.Dispatch<React.SetStateAction<ServerData[]>>; setServers: React.Dispatch<React.SetStateAction<ServerData[]>>;
}) { };
export default function AddServer({ servers, setServers }: AddServerProps) {
const { showAlert } = useLoadingContext();
const navigate = useNavigate(); const navigate = useNavigate();
const containerRef = useRef<HTMLElement>(null); const containerRef = useRef<HTMLElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -68,7 +66,7 @@ export default function AddServer({
setIsLoading(true); setIsLoading(true);
const response = await postData(inputText); const response = await postData(inputText);
if (response.success) { if (response.success) {
setAlertInfo({ showAlert({
message: response.message, message: response.message,
severity: 'success', severity: 'success',
}); });
@ -81,7 +79,7 @@ export default function AddServer({
toggleShowAddServer(); toggleShowAddServer();
} }
} else { } else {
setAlertInfo({ showAlert({
message: response.message || 'Failed to send data to backend.', message: response.message || 'Failed to send data to backend.',
severity: 'error', severity: 'error',
}); });

View File

@ -1,29 +1,21 @@
import { Alert, Box, Collapse } from '@mui/material'; import { Alert, Box, Collapse } from '@mui/material';
import { useLoadingContext } from '../context/LoadingContext';
export type AlertResponse = { export default function AlertComponent() {
message: string; const { alert, showAlert } = useLoadingContext();
severity: 'error' | 'warning' | 'info' | 'success';
} | null;
export default function AlertComponent({
alertInfo,
setAlertInfo,
}: {
alertInfo: AlertResponse;
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
}) {
return ( return (
<Box className="m-0"> <Box className="m-0">
{alertInfo && ( {alert && (
<Collapse in> <Collapse in>
<Alert <Alert
severity={alertInfo.severity} severity={alert.severity}
onClose={() => setAlertInfo(null)} onClose={() => showAlert(null)}
variant="filled" variant="filled"
className="py-0" className="py-0"
square square
> >
{alertInfo.message} {alert.message}
</Alert> </Alert>
</Collapse> </Collapse>
)} )}

View File

@ -1,6 +1,8 @@
import { Card, CardContent, Typography } from '@mui/material'; import { Card, CardContent, Typography } from '@mui/material';
export default function ErrorCard({ error }: { error: string }) { type ErrorCardProps = { error: string };
export default function ErrorCard({ error }: ErrorCardProps) {
return ( return (
<Card className="mb-2"> <Card className="mb-2">
<CardContent className="p-4"> <CardContent className="p-4">

View File

@ -11,26 +11,26 @@ import IconButton from '@mui/material/IconButton';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { fetchDataFromBackend } from '../apiUtil'; import { fetchDataFromBackend } from '../apiUtil';
import { useLoadingContext } from '../context/LoadingContext';
import { SearchState } from '../data/SearchState'; import { SearchState } from '../data/SearchState';
import { ServerData } from '../data/ServerData'; import { ServerData } from '../data/ServerData';
import AddServer from './AddServer'; import AddServer from './AddServer';
import { AlertResponse } from './Alert';
import SearchServers from './SearchServers'; import SearchServers from './SearchServers';
export default function Header({ type HeaderProps = {
setAlertInfo,
servers,
setServers,
searchState,
setSearchState,
}: {
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
servers: ServerData[]; servers: ServerData[];
setServers: React.Dispatch<React.SetStateAction<ServerData[]>>; setServers: React.Dispatch<React.SetStateAction<ServerData[]>>;
searchState: SearchState; searchState: SearchState;
setSearchState: React.Dispatch<React.SetStateAction<SearchState>>; setSearchState: React.Dispatch<React.SetStateAction<SearchState>>;
}) { };
const [fetchLoading, setFetchLoading] = useState(0);
export default function Header({
servers,
setServers,
searchState,
setSearchState,
}: HeaderProps) {
const { progress, setProgress, showAlert } = useLoadingContext();
const [showSearchServer, setShowSearchServer] = useState(false); const [showSearchServer, setShowSearchServer] = useState(false);
const toggleShowSearchServer = () => { const toggleShowSearchServer = () => {
setShowSearchServer((prev) => !prev); setShowSearchServer((prev) => !prev);
@ -39,27 +39,27 @@ export default function Header({
const fetchServerData = useCallback(async () => { const fetchServerData = useCallback(async () => {
let data: ServerData[] = []; let data: ServerData[] = [];
try { try {
setFetchLoading(25); setProgress(25);
data = await fetchDataFromBackend(); data = await fetchDataFromBackend();
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
setAlertInfo({ showAlert({
message: err.message, message: err.message,
severity: 'error', severity: 'error',
}); });
} else { } else {
setAlertInfo({ showAlert({
message: 'An unknown error occurred.', message: 'An unknown error occurred.',
severity: 'error', severity: 'error',
}); });
} }
} }
setServers(data); setServers(data);
setFetchLoading(100); setProgress(100);
setTimeout(() => { setTimeout(() => {
setFetchLoading(0); setProgress(0);
}, 500); }, 500);
}, [setFetchLoading, setAlertInfo, setServers]); }, [showAlert, setServers, setProgress]);
useEffect(() => { useEffect(() => {
fetchServerData(); fetchServerData();
@ -81,13 +81,9 @@ export default function Header({
> >
IXION IXION
</Typography> </Typography>
<AddServer <AddServer servers={servers} setServers={setServers} />
setAlertInfo={setAlertInfo}
servers={servers}
setServers={setServers}
/>
<Tooltip arrow disableInteractive title="Refresh server data."> <Tooltip arrow disableInteractive title="Refresh server data.">
<IconButton onClick={fetchServerData} disabled={fetchLoading !== 0}> <IconButton onClick={fetchServerData} disabled={progress !== 0}>
<Refresh /> <Refresh />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@ -100,10 +96,10 @@ export default function Header({
</AppBar> </AppBar>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={fetchLoading} value={progress}
sx={{ sx={{
visibility: fetchLoading === 0 ? 'hidden' : 'visible', visibility: progress === 0 ? 'hidden' : 'visible',
height: fetchLoading === 0 ? 0 : '1px', height: progress === 0 ? 0 : '1px',
}} }}
/> />
<SearchServers <SearchServers

View File

@ -12,15 +12,17 @@ import {
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { SearchState } from '../data/SearchState'; import { SearchState } from '../data/SearchState';
type SearchServersProps = {
showSearchServer: boolean;
searchState: SearchState;
setSearchState: React.Dispatch<React.SetStateAction<SearchState>>;
};
export default function SearchServers({ export default function SearchServers({
showSearchServer, showSearchServer,
searchState, searchState,
setSearchState, setSearchState,
}: { }: SearchServersProps) {
showSearchServer: boolean;
searchState: SearchState;
setSearchState: React.Dispatch<React.SetStateAction<SearchState>>;
}) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [contentHeight, setContentHeight] = useState(0); const [contentHeight, setContentHeight] = useState(0);
const minDistance = 0; const minDistance = 0;

View File

@ -49,7 +49,9 @@ const expansions = [
</ToggleButton>, </ToggleButton>,
]; ];
export default function ExpansionBar({ server }: { server: ServerData }) { type ExpansionBarProps = { server: ServerData };
export default function ExpansionBar({ server }: ExpansionBarProps) {
return ( return (
<ToggleButtonGroup <ToggleButtonGroup
size="small" size="small"

View File

@ -6,7 +6,9 @@ import {
ServerSettingsInfo, ServerSettingsInfo,
} from '../../data/ServerData'; } from '../../data/ServerData';
export default function SettingsChipCloud({ server }: { server: ServerData }) { type SettingsChipCloudProps = { server: ServerData };
export default function SettingsChipCloud({ server }: SettingsChipCloudProps) {
const renderSettingsChip = ([key, value]: [ const renderSettingsChip = ([key, value]: [
string, string,
boolean | string | number, boolean | string | number,

View File

@ -0,0 +1,54 @@
import {
ReactNode,
createContext,
useCallback,
useContext,
useState,
} from 'react';
export type AlertInfo = {
message: string;
severity: 'error' | 'warning' | 'info' | 'success';
} | null;
type LoadingState = {
alert: AlertInfo;
showAlert: (alertInfo: AlertInfo) => void;
progress: number;
setProgress: (progress: number) => void;
};
const LoadingContext = createContext<LoadingState | null>(null);
// eslint-disable-next-line react-refresh/only-export-components
export function useLoadingContext() {
const context = useContext(LoadingContext);
if (!context) {
throw new Error('Loading context error.');
}
return context;
}
type LoadingContextProviderProps = {
children: ReactNode;
};
export function LoadingContextProvider({
children,
}: LoadingContextProviderProps) {
const [alert, setAlert] = useState<AlertInfo>(null);
const [progress, setProgress] = useState(0);
const showAlert = useCallback((alertInfo: AlertInfo) => {
setAlert(alertInfo);
}, []);
return (
<LoadingContext.Provider
// eslint-disable-next-line react/jsx-no-constructed-context-values
value={{ alert, showAlert, progress, setProgress }}
>
{children}
</LoadingContext.Provider>
);
}

View File

@ -16,7 +16,16 @@ type ThemeModeState = {
setThemeMode: React.Dispatch<React.SetStateAction<ThemeMode>>; setThemeMode: React.Dispatch<React.SetStateAction<ThemeMode>>;
}; };
export const ThemeModeContext = createContext<ThemeModeState | null>(null); const ThemeModeContext = createContext<ThemeModeState | null>(null);
// eslint-disable-next-line react-refresh/only-export-components
export function useThemeModeContext() {
const context = useContext(ThemeModeContext);
if (!context) {
throw new Error('Theme context error.');
}
return context;
}
type ThemeModeContextProviderProps = { type ThemeModeContextProviderProps = {
children: ReactNode; children: ReactNode;
@ -82,12 +91,3 @@ export function ThemeContextProvider({
</StyledEngineProvider> </StyledEngineProvider>
); );
} }
// eslint-disable-next-line react-refresh/only-export-components
export function useThemeModeContext() {
const context = useContext(ThemeModeContext);
if (!context) {
throw new Error('Theme context error.');
}
return context;
}

View File

@ -3,6 +3,7 @@ import { StyledEngineProvider } from '@mui/material/styles';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { WrappedApp } from './App'; import { WrappedApp } from './App';
import { LoadingContextProvider } from './context/LoadingContext';
import { ThemeContextProvider } from './context/ThemeContext'; import { ThemeContextProvider } from './context/ThemeContext';
import './index.css'; import './index.css';
@ -13,8 +14,10 @@ root.render(
<StrictMode> <StrictMode>
<StyledEngineProvider injectFirst> <StyledEngineProvider injectFirst>
<ThemeContextProvider> <ThemeContextProvider>
<CssBaseline /> <LoadingContextProvider>
<WrappedApp /> <CssBaseline />
<WrappedApp />
</LoadingContextProvider>
</ThemeContextProvider> </ThemeContextProvider>
</StyledEngineProvider> </StyledEngineProvider>
</StrictMode> </StrictMode>

View File

@ -2,17 +2,14 @@ import { Box } from '@mui/material';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { fetchDataFromBackend, fetchDemo } from '../apiUtil'; import { fetchDataFromBackend, fetchDemo } from '../apiUtil';
import { AlertResponse } from '../components/Alert';
import ErrorCard from '../components/ErrorCard'; import ErrorCard from '../components/ErrorCard';
import SettingsDataGrid from '../components/Server/SettingsDataGrid'; import SettingsDataGrid from '../components/Server/SettingsDataGrid';
import ServerCard from '../components/ServerCard'; import ServerCard from '../components/ServerCard';
import { useLoadingContext } from '../context/LoadingContext';
import { ServerData } from '../data/ServerData'; import { ServerData } from '../data/ServerData';
export default function ServerDetails({ export default function ServerDetails() {
setAlertInfo, const { showAlert } = useLoadingContext();
}: {
setAlertInfo: React.Dispatch<React.SetStateAction<AlertResponse>>;
}) {
const { url } = useParams(); const { url } = useParams();
const [server, setServer] = useState<ServerData>(); const [server, setServer] = useState<ServerData>();
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
@ -28,7 +25,7 @@ export default function ServerDetails({
} }
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
setAlertInfo({ showAlert({
message: err.message, message: err.message,
severity: 'error', severity: 'error',
}); });
@ -41,7 +38,7 @@ export default function ServerDetails({
}; };
fetchServerData(); fetchServerData();
}, [url, setServer, setAlertInfo, setError]); }, [url, setServer, showAlert, setError]);
return ( return (
<Box> <Box>