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 && (
-
- )}
+
+ {visibleLabels.map((label) => (
+
setIsPopoverOpen(true)}
+ {...label}
+ />
+ ))}
+ setIsPopoverOpen(true)}
+ />
);
}
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 (
-
- );
- }
- 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 && (
-
- )}
+
+ {visibleLabels.map((label) => (
+
setIsPopoverOpen(true)}
+ {...label}
+ />
+ ))}
+ setIsPopoverOpen(true)}
+ />
);
}
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 && (
-
- )}
+
+ {visibleLabels.map((label) => (
+
setIsPopoverOpen(true)}
+ {...label}
+ />
+ ))}
+ setIsPopoverOpen(true)}
+ />
);
}
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 && (
-
- )}
+
+ {visibleLabels.map((label) => (
+
setIsPopoverOpen(true)}
+ {...label}
+ />
+ ))}
+ setIsPopoverOpen(true)}
+ />
);
}
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}
/>
-