Openstatus www.openstatus.dev

fix: small stuff (#1406)

authored by

Maximilian Kaske and committed by
GitHub
c53522ce 8bbb4806

+101 -79
+4 -19
apps/dashboard/src/components/forms/status-page/form-configuration.tsx
··· 44 44 import { zodResolver } from "@hookform/resolvers/zod"; 45 45 import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; 46 46 import { isTRPCClientError } from "@trpc/client"; 47 - import { ArrowUpRight, Globe, Info } from "lucide-react"; 47 + import { ArrowUpRight, Info } from "lucide-react"; 48 48 import { parseAsStringLiteral, useQueryStates } from "nuqs"; 49 49 import { useForm } from "react-hook-form"; 50 50 import { toast } from "sonner"; ··· 155 155 Use the latest version of the status page and customize it. 156 156 </FormCardDescription> 157 157 </FormCardHeader> 158 - <FormCardContent className="grid gap-4 sm:grid-cols-2"> 158 + <FormCardContent className="grid gap-4 md:grid-cols-3"> 159 159 <FormField 160 160 control={form.control} 161 161 name="new" ··· 176 176 </FormItem> 177 177 )} 178 178 /> 179 - <div className="flex items-center sm:justify-end"> 180 - <Button variant="secondary" size="sm" asChild> 179 + <div className="flex items-center md:col-span-2 md:justify-end"> 180 + <Button size="sm" asChild> 181 181 <Link 182 182 href={configLink} 183 183 rel="noreferrer" ··· 189 189 </Link> 190 190 </Button> 191 191 </div> 192 - <Note color="info" className="col-span-full"> 193 - <Globe /> 194 - <p className="text-sm"> 195 - With that version, we provide a new shorter domain{" "} 196 - <code className="font-commit-mono"> 197 - https://[slug].stpg.dev 198 - </code> 199 - . Once globally enabled, it will act as redirector for the old 200 - domain{" "} 201 - <code className="font-commit-mono"> 202 - https://[slug].openstatus.dev 203 - </code> 204 - . 205 - </p> 206 - </Note> 207 192 </FormCardContent> 208 193 {watchNew && ( 209 194 <>
+83 -54
apps/status-page/src/components/chart/chart-legend-badge.tsx
··· 7 7 TooltipTrigger, 8 8 } from "@/components/ui/tooltip"; 9 9 import { cn } from "@/lib/utils"; 10 + import * as React from "react"; 10 11 import type * as RechartsPrimitive from "recharts"; 11 12 import type { Payload } from "recharts/types/component/DefaultLegendContent"; 12 13 ··· 33 34 tooltip?: Record<string, string | undefined>; 34 35 }) { 35 36 const { config } = useChart(); 37 + const [focusedIndex, setFocusedIndex] = React.useState(0); 38 + const buttonRefs = React.useRef<(HTMLButtonElement | null)[]>([]); 36 39 37 40 if (!payload?.length) { 38 41 return null; 39 42 } 40 43 44 + const filteredPayload = payload.filter((item) => item.type !== "none"); 41 45 const hasMaxActive = active && maxActive ? active.length >= maxActive : false; 42 46 47 + const handleKeyDown = (event: React.KeyboardEvent) => { 48 + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { 49 + event.preventDefault(); 50 + const direction = event.key === "ArrowLeft" ? -1 : 1; 51 + let nextIndex = 0; 52 + nextIndex = 53 + (focusedIndex + direction + filteredPayload.length) % 54 + filteredPayload.length; 55 + setFocusedIndex(nextIndex); 56 + while (buttonRefs.current[nextIndex]?.disabled === true) { 57 + nextIndex = 58 + (nextIndex + direction + filteredPayload.length) % 59 + filteredPayload.length; 60 + } 61 + buttonRefs.current[nextIndex]?.focus(); 62 + } 63 + }; 64 + 43 65 return ( 44 66 <div 45 67 className={cn( ··· 47 69 verticalAlign === "top" ? "pb-3" : "pt-3", 48 70 className, 49 71 )} 72 + onKeyDown={handleKeyDown} 73 + role="group" 74 + aria-label="Chart legend" 50 75 > 51 - {payload 52 - // NOTE: recharts supports "none" type for legend items 53 - .filter((item) => item.type !== "none") 54 - .map((item) => { 55 - const key = `${nameKey || item.dataKey || "value"}`; 56 - const itemConfig = getPayloadConfigFromPayload(config, item, key); 57 - const suffix = annotation?.[item.dataKey as string]; 58 - const tooltipLabel = tooltip?.[item.dataKey as string]; 59 - const isActive = active?.includes(item.dataKey); 76 + {filteredPayload.map((item, index) => { 77 + const key = `${nameKey || item.dataKey || "value"}`; 78 + const itemConfig = getPayloadConfigFromPayload(config, item, key); 79 + const suffix = annotation?.[item.dataKey as string]; 80 + const tooltipLabel = tooltip?.[item.dataKey as string]; 81 + const isActive = active ? active?.includes(item.dataKey) : true; 82 + const isFocused = index === focusedIndex; 83 + 84 + const badge = ( 85 + <button 86 + key={item.value} 87 + type="button" 88 + ref={(el) => { 89 + buttonRefs.current[index] = el; 90 + }} 91 + className={cn( 92 + badgeVariants({ variant: "outline" }), 93 + "outline-none", 94 + "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", 95 + !isActive && "opacity-60", 96 + !isActive && hasMaxActive && "cursor-not-allowed opacity-40", 97 + )} 98 + onClick={(e) => { 99 + e.stopPropagation(); 100 + e.preventDefault(); 101 + setFocusedIndex(index); 102 + handleActive?.(item); 103 + }} 104 + onFocus={() => setFocusedIndex(index)} 105 + disabled={!isActive && hasMaxActive} 106 + tabIndex={isFocused ? 0 : -1} 107 + > 108 + {itemConfig?.icon && !hideIcon ? ( 109 + <itemConfig.icon /> 110 + ) : ( 111 + <div 112 + className="h-2 w-2 shrink-0 rounded-[2px]" 113 + style={{ 114 + backgroundColor: item.color, 115 + }} 116 + /> 117 + )} 118 + {itemConfig?.label} 119 + {suffix !== undefined ? ( 120 + <span className="font-mono text-[10px] text-muted-foreground"> 121 + {suffix} 122 + </span> 123 + ) : null} 124 + </button> 125 + ); 60 126 61 - const badge = ( 62 - <button 63 - key={item.value} 64 - type="button" 65 - className={cn( 66 - badgeVariants({ variant: "outline" }), 67 - "outline-none", 68 - "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", 69 - !isActive && "opacity-60", 70 - !isActive && hasMaxActive && "cursor-not-allowed opacity-40", 71 - )} 72 - onClick={(e) => { 73 - e.stopPropagation(); 74 - e.preventDefault(); 75 - handleActive?.(item); 76 - }} 77 - disabled={!isActive && hasMaxActive} 78 - > 79 - {itemConfig?.icon && !hideIcon ? ( 80 - <itemConfig.icon /> 81 - ) : ( 82 - <div 83 - className="h-2 w-2 shrink-0 rounded-(--radius-xs)" 84 - style={{ 85 - backgroundColor: item.color, 86 - }} 87 - /> 88 - )} 89 - {itemConfig?.label} 90 - {suffix !== undefined ? ( 91 - <span className="font-mono text-[10px] text-muted-foreground"> 92 - {suffix} 93 - </span> 94 - ) : null} 95 - </button> 127 + if (tooltipLabel) { 128 + return ( 129 + <ChartLegendTooltip key={item.value} tooltip={tooltipLabel}> 130 + {badge} 131 + </ChartLegendTooltip> 96 132 ); 133 + } 97 134 98 - if (tooltipLabel) { 99 - return ( 100 - <ChartLegendTooltip key={item.value} tooltip={tooltipLabel}> 101 - {badge} 102 - </ChartLegendTooltip> 103 - ); 104 - } 105 - 106 - return badge; 107 - })} 135 + return badge; 136 + })} 108 137 </div> 109 138 ); 110 139 }
+8 -1
apps/status-page/src/components/chart/chart-line-regions.tsx
··· 90 90 {} as Record<string, string>, 91 91 ); 92 92 93 - // TODO: tooltip 93 + const tooltip = regions.reduce( 94 + (acc, region) => { 95 + acc[region.code] = region.location; 96 + return acc; 97 + }, 98 + {} as Record<string, string>, 99 + ); 94 100 95 101 return ( 96 102 <ChartContainer ··· 171 177 }} 172 178 active={activeSeries} 173 179 annotation={annotation} 180 + tooltip={tooltip} 174 181 maxActive={6} 175 182 className="justify-start overflow-x-scroll ps-1 pt-1 font-mono" 176 183 />
+1 -1
apps/status-page/src/components/nav/footer.tsx
··· 22 22 <div className="mx-auto flex max-w-2xl items-center justify-between gap-4 px-3 py-2"> 23 23 <div> 24 24 <p className="font-mono text-muted-foreground text-sm leading-none"> 25 - powered by <Link href="#">openstatus</Link> 25 + powered by <Link href="https://openstatus.dev">openstatus</Link> 26 26 </p> 27 27 <TimestampHoverCard date={new Date(dataUpdatedAt)} side="top"> 28 28 <span className="text-muted-foreground/70 text-xs">
+2 -2
apps/status-page/src/components/status-page/floating-button.tsx
··· 194 194 } 195 195 196 196 if (configToken) setConfigToken(null); 197 - }, [open, token]); 197 + }, [token]); 198 198 199 199 if (!display) return null; 200 200 ··· 348 348 target="_blank" 349 349 rel="noreferrer" 350 350 > 351 - Dashboard 351 + Save Configuration 352 352 </a> 353 353 </Button> 354 354 </div>
+1 -1
packages/api/src/router/statusPage.ts
··· 232 232 if (!_page) return null; 233 233 234 234 const monitors = _page.monitorsToPages.filter( 235 - (m) => m.monitor.active && !m.monitor.deletedAt, 235 + (m) => !m.monitor.deletedAt, 236 236 ); 237 237 238 238 if (monitors.length !== opts.input.monitorIds.length) return null;
+2 -1
packages/api/src/router/statusPage.utils.ts
··· 404 404 405 405 return segments.map((segment) => ({ 406 406 status: segment.status, 407 - height: (segment.count / totalDuration) * 100, 407 + // NOTE: if totalDuration is 0 (single event without duration), we want to show 100% for the segment 408 + height: totalDuration > 0 ? (segment.count / totalDuration) * 100 : 100, 408 409 })); 409 410 } 410 411