labels on proxy resources

This commit is contained in:
Fred KISSIE
2026-05-11 18:37:16 +02:00
parent 549e1ead1d
commit ab494521b1
3 changed files with 114 additions and 8 deletions

View File

@@ -417,7 +417,9 @@ export async function listResources(
conditions.push(or(...queryList)); conditions.push(or(...queryList));
} }
const baseQuery = queryResourcesBase(isLabelFeatureEnabled).where(and(...conditions)); const baseQuery = queryResourcesBase(isLabelFeatureEnabled).where(
and(...conditions)
);
// we need to add `as` so that drizzle filters the result as a subquery // we need to add `as` so that drizzle filters the result as a subquery
const countQuery = db.$count(baseQuery.as("filtered_resources")); const countQuery = db.$count(baseQuery.as("filtered_resources"));
@@ -463,7 +465,8 @@ export async function listResources(
) )
.where( .where(
inArray(resourceLabels.resourceId, resourceIdList) inArray(resourceLabels.resourceId, resourceIdList)
); )
.orderBy(asc(resourceLabels.resourceLabelId));
} }
const allResourceTargets = const allResourceTargets =

View File

@@ -423,7 +423,8 @@ export async function listSites(
siteLabels, siteLabels,
eq(siteLabels.labelId, labels.labelId) eq(siteLabels.labelId, labels.labelId)
) )
.where(inArray(siteLabels.siteId, siteIds)); .where(inArray(siteLabels.siteId, siteIds))
.orderBy(asc(siteLabels.siteLabelId));
} }
const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => { const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => {

View File

@@ -47,6 +47,7 @@ import {
Clock, Clock,
Funnel, Funnel,
MoreHorizontal, MoreHorizontal,
PlusIcon,
ShieldCheck, ShieldCheck,
ShieldOff, ShieldOff,
XCircle XCircle
@@ -55,6 +56,7 @@ import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { import {
startTransition,
useEffect, useEffect,
useMemo, useMemo,
useOptimistic, useOptimistic,
@@ -68,6 +70,8 @@ import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton"; import { ColumnFilterButton } from "./ColumnFilterButton";
import { ControlledDataTable } from "./ui/controlled-data-table"; import { ControlledDataTable } from "./ui/controlled-data-table";
import UptimeMiniBar from "./UptimeMiniBar"; import UptimeMiniBar from "./UptimeMiniBar";
import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { LabelBadge } from "./label-badge";
export type TargetHealth = { export type TargetHealth = {
targetId: number; targetId: number;
@@ -538,11 +542,10 @@ export default function ProxyResourcesTable({
), ),
cell: ({ row }: { row: { original: ResourceRow } }) => { cell: ({ row }: { row: { original: ResourceRow } }) => {
return ( return (
// <SiteLabelCell <ResourceLabelCell
// site={row.original} resource={row.original}
// orgId={orgId} orgId={orgId}
// /> />
<></>
); );
} }
} }
@@ -707,6 +710,105 @@ export default function ProxyResourcesTable({
); );
} }
type ResourceLabelCellProps = {
resource: ResourceRow;
orgId: string;
};
function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) {
const t = useTranslations();
const api = createApiClient(useEnvContext());
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const router = useRouter();
const labels = resource.labels ?? [];
const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels);
function toggleSiteLabel(
label: SelectedLabel,
action: "attach" | "detach"
) {
startTransition(async () => {
try {
if (action === "attach") {
setOptimisticLabels([...optimisticLabels, label]);
await api.put(
`/org/${orgId}/label/${label.labelId}/attach`,
{ resourceId: resource.id }
);
} else {
setOptimisticLabels(
optimisticLabels.filter(
(lb) => lb.labelId !== label.labelId
)
);
await api.put(
`/org/${orgId}/label/${label.labelId}/detach`,
{ resourceId: resource.id }
);
}
} catch (e) {
toast({
title: t("error"),
description: formatAxiosError(e, t("errorOccurred")),
variant: "destructive"
});
} finally {
router.refresh();
}
});
}
return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1">
{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}>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="p-1 size-auto rounded-full"
title={t("addLabels")}
>
<span className="sr-only">{t("addLabels")}</span>
<PlusIcon className="size-3" />
</Button>
</PopoverTrigger>
<PopoverContent align="center" className="p-0 w-full">
<LabelsSelector
orgId={orgId}
selectedLabels={optimisticLabels}
toggleLabel={toggleSiteLabel}
/>
</PopoverContent>
</Popover>
</div>
);
}
function TargetStatusCell({ function TargetStatusCell({
targets, targets,
healthStatus healthStatus