Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions fmo_api_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,18 @@ pub struct GatewayUptimeTrendPoint {
pub total_samples: u64,
pub uptime_pct: f64,
}

/// A federation's gateways and their availability trend for one window,
/// computed at the same moment so both always agree
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayOverview {
/// The window label, e.g. `7d`
pub window: String,
pub computed_at: DateTime<Utc>,
/// Left out when `include` doesn't ask for `gateways`
#[serde(skip_serializing_if = "Option::is_none")]
pub gateways: Option<Vec<GatewayInfo>>,
/// Left out when `include` doesn't ask for `trend`
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_trend: Option<Vec<GatewayUptimeTrendPoint>>,
}
42 changes: 42 additions & 0 deletions fmo_frontend_react/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Component, type ErrorInfo, type ReactNode } from 'react';

interface ErrorBoundaryProps {
fallback: ReactNode;
children: ReactNode;
}

interface ErrorBoundaryState {
failed: boolean;
}

// Without a boundary, React unmounts the whole app on a render error, leaving a blank page
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState = { failed: false };

static getDerivedStateFromError(): ErrorBoundaryState {
return { failed: true };
}

componentDidCatch(error: Error, info: ErrorInfo) {
console.error('Render error:', error, info.componentStack);
}

render() {
return this.state.failed ? this.props.fallback : this.props.children;
}
}

export function ReloadMessage({ message }: { message: string }) {
return (
<div className="flex flex-col items-center justify-center gap-3 p-6 text-center text-sm text-gray-600 dark:text-gray-300">
<p>{message}</p>
<button
type="button"
onClick={() => window.location.reload()}
className="rounded-lg border border-blue-300 bg-blue-50 px-4 py-2 font-semibold text-blue-800 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-950/50 dark:text-blue-200"
>
Reload page
</button>
</div>
);
}
13 changes: 12 additions & 1 deletion fmo_frontend_react/src/components/GatewayWarningPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface GatewayWarningState {
interface GatewayWarningPageProps {
warning: GatewayWarningState;
className?: string;
action?: { label: string; onClick: () => void; busy?: boolean };
}

function levelClasses(level: GatewayWarningLevel): string {
Expand All @@ -23,7 +24,7 @@ function levelClasses(level: GatewayWarningLevel): string {
}
}

export function GatewayWarningPage({ warning, className = '' }: GatewayWarningPageProps) {
export function GatewayWarningPage({ warning, className = '', action }: GatewayWarningPageProps) {
return (
<section
className={`rounded-lg border p-4 sm:p-5 ${levelClasses(warning.level)} ${className}`}
Expand All @@ -37,6 +38,16 @@ export function GatewayWarningPage({ warning, className = '' }: GatewayWarningPa
{warning.detail}
</p>
)}
{action && (
<button
type="button"
onClick={action.onClick}
aria-disabled={action.busy}
className={`mt-3 rounded-md border border-current px-3 py-1.5 text-xs font-semibold ${action.busy ? 'cursor-wait opacity-70' : 'hover:opacity-80'}`}
>
{action.label}
</button>
)}
</section>
);
}
13 changes: 13 additions & 0 deletions fmo_frontend_react/src/components/UptimeTrendChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ReactEChartsCore from 'echarts-for-react/lib/core';
import * as echarts from 'echarts/core';
import { LineChart } from 'echarts/charts';
import { GridComponent, TooltipComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import type { EChartsOption } from 'echarts';

// Register only what the chart uses instead of loading all of echarts
echarts.use([LineChart, GridComponent, TooltipComponent, CanvasRenderer]);

export function UptimeTrendChart({ option }: { option: EChartsOption }) {
return <ReactEChartsCore echarts={echarts} option={option} style={{ height: 210 }} notMerge lazyUpdate />;
}
Loading
Loading