Openstatus www.openstatus.dev

fix: maintenance markdown preview and more (#1386)

* fix: maintenance markdown preview

* fix: monitors list

* fix: date picker

authored by

Maximilian Kaske and committed by
GitHub
e5ec1862 75e97f90

+174 -69
+1 -1
apps/dashboard/src/app/(dashboard)/status-pages/[id]/status-reports/page.tsx
··· 78 78 </> 79 79 ) : undefined 80 80 } 81 - monitors={monitors} 81 + monitors={page.monitors} 82 82 onSubmit={async (values) => { 83 83 // NOTE: for type safety, we need to check if the values have a date property 84 84 // because of the union type
+2 -1
apps/dashboard/src/components/data-table/maintenances/columns.tsx
··· 1 1 "use client"; 2 2 3 + import { ProcessMessage } from "@/components/content/process-message"; 3 4 import { TableCellNumber } from "@/components/data-table/table-cell-number"; 4 5 import { DataTableColumnHeader } from "@/components/ui/data-table/data-table-column-header"; 5 6 import type { RouterOutputs } from "@openstatus/api"; ··· 25 26 const value = String(row.getValue("message")); 26 27 return ( 27 28 <div className="max-w-[200px] truncate text-muted-foreground"> 28 - {value} 29 + <ProcessMessage value={value} /> 29 30 </div> 30 31 ); 31 32 },
+15 -10
apps/dashboard/src/components/data-table/status-reports/data-table-row-actions.tsx
··· 8 8 import type { RouterOutputs } from "@openstatus/api"; 9 9 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 10 10 import type { Row } from "@tanstack/react-table"; 11 - import { useParams } from "next/navigation"; 12 11 import { useRef } from "react"; 13 12 14 13 type StatusReport = RouterOutputs["statusReport"]["list"][number]; ··· 16 15 interface DataTableRowActionsProps { 17 16 row: Row<StatusReport>; 18 17 } 18 + 19 + // NOTE: avoid using useParams to get status page :id 20 + // because we are using the table in the /overview page 19 21 20 22 export function DataTableRowActions({ row }: DataTableRowActionsProps) { 23 + if (!row.original.pageId) return null; 21 24 const buttonCreateRef = useRef<HTMLButtonElement>(null); 22 25 const buttonUpdateRef = useRef<HTMLButtonElement>(null); 23 26 const actions = getActions({ ··· 26 29 "view-report": () => { 27 30 if (typeof window !== "undefined") { 28 31 window.open( 29 - `https://${row.original.page.customDomain || `${row.original.page.slug}.openstatus.dev`}/events/report/${row.original.id}`, 32 + `https://${ 33 + row.original.page.customDomain || 34 + `${row.original.page.slug}.openstatus.dev` 35 + }/events/report/${row.original.id}`, 30 36 "_blank", 31 37 ); 32 38 } ··· 34 40 }); 35 41 const trpc = useTRPC(); 36 42 const queryClient = useQueryClient(); 37 - const { id } = useParams<{ id: string }>(); 38 - const { data: monitors } = useQuery(trpc.monitor.list.queryOptions()); 43 + const { data: page } = useQuery( 44 + trpc.page.get.queryOptions({ id: row.original.pageId }), 45 + ); 39 46 const sendStatusReportUpdateMutation = useMutation( 40 47 trpc.emailRouter.sendStatusReport.mutationOptions(), 41 48 ); ··· 44 51 onSuccess: () => { 45 52 queryClient.invalidateQueries({ 46 53 queryKey: trpc.statusReport.list.queryKey({ 47 - pageId: Number.parseInt(id), 54 + pageId: row.original.pageId ?? undefined, 48 55 }), 49 56 }); 50 57 queryClient.invalidateQueries({ ··· 63 70 // 64 71 queryClient.invalidateQueries({ 65 72 queryKey: trpc.statusReport.list.queryKey({ 66 - pageId: Number.parseInt(id), 73 + pageId: row.original.pageId ?? undefined, 67 74 }), 68 75 }); 69 76 queryClient.invalidateQueries({ ··· 77 84 onSuccess: () => { 78 85 queryClient.invalidateQueries({ 79 86 queryKey: trpc.statusReport.list.queryKey({ 80 - pageId: Number.parseInt(id), 87 + pageId: row.original.pageId ?? undefined, 81 88 }), 82 89 }); 83 90 queryClient.invalidateQueries({ ··· 86 93 }, 87 94 }), 88 95 ); 89 - 90 - if (!monitors) return null; 91 96 92 97 return ( 93 98 <> ··· 103 108 }} 104 109 /> 105 110 <FormSheetStatusReport 106 - monitors={monitors} 111 + monitors={page?.monitors ?? []} 107 112 defaultValues={{ 108 113 title: row.original.title, 109 114 status: row.original.status,
+66 -17
apps/dashboard/src/components/forms/maintenance/form.tsx
··· 4 4 EmptyStateContainer, 5 5 EmptyStateTitle, 6 6 } from "@/components/content/empty-state"; 7 + import { ProcessMessage } from "@/components/content/process-message"; 7 8 import { 8 9 FormCardContent, 9 10 FormCardSeparator, ··· 164 165 <Calendar 165 166 mode="single" 166 167 selected={field.value} 167 - onSelect={field.onChange} 168 + onSelect={(selectedDate) => { 169 + if (!selectedDate) return; 170 + 171 + const newDate = new Date(selectedDate); 172 + newDate.setHours( 173 + field.value.getHours(), 174 + field.value.getMinutes(), 175 + field.value.getSeconds(), 176 + field.value.getMilliseconds(), 177 + ); 178 + field.onChange(newDate); 179 + }} 168 180 initialFocus 169 181 /> 170 182 <div className="border-t p-3"> ··· 177 189 id="time" 178 190 type="time" 179 191 step="1" 180 - defaultValue={format(field.value, "HH:mm")} 192 + value={ 193 + field.value 194 + ? field.value.toTimeString().slice(0, 8) 195 + : new Date().toTimeString().slice(0, 8) 196 + } 181 197 className="peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" 182 198 onChange={(e) => { 183 199 try { 184 - const date = field.value 185 - ?.toISOString() 186 - .split("T")[0]; 200 + const timeValue = e.target.value; 201 + if (!timeValue || !field.value) return; 187 202 188 - field.onChange( 189 - new Date(`${date}T${e.target.value}`), 203 + const [hours, minutes, seconds] = timeValue 204 + .split(":") 205 + .map(Number); 206 + 207 + const newDate = new Date(field.value); 208 + newDate.setHours( 209 + hours, 210 + minutes, 211 + seconds || 0, 212 + 0, 190 213 ); 214 + 215 + field.onChange(newDate); 191 216 } catch (error) { 192 217 console.error(error); 193 218 } ··· 244 269 <Calendar 245 270 mode="single" 246 271 selected={field.value} 247 - onSelect={field.onChange} 272 + onSelect={(selectedDate) => { 273 + if (!selectedDate) return; 274 + 275 + const newDate = new Date(selectedDate); 276 + newDate.setHours( 277 + field.value.getHours(), 278 + field.value.getMinutes(), 279 + field.value.getSeconds(), 280 + field.value.getMilliseconds(), 281 + ); 282 + field.onChange(newDate); 283 + }} 248 284 initialFocus 249 285 /> 250 286 <div className="border-t p-3"> ··· 257 293 id="time" 258 294 type="time" 259 295 step="1" 260 - defaultValue={format(field.value, "HH:mm")} 296 + value={ 297 + field.value 298 + ? field.value.toTimeString().slice(0, 8) 299 + : new Date().toTimeString().slice(0, 8) 300 + } 261 301 className="peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" 262 302 onChange={(e) => { 263 303 try { 264 - const date = field.value 265 - ?.toISOString() 266 - .split("T")[0]; 304 + const timeValue = e.target.value; 305 + if (!timeValue || !field.value) return; 306 + 307 + const [hours, minutes, seconds] = timeValue 308 + .split(":") 309 + .map(Number); 267 310 268 - field.onChange( 269 - new Date(`${date}T${e.target.value}`), 311 + const newDate = new Date(field.value); 312 + newDate.setHours( 313 + hours, 314 + minutes, 315 + seconds || 0, 316 + 0, 270 317 ); 318 + 319 + field.onChange(newDate); 271 320 } catch (error) { 272 321 console.error(error); 273 322 } ··· 313 362 <TabsContent value="tab-2"> 314 363 <div className="grid gap-2"> 315 364 <Label>Preview</Label> 316 - <p className="rounded-md border px-3 py-2 text-foreground text-sm"> 317 - {watchMessage} 318 - </p> 365 + <div className="rounded-md border px-3 py-2 text-foreground text-sm"> 366 + <ProcessMessage value={watchMessage} /> 367 + </div> 319 368 </div> 320 369 </TabsContent> 321 370 </Tabs>
+26 -7
apps/dashboard/src/components/forms/status-report-update/form.tsx
··· 176 176 <Calendar 177 177 mode="single" 178 178 selected={field.value} 179 - onSelect={field.onChange} 179 + onSelect={(selectedDate) => { 180 + if (!selectedDate) return; 181 + const newDate = new Date(selectedDate); 182 + newDate.setHours( 183 + field.value.getHours(), 184 + field.value.getMinutes(), 185 + field.value.getSeconds(), 186 + field.value.getMilliseconds(), 187 + ); 188 + field.onChange(newDate); 189 + }} 180 190 disabled={(date) => 181 191 date > new Date() || date < new Date("1900-01-01") 182 192 } ··· 192 202 id="time" 193 203 type="time" 194 204 step="1" 195 - defaultValue={format(field.value, "HH:mm")} 205 + defaultValue={new Date().toTimeString().slice(0, 8)} 196 206 className="peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" 197 207 onChange={(e) => { 198 208 try { 199 - const date = field.value 200 - ?.toISOString() 201 - .split("T")[0]; 209 + const timeValue = e.target.value; 210 + if (!timeValue || !field.value) return; 211 + 212 + const [hours, minutes, seconds] = timeValue 213 + .split(":") 214 + .map(Number); 202 215 203 - field.onChange( 204 - new Date(`${date}T${e.target.value}`), 216 + const newDate = new Date(field.value); 217 + newDate.setHours( 218 + hours, 219 + minutes, 220 + seconds || 0, 221 + 0, 205 222 ); 223 + 224 + field.onChange(newDate); 206 225 } catch (error) { 207 226 console.error(error); 208 227 }
+64 -33
apps/dashboard/src/components/forms/status-report/form.tsx
··· 1 1 "use client"; 2 2 3 + import { 4 + EmptyStateContainer, 5 + EmptyStateTitle, 6 + } from "@/components/content/empty-state"; 3 7 import { ProcessMessage } from "@/components/content/process-message"; 4 8 import { 5 9 FormCardContent, ··· 209 213 <Calendar 210 214 mode="single" 211 215 selected={field.value} 212 - onSelect={field.onChange} 216 + onSelect={(selectedDate) => { 217 + if (!selectedDate) return; 218 + const newDate = new Date(selectedDate); 219 + newDate.setHours( 220 + field.value.getHours(), 221 + field.value.getMinutes(), 222 + field.value.getSeconds(), 223 + field.value.getMilliseconds(), 224 + ); 225 + field.onChange(newDate); 226 + }} 213 227 disabled={(date) => 214 228 date > new Date() || date < new Date("1900-01-01") 215 229 } ··· 225 239 id="time" 226 240 type="time" 227 241 step="1" 228 - defaultValue="12:00:00" 242 + defaultValue={new Date() 243 + .toTimeString() 244 + .slice(0, 8)} 229 245 className="peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" 230 246 onChange={(e) => { 231 247 try { 232 - const date = field.value 233 - ?.toISOString() 234 - .split("T")[0]; 248 + const timeValue = e.target.value; 249 + if (!timeValue || !field.value) return; 250 + 251 + const [hours, minutes, seconds] = timeValue 252 + .split(":") 253 + .map(Number); 235 254 236 - field.onChange( 237 - new Date(`${date}T${e.target.value}`), 255 + const newDate = new Date(field.value); 256 + newDate.setHours( 257 + hours, 258 + minutes, 259 + seconds || 0, 260 + 0, 238 261 ); 262 + 263 + field.onChange(newDate); 239 264 } catch (error) { 240 265 console.error(error); 241 266 } ··· 303 328 <FormDescription> 304 329 Select the monitors you want to notify. 305 330 </FormDescription> 306 - <div className="grid gap-3"> 307 - <div className="flex items-center gap-2"> 308 - <FormControl> 309 - <Checkbox 310 - id="all" 311 - checked={field.value?.length === monitors.length} 312 - onCheckedChange={(checked) => { 313 - field.onChange( 314 - checked ? monitors.map((m) => m.id) : [], 315 - ); 316 - }} 317 - /> 318 - </FormControl> 319 - <Label htmlFor="all">Select all</Label> 320 - </div> 321 - {monitors.map((item) => ( 322 - <div key={item.id} className="flex items-center gap-2"> 331 + {monitors.length ? ( 332 + <div className="grid gap-3"> 333 + <div className="flex items-center gap-2"> 323 334 <FormControl> 324 335 <Checkbox 325 - id={String(item.id)} 326 - checked={field.value?.includes(item.id)} 336 + id="all" 337 + checked={field.value?.length === monitors.length} 327 338 onCheckedChange={(checked) => { 328 - const newValue = checked 329 - ? [...(field.value || []), item.id] 330 - : field.value?.filter((id) => id !== item.id); 331 - field.onChange(newValue); 339 + field.onChange( 340 + checked ? monitors.map((m) => m.id) : [], 341 + ); 332 342 }} 333 343 /> 334 344 </FormControl> 335 - <Label htmlFor={String(item.id)}>{item.name}</Label> 345 + <Label htmlFor="all">Select all</Label> 336 346 </div> 337 - ))} 338 - </div> 347 + {monitors.map((item) => ( 348 + <div key={item.id} className="flex items-center gap-2"> 349 + <FormControl> 350 + <Checkbox 351 + id={String(item.id)} 352 + checked={field.value?.includes(item.id)} 353 + onCheckedChange={(checked) => { 354 + const newValue = checked 355 + ? [...(field.value || []), item.id] 356 + : field.value?.filter((id) => id !== item.id); 357 + field.onChange(newValue); 358 + }} 359 + /> 360 + </FormControl> 361 + <Label htmlFor={String(item.id)}>{item.name}</Label> 362 + </div> 363 + ))} 364 + </div> 365 + ) : ( 366 + <EmptyStateContainer> 367 + <EmptyStateTitle>No monitors found</EmptyStateTitle> 368 + </EmptyStateContainer> 369 + )} 339 370 <FormMessage /> 340 371 </FormItem> 341 372 )}