Merge remote-tracking branch 'upstream/main' into webhook
commit
d95ad33d67
|
|
@ -61,3 +61,7 @@ Please refer to [Wiki](https://github.com/lyc8503/UptimeFlare/wiki)
|
||||||
- [ ] Update wiki/README and add docs for dev
|
- [ ] Update wiki/README and add docs for dev
|
||||||
- [ ] Migration to Terraform Cloudflare provider version 5.x
|
- [ ] Migration to Terraform Cloudflare provider version 5.x
|
||||||
- [ ] Cloudflare D1 database
|
- [ ] Cloudflare D1 database
|
||||||
|
- [x] Scheduled maintenances (via IIFE)
|
||||||
|
- [ ] Simpler config example
|
||||||
|
- [ ] Upcoming maintenances
|
||||||
|
- [ ] Universal Webhook upgrade
|
||||||
|
|
@ -2,13 +2,16 @@ import { Alert, List, Text, useMantineTheme } from '@mantine/core'
|
||||||
import { useMediaQuery } from '@mantine/hooks'
|
import { useMediaQuery } from '@mantine/hooks'
|
||||||
import { IconAlertTriangle } from '@tabler/icons-react'
|
import { IconAlertTriangle } from '@tabler/icons-react'
|
||||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||||
|
import { pageConfig } from '@/uptime.config'
|
||||||
|
|
||||||
export default function MaintenanceAlert({
|
export default function MaintenanceAlert({
|
||||||
maintenance,
|
maintenance,
|
||||||
style,
|
style,
|
||||||
|
upcoming = false,
|
||||||
}: {
|
}: {
|
||||||
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
|
maintenance: Omit<MaintenanceConfig, 'monitors'> & { monitors?: MonitorTarget[] }
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
|
upcoming?: boolean
|
||||||
}) {
|
}) {
|
||||||
const theme = useMantineTheme()
|
const theme = useMantineTheme()
|
||||||
const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`)
|
const isDesktop = useMediaQuery(`(min-width: ${theme.breakpoints.sm})`)
|
||||||
|
|
@ -23,12 +26,14 @@ export default function MaintenanceAlert({
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{maintenance.title || 'Scheduled Maintenance'}
|
{(upcoming ? '[Upcoming] ' : '') + (maintenance.title || 'Scheduled Maintenance')}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
color={maintenance.color || 'yellow'}
|
color={
|
||||||
|
upcoming ? pageConfig.maintenances?.upcomingColor ?? 'gray' : maintenance.color || 'yellow'
|
||||||
|
}
|
||||||
withCloseButton={false}
|
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) */}
|
{/* Date range in top right (desktop) or inline (mobile) */}
|
||||||
<div
|
<div
|
||||||
|
|
@ -45,16 +50,30 @@ export default function MaintenanceAlert({
|
||||||
padding: '2px 8px',
|
padding: '2px 8px',
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
}
|
}
|
||||||
: {}),
|
: { marginBottom: 4 }),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<b>From:</b> {new Date(maintenance.start).toLocaleString()}
|
<div
|
||||||
<br />
|
style={{
|
||||||
<b>To:</b>{' '}
|
display: 'grid',
|
||||||
{maintenance.end ? new Date(maintenance.end).toLocaleString() : 'Until further notice'}
|
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>
|
</div>
|
||||||
|
|
||||||
<Text style={{ paddingTop: '3px' }}>{maintenance.body}</Text>
|
<Text style={{ paddingTop: '3px', whiteSpace: 'pre-line' }}>{maintenance.body}</Text>
|
||||||
{maintenance.monitors && maintenance.monitors.length > 0 && (
|
{maintenance.monitors && maintenance.monitors.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Text mt="xs">
|
<Text mt="xs">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
import { MaintenanceConfig, MonitorTarget } from '@/types/config'
|
||||||
import { Center, Container, Title } from '@mantine/core'
|
import { Center, Container, Title, Collapse, Button, Box } from '@mantine/core'
|
||||||
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
|
import { IconCircleCheck, IconAlertCircle, IconPlus, IconMinus } from '@tabler/icons-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import MaintenanceAlert from './MaintenanceAlert'
|
import MaintenanceAlert from './MaintenanceAlert'
|
||||||
import { pageConfig } from '@/uptime.config'
|
import { pageConfig } from '@/uptime.config'
|
||||||
|
|
@ -45,6 +45,7 @@ export default function OverallStatus({
|
||||||
const [openTime] = useState(Math.round(Date.now() / 1000))
|
const [openTime] = useState(Math.round(Date.now() / 1000))
|
||||||
const [currentTime, setCurrentTime] = useState(Math.round(Date.now() / 1000))
|
const [currentTime, setCurrentTime] = useState(Math.round(Date.now() / 1000))
|
||||||
const isWindowVisible = useWindowVisibility()
|
const isWindowVisible = useWindowVisibility()
|
||||||
|
const [expandUpcoming, setExpandUpcoming] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
|
|
@ -58,7 +59,8 @@ export default function OverallStatus({
|
||||||
})
|
})
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
let filteredMaintenances: (Omit<MaintenanceConfig, 'monitors'> & {
|
|
||||||
|
const activeMaintenances: (Omit<MaintenanceConfig, 'monitors'> & {
|
||||||
monitors?: MonitorTarget[]
|
monitors?: MonitorTarget[]
|
||||||
})[] = maintenances
|
})[] = maintenances
|
||||||
.filter((m) => now >= new Date(m.start) && (!m.end || now <= new Date(m.end)))
|
.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 (
|
return (
|
||||||
<Container size="md" mt="xl">
|
<Container size="md" mt="xl">
|
||||||
<Center>{icon}</Center>
|
<Center>{icon}</Center>
|
||||||
|
|
@ -82,9 +95,36 @@ export default function OverallStatus({
|
||||||
} sec ago)`}
|
} sec ago)`}
|
||||||
</Title>
|
</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
|
<MaintenanceAlert
|
||||||
key={idx}
|
key={`active-${idx}`}
|
||||||
maintenance={maintenance}
|
maintenance={maintenance}
|
||||||
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
|
style={{ maxWidth: groupedMonitor ? '897px' : '865px' }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export default function IncidentsPage() {
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{pageConfig.title}</title>
|
<title>{pageConfig.title}</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href={pageConfig.favicon ?? '/favicon.ico'} />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<main className={inter.className}>
|
<main className={inter.className}>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ export default function Home({
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{pageConfig.title}</title>
|
<title>{pageConfig.title}</title>
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href={pageConfig.favicon ?? '/favicon.ico'} />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<main className={inter.className}>
|
<main className={inter.className}>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ export type PageConfig = {
|
||||||
title?: string
|
title?: string
|
||||||
links?: PageConfigLink[]
|
links?: PageConfigLink[]
|
||||||
group?: PageConfigGroup
|
group?: PageConfigGroup
|
||||||
|
favicon?: string
|
||||||
|
maintenances?: {
|
||||||
|
upcomingColor?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaintenanceConfig = {
|
export type MaintenanceConfig = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ const pageConfig: PageConfig = {
|
||||||
'🌐 Public (example group name)': ['foo_monitor', 'bar_monitor', 'more monitor ids...'],
|
'🌐 Public (example group name)': ['foo_monitor', 'bar_monitor', 'more monitor ids...'],
|
||||||
'🔐 Private': ['test_tcp_monitor'],
|
'🔐 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 = {
|
const workerConfig: WorkerConfig = {
|
||||||
|
|
@ -130,7 +138,7 @@ const workerConfig: WorkerConfig = {
|
||||||
reason: string
|
reason: string
|
||||||
) => {
|
) => {
|
||||||
// This callback will be called when there's a status change for any monitor
|
// This callback will be called when there's a status change for any monitor
|
||||||
// Write any Typescript code here
|
// Write any TypeScript code here
|
||||||
// This will not follow the grace period settings and will be called immediately when the status changes
|
// This will not follow the grace period settings and will be called immediately when the status changes
|
||||||
// You need to handle the grace period manually if you want to implement it
|
// You need to handle the grace period manually if you want to implement it
|
||||||
},
|
},
|
||||||
|
|
@ -142,7 +150,7 @@ const workerConfig: WorkerConfig = {
|
||||||
reason: string
|
reason: string
|
||||||
) => {
|
) => {
|
||||||
// This callback will be called EVERY 1 MINTUE if there's an on-going incident for any monitor
|
// This callback will be called EVERY 1 MINTUE if there's an on-going incident for any monitor
|
||||||
// Write any Typescript code here
|
// Write any TypeScript code here
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +176,31 @@ const maintenances: MaintenanceConfig[] = [
|
||||||
// [Optional] color of the maintenance alert at status page, default to "yellow"
|
// [Optional] color of the maintenance alert at status page, default to "yellow"
|
||||||
color: 'blue',
|
color: 'blue',
|
||||||
},
|
},
|
||||||
|
// As this config file is a TypeScript file, you can even use IIFE to generate scheduled maintenances
|
||||||
|
// The following example shows a scheduled maintenance from 2 AM to 4 AM on the 15th of every month (UTC+8)
|
||||||
|
// This COULD BE DANGEROUS, as generating too many maintenance entries can lead to performance problems
|
||||||
|
// Undeterministic outputs may also lead to bugs or unexpected behavior
|
||||||
|
// If you don't know how to DEBUG, use this approach WITH CAUTION
|
||||||
|
...(function (){
|
||||||
|
const schedules = [];
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
for (let i = -1; i <= 1; i++) {
|
||||||
|
// JavaScript's Date object will automatically handle year rollovers
|
||||||
|
const date = new Date(today.getFullYear(), today.getMonth() + i, 15);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
|
||||||
|
schedules.push({
|
||||||
|
title: `${year}/${parseInt(month)} - Test scheduled maintenance`,
|
||||||
|
monitors: ['foo_monitor'],
|
||||||
|
body: 'Monthly scheduled maintenance',
|
||||||
|
start: `${year}-${month}-15T02:00:00.000+08:00`,
|
||||||
|
end: `${year}-${month}-15T04:00:00.000+08:00`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return schedules;
|
||||||
|
})()
|
||||||
]
|
]
|
||||||
|
|
||||||
// Don't forget this, otherwise compilation fails.
|
// Don't forget this, otherwise compilation fails.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue