diff --git a/apps/dashboard/src/components/chat/tool-renderers/list-private-locations.tsx b/apps/dashboard/src/components/chat/tool-renderers/list-private-locations.tsx index 0e4f6277..3471bbf6 100644 --- a/apps/dashboard/src/components/chat/tool-renderers/list-private-locations.tsx +++ b/apps/dashboard/src/components/chat/tool-renderers/list-private-locations.tsx @@ -2,6 +2,7 @@ import type { AgentToolOutput } from "@openstatus/services/agent-tools"; import { TableCellDate } from "@/components/data-table/table-cell-date"; import { TableCellLink } from "@/components/data-table/table-cell-link"; +import { TableCellMetadata } from "@/components/data-table/table-cell-metadata"; import { TableCellNumber } from "@/components/data-table/table-cell-number"; import { TableCellText } from "@/components/data-table/table-cell-text"; @@ -11,13 +12,14 @@ type Output = AgentToolOutput<"list_private_locations">; export function listPrivateLocationsTable( output: Output, -): ResultTableData<"name" | "monitors" | "lastSeenAt" | "id"> { +): ResultTableData<"name" | "monitors" | "metadata" | "lastSeenAt" | "id"> { const items = output?.items ?? []; return { empty: "No private locations.", columns: [ { key: "name", header: "Name" }, { key: "monitors", header: "Monitors" }, + { key: "metadata", header: "Metadata" }, { key: "lastSeenAt", header: "Last seen" }, { key: "id", header: "ID" }, ], @@ -40,6 +42,7 @@ export function listPrivateLocationsTable( className="text-muted-foreground font-mono" /> ), + metadata: , lastSeenAt: location.lastSeenAt ? ( ) : ( diff --git a/apps/dashboard/src/components/controls-search/command-region.tsx b/apps/dashboard/src/components/controls-search/command-region.tsx index db8f7dbe..5b0805c8 100644 --- a/apps/dashboard/src/components/controls-search/command-region.tsx +++ b/apps/dashboard/src/components/controls-search/command-region.tsx @@ -28,6 +28,10 @@ import { BillingOverlayButton, BillingOverlayDescription, } from "@/components/content/billing-overlay"; +import { + TableCellMetadata, + getMetadataEntries, +} from "@/components/data-table/table-cell-metadata"; import type { REGIONS } from "@/data/metrics.client"; import { useTRPC } from "@/lib/trpc/client"; @@ -36,7 +40,11 @@ export function CommandRegion({ privateLocations, }: { regions: (typeof REGIONS)[number][]; - privateLocations?: { id: number; name: string }[]; + privateLocations?: { + id: number; + name: string; + metadata?: Record | null; + }[]; }) { const trpc = useTRPC(); const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); @@ -151,31 +159,44 @@ export function CommandRegion({ )} {privateLocations && privateLocations.length > 0 ? ( - {privateLocations.map((location) => ( - { - setSelectedRegions((prev) => - prev.includes(location.id.toString()) - ? prev.filter((r) => r !== location.id.toString()) - : [...prev, location.id.toString()], - ); - }} - > - - {location.name} - - - ))} + {privateLocations.map((location) => { + const metadata = getMetadataEntries(location.metadata); + const keywords = [location.name, ...metadata.flat()]; + + return ( + { + setSelectedRegions((prev) => + prev.includes(location.id.toString()) + ? prev.filter((r) => r !== location.id.toString()) + : [...prev, location.id.toString()], + ); + }} + > + + + {location.name} + + {metadata.length > 0 ? ( + + ) : null} + + + ); + })} ) : null} No region found. diff --git a/apps/dashboard/src/components/data-table/private-locations/columns.tsx b/apps/dashboard/src/components/data-table/private-locations/columns.tsx index d01f8cc2..c4c2a6cd 100644 --- a/apps/dashboard/src/components/data-table/private-locations/columns.tsx +++ b/apps/dashboard/src/components/data-table/private-locations/columns.tsx @@ -9,6 +9,7 @@ import { cn } from "@/lib/utils"; import { TableCellBadge } from "../table-cell-badge"; import { TableCellDate } from "../table-cell-date"; +import { TableCellMetadata } from "../table-cell-metadata"; import { TableCellText } from "../table-cell-text"; import { DataTableRowActions } from "./data-table-row-actions"; @@ -43,6 +44,15 @@ export const columns: ColumnDef[] = [ ); }, }, + { + accessorKey: "metadata", + header: ({ column }) => ( + + ), + enableSorting: false, + enableHiding: true, + cell: ({ row }) => , + }, { accessorKey: "lastSeenAt", header: ({ column }) => ( diff --git a/apps/dashboard/src/components/data-table/table-cell-metadata.tsx b/apps/dashboard/src/components/data-table/table-cell-metadata.tsx new file mode 100644 index 00000000..40a6e334 --- /dev/null +++ b/apps/dashboard/src/components/data-table/table-cell-metadata.tsx @@ -0,0 +1,109 @@ +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@openstatus/ui/components/ui/tooltip"; +import { useEffect, useRef, useState } from "react"; + +import { Pill } from "@/components/common/pill"; +import { cn } from "@/lib/utils"; + +/** Sorted `[key, value]` pairs with empty values dropped; `[]` for non-objects. */ +export function getMetadataEntries(value: unknown): [string, string][] { + if (!value || typeof value !== "object" || Array.isArray(value)) return []; + return Object.entries(value) + .filter((entry): entry is [string, string] => { + return typeof entry[1] === "string" && entry[1].trim().length > 0; + }) + .sort(([a], [b]) => a.localeCompare(b)); +} + +export function TableCellMetadata({ + value, + maxEntries, + className, + ...props +}: React.ComponentProps<"div"> & { + value: unknown; + /** Collapse the rest into a `+n` chip with the full list in a tooltip. */ + maxEntries?: number; +}) { + const entries = getMetadataEntries(value); + + if (entries.length === 0) { + return ( +
+ - +
+ ); + } + + const visible = maxEntries ? entries.slice(0, maxEntries) : entries; + const hidden = maxEntries ? entries.slice(maxEntries) : []; + + return ( +
+ + {visible.map(([key, val]) => ( + + ))} + {hidden.length > 0 ? ( + + + + +{hidden.length} + + + + {hidden.map(([key, val]) => ( +
+ {key}: {val} +
+ ))} +
+
+ ) : null} +
+
+ ); +} + +function MetadataPill({ label, value }: { label: string; value: string }) { + const ref = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + + useEffect(() => { + if (ref.current) { + setIsTruncated(ref.current.scrollWidth > ref.current.clientWidth); + } + }, [value]); + + const pill = ( + + {value} + + } + variant="outline" + /> + ); + + if (!isTruncated) return pill; + + return ( + + event.preventDefault()} asChild> +
{pill}
+
+ + {label}: {value} + +
+ ); +} diff --git a/apps/dashboard/src/components/forms/monitor/form-scheduling-regions.tsx b/apps/dashboard/src/components/forms/monitor/form-scheduling-regions.tsx index 884e1cff..df06bf1b 100644 --- a/apps/dashboard/src/components/forms/monitor/form-scheduling-regions.tsx +++ b/apps/dashboard/src/components/forms/monitor/form-scheduling-regions.tsx @@ -33,6 +33,10 @@ import { z } from "zod"; import { IconCloudProviderTooltip } from "@/components/common/icon-cloud-provider"; import { Link } from "@/components/common/link"; import { Note, NoteButton } from "@/components/common/note"; +import { + TableCellMetadata, + getMetadataEntries, +} from "@/components/data-table/table-cell-metadata"; import { UpgradeDialog } from "@/components/dialogs/upgrade"; import { FormCard, @@ -49,7 +53,11 @@ import { useTRPC } from "@/lib/trpc/client"; const DEFAULT_PERIODICITY = "10m"; const DEFAULT_REGIONS = ["ams", "fra", "iad", "syd", "jnb", "gru"]; const PERIODICITY = monitorPeriodicity.filter((p) => p !== "other"); -const DEFAULT_PRIVATE_LOCATIONS = [] satisfies { id: number; name: string }[]; +const DEFAULT_PRIVATE_LOCATIONS = [] satisfies { + id: number; + name: string; + metadata?: Record | null; +}[]; const schema = z.object({ regions: z.array(z.string()), @@ -67,7 +75,11 @@ export function FormSchedulingRegions({ }: Omit, "onSubmit"> & { defaultValues?: FormValues; onSubmit: (values: FormValues) => Promise; - privateLocations: { id: number; name: string }[]; + privateLocations: { + id: number; + name: string; + metadata?: Record | null; + }[]; }) { const trpc = useTRPC(); const [openDialog, setOpenDialog] = useState(false); @@ -402,6 +414,7 @@ export function FormSchedulingRegions({ > - - {item.name} + + + {item.name} + + {getMetadataEntries(item.metadata).length > + 0 ? ( + + ) : null}