Openstatus www.openstatus.dev

chore: add chart filter details (#598)

authored by

Maximilian Kaske and committed by
GitHub
dda921ed fd24ab31

+163 -60
+33 -16
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/date-picker-preset.tsx
··· 2 2 3 3 import * as React from "react"; 4 4 import { usePathname, useRouter } from "next/navigation"; 5 - import { CalendarIcon } from "lucide-react"; 5 + import { CalendarIcon, HelpCircle } from "lucide-react"; 6 6 7 7 import { 8 + Label, 9 + Popover, 10 + PopoverContent, 11 + PopoverTrigger, 8 12 Select, 9 13 SelectContent, 10 14 SelectItem, ··· 34 38 } 35 39 36 40 return ( 37 - <Select defaultValue={period} onValueChange={onSelect}> 38 - <SelectTrigger className="w-[150px] text-left"> 39 - <span className="flex items-center gap-2"> 40 - <CalendarIcon className="h-4 w-4" /> 41 - <SelectValue placeholder="Pick a range" /> 42 - </span> 43 - </SelectTrigger> 44 - <SelectContent> 45 - {periods.map((period) => ( 46 - <SelectItem key={period} value={period}> 47 - {renderLabel(period)} 48 - </SelectItem> 49 - ))} 50 - </SelectContent> 51 - </Select> 41 + <div className="grid gap-1"> 42 + <div className="flex items-center gap-1.5"> 43 + <Label htmlFor="period">Period</Label> 44 + <Popover> 45 + <PopoverTrigger> 46 + <HelpCircle className="text-muted-foreground h-4 w-4" /> 47 + </PopoverTrigger> 48 + <PopoverContent side="top" className="text-sm"> 49 + <p>Specifies a time range for analysis.</p> 50 + </PopoverContent> 51 + </Popover> 52 + </div> 53 + <Select defaultValue={period} onValueChange={onSelect}> 54 + <SelectTrigger className="w-[150px] text-left" id="period"> 55 + <span className="flex items-center gap-2"> 56 + <CalendarIcon className="h-4 w-4" /> 57 + <SelectValue placeholder="Pick a range" /> 58 + </span> 59 + </SelectTrigger> 60 + <SelectContent> 61 + {periods.map((period) => ( 62 + <SelectItem key={period} value={period}> 63 + {renderLabel(period)} 64 + </SelectItem> 65 + ))} 66 + </SelectContent> 67 + </Select> 68 + </div> 52 69 ); 53 70 }
+39 -16
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/interval-preset.tsx
··· 2 2 3 3 import * as React from "react"; 4 4 import { usePathname, useRouter } from "next/navigation"; 5 - import { Hourglass } from "lucide-react"; 5 + import { HelpCircle, Hourglass } from "lucide-react"; 6 6 7 7 import { 8 + Label, 9 + Popover, 10 + PopoverContent, 11 + PopoverTrigger, 8 12 Select, 9 13 SelectContent, 10 14 SelectItem, 11 15 SelectTrigger, 12 16 SelectValue, 17 + Separator, 13 18 } from "@openstatus/ui"; 14 19 15 20 import useUpdateSearchParams from "@/hooks/use-update-search-params"; ··· 27 32 } 28 33 29 34 return ( 30 - <Select onValueChange={onSelect} defaultValue={interval}> 31 - <SelectTrigger className="w-[100px]"> 32 - <span className="flex items-center gap-2"> 33 - <Hourglass className="h-4 w-4" /> 34 - <SelectValue /> 35 - </span> 36 - </SelectTrigger> 37 - <SelectContent> 38 - {intervals.map((interval) => ( 39 - <SelectItem key={interval} value={interval}> 40 - {interval} 41 - </SelectItem> 42 - ))} 43 - </SelectContent> 44 - </Select> 35 + <div className="grid gap-1"> 36 + <div className="flex items-center gap-1.5"> 37 + <Label htmlFor="interval">Interval</Label> 38 + <Popover> 39 + <PopoverTrigger> 40 + <HelpCircle className="text-muted-foreground h-4 w-4" /> 41 + </PopoverTrigger> 42 + <PopoverContent side="top" className="text-sm"> 43 + <p>Aggregate and process data at regular time intervals.</p> 44 + <Separator className="my-1" /> 45 + <p className="text-muted-foreground"> 46 + 30m should be aligned to the beginning of 30-minute intervals for 47 + data analysis and aggregation purposes 48 + </p> 49 + </PopoverContent> 50 + </Popover> 51 + </div> 52 + <Select onValueChange={onSelect} defaultValue={interval}> 53 + <SelectTrigger className="w-[150px]" id="interval"> 54 + <span className="flex items-center gap-2"> 55 + <Hourglass className="h-4 w-4" /> 56 + <SelectValue /> 57 + </span> 58 + </SelectTrigger> 59 + <SelectContent> 60 + {intervals.map((interval) => ( 61 + <SelectItem key={interval} value={interval}> 62 + {interval} 63 + </SelectItem> 64 + ))} 65 + </SelectContent> 66 + </Select> 67 + </div> 45 68 ); 46 69 }
+58 -22
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/_components/quantile-preset.tsx
··· 2 2 3 3 import * as React from "react"; 4 4 import { usePathname, useRouter } from "next/navigation"; 5 - import { CandlestickChart } from "lucide-react"; 5 + import { CandlestickChart, HelpCircle } from "lucide-react"; 6 6 7 7 import { 8 + Label, 9 + Popover, 10 + PopoverContent, 11 + PopoverTrigger, 8 12 Select, 9 13 SelectContent, 10 14 SelectItem, 11 15 SelectSeparator, 12 16 SelectTrigger, 13 17 SelectValue, 18 + Separator, 14 19 } from "@openstatus/ui"; 15 20 16 21 import useUpdateSearchParams from "@/hooks/use-update-search-params"; 17 22 import { quantiles } from "../utils"; 18 23 import type { Quantile } from "../utils"; 19 24 20 - export function QuantilePreset({ quantile }: { quantile: Quantile }) { 25 + export function QuantilePreset({ 26 + quantile, 27 + disabled, 28 + }: { 29 + quantile: Quantile; 30 + disabled?: boolean; 31 + }) { 21 32 const router = useRouter(); 22 33 const pathname = usePathname(); 23 34 const updateSearchParams = useUpdateSearchParams(); ··· 28 39 } 29 40 30 41 return ( 31 - <Select onValueChange={onSelect} defaultValue={quantile}> 32 - <SelectTrigger className="w-[100px] uppercase"> 33 - <span className="flex items-center gap-2"> 34 - <CandlestickChart className="h-4 w-4" /> 35 - <SelectValue /> 36 - </span> 37 - </SelectTrigger> 38 - <SelectContent> 39 - {quantiles.map((quantile) => { 40 - return ( 41 - <React.Fragment key={quantile}> 42 - {quantile === "avg" && <SelectSeparator />} 43 - <SelectItem value={quantile} className="uppercase"> 44 - {quantile} 45 - </SelectItem> 46 - </React.Fragment> 47 - ); 48 - })} 49 - </SelectContent> 50 - </Select> 42 + <div className="grid gap-1"> 43 + <div className="flex items-center gap-1.5"> 44 + <Label htmlFor="quantile">Quantile</Label> 45 + <Popover> 46 + <PopoverTrigger> 47 + <HelpCircle className="text-muted-foreground h-4 w-4" /> 48 + </PopoverTrigger> 49 + <PopoverContent side="top" className="text-sm"> 50 + <p> 51 + Defines a statistical measure representing a specific percentile. 52 + </p> 53 + <Separator className="my-1" /> 54 + <p className="text-muted-foreground"> 55 + The p95 quantile represents the value below which 95% of the data 56 + points in a dataset fall, indicating a high percentile level 57 + within the distribution. 58 + </p> 59 + </PopoverContent> 60 + </Popover> 61 + </div> 62 + <Select 63 + onValueChange={onSelect} 64 + defaultValue={quantile} 65 + disabled={disabled} 66 + > 67 + <SelectTrigger className="w-[150px] uppercase" id="quantile"> 68 + <span className="flex items-center gap-2"> 69 + <CandlestickChart className="h-4 w-4" /> 70 + <SelectValue /> 71 + </span> 72 + </SelectTrigger> 73 + <SelectContent> 74 + {quantiles.map((quantile) => { 75 + return ( 76 + <React.Fragment key={quantile}> 77 + {quantile === "avg" && <SelectSeparator />} 78 + <SelectItem value={quantile} className="uppercase"> 79 + {quantile} 80 + </SelectItem> 81 + </React.Fragment> 82 + ); 83 + })} 84 + </SelectContent> 85 + </Select> 86 + </div> 51 87 ); 52 88 }
+4 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/data/loading.tsx
··· 6 6 return ( 7 7 <div className="grid gap-4"> 8 8 <div className="flex flex-wrap items-center gap-2 sm:justify-end"> 9 - <Skeleton className="h-10 w-32" /> 9 + <div className="grid gap-1"> 10 + <Skeleton className="h-4 w-12" /> 11 + <Skeleton className="h-10 w-[150px]" /> 12 + </div> 10 13 </div> 11 14 <div className="grid gap-3"> 12 15 <div className="flex items-center gap-2">
+1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/_components/chart.tsx
··· 40 40 ]} 41 41 onValueChange={(v) => void 0} // that prop makes the chart interactive 42 42 valueFormatter={dataFormatter} 43 + curveType="natural" 43 44 /> 44 45 </Card> 45 46 );
+12 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/loading.tsx
··· 5 5 <div className="grid gap-4"> 6 6 <Skeleton className="h-5 w-40" /> 7 7 <div className="flex flex-wrap items-center gap-2 sm:justify-end"> 8 - <Skeleton className="h-10 w-32" /> 9 - <Skeleton className="h-10 w-24" /> 8 + <div className="grid gap-1"> 9 + <Skeleton className="h-4 w-12" /> 10 + <Skeleton className="h-10 w-[150px]" /> 11 + </div> 12 + <div className="grid gap-1"> 13 + <Skeleton className="h-4 w-12" /> 14 + <Skeleton className="h-10 w-[150px]" /> 15 + </div> 16 + <div className="grid gap-1"> 17 + <Skeleton className="h-4 w-12" /> 18 + <Skeleton className="h-10 w-[150px]" /> 19 + </div> 10 20 </div> 11 21 <Skeleton className="h-[396px] w-full" /> 12 22 </div>
+6 -2
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 53 53 } 54 54 55 55 const date = getDateByPeriod(search.data.period); 56 - const minutes = getMinutesByInterval(search.data.interval); 56 + const intervalMinutes = getMinutesByInterval(search.data.interval); 57 + const periodicityMinutes = getMinutesByInterval(monitor.periodicity); 58 + 59 + const isQuantileDisabled = intervalMinutes <= periodicityMinutes; 60 + const minutes = isQuantileDisabled ? periodicityMinutes : intervalMinutes; 57 61 58 62 const data = await getResponseGraphData({ 59 63 monitorId: id, ··· 89 93 <div className="flex flex-wrap items-center gap-2 sm:justify-end"> 90 94 {/* IDEA: add tooltip for description */} 91 95 <DatePickerPreset period={period} /> 92 - <QuantilePreset quantile={quantile} /> 96 + <QuantilePreset quantile={quantile} disabled={isQuantileDisabled} /> 93 97 <IntervalPreset interval={interval} /> 94 98 </div> 95 99 <ChartWrapper period={period} quantile={quantile} data={data} />
+10 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/utils.ts
··· 7 7 subHours, 8 8 } from "date-fns"; 9 9 10 + import type { MonitorPeriodicity } from "@openstatus/db/src/schema"; 11 + 10 12 export const periods = ["1h", "1d", "3d"] as const; // If neeeded (e.g. Pro plans), "7d", "30d" 11 13 export const quantiles = ["p99", "p95", "p90", "p75", "avg"] as const; 12 14 export const intervals = ["1m", "10m", "30m", "1h"] as const; ··· 38 40 } 39 41 } 40 42 41 - export function getMinutesByInterval(interval: Interval) { 43 + export function getMinutesByInterval(interval: MonitorPeriodicity) { 42 44 switch (interval) { 45 + case "30s": 46 + // return 0.5; 47 + return 1; // FIX TINYBIRD 43 48 case "1m": 44 49 return 1; 50 + case "5m": 51 + return 5; 45 52 case "10m": 46 53 return 10; 47 54 case "30m": 48 55 return 30; 49 56 case "1h": 50 57 return 60; 58 + case "other": 59 + return 60; // TODO: remove "other" from here 51 60 default: 52 61 const _exhaustiveCheck: never = interval; 53 62 throw new Error(`Unhandled interval: ${_exhaustiveCheck}`);