diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 6756d8657..c6d7d036d 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -417,7 +417,9 @@ export async function listResources( 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 const countQuery = db.$count(baseQuery.as("filtered_resources")); @@ -463,7 +465,8 @@ export async function listResources( ) .where( inArray(resourceLabels.resourceId, resourceIdList) - ); + ) + .orderBy(asc(resourceLabels.resourceLabelId)); } const allResourceTargets = diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 829379412..99f931bb9 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -423,7 +423,8 @@ export async function listSites( siteLabels, 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) => { diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 21a770a68..164171a70 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -47,6 +47,7 @@ import { Clock, Funnel, MoreHorizontal, + PlusIcon, ShieldCheck, ShieldOff, XCircle @@ -55,6 +56,7 @@ import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { + startTransition, useEffect, useMemo, useOptimistic, @@ -68,6 +70,8 @@ import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import { ControlledDataTable } from "./ui/controlled-data-table"; import UptimeMiniBar from "./UptimeMiniBar"; +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; +import { LabelBadge } from "./label-badge"; export type TargetHealth = { targetId: number; @@ -538,11 +542,10 @@ export default function ProxyResourcesTable({ ), cell: ({ row }: { row: { original: ResourceRow } }) => { return ( - // - <> + ); } } @@ -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 ( +
+ {optimisticLabels.slice(0, 3).map((label) => ( + setIsPopoverOpen(true)} + {...label} + /> + ))} + {optimisticLabels.length > 3 && ( + + )} + + + + + + + + +
+ ); +} + function TargetStatusCell({ targets, healthStatus