Openstatus www.openstatus.dev

Status Report Updates Not Refreshing in Server Components (#1717)

* Update router referesh

Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>

* Update status report to client component

Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>

* ci: apply automated fixes

* fix: invalide queries

* fix: build

* fix: build

* chore: comments

---------

Signed-off-by: Moulik Aggarwal <qwertymoulik@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Maximilian Kaske <maximilian@kaske.org>
Co-authored-by: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com>

+225 -140
+38 -13
apps/dashboard/src/app/(dashboard)/overview/layout.tsx
··· 5 5 } from "@/components/nav/app-header"; 6 6 import { AppSidebarTrigger } from "@/components/nav/app-sidebar"; 7 7 8 + import { HydrateClient, getQueryClient, trpc } from "@/lib/trpc/server"; 8 9 import { Breadcrumb } from "./breadcrumb"; 9 10 import { NavActions } from "./nav-actions"; 10 11 11 - export default function Layout({ children }: { children: React.ReactNode }) { 12 + export default async function Layout({ 13 + children, 14 + }: { children: React.ReactNode }) { 15 + const queryClient = getQueryClient(); 16 + 17 + await queryClient.prefetchQuery(trpc.monitor.list.queryOptions()); 18 + await queryClient.prefetchQuery(trpc.page.list.queryOptions()); 19 + await queryClient.prefetchQuery( 20 + trpc.incident.list.queryOptions({ 21 + period: "7d", 22 + }), 23 + ); 24 + await queryClient.prefetchQuery( 25 + trpc.statusReport.list.queryOptions({ 26 + period: "7d", 27 + }), 28 + ); 29 + await queryClient.prefetchQuery( 30 + trpc.maintenance.list.queryOptions({ 31 + period: "7d", 32 + }), 33 + ); 34 + 12 35 return ( 13 - <div> 14 - <AppHeader> 15 - <AppHeaderContent> 16 - <AppSidebarTrigger /> 17 - <Breadcrumb /> 18 - </AppHeaderContent> 19 - <AppHeaderActions> 20 - <NavActions /> 21 - </AppHeaderActions> 22 - </AppHeader> 23 - <main className="w-full flex-1">{children}</main> 24 - </div> 36 + <HydrateClient> 37 + <div> 38 + <AppHeader> 39 + <AppHeaderContent> 40 + <AppSidebarTrigger /> 41 + <Breadcrumb /> 42 + </AppHeaderContent> 43 + <AppHeaderActions> 44 + <NavActions /> 45 + </AppHeaderActions> 46 + </AppHeader> 47 + <main className="w-full flex-1">{children}</main> 48 + </div> 49 + </HydrateClient> 25 50 ); 26 51 }
+97 -106
apps/dashboard/src/app/(dashboard)/overview/page.tsx
··· 1 + "use client"; 2 + 1 3 import { 2 4 SectionDescription, 3 5 SectionGroup, ··· 21 23 MetricCardValue, 22 24 } from "@/components/metric/metric-card"; 23 25 import { DataTable } from "@/components/ui/data-table/data-table"; 24 - import { HydrateClient, getQueryClient, trpc } from "@/lib/trpc/server"; 26 + import { useTRPC } from "@/lib/trpc/client"; 25 27 import { cn } from "@/lib/utils"; 28 + import { useQuery } from "@tanstack/react-query"; 26 29 import { formatDistanceToNowStrict } from "date-fns"; 27 30 import { List, Search } from "lucide-react"; 28 31 import { Terminal } from "lucide-react"; ··· 33 36 // whenever I change the maintenances, the page is not updated 34 37 // we need to move the queryClient to the layout and prefetch the data there 35 38 36 - export default async function Page() { 37 - const queryClient = getQueryClient(); 39 + export default function Page() { 40 + const trpc = useTRPC(); 38 41 39 - const monitors = await queryClient.fetchQuery( 40 - trpc.monitor.list.queryOptions(), 41 - ); 42 - const pages = await queryClient.fetchQuery(trpc.page.list.queryOptions()); 43 - const incidents = await queryClient.fetchQuery( 42 + const { data: monitors } = useQuery(trpc.monitor.list.queryOptions()); 43 + const { data: pages } = useQuery(trpc.page.list.queryOptions()); 44 + const { data: incidents } = useQuery( 44 45 trpc.incident.list.queryOptions({ 45 - order: "desc", 46 - startedAt: { 47 - gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago 48 - }, 46 + period: "7d", 49 47 }), 50 48 ); 51 - const statusReports = await queryClient.fetchQuery( 49 + const { data: statusReports } = useQuery( 52 50 trpc.statusReport.list.queryOptions({ 53 - order: "desc", 54 - createdAt: { 55 - gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago 56 - }, 51 + period: "7d", 57 52 }), 58 53 ); 59 - const maintenances = await queryClient.fetchQuery( 54 + const { data: maintenances } = useQuery( 60 55 trpc.maintenance.list.queryOptions({ 61 - order: "desc", 62 - createdAt: { 63 - gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 7 days ago 64 - }, 56 + period: "7d", 65 57 }), 66 58 ); 59 + 60 + if (!monitors || !pages || !incidents || !statusReports || !maintenances) 61 + return null; 67 62 68 63 const lastIncident = incidents.length > 0 ? incidents[0] : null; 69 64 const lastStatusReport = statusReports.length > 0 ? statusReports[0] : null; ··· 135 130 ]; 136 131 137 132 return ( 138 - <HydrateClient> 139 - <SectionGroup> 140 - <Note> 141 - <Terminal /> 142 - Use Monitoring as Code to manage your monitors with our CLI. 143 - <NoteButton variant="default" asChild> 144 - <Link href="/cli">Learn more</Link> 145 - </NoteButton> 146 - </Note> 147 - <Section> 148 - <SectionHeader> 149 - <SectionTitle>Overview</SectionTitle> 150 - <SectionDescription> 151 - Welcome to your OpenStatus dashboard. 152 - </SectionDescription> 153 - </SectionHeader> 154 - <MetricCardGroup> 155 - {metrics.map((metric) => ( 156 - <Link 157 - href={metric.href} 158 - key={metric.title} 159 - className={cn(metric.disabled && "pointer-events-none")} 160 - aria-disabled={metric.disabled} 161 - > 162 - <MetricCard variant={metric.variant}> 163 - <MetricCardHeader className="flex items-center justify-between gap-2"> 164 - <MetricCardTitle className="truncate"> 165 - {metric.title} 166 - </MetricCardTitle> 167 - <metric.icon className="size-4" /> 168 - </MetricCardHeader> 169 - <MetricCardValue>{metric.value}</MetricCardValue> 170 - </MetricCard> 171 - </Link> 172 - ))} 173 - </MetricCardGroup> 174 - </Section> 175 - <Section> 176 - <SectionHeader> 177 - <SectionTitle>Incidents</SectionTitle> 178 - <SectionDescription> 179 - Incidents over the last 7 days. 180 - </SectionDescription> 181 - </SectionHeader> 182 - {incidents.length > 0 ? ( 183 - <DataTable columns={incidentsColumns} data={incidents} /> 184 - ) : ( 185 - <EmptyStateContainer> 186 - <EmptyStateTitle>No incidents found</EmptyStateTitle> 187 - </EmptyStateContainer> 188 - )} 189 - </Section> 190 - <Section> 191 - <SectionHeader> 192 - <SectionTitle>Reports</SectionTitle> 193 - <SectionDescription> 194 - Reports over the last 7 days. 195 - </SectionDescription> 196 - </SectionHeader> 197 - {statusReports.length > 0 ? ( 198 - <DataTableStatusReports statusReports={statusReports} /> 199 - ) : ( 200 - <EmptyStateContainer> 201 - <EmptyStateTitle>No reports found</EmptyStateTitle> 202 - </EmptyStateContainer> 203 - )} 204 - </Section> 205 - <Section> 206 - <SectionHeader> 207 - <SectionTitle>Maintenance</SectionTitle> 208 - <SectionDescription> 209 - Maintenance over the last 7 days. 210 - </SectionDescription> 211 - </SectionHeader> 212 - {maintenances.length > 0 ? ( 213 - <DataTable columns={maintenancesColumns} data={maintenances} /> 214 - ) : ( 215 - <EmptyStateContainer> 216 - <EmptyStateTitle>No maintenances found</EmptyStateTitle> 217 - </EmptyStateContainer> 218 - )} 219 - </Section> 220 - </SectionGroup> 221 - </HydrateClient> 133 + <SectionGroup> 134 + <Note> 135 + <Terminal /> 136 + Use Monitoring as Code to manage your monitors with our CLI. 137 + <NoteButton variant="default" asChild> 138 + <Link href="/cli">Learn more</Link> 139 + </NoteButton> 140 + </Note> 141 + <Section> 142 + <SectionHeader> 143 + <SectionTitle>Overview</SectionTitle> 144 + <SectionDescription> 145 + Welcome to your OpenStatus dashboard. 146 + </SectionDescription> 147 + </SectionHeader> 148 + <MetricCardGroup> 149 + {metrics.map((metric) => ( 150 + <Link 151 + href={metric.href} 152 + key={metric.title} 153 + className={cn(metric.disabled && "pointer-events-none")} 154 + aria-disabled={metric.disabled} 155 + > 156 + <MetricCard variant={metric.variant}> 157 + <MetricCardHeader className="flex items-center justify-between gap-2"> 158 + <MetricCardTitle className="truncate"> 159 + {metric.title} 160 + </MetricCardTitle> 161 + <metric.icon className="size-4" /> 162 + </MetricCardHeader> 163 + <MetricCardValue>{metric.value}</MetricCardValue> 164 + </MetricCard> 165 + </Link> 166 + ))} 167 + </MetricCardGroup> 168 + </Section> 169 + <Section> 170 + <SectionHeader> 171 + <SectionTitle>Incidents</SectionTitle> 172 + <SectionDescription> 173 + Incidents over the last 7 days. 174 + </SectionDescription> 175 + </SectionHeader> 176 + {incidents.length > 0 ? ( 177 + <DataTable columns={incidentsColumns} data={incidents} /> 178 + ) : ( 179 + <EmptyStateContainer> 180 + <EmptyStateTitle>No incidents found</EmptyStateTitle> 181 + </EmptyStateContainer> 182 + )} 183 + </Section> 184 + <Section> 185 + <SectionHeader> 186 + <SectionTitle>Reports</SectionTitle> 187 + <SectionDescription>Reports over the last 7 days.</SectionDescription> 188 + </SectionHeader> 189 + {statusReports.length > 0 ? ( 190 + <DataTableStatusReports statusReports={statusReports} /> 191 + ) : ( 192 + <EmptyStateContainer> 193 + <EmptyStateTitle>No reports found</EmptyStateTitle> 194 + </EmptyStateContainer> 195 + )} 196 + </Section> 197 + <Section> 198 + <SectionHeader> 199 + <SectionTitle>Maintenance</SectionTitle> 200 + <SectionDescription> 201 + Maintenance over the last 7 days. 202 + </SectionDescription> 203 + </SectionHeader> 204 + {maintenances.length > 0 ? ( 205 + <DataTable columns={maintenancesColumns} data={maintenances} /> 206 + ) : ( 207 + <EmptyStateContainer> 208 + <EmptyStateTitle>No maintenances found</EmptyStateTitle> 209 + </EmptyStateContainer> 210 + )} 211 + </Section> 212 + </SectionGroup> 222 213 ); 223 214 }
+10
apps/dashboard/src/components/data-table/maintenances/data-table-row-actions.tsx
··· 34 34 pageId: row.original.pageId ?? undefined, 35 35 }), 36 36 }); 37 + queryClient.invalidateQueries({ 38 + queryKey: trpc.maintenance.list.queryKey({ 39 + period: "7d", 40 + }), 41 + }); 37 42 }, 38 43 }), 39 44 ); ··· 44 49 queryClient.refetchQueries({ 45 50 queryKey: trpc.maintenance.list.queryKey({ 46 51 pageId: row.original.pageId ?? undefined, 52 + }), 53 + }); 54 + queryClient.invalidateQueries({ 55 + queryKey: trpc.maintenance.list.queryKey({ 56 + period: "7d", 47 57 }), 48 58 }); 49 59 },
+10
apps/dashboard/src/components/data-table/status-report-updates/data-table-row-actions.tsx
··· 32 32 queryClient.invalidateQueries({ 33 33 queryKey: trpc.page.list.queryKey(), 34 34 }); 35 + queryClient.invalidateQueries({ 36 + queryKey: trpc.statusReport.list.queryKey({ 37 + period: "7d", 38 + }), 39 + }); 35 40 }, 36 41 }), 37 42 ); ··· 41 46 queryClient.invalidateQueries({ 42 47 queryKey: trpc.statusReport.list.queryKey({ 43 48 pageId: Number.parseInt(id), 49 + }), 50 + }); 51 + queryClient.invalidateQueries({ 52 + queryKey: trpc.statusReport.list.queryKey({ 53 + period: "7d", 44 54 }), 45 55 }); 46 56 },
+5
apps/dashboard/src/components/data-table/status-report-updates/data-table.tsx
··· 60 60 queryClient.invalidateQueries({ 61 61 queryKey: trpc.page.list.queryKey(), 62 62 }); 63 + queryClient.invalidateQueries({ 64 + queryKey: trpc.statusReport.list.queryKey({ 65 + period: "7d", 66 + }), 67 + }); 63 68 }, 64 69 }), 65 70 );
+15
apps/dashboard/src/components/data-table/status-reports/data-table-row-actions.tsx
··· 57 57 queryClient.invalidateQueries({ 58 58 queryKey: trpc.page.list.queryKey(), 59 59 }); 60 + queryClient.invalidateQueries({ 61 + queryKey: trpc.statusReport.list.queryKey({ 62 + period: "7d", 63 + }), 64 + }); 60 65 }, 61 66 }), 62 67 ); ··· 76 81 queryClient.invalidateQueries({ 77 82 queryKey: trpc.page.list.queryKey(), 78 83 }); 84 + queryClient.invalidateQueries({ 85 + queryKey: trpc.statusReport.list.queryKey({ 86 + period: "7d", 87 + }), 88 + }); 79 89 }, 80 90 }), 81 91 ); ··· 89 99 }); 90 100 queryClient.invalidateQueries({ 91 101 queryKey: trpc.page.list.queryKey(), 102 + }); 103 + queryClient.invalidateQueries({ 104 + queryKey: trpc.statusReport.list.queryKey({ 105 + period: "7d", 106 + }), 92 107 }); 93 108 }, 94 109 }),
+4 -7
packages/api/src/router/incident.ts
··· 19 19 import { Events } from "@openstatus/analytics"; 20 20 import { TRPCError } from "@trpc/server"; 21 21 import { createTRPCRouter, protectedProcedure } from "../trpc"; 22 + import { getPeriodDate, periods } from "./utils"; 22 23 23 24 export const incidentRouter = createTRPCRouter({ 24 25 // TODO: rename getIncidentsByWorkspace to make it consistent with the other methods ··· 181 182 .input( 182 183 z 183 184 .object({ 184 - startedAt: z 185 - .object({ 186 - gte: z.date().optional(), 187 - }) 188 - .optional(), 185 + period: z.enum(periods).optional(), 189 186 monitorId: z.number().nullish(), 190 187 order: z.enum(["asc", "desc"]).optional(), 191 188 }) ··· 196 193 eq(incidentTable.workspaceId, opts.ctx.workspace.id), 197 194 ]; 198 195 199 - if (opts.input?.startedAt?.gte) { 196 + if (opts.input?.period) { 200 197 whereConditions.push( 201 - gte(incidentTable.startedAt, opts.input.startedAt.gte), 198 + gte(incidentTable.startedAt, getPeriodDate(opts.input.period)), 202 199 ); 203 200 } 204 201
+4 -7
packages/api/src/router/maintenance.ts
··· 22 22 import { Events } from "@openstatus/analytics"; 23 23 import { TRPCError } from "@trpc/server"; 24 24 import { createTRPCRouter, protectedProcedure } from "../trpc"; 25 + import { getPeriodDate, periods } from "./utils"; 25 26 26 27 export const maintenanceRouter = createTRPCRouter({ 27 28 getById: protectedProcedure ··· 120 121 .input( 121 122 z 122 123 .object({ 123 - createdAt: z 124 - .object({ 125 - gte: z.date().optional(), 126 - }) 127 - .optional(), 124 + period: z.enum(periods).optional(), 128 125 pageId: z.number().optional(), 129 126 order: z.enum(["asc", "desc"]).optional(), 130 127 }) ··· 135 132 eq(maintenance.workspaceId, opts.ctx.workspace.id), 136 133 ]; 137 134 138 - if (opts.input?.createdAt?.gte) { 135 + if (opts.input?.period) { 139 136 whereConditions.push( 140 - gte(maintenance.createdAt, opts.input.createdAt.gte), 137 + gte(maintenance.createdAt, getPeriodDate(opts.input.period)), 141 138 ); 142 139 } 143 140
+4 -7
packages/api/src/router/statusReport.ts
··· 31 31 import { Events } from "@openstatus/analytics"; 32 32 import { TRPCError } from "@trpc/server"; 33 33 import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 34 + import { getPeriodDate, periods } from "./utils"; 34 35 35 36 export const statusReportRouter = createTRPCRouter({ 36 37 createStatusReport: protectedProcedure ··· 387 388 list: protectedProcedure 388 389 .input( 389 390 z.object({ 390 - createdAt: z 391 - .object({ 392 - gte: z.date().optional(), 393 - }) 394 - .optional(), 391 + period: z.enum(periods).optional(), 395 392 order: z.enum(["asc", "desc"]).optional(), 396 393 pageId: z.number().optional(), 397 394 }), ··· 401 398 eq(statusReport.workspaceId, opts.ctx.workspace.id), 402 399 ]; 403 400 404 - if (opts.input?.createdAt?.gte) { 401 + if (opts.input?.period) { 405 402 whereConditions.push( 406 - gte(statusReport.createdAt, opts.input.createdAt.gte), 403 + gte(statusReport.createdAt, getPeriodDate(opts.input.period)), 407 404 ); 408 405 } 409 406
+38
packages/api/src/router/utils.ts
··· 1 + /** 2 + * Shared utilities for API routers 3 + */ 4 + 5 + /** 6 + * Supported time period values for filtering data 7 + */ 8 + export const periods = ["1d", "7d", "14d"] as const; 9 + 10 + /** 11 + * Period type for filtering data by time range 12 + */ 13 + export type Period = (typeof periods)[number]; 14 + 15 + /** 16 + * Converts a period string to a Date object representing the start of that period 17 + * @param period - The period to convert (e.g., "1d", "7d", "14d") 18 + * @returns Date object representing the start of the period (for use with gte filters) 19 + * @example 20 + * // Get date for 7 days ago 21 + * const date = getPeriodDate("7d"); 22 + * // Returns: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) 23 + */ 24 + export function getPeriodDate(period: Period): Date { 25 + const now = Date.now(); 26 + 27 + switch (period) { 28 + case "1d": 29 + return new Date(now - 1 * 24 * 60 * 60 * 1000); 30 + case "7d": 31 + return new Date(now - 7 * 24 * 60 * 60 * 1000); 32 + case "14d": 33 + return new Date(now - 14 * 24 * 60 * 60 * 1000); 34 + default: 35 + // TypeScript ensures this is exhaustive, but return 7d as safe fallback 36 + return new Date(now - 7 * 24 * 60 * 60 * 1000); 37 + } 38 + }