pull/143/head^2
lyc8503 2025-10-15 01:49:43 +08:00
commit d11f8deeb2
6 changed files with 87 additions and 16 deletions

View File

@ -2,13 +2,16 @@ import { Alert, List, Text, useMantineTheme } from '@mantine/core'
import { useMediaQuery } from '@mantine/hooks'
import { IconAlertTriangle } from '@tabler/icons-react'
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
import { pageConfig } from '@/uptime.config'
export default function MaintenanceAlert({
maintenance,
style,
upcoming = false,
}: {
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
style?: React.CSSProperties
upcoming?: boolean
}) {
const theme = useMantineTheme()
const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`)
@ -23,12 +26,14 @@ export default function MaintenanceAlert({
fontWeight: 700,
}}
>
{maintenance.title || 'Scheduled Maintenance'}
{(upcoming ? '[Upcoming] ' : '') + (maintenance.title || 'Scheduled Maintenance')}
</span>
}
color={maintenance.color || 'yellow'}
color={
upcoming ? pageConfig.maintenances?.upcomingColor ?? 'gray' : maintenance.color || 'yellow'
}
withCloseButton={false}
style={{ position: 'relative', margin: '16px auto 0 auto', ...style }}
style={{ margin: '16px auto 0 auto', ...style }}
>
{/* Date range in top right (desktop) or inline (mobile) */}
<div
@ -45,16 +50,30 @@ export default function MaintenanceAlert({
padding: '2px 8px',
textAlign: 'right',
}
: {}),
: { marginBottom: 4 }),
}}
>
<b>From:</b> {new Date(maintenance.start).toLocaleString()}
<br />
<b>To:</b>{' '}
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
<div
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr',
gridColumnGap: '3px',
}}
>
<div style={{ textAlign: 'right', fontWeight: 'bold' }}>
{upcoming ? 'Scheduled for:' : 'From:'}
</div>
<div>{new Date(maintenance.start).toLocaleString()}</div>
<div style={{ textAlign: 'right', fontWeight: 'bold' }}>
{upcoming ? 'Expected end:' : 'To:'}
</div>
<div>
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
</div>
</div>
</div>
<Text style={{ paddingTop: '3px' }}>{maintenance.body}</Text>
<Text style={{ paddingTop: '3px', whiteSpace: 'pre-line' }}>{maintenance.body}</Text>
{maintenance.monitors && maintenance.monitors.length > 0 && (
<>
<Text mt="xs">

View File

@ -1,6 +1,6 @@
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
import { Center, Container, Title } from '@mantine/core'
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
import { Center, Container, Title, Collapse, Button, Box } from '@mantine/core'
import { IconCircleCheck, IconAlertCircle, IconPlus, IconMinus } from '@tabler/icons-react'
import { useEffect, useState } from 'react'
import MaintenanceAlert from './MaintenanceAlert'
import { pageConfig } from '@/uptime.config'
@ -45,6 +45,7 @@ export default function OverallStatus({
const [openTime] = useState(Math.round(Date.now() / 1000))
const [currentTime, setCurrentTime] = useState(Math.round(Date.now() / 1000))
const isWindowVisible = useWindowVisibility()
const [expandUpcoming, setExpandUpcoming] = useState(false)
useEffect(() => {
const interval = setInterval(() => {
@ -58,7 +59,8 @@ export default function OverallStatus({
})
const now = new Date()
let filteredMaintenances: (Omit<MaintenanceConfig, 'monitors'> & {
const activeMaintenances: (Omit<MaintenanceConfig, 'monitors'> & {
monitors?: MonitorTarget[]
})[] = maintenances
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
@ -69,6 +71,17 @@ export default function OverallStatus({
),
}))
const upcomingMaintenances: (Omit<MaintenanceConfig, 'monitors'> & {
monitors?: MonitorTarget[]
})[] = maintenances
.filter((m) => now < new Date(m.start))
.map((maintenance) => ({
...maintenance,
monitors: maintenance.monitors?.map(
(monitorId) => monitors.find((mon) => monitorId === mon.id)!
),
}))
return (
<Container size="md" mt="xl">
<Center>{icon}</Center>
@ -82,9 +95,36 @@ export default function OverallStatus({
} sec ago)`}
</Title>
{filteredMaintenances.map((maintenance, idx) => (
{/* Upcoming Maintenance */}
{upcomingMaintenances.length > 0 && (
<>
<Title mt="4px" style={{ textAlign: 'center', color: '#70778c' }} order={5}>
{`${upcomingMaintenances.length} upcoming maintenances`}{' '}
<span
style={{ textDecoration: 'underline' }}
onClick={() => setExpandUpcoming(!expandUpcoming)}
>
{expandUpcoming ? '[HIDE]' : '[SHOW]'}
</span>
</Title>
<Collapse in={expandUpcoming}>
{upcomingMaintenances.map((maintenance, idx) => (
<MaintenanceAlert
key={`upcoming-${idx}`}
maintenance={maintenance}
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
upcoming
/>
))}
</Collapse>
</>
)}
{/* Active Maintenance */}
{activeMaintenances.map((maintenance, idx) => (
<MaintenanceAlert
key={idx}
key={`active-${idx}`}
maintenance={maintenance}
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
/>

View File

@ -81,7 +81,7 @@ export default function IncidentsPage() {
<>
<Head>
<title>{pageConfig.title}</title>
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href={pageConfig.favicon ?? '/favicon.ico'} />
</Head>
<main className={inter.className}>

View File

@ -46,7 +46,7 @@ export default function Home({
<>
<Head>
<title>{pageConfig.title}</title>
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href={pageConfig.favicon ?? '/favicon.ico'} />
</Head>
<main className={inter.className}>

View File

@ -4,6 +4,10 @@ export type PageConfig = {
title?: string
links?: PageConfigLink[]
group?: PageConfigGroup
favicon?: string
maintenances?: {
upcomingColor?: string
}
}
export type MaintenanceConfig = {

View File

@ -16,6 +16,14 @@ const pageConfig: PageConfig = {
'🌐 Public (example group name)': ['foo_monitor', 'bar_monitor', 'more monitor ids...'],
'🔐 Private': ['test_tcp_monitor'],
},
// [OPTIONAL] Set the path to your favicon, default to '/favicon.ico' if not specified
favicon: '/favicon.ico',
// [OPTIONAL] Maintenance related settings
maintenances: {
// [OPTIONAL] The color of upcoming maintenance alerts, default to 'gray'
// Active alerts will always use the color specified in the MaintenanceConfig
upcomingColor: 'gray',
},
}
const workerConfig: WorkerConfig = {