Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: incident status banner (#1487)

* fix: incident status banner

* refactor: open events

* chore: include blog post in docs

* chore: docs

* fix: link

* fix: link

* fix: rounded theme

* chore: include theme mode

* fix: missing tw utility

* fix: recompute styles

* fix: recompute styles

* chore: remove and append

* chore: insertin same location

* chore: is mounted

* wtf: hack fix

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Maximilian Kaske
autofix-ci[bot]
and committed by
GitHub
24c9fbbe a9ab060b

+82 -38
-3
apps/docs/src/content/docs/concept/getting-started.mdx
··· 6 6 Here’s some content to help you get started with the concept behind OpenStatus. 7 7 8 8 - [Uptime Monitoring](/concept/uptime-monitoring) 9 - 10 9 - [Best Practices for Status Pages](/concept/best-practices-status-page) 11 - 12 10 - [Uptime Monitoring as Code](/concept/uptime-monitoring-as-code) 13 - 14 11 - [Latency vs Response Time](/concept/latency-vs-response-time)
-4
apps/docs/src/content/docs/guides/getting-started.mdx
··· 10 10 11 11 12 12 - [How to monitor your MCP Server using OpenStatus](/guides/how-to-monitor-mcp-server/) 13 - 14 13 - [How to run synthetic tests in your GitHub Actions](/guides/how-to-run-synthetic-test-github-action/) 15 - 16 14 - [Export Metrics to your OTLP endpoint](/guides/how-to-export-metrics-to-otlp-endpoint) 17 - 18 15 - [How to Add an SVG Status Badge to your GitHub README](/guides/how-to-add-svg-status-badge) 19 - 20 16 - [How to use React Status Widget](/guides/how-to-use-react-widget)
+3 -1
apps/docs/src/content/docs/tutorial/getting-started.mdx
··· 6 6 7 7 - [How to create your first monitor with OpenStatus](/tutorial/how-to-create-monitor) 8 8 - [How to create a status page?](/tutorial/how-to-create-status-page) 9 - - [Get Started with openstatus CLI](/tutorial/get-started-with-openstatus-cli) 9 + - [How to configure a status page? (beta)](/tutorial/how-to-configure-status-page) 10 + - [How to create a private location? (beta)](/tutorial/how-to-create-private-location) 11 + - [Get Started with openstatus CLI](/tutorial/get-started-with-openstatus-cli)
+2
apps/docs/src/content/docs/tutorial/how-to-create-private-location.mdx
··· 18 18 3. in the private location settings, choose which monitors to track (or vice versa, configure it in your monitor settings) 19 19 4. done (within a couple of minutes, we'll check the everything) 20 20 21 + We have published a blog post on how we [deployed the docker image on a Raspberry Pi (2016)](https://www.openstatus.dev/blog/deploy-private-locations-raspberry-pi). 22 + 21 23 ### What is missing? 22 24 23 25 - Incidents will not be created (yet) if you run your monitors via privat regions. That means you should still use the openstatus regions to get alerts if a monitor is down.
+10 -14
apps/status-page/src/app/(public)/client.tsx
··· 9 9 SectionHeader, 10 10 SectionTitle, 11 11 } from "@/components/content/section"; 12 + import { recomputeStyles } from "@/components/status-page/floating-button"; 12 13 import { 13 14 Status, 14 15 StatusContent, ··· 33 34 import { monitors } from "@/data/monitors"; 34 35 import { useTRPC } from "@/lib/trpc/client"; 35 36 import { cn } from "@/lib/utils"; 36 - import { 37 - THEMES, 38 - THEME_KEYS, 39 - generateThemeStyles, 40 - } from "@openstatus/theme-store"; 37 + import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; 41 38 import { useQuery } from "@tanstack/react-query"; 42 39 import { useTheme } from "next-themes"; 43 40 import { useQueryStates } from "nuqs"; ··· 56 53 57 54 export function Client() { 58 55 const { resolvedTheme } = useTheme(); 59 - const [mounted, setMounted] = useState(false); 56 + const [isMounted, setIsMounted] = useState(false); 60 57 const [searchParams, setSearchParams] = useQueryStates(searchParamsParsers); 61 58 const { q, t } = searchParams; 62 59 const theme = t ? THEMES[t as keyof typeof THEMES] : undefined; 63 60 64 61 useEffect(() => { 65 - setMounted(true); 62 + setIsMounted(true); 66 63 }, []); 67 64 68 65 useEffect(() => { 69 - const themeStyles = document.getElementById("theme-styles"); 70 - if (t && themeStyles) { 71 - themeStyles.innerHTML = generateThemeStyles(t); 66 + if (isMounted && t) { 67 + recomputeStyles(t); 72 68 } 73 - }, [t]); 69 + }, [t, isMounted]); 74 70 75 71 return ( 76 72 <SectionGroup> ··· 125 121 ); 126 122 }).map((k) => { 127 123 const theme = THEMES[k]; 128 - const style = mounted 124 + const style = isMounted 129 125 ? theme[resolvedTheme as "dark" | "light"] 130 126 : undefined; 131 127 ··· 145 141 } 146 142 }} 147 143 > 148 - {mounted ? ( 144 + {isMounted ? ( 149 145 <div 150 146 className="absolute h-full w-full bg-background text-foreground" 151 147 style={style as React.CSSProperties} ··· 179 175 ? style[color.key] 180 176 : undefined; 181 177 182 - if (!mounted) { 178 + if (!isMounted) { 183 179 return ( 184 180 <Skeleton 185 181 key={color.key}
+30 -5
apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx
··· 28 28 import { cn } from "@/lib/utils"; 29 29 import { useQuery } from "@tanstack/react-query"; 30 30 import { useParams } from "next/navigation"; 31 + import { useMemo } from "react"; 31 32 32 33 export default function Page() { 33 34 const { domain } = useParams<{ domain: string }>(); ··· 48 49 }), 49 50 ); 50 51 52 + // NOTE: we need to filter out the incidents as we don't want to show all of them in the banner - a single one is enough 53 + // REMINDER: we could move that to the server - but we might wanna have the info of all openEvents actually 54 + const events = useMemo(() => { 55 + let hasIncident = false; 56 + return ( 57 + page?.openEvents.filter((e) => { 58 + if (e.type !== "incident") return true; 59 + if (hasIncident) return false; 60 + hasIncident = true; 61 + return true; 62 + }) ?? [] 63 + ); 64 + }, [page]); 65 + 51 66 if (!page) return null; 52 67 53 68 return ( ··· 57 72 <StatusTitle>{page.title}</StatusTitle> 58 73 <StatusDescription>{page.description}</StatusDescription> 59 74 </StatusHeader> 60 - {page.openEvents.length > 0 ? ( 75 + {events.length > 0 ? ( 61 76 <StatusContent> 62 77 <StatusBannerTabs 63 - defaultValue={`${page.openEvents[0].type}-${page.openEvents[0].id}`} 78 + defaultValue={`${events[0].type}-${events[0].id}`} 64 79 > 65 80 <StatusBannerTabsList> 66 - {page.openEvents.map((e, i) => { 81 + {events.map((e, i) => { 67 82 return ( 68 83 <StatusBannerTabsTrigger 69 84 value={`${e.type}-${e.id}`} ··· 71 86 key={e.id} 72 87 className={cn( 73 88 i === 0 && "rounded-tl-lg", 74 - i === page.openEvents.length - 1 && "rounded-tr-lg", 89 + i === events.length - 1 && "rounded-tr-lg", 75 90 )} 76 91 > 77 92 {e.name} ··· 79 94 ); 80 95 })} 81 96 </StatusBannerTabsList> 82 - {page.openEvents.map((e) => { 97 + {events.map((e) => { 83 98 if (e.type === "report") { 84 99 const report = page.statusReports.find( 85 100 (report) => report.id === e.id, ··· 119 134 /> 120 135 </StatusBannerContent> 121 136 </StatusBannerContainer> 137 + </StatusBannerTabsContent> 138 + ); 139 + } 140 + if (e.type === "incident") { 141 + return ( 142 + <StatusBannerTabsContent 143 + value={`${e.type}-${e.id}`} 144 + key={e.id} 145 + > 146 + <StatusBanner status={e.status} /> 122 147 </StatusBannerTabsContent> 123 148 ); 124 149 }
+8
apps/status-page/src/app/globals.css
··· 184 184 .rounded-full { 185 185 border-radius: calc(var(--radius) * 99999999); 186 186 } 187 + .rounded-b-full { 188 + border-bottom-left-radius: calc(var(--radius) * 99999999); 189 + border-bottom-right-radius: calc(var(--radius) * 99999999); 190 + } 191 + .rounded-t-full { 192 + border-top-left-radius: calc(var(--radius) * 99999999); 193 + border-top-right-radius: calc(var(--radius) * 99999999); 194 + } 187 195 }
+25 -10
apps/status-page/src/components/status-page/floating-button.tsx
··· 89 89 const [communityTheme, setCommunityTheme] = useState<CommunityTheme>( 90 90 defaultCommunityTheme, 91 91 ); 92 + const [isMounted, setIsMounted] = useState(false); 92 93 93 94 useEffect(() => { 94 - const themeStyles = document.getElementById("theme-styles"); 95 - if (themeStyles) { 96 - themeStyles.innerHTML = generateThemeStyles(communityTheme); 95 + setIsMounted(true); 96 + }, []); 97 + 98 + useEffect(() => { 99 + if (isMounted) { 100 + recomputeStyles(communityTheme); 97 101 } 98 - }, [communityTheme]); 102 + }, [communityTheme, isMounted]); 99 103 100 104 return ( 101 105 <StatusPageContext.Provider ··· 253 257 </SelectContent> 254 258 </Select> 255 259 </div> 256 - {IS_DEV ? ( 257 - <div className="space-y-2"> 258 - <Label htmlFor="theme">Theme</Label> 259 - <ThemeSelect id="theme" className="w-full" /> 260 - </div> 261 - ) : null} 260 + <div className="space-y-2"> 261 + <Label htmlFor="theme">Theme</Label> 262 + <ThemeSelect id="theme" className="w-full" /> 263 + </div> 262 264 <div className="space-y-2"> 263 265 <Label htmlFor="community-theme">Community Theme</Label> 264 266 <Popover> ··· 337 339 </div> 338 340 ); 339 341 } 342 + 343 + export function recomputeStyles(newTheme: CommunityTheme) { 344 + // FIXME: only on prod, we have two style elements with the same id 345 + // we need to get rid of all of them except the one we want to update 346 + const allThemeStyles = document.querySelectorAll("style[id='theme-styles']"); 347 + allThemeStyles.forEach((style, index) => { 348 + if (index === 0) { 349 + style.textContent = generateThemeStyles(newTheme); 350 + } else { 351 + style.remove(); 352 + } 353 + }); 354 + }
+4 -1
apps/status-page/src/components/status-page/status-tracker.tsx
··· 253 253 {item.bar.map((segment, segmentIndex) => ( 254 254 <div 255 255 key={`${item.day}-${segment.status}-${segmentIndex}`} 256 - className="w-full rounded-full transition-all" 256 + className={cn("w-full transition-all", { 257 + "rounded-t-full": segmentIndex === 0, 258 + "rounded-b-full": segmentIndex === item.bar.length - 1, 259 + })} 257 260 style={{ 258 261 height: `${segment.height}%`, 259 262 backgroundColor: chartConfig[segment.status].color,