mirror of https://github.com/buster-so/buster.git
update healthcheck styling
This commit is contained in:
parent
6b2139fa5d
commit
25967e4f19
|
@ -5,5 +5,6 @@ export const useHealthcheck = () => {
|
|||
return useQuery({
|
||||
queryKey: ['healthcheck'],
|
||||
queryFn: getHealthcheck,
|
||||
refetchInterval: 1000 * 30, // 30 seconds
|
||||
});
|
||||
};
|
||||
|
|
|
@ -32,7 +32,6 @@ import { Route as EmbedDashboardDashboardIdRouteImport } from './routes/embed/da
|
|||
import { Route as AppSettingsRestricted_layoutRouteImport } from './routes/app/_settings/_restricted_layout'
|
||||
import { Route as AppSettingsPermissionsRouteImport } from './routes/app/_settings/_permissions'
|
||||
import { Route as AppAppHomeRouteImport } from './routes/app/_app/home'
|
||||
import { Route as AppAppHealthcheckRouteImport } from './routes/app/_app/healthcheck'
|
||||
import { Route as AppAppAssetRouteImport } from './routes/app/_app/_asset'
|
||||
import { Route as AppSettingsSettingsIndexRouteImport } from './routes/app/_settings/settings.index'
|
||||
import { Route as AppAppReportsIndexRouteImport } from './routes/app/_app/reports.index'
|
||||
|
@ -263,11 +262,6 @@ const AppAppHomeRoute = AppAppHomeRouteImport.update({
|
|||
path: '/home',
|
||||
getParentRoute: () => AppAppRoute,
|
||||
} as any)
|
||||
const AppAppHealthcheckRoute = AppAppHealthcheckRouteImport.update({
|
||||
id: '/healthcheck',
|
||||
path: '/healthcheck',
|
||||
getParentRoute: () => AppAppRoute,
|
||||
} as any)
|
||||
const AppAppAssetRoute = AppAppAssetRouteImport.update({
|
||||
id: '/_asset',
|
||||
getParentRoute: () => AppAppRoute,
|
||||
|
@ -964,7 +958,6 @@ export interface FileRoutesByFullPath {
|
|||
'/auth/reset-password': typeof AuthResetPasswordRoute
|
||||
'/info/getting-started': typeof InfoGettingStartedRoute
|
||||
'/app/': typeof AppIndexRoute
|
||||
'/app/healthcheck': typeof AppAppHealthcheckRoute
|
||||
'/app/home': typeof AppAppHomeRoute
|
||||
'/embed/dashboard/$dashboardId': typeof EmbedDashboardDashboardIdRoute
|
||||
'/embed/metric/$metricId': typeof EmbedMetricMetricIdRoute
|
||||
|
@ -1074,7 +1067,6 @@ export interface FileRoutesByTo {
|
|||
'/auth/logout': typeof AuthLogoutRoute
|
||||
'/auth/reset-password': typeof AuthResetPasswordRoute
|
||||
'/info/getting-started': typeof InfoGettingStartedRoute
|
||||
'/app/healthcheck': typeof AppAppHealthcheckRoute
|
||||
'/app/home': typeof AppAppHomeRoute
|
||||
'/embed/dashboard/$dashboardId': typeof EmbedDashboardDashboardIdRoute
|
||||
'/embed/metric/$metricId': typeof EmbedMetricMetricIdRoute
|
||||
|
@ -1172,7 +1164,6 @@ export interface FileRoutesById {
|
|||
'/info/getting-started': typeof InfoGettingStartedRoute
|
||||
'/app/': typeof AppIndexRoute
|
||||
'/app/_app/_asset': typeof AppAppAssetRouteWithChildren
|
||||
'/app/_app/healthcheck': typeof AppAppHealthcheckRoute
|
||||
'/app/_app/home': typeof AppAppHomeRoute
|
||||
'/app/_settings/_permissions': typeof AppSettingsPermissionsRouteWithChildren
|
||||
'/app/_settings/_restricted_layout': typeof AppSettingsRestricted_layoutRouteWithChildren
|
||||
|
@ -1298,7 +1289,6 @@ export interface FileRouteTypes {
|
|||
| '/auth/reset-password'
|
||||
| '/info/getting-started'
|
||||
| '/app/'
|
||||
| '/app/healthcheck'
|
||||
| '/app/home'
|
||||
| '/embed/dashboard/$dashboardId'
|
||||
| '/embed/metric/$metricId'
|
||||
|
@ -1408,7 +1398,6 @@ export interface FileRouteTypes {
|
|||
| '/auth/logout'
|
||||
| '/auth/reset-password'
|
||||
| '/info/getting-started'
|
||||
| '/app/healthcheck'
|
||||
| '/app/home'
|
||||
| '/embed/dashboard/$dashboardId'
|
||||
| '/embed/metric/$metricId'
|
||||
|
@ -1505,7 +1494,6 @@ export interface FileRouteTypes {
|
|||
| '/info/getting-started'
|
||||
| '/app/'
|
||||
| '/app/_app/_asset'
|
||||
| '/app/_app/healthcheck'
|
||||
| '/app/_app/home'
|
||||
| '/app/_settings/_permissions'
|
||||
| '/app/_settings/_restricted_layout'
|
||||
|
@ -1793,13 +1781,6 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AppAppHomeRouteImport
|
||||
parentRoute: typeof AppAppRoute
|
||||
}
|
||||
'/app/_app/healthcheck': {
|
||||
id: '/app/_app/healthcheck'
|
||||
path: '/healthcheck'
|
||||
fullPath: '/app/healthcheck'
|
||||
preLoaderRoute: typeof AppAppHealthcheckRouteImport
|
||||
parentRoute: typeof AppAppRoute
|
||||
}
|
||||
'/app/_app/_asset': {
|
||||
id: '/app/_app/_asset'
|
||||
path: ''
|
||||
|
@ -3017,7 +2998,6 @@ const AppAppDatasetsDatasetIdRouteWithChildren =
|
|||
|
||||
interface AppAppRouteChildren {
|
||||
AppAppAssetRoute: typeof AppAppAssetRouteWithChildren
|
||||
AppAppHealthcheckRoute: typeof AppAppHealthcheckRoute
|
||||
AppAppHomeRoute: typeof AppAppHomeRoute
|
||||
AppAppDatasetsDatasetIdRoute: typeof AppAppDatasetsDatasetIdRouteWithChildren
|
||||
AppAppChatsIndexRoute: typeof AppAppChatsIndexRoute
|
||||
|
@ -3031,7 +3011,6 @@ interface AppAppRouteChildren {
|
|||
|
||||
const AppAppRouteChildren: AppAppRouteChildren = {
|
||||
AppAppAssetRoute: AppAppAssetRouteWithChildren,
|
||||
AppAppHealthcheckRoute: AppAppHealthcheckRoute,
|
||||
AppAppHomeRoute: AppAppHomeRoute,
|
||||
AppAppDatasetsDatasetIdRoute: AppAppDatasetsDatasetIdRouteWithChildren,
|
||||
AppAppChatsIndexRoute: AppAppChatsIndexRoute,
|
||||
|
|
|
@ -1,375 +0,0 @@
|
|||
import type { HealthCheckResponse } from '@buster/server-shared/healthcheck';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useHealthcheck } from '@/api/buster_rest/healthcheck/queryRequests';
|
||||
import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo';
|
||||
import type { RustApiError } from '@/api/errors';
|
||||
import { useGetSupabaseUser } from '@/context/Supabase';
|
||||
|
||||
export const Route = createFileRoute('/app/_app/healthcheck')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { data, isLoading, error } = useHealthcheck();
|
||||
const supabaseUser = useGetSupabaseUser();
|
||||
const user = useGetUserBasicInfo();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorState error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <ErrorState error={new Error('No data received')} />;
|
||||
}
|
||||
|
||||
return <HealthcheckDashboard data={data} supabaseUser={supabaseUser} user={user} />;
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Checking system health...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorState({ error }: { error: Error | RustApiError }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-white flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full mx-4">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Health Check Failed</h2>
|
||||
<p className="text-gray-600">Unable to retrieve system status</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-3">
|
||||
<p className="text-sm text-red-800">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HealthcheckDashboard({
|
||||
data,
|
||||
supabaseUser,
|
||||
user,
|
||||
}: {
|
||||
data: HealthCheckResponse;
|
||||
supabaseUser: ReturnType<typeof useGetSupabaseUser>;
|
||||
user: ReturnType<typeof useGetUserBasicInfo>;
|
||||
}) {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
case 'pass':
|
||||
return 'text-green-600 bg-green-100';
|
||||
case 'degraded':
|
||||
case 'warn':
|
||||
return 'text-yellow-600 bg-yellow-100';
|
||||
case 'unhealthy':
|
||||
case 'fail':
|
||||
return 'text-red-600 bg-red-100';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
case 'pass':
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
);
|
||||
case 'degraded':
|
||||
case 'warn':
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'unhealthy':
|
||||
case 'fail':
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white p-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">System Health Dashboard</h1>
|
||||
<p className="text-gray-600">Real-time monitoring of system components</p>
|
||||
</div>
|
||||
|
||||
{/* User Data Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{/* Supabase User Data */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-4 h-4 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
Supabase User
|
||||
</h3>
|
||||
<div className="bg-gray-50 rounded-md p-4">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{JSON.stringify(supabaseUser, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Basic Info */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-4 h-4 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
User Basic Info
|
||||
</h3>
|
||||
<div className="bg-gray-50 rounded-md p-4">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{JSON.stringify(user, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overall Status Card */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center mr-4 ${getStatusColor(
|
||||
data.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(data.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 capitalize">{data.status}</h2>
|
||||
<p className="text-gray-600">Overall System Status</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">Last checked</p>
|
||||
<p className="text-gray-900 font-medium">{formatTimestamp(data.timestamp)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Uptime</p>
|
||||
<p className="text-xl font-semibold text-gray-900">{formatUptime(data.uptime)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 011 1v1a1 1 0 01-1 1h-1v12a2 2 0 01-2 2H6a2 2 0 01-2-2V7H3a1 1 0 01-1-1V5a1 1 0 011-1h4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Version</p>
|
||||
<p className="text-xl font-semibold text-gray-900">{data.version}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Environment</p>
|
||||
<p className="text-xl font-semibold text-gray-900 capitalize">{data.environment}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Component Checks */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-6">Component Health Checks</h3>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(data.checks).map(([componentName, check]) => (
|
||||
<div key={componentName} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mr-3 ${getStatusColor(
|
||||
check.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(check.status)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 capitalize">{componentName}</h4>
|
||||
{check.message && <p className="text-sm text-gray-600">{check.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${getStatusColor(
|
||||
check.status
|
||||
)}`}
|
||||
>
|
||||
{check.status}
|
||||
</span>
|
||||
{check.responseTime && (
|
||||
<p className="text-sm text-gray-500 mt-1">{check.responseTime}ms</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center text-gray-500 text-sm">
|
||||
<p>System health is monitored continuously. Data refreshes automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,14 +1,41 @@
|
|||
import type { HealthCheckResponse } from '@buster/server-shared/healthcheck';
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useHealthcheck } from '@/api/buster_rest/healthcheck/queryRequests';
|
||||
import type { RustApiError } from '../api/errors';
|
||||
import { prefetchGetMyUserInfo } from '@/api/buster_rest/users';
|
||||
import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo';
|
||||
import type { RustApiError } from '@/api/errors';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card/CardBase';
|
||||
import { Text } from '@/components/ui/typography/Text';
|
||||
import { useAppVersionMeta } from '@/context/AppVersion/useAppVersion';
|
||||
import { getSupabaseUser } from '@/integrations/supabase/getSupabaseUserClient';
|
||||
import { formatDate } from '@/lib/date';
|
||||
|
||||
export const Route = createFileRoute('/healthcheck')({
|
||||
component: RouteComponent,
|
||||
beforeLoad: async () => {
|
||||
const supabaseUser = await getSupabaseUser();
|
||||
return { supabaseUser };
|
||||
},
|
||||
loader: async ({ context }) => {
|
||||
await prefetchGetMyUserInfo(context.queryClient);
|
||||
const { supabaseUser } = context;
|
||||
return { supabaseUser };
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { data, isLoading, error } = useHealthcheck();
|
||||
const supabaseUser = Route.useLoaderData().supabaseUser;
|
||||
const user = useGetUserBasicInfo();
|
||||
const appVersionData = useAppVersionMeta();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingState />;
|
||||
|
@ -22,65 +49,92 @@ function RouteComponent() {
|
|||
return <ErrorState error={new Error('No data received')} />;
|
||||
}
|
||||
|
||||
return <HealthcheckDashboard data={data} />;
|
||||
return (
|
||||
<HealthcheckDashboard
|
||||
data={data}
|
||||
supabaseUser={supabaseUser}
|
||||
user={user}
|
||||
appVersionData={appVersionData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingState() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||
<p className="text-gray-600">Checking system health...</p>
|
||||
</div>
|
||||
<Card className="text-center max-w-md mx-4">
|
||||
<CardContent className="pt-6">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-400 mx-auto mb-4"></div>
|
||||
<Text variant="secondary" size="base">
|
||||
Checking system health...
|
||||
</Text>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorState({ error }: { error: Error | RustApiError }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-red-50 to-white flex items-center justify-center">
|
||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full mx-4">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-red-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center">
|
||||
<Card className="max-w-md w-full mx-4">
|
||||
<CardHeader>
|
||||
<div className="flex items-center">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle>Health Check Failed</CardTitle>
|
||||
<CardDescription>Unable to retrieve system status</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Health Check Failed</h2>
|
||||
<p className="text-gray-600">Unable to retrieve system status</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-md p-3">
|
||||
<Text variant="secondary" size="sm">
|
||||
{error.message}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-3">
|
||||
<p className="text-sm text-red-800">{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HealthcheckDashboard({ data }: { data: HealthCheckResponse }) {
|
||||
function HealthcheckDashboard({
|
||||
data,
|
||||
supabaseUser,
|
||||
user,
|
||||
appVersionData,
|
||||
}: {
|
||||
data: HealthCheckResponse;
|
||||
supabaseUser: Pick<User, 'email' | 'is_anonymous'>;
|
||||
user: ReturnType<typeof useGetUserBasicInfo>;
|
||||
appVersionData: ReturnType<typeof useAppVersionMeta>;
|
||||
}) {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
case 'pass':
|
||||
return 'text-green-600 bg-green-100';
|
||||
return 'text-gray-700 bg-gray-200';
|
||||
case 'degraded':
|
||||
case 'warn':
|
||||
return 'text-yellow-600 bg-yellow-100';
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
case 'unhealthy':
|
||||
case 'fail':
|
||||
return 'text-red-600 bg-red-100';
|
||||
return 'text-gray-800 bg-gray-300';
|
||||
default:
|
||||
return 'text-gray-600 bg-gray-100';
|
||||
}
|
||||
|
@ -148,157 +202,302 @@ function HealthcheckDashboard({ data }: { data: HealthCheckResponse }) {
|
|||
};
|
||||
|
||||
const formatTimestamp = (timestamp: string) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
return formatDate({ date: timestamp, format: 'LLL' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white p-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white p-6 flex flex-col gap-4">
|
||||
<div className="max-w-6xl mx-auto flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">System Health Dashboard</h1>
|
||||
<p className="text-gray-600">Real-time monitoring of system components</p>
|
||||
<div className="mb-4 flex flex-col gap-1">
|
||||
<Text size="4xl" className="font-bold">
|
||||
System Health Dashboard
|
||||
</Text>
|
||||
<Text variant="secondary" size="lg">
|
||||
Real-time monitoring of system components
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* User Data Section */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Supabase User Data */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<div className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
Supabase User
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-gray-50 rounded-md p-4">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{JSON.stringify(supabaseUser, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* User Basic Info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<div className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
User Basic Info
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-gray-50 rounded-md p-4">
|
||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
|
||||
{JSON.stringify(user, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Overall Status Card */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center mr-4 ${getStatusColor(
|
||||
data.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(data.status)}
|
||||
<Card>
|
||||
<CardContent size="default">
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-16 h-16 rounded-full flex items-center justify-center mr-6 ${getStatusColor(
|
||||
data.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(data.status)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Text size="3xl" className="font-semibold capitalize">
|
||||
{data.status}
|
||||
</Text>
|
||||
<Text variant="secondary" size="lg">
|
||||
Overall System Status
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 capitalize">{data.status}</h2>
|
||||
<p className="text-gray-600">Overall System Status</p>
|
||||
<div className="text-right flex flex-col gap-0">
|
||||
<Text variant="tertiary" size="sm" className="mb-0">
|
||||
Last checked
|
||||
</Text>
|
||||
<Text className="font-medium" size="lg">
|
||||
{formatTimestamp(data.timestamp)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-500">Last checked</p>
|
||||
<p className="text-gray-900 font-medium">{formatTimestamp(data.timestamp)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-blue-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent size="default">
|
||||
<div className="flex items-center py-2">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<Text variant="tertiary" size="sm" className="">
|
||||
Uptime
|
||||
</Text>
|
||||
<Text size="xl" className="font-semibold">
|
||||
{formatUptime(data.uptime)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Uptime</p>
|
||||
<p className="text-xl font-semibold text-gray-900">{formatUptime(data.uptime)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-purple-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 011 1v1a1 1 0 01-1 1h-1v12a2 2 0 01-2 2H6a2 2 0 01-2-2V7H3a1 1 0 01-1-1V5a1 1 0 011-1h4z"
|
||||
/>
|
||||
</svg>
|
||||
<Card>
|
||||
<CardContent size="default">
|
||||
<div className="flex items-center py-2">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 011 1v1a1 1 0 01-1 1h-1v12a2 2 0 01-2 2H6a2 2 0 01-2-2V7H3a1 1 0 01-1-1V5a1 1 0 011-1h4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<Text variant="tertiary" size="sm" className="">
|
||||
Server Version
|
||||
</Text>
|
||||
<Text size="xl" className="font-semibold">
|
||||
{data.version}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Version</p>
|
||||
<p className="text-xl font-semibold text-gray-900">{data.version}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mr-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"
|
||||
/>
|
||||
</svg>
|
||||
<Card>
|
||||
<CardContent size="default">
|
||||
<div className="flex items-center py-2">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<Text variant="tertiary" size="sm" className="">
|
||||
Environment
|
||||
</Text>
|
||||
<Text size="xl" className="font-semibold capitalize">
|
||||
{data.environment}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">Environment</p>
|
||||
<p className="text-xl font-semibold text-gray-900 capitalize">{data.environment}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent size="default">
|
||||
<div className="flex items-center py-2">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-1">
|
||||
<Text variant="tertiary" size="sm" className="">
|
||||
App Builds
|
||||
</Text>
|
||||
<Text size="sm" className="font-mono">
|
||||
Browser: {appVersionData.browserBuild || 'N/A'}
|
||||
</Text>
|
||||
|
||||
<Text variant="tertiary" size="xs" className="">
|
||||
Server: {appVersionData.buildId}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Component Checks */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 className="text-xl font-semibold text-gray-900 mb-6">Component Health Checks</h3>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(data.checks).map(([componentName, check]) => (
|
||||
<div key={componentName} className="border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center mr-3 ${getStatusColor(
|
||||
check.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(check.status)}
|
||||
<Card>
|
||||
<CardHeader size="default">
|
||||
<CardTitle>Component Health Checks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent size="default">
|
||||
<div className="space-y-3">
|
||||
{Object.entries(data.checks).map(([componentName, check]) => (
|
||||
<div key={componentName} className="border border-gray-200 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center mr-4 ${getStatusColor(
|
||||
check.status
|
||||
)}`}
|
||||
>
|
||||
{getStatusIcon(check.status)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0">
|
||||
<Text className="font-medium capitalize" size="lg">
|
||||
{componentName}
|
||||
</Text>
|
||||
{check.message && (
|
||||
<Text variant="secondary" size="sm" className="">
|
||||
{check.message}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 capitalize">{componentName}</h4>
|
||||
{check.message && <p className="text-sm text-gray-600">{check.message}</p>}
|
||||
<div className="text-right">
|
||||
<span
|
||||
className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium capitalize ${getStatusColor(
|
||||
check.status
|
||||
)}`}
|
||||
>
|
||||
{check.status}
|
||||
</span>
|
||||
{check.responseTime && (
|
||||
<Text variant="tertiary" size="sm" className="mt-2 ml-3">
|
||||
{check.responseTime}ms
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span
|
||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${getStatusColor(
|
||||
check.status
|
||||
)}`}
|
||||
>
|
||||
{check.status}
|
||||
</span>
|
||||
{check.responseTime && (
|
||||
<p className="text-sm text-gray-500 mt-1">{check.responseTime}ms</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="mt-8 text-center text-gray-500 text-sm">
|
||||
<p>System health is monitored continuously. Data refreshes automatically.</p>
|
||||
<div className="mt-12 text-center">
|
||||
<Text variant="tertiary" size="sm">
|
||||
System health is monitored continuously. Data refreshes automatically.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue