minor visual adjustments to tags

This commit is contained in:
miloschwartz
2026-05-26 21:34:15 -07:00
parent 9eb55ba68c
commit 05e4ad3200
11 changed files with 263 additions and 151 deletions

View File

@@ -1165,6 +1165,7 @@
"labelsNotFound": "Labels not found", "labelsNotFound": "Labels not found",
"labelSearch": "Search labels", "labelSearch": "Search labels",
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}", "accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
"accessLabelFilterClear": "Clear label filters", "accessLabelFilterClear": "Clear label filters",
"selectColor": "Select color", "selectColor": "Select color",
"createNewLabel": "Create new org label \"{label}\"", "createNewLabel": "Create new org label \"{label}\"",

View File

@@ -63,6 +63,7 @@ import { build } from "@server/build";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LabelBadge } from "./label-badge"; import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
@@ -740,34 +741,17 @@ function ClientResourceLabelCell({
}); });
} }
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return ( return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1"> <div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
size="icon" size="icon"
variant="outline" variant="outline"
className="p-1 size-auto rounded-full" className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")} title={t("addLabels")}
> >
<span className="sr-only">{t("addLabels")}</span> <span className="sr-only">{t("addLabels")}</span>
@@ -782,6 +766,18 @@ function ClientResourceLabelCell({
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div> </div>
); );
} }

View File

@@ -19,10 +19,14 @@ import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilter
import { CheckIcon, Funnel } from "lucide-react"; import { CheckIcon, Funnel } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Badge } from "./ui/badge";
import { orgQueries } from "@app/lib/queries"; import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "use-debounce"; 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 = { type LabelColumnFilterButtonProps = {
selectedValues: string[]; selectedValues: string[];
@@ -58,33 +62,47 @@ export function LabelColumnFilterButton({
[selectedValues] [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(() => { const summary = useMemo(() => {
if (selectedValues.length === 0) { if (selectedLabels.length === 0) {
return null; return null;
} }
if (selectedValues.length === 1) {
const foundLabel = labels.find((o) => o.name === selectedValues[0]);
if (foundLabel) { const visibleLabels = selectedLabels.slice(0, MAX_VISIBLE_SUMMARY_LABELS);
return ( const overflowLabels = selectedLabels.slice(MAX_VISIBLE_SUMMARY_LABELS);
<div className="inline-flex items-center gap-1">
<div return (
className="size-3 rounded-full bg-(--color) flex-none" <div className="flex min-w-0 flex-nowrap items-center gap-1">
style={{ {visibleLabels.map((label) => (
// @ts-expect-error css color <LabelBadge
"--color": foundLabel.color key={label.name}
}} displayOnly
/> name={label.name}
{foundLabel.name} color={label.color}
</div> className="shrink-0"
); />
} ))}
return selectedValues[0]; {overflowLabels.length > 0 && (
} <LabelOverflowBadge
return t("accessLabelFilterCount", { labels={overflowLabels}
count: selectedValues.length displayOnly
}); className="shrink-0"
}, [selectedValues, labels, t]); />
)}
</div>
);
}, [selectedLabels]);
function toggle(value: string) { function toggle(value: string) {
const next = selectedSet.has(value) const next = selectedSet.has(value)
@@ -94,7 +112,7 @@ export function LabelColumnFilterButton({
} }
return ( return (
<div className="flex items-center justify-end"> <div className="flex items-center">
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@@ -111,18 +129,7 @@ export function LabelColumnFilterButton({
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className="shrink-0">{label}</span> <span className="shrink-0">{label}</span>
<Funnel className="size-4 flex-none shrink-0" /> <Funnel className="size-4 flex-none shrink-0" />
{summary && ( {summary}
<Badge
className={cn(
"truncate max-w-40",
selectedValues.length === 1 &&
"pl-1.5 pr-2 h-auto"
)}
variant="secondary"
>
{summary}
</Badge>
)}
</div> </div>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -168,7 +175,7 @@ export function LabelColumnFilterButton({
)} )}
/> />
<div <div
className="size-4 rounded-full bg-(--color) flex-none" className="size-2 rounded-full bg-(--color) flex-none"
style={{ style={{
// @ts-expect-error css color // @ts-expect-error css color
"--color": label.color "--color": label.color

View File

@@ -41,6 +41,7 @@ import { useDebouncedCallback } from "use-debounce";
import z from "zod"; import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton"; import { ColumnFilterButton } from "./ColumnFilterButton";
import { LabelBadge } from "./label-badge"; import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
import { ControlledDataTable } from "./ui/controlled-data-table"; import { ControlledDataTable } from "./ui/controlled-data-table";
@@ -657,34 +658,17 @@ function MachineClientLabelCell({
}); });
} }
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return ( return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1"> <div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
size="icon" size="icon"
variant="outline" variant="outline"
className="p-1 size-auto rounded-full" className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")} title={t("addLabels")}
> >
<span className="sr-only">{t("addLabels")}</span> <span className="sr-only">{t("addLabels")}</span>
@@ -699,6 +683,18 @@ function MachineClientLabelCell({
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div> </div>
); );
} }

View File

@@ -72,7 +72,6 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
placeholder={t("labelPlaceholder")}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -104,13 +103,16 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<div <div
className="size-4 rounded-full bg-(--color) flex-none" className="size-2 rounded-full bg-(--color) flex-none"
style={{ style={{
// @ts-expect-error css color // @ts-expect-error css color
"--color": value "--color": value
}} }}
/> />
<span data-name>{color}</span> <span data-name>
{color.charAt(0).toUpperCase() +
color.slice(1)}
</span>
</SelectItem> </SelectItem>
) )
)} )}

View File

@@ -121,11 +121,9 @@ export default function OrgLabelsTable({
) )
}, },
{ {
accessorKey: "actions", id: "actions",
enableHiding: false, enableHiding: false,
header: () => { header: () => <span className="p-3"></span>,
return <span className="p-3">{t("actions")}</span>;
},
cell: ({ row }) => ( cell: ({ row }) => (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -234,6 +232,7 @@ export default function OrgLabelsTable({
onRefresh={refreshData} onRefresh={refreshData}
isRefreshing={isRefreshing || isFiltering} isRefreshing={isRefreshing || isFiltering}
rowCount={rowCount} rowCount={rowCount}
stickyRightColumn="actions"
/> />
</> </>
); );

View File

@@ -72,6 +72,7 @@ import { ControlledDataTable } from "./ui/controlled-data-table";
import UptimeMiniBar from "./UptimeMiniBar"; import UptimeMiniBar from "./UptimeMiniBar";
import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { LabelBadge } from "./label-badge"; import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
export type TargetHealth = { export type TargetHealth = {
@@ -808,34 +809,17 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) {
}); });
} }
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return ( return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1"> <div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
size="icon" size="icon"
variant="outline" variant="outline"
className="p-1 size-auto rounded-full" className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")} title={t("addLabels")}
> >
<span className="sr-only">{t("addLabels")}</span> <span className="sr-only">{t("addLabels")}</span>
@@ -850,6 +834,18 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) {
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div> </div>
); );
} }

View File

@@ -62,6 +62,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LabelBadge } from "./label-badge"; import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
@@ -395,7 +396,7 @@ export default function SitesTable({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setResourcesDialogSite(siteRow)} 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"
> >
<span className="text-sm tabular-nums"> <span className="text-sm tabular-nums">
{siteRow.resourceCount} {t("resources")} {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 ( return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1"> <div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
size="icon" size="icon"
variant="outline" variant="outline"
className="p-1 size-auto rounded-full" className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")} title={t("addLabels")}
> >
<span className="sr-only">{t("addLabels")}</span> <span className="sr-only">{t("addLabels")}</span>
@@ -777,6 +761,18 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) {
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div> </div>
); );
} }

View File

@@ -1,40 +1,62 @@
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { Button } from "./ui/button"; 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 = { export type LabelBadgeProps = {
name: string; name: string;
color: string; color: string;
onClick?: () => void; onClick?: () => void;
className?: string; className?: string;
displayOnly?: boolean;
}; };
export function LabelBadge({ export function LabelBadge({
onClick, onClick,
name, name,
color, color,
className className,
displayOnly = false
}: LabelBadgeProps) { }: LabelBadgeProps) {
return ( const content = (
<Button <>
variant="outline"
onClick={onClick}
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"pl-1.5 pr-2 py-0 h-auto",
className
)}
>
<div <div
className="size-3 rounded-full bg-(--color) flex-none" className="size-2 flex-none rounded-full bg-(--color)"
style={{ style={{
// @ts-expect-error css color // @ts-expect-error css color
"--color": color "--color": color
}} }}
/> />
<span className="whitespace-nowrap text-ellipsis max-w-16 overflow-hidden relative"> <span className="relative max-w-24 overflow-hidden text-ellipsis whitespace-nowrap">
{name} {name}
</span> </span>
</Button> </>
);
return (
<Tooltip>
<TooltipTrigger asChild>
{displayOnly ? (
<span className={cn(labelBadgeClassName, className)}>
{content}
</span>
) : (
<Button
variant="outline"
onClick={onClick}
className={cn(
labelBadgeClassName,
"cursor-pointer",
className
)}
>
{content}
</Button>
)}
</TooltipTrigger>
<TooltipContent>{name}</TooltipContent>
</Tooltip>
); );
} }

View File

@@ -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 = (
<>
<span className="inline-flex items-center">
{displayColors.map((color, index) => (
<span
key={index}
className={cn(
"size-2 flex-none rounded-full bg-(--color) ring-1 ring-background",
index > 0 && "-ml-1"
)}
style={{
// @ts-expect-error css color
"--color": color
}}
/>
))}
</span>
<span className="whitespace-nowrap text-muted-foreground">
{t("labelOverflowCount", { count: labels.length })}
</span>
</>
);
return (
<Tooltip>
<TooltipTrigger asChild>
{displayOnly ? (
<span
className={cn(labelOverflowBadgeClassName, className)}
>
{content}
</span>
) : (
<Button
variant="outline"
onClick={onClick}
className={cn(
labelOverflowBadgeClassName,
"cursor-pointer",
className
)}
>
{content}
</Button>
)}
</TooltipTrigger>
<TooltipContent>{tooltipContent}</TooltipContent>
</Tooltip>
);
}

View File

@@ -215,9 +215,9 @@ export function LabelsSelector({
aria-hidden aria-hidden
tabIndex={-1} tabIndex={-1}
/> />
<div className="min-w-0 flex-1 flex items-center gap-2"> <div className="flex min-w-0 flex-1 items-center gap-2">
<span <span
className="inline-block size-3 flex-none rounded-full bg-(--label-color)" className="inline-block size-2 flex-none rounded-full bg-(--label-color)"
style={{ style={{
// @ts-expect-error CSS variable // @ts-expect-error CSS variable
"--label-color": label.color "--label-color": label.color