diff --git a/messages/en-US.json b/messages/en-US.json index 1f3717110..027d9fc38 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1165,6 +1165,7 @@ "labelsNotFound": "Labels not found", "labelSearch": "Search labels", "accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}", + "labelOverflowCount": "+{count, plural, one {# label} other {# labels}}", "accessLabelFilterClear": "Clear label filters", "selectColor": "Select color", "createNewLabel": "Create new org label \"{label}\"", diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 1d6971f73..be60b7b26 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -63,6 +63,7 @@ import { build } from "@server/build"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { LabelBadge } from "./label-badge"; +import { LabelOverflowBadge } from "./label-overflow-badge"; import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; @@ -740,34 +741,17 @@ function ClientResourceLabelCell({ }); } + const visibleLabels = optimisticLabels.slice(0, 3); + const overflowLabels = optimisticLabels.slice(3); + return ( -
- {optimisticLabels.slice(0, 3).map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - {optimisticLabels.length > 3 && ( - - )} +
); } diff --git a/src/components/LabelColumnFilterButton.tsx b/src/components/LabelColumnFilterButton.tsx index ff55e3ebd..84814b252 100644 --- a/src/components/LabelColumnFilterButton.tsx +++ b/src/components/LabelColumnFilterButton.tsx @@ -19,10 +19,14 @@ import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilter import { CheckIcon, Funnel } from "lucide-react"; import { useTranslations } from "next-intl"; import { useMemo, useState } from "react"; -import { Badge } from "./ui/badge"; import { orgQueries } from "@app/lib/queries"; import { useQuery } from "@tanstack/react-query"; import { useDebounce } from "use-debounce"; +import { LabelBadge } from "./label-badge"; +import { LabelOverflowBadge } from "./label-overflow-badge"; +import { LABEL_COLORS } from "./labels-selector"; + +const MAX_VISIBLE_SUMMARY_LABELS = 3; type LabelColumnFilterButtonProps = { selectedValues: string[]; @@ -58,33 +62,47 @@ export function LabelColumnFilterButton({ [selectedValues] ); + const selectedLabels = useMemo( + () => + selectedValues.map((name) => { + const foundLabel = labels.find((label) => label.name === name); + return { + name, + color: foundLabel?.color ?? LABEL_COLORS.gray + }; + }), + [selectedValues, labels] + ); + const summary = useMemo(() => { - if (selectedValues.length === 0) { + if (selectedLabels.length === 0) { return null; } - if (selectedValues.length === 1) { - const foundLabel = labels.find((o) => o.name === selectedValues[0]); - if (foundLabel) { - return ( -
-
- {foundLabel.name} -
- ); - } - return selectedValues[0]; - } - return t("accessLabelFilterCount", { - count: selectedValues.length - }); - }, [selectedValues, labels, t]); + const visibleLabels = selectedLabels.slice(0, MAX_VISIBLE_SUMMARY_LABELS); + const overflowLabels = selectedLabels.slice(MAX_VISIBLE_SUMMARY_LABELS); + + return ( +
+ {visibleLabels.map((label) => ( + + ))} + {overflowLabels.length > 0 && ( + + )} +
+ ); + }, [selectedLabels]); function toggle(value: string) { const next = selectedSet.has(value) @@ -94,7 +112,7 @@ export function LabelColumnFilterButton({ } return ( -
+
@@ -168,7 +175,7 @@ export function LabelColumnFilterButton({ )} />
- {optimisticLabels.slice(0, 3).map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - {optimisticLabels.length > 3 && ( - - )} +
); } diff --git a/src/components/OrgLabelForm.tsx b/src/components/OrgLabelForm.tsx index 9bc81fa07..68257fa22 100644 --- a/src/components/OrgLabelForm.tsx +++ b/src/components/OrgLabelForm.tsx @@ -72,7 +72,6 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) { @@ -104,13 +103,16 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) { className="flex items-center gap-2" >
- {color} + + {color.charAt(0).toUpperCase() + + color.slice(1)} + ) )} diff --git a/src/components/OrgLabelsTable.tsx b/src/components/OrgLabelsTable.tsx index 3ed95a9dc..0b022d2ec 100644 --- a/src/components/OrgLabelsTable.tsx +++ b/src/components/OrgLabelsTable.tsx @@ -121,11 +121,9 @@ export default function OrgLabelsTable({ ) }, { - accessorKey: "actions", + id: "actions", enableHiding: false, - header: () => { - return {t("actions")}; - }, + header: () => , cell: ({ row }) => ( @@ -234,6 +232,7 @@ export default function OrgLabelsTable({ onRefresh={refreshData} isRefreshing={isRefreshing || isFiltering} rowCount={rowCount} + stickyRightColumn="actions" /> ); diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index ac7e424f6..1cd80e136 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -72,6 +72,7 @@ import { ControlledDataTable } from "./ui/controlled-data-table"; import UptimeMiniBar from "./UptimeMiniBar"; import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { LabelBadge } from "./label-badge"; +import { LabelOverflowBadge } from "./label-overflow-badge"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; export type TargetHealth = { @@ -808,34 +809,17 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) { }); } + const visibleLabels = optimisticLabels.slice(0, 3); + const overflowLabels = optimisticLabels.slice(3); + return ( -
- {optimisticLabels.slice(0, 3).map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - {optimisticLabels.length > 3 && ( - - )} +
); } diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 3db076819..08dd66d42 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -62,6 +62,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { cn } from "@app/lib/cn"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { LabelBadge } from "./label-badge"; +import { LabelOverflowBadge } from "./label-overflow-badge"; import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; @@ -395,7 +396,7 @@ export default function SitesTable({ variant="ghost" size="sm" onClick={() => setResourcesDialogSite(siteRow)} - className="flex h-8 items-center gap-2 px-2 font-normal" + className="flex h-8 items-center gap-2 px-0 font-normal" > {siteRow.resourceCount} {t("resources")} @@ -735,34 +736,17 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { }); } + const visibleLabels = optimisticLabels.slice(0, 3); + const overflowLabels = optimisticLabels.slice(3); + return ( -
- {optimisticLabels.slice(0, 3).map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - {optimisticLabels.length > 3 && ( - - )} +
); } diff --git a/src/components/label-badge.tsx b/src/components/label-badge.tsx index 9d84cc350..1519066b3 100644 --- a/src/components/label-badge.tsx +++ b/src/components/label-badge.tsx @@ -1,40 +1,62 @@ import { cn } from "@app/lib/cn"; import { Button } from "./ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; + +const labelBadgeClassName = + "inline-flex h-auto items-center gap-1 rounded-full border border-input bg-background py-0 pl-1.5 pr-2 text-sm shadow-xs"; export type LabelBadgeProps = { name: string; color: string; onClick?: () => void; className?: string; + displayOnly?: boolean; }; export function LabelBadge({ onClick, name, color, - className + className, + displayOnly = false }: LabelBadgeProps) { - return ( - + + ); + + return ( + + + {displayOnly ? ( + + {content} + + ) : ( + + )} + + {name} + ); } diff --git a/src/components/label-overflow-badge.tsx b/src/components/label-overflow-badge.tsx new file mode 100644 index 000000000..cadd2117f --- /dev/null +++ b/src/components/label-overflow-badge.tsx @@ -0,0 +1,97 @@ +import { cn } from "@app/lib/cn"; +import { useTranslations } from "next-intl"; +import { Button } from "./ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; + +export type LabelOverflowItem = { + color: string; + name?: string; +}; + +const labelOverflowBadgeClassName = + "inline-flex h-auto shrink-0 items-center gap-1.5 rounded-full border border-input bg-background py-0 pl-1.5 pr-2 text-sm shadow-xs"; + +export type LabelOverflowBadgeProps = { + labels: LabelOverflowItem[]; + onClick?: () => void; + className?: string; + displayOnly?: boolean; +}; + +const MAX_OVERFLOW_COLORS = 3; + +export function LabelOverflowBadge({ + labels, + onClick, + className, + displayOnly = false +}: LabelOverflowBadgeProps) { + const t = useTranslations(); + + if (labels.length === 0) { + return null; + } + + const displayColors = labels + .slice(0, MAX_OVERFLOW_COLORS) + .map((label) => label.color); + + const overflowNames = labels + .map((label) => label.name) + .filter((name): name is string => Boolean(name)); + + const tooltipContent = + overflowNames.length > 0 + ? overflowNames.join(", ") + : t("labelOverflowCount", { count: labels.length }); + + const content = ( + <> + + {displayColors.map((color, index) => ( + 0 && "-ml-1" + )} + style={{ + // @ts-expect-error css color + "--color": color + }} + /> + ))} + + + {t("labelOverflowCount", { count: labels.length })} + + + ); + + return ( + + + {displayOnly ? ( + + {content} + + ) : ( + + )} + + {tooltipContent} + + ); +} diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index 94b8925b0..1187f72d9 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -215,9 +215,9 @@ export function LabelsSelector({ aria-hidden tabIndex={-1} /> -
+