diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 5b146da18..5e20f6db6 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -765,7 +765,7 @@ authenticated.put( labels.attachLabelToItem ); -authenticated.delete( +authenticated.put( "/org/:orgId/label/:labelId/detach", verifyValidLicense, verifyOrgAccess, diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 8c35a2521..ac1942574 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -392,7 +392,7 @@ export async function listSites( .select({ labelId: labels.labelId, name: labels.name, - color: labels.name, + color: labels.color, siteId: siteLabels.siteId }) .from(labels) diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 631baee41..6542959a3 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -60,6 +60,7 @@ export default async function SitesPage(props: SitesPageProps) { return { name: site.name, id: site.siteId, + labels: site.labels, nice: site.niceId.toString(), address: site.address?.split("/")[0], mbIn: formatSize(site.megabytesIn || 0, site.type), diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index a50bc8b20..57e9ea8a9 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -3,6 +3,16 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import UptimeMiniBar from "@app/components/UptimeMiniBar"; +import { + Credenza, + CredenzaBody, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import SiteResourcesOverview from "@app/components/SiteResourcesOverview"; import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import { @@ -14,9 +24,9 @@ import { import { InfoPopup } from "@app/components/ui/info-popup"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; -import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { build } from "@server/build"; import { type PaginationState } from "@tanstack/react-table"; import { @@ -27,32 +37,30 @@ import { ChevronDown, ChevronsUpDownIcon, MoreHorizontal, - PlusIcon + PlusIcon, + XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { useState, useTransition, useEffect } from "react"; +import { + startTransition, + useEffect, + useOptimistic, + useState, + useTransition +} from "react"; import { useDebouncedCallback } from "use-debounce"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; -import SiteResourcesOverview from "@app/components/SiteResourcesOverview"; -import { - Credenza, - CredenzaBody, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; import { ControlledDataTable, type ExtendedColumnDef } from "./ui/controlled-data-table"; -import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip"; + +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { LabelsSelector } from "./labels-selector"; +import { cn } from "@app/lib/cn"; export type SiteRow = { id: number; @@ -463,36 +471,7 @@ export default function SitesTable({ accessorKey: "labels", header: () => {t("labels")}, cell: ({ row }) => { - const labels = row.original.labels ?? []; - return ( -
- - - - - - {}} - /> - - -
- ); + return ; } }, { @@ -653,12 +632,6 @@ export default function SitesTable({ string={selectedSite.name} title={t("siteDelete")} /> - - {/* */} )} @@ -696,36 +669,104 @@ export default function SitesTable({ ); } -type SiteLabelsDialogProps = { +type SiteLabelCellProps = { site: SiteRow; - isOpen: boolean; - setIsOpen: (open: boolean) => void; + orgId: string; }; -function SiteLabelsDialog({ site, isOpen, setIsOpen }: SiteLabelsDialogProps) { +function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const router = useRouter(); + + const labels = site.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`, + { siteId: site.id } + ); + } else { + setOptimisticLabels( + optimisticLabels.filter( + (lb) => lb.labelId !== label.labelId + ) + ); + await api.put( + `/org/${orgId}/label/${label.labelId}/detach`, + { siteId: site.id } + ); + } + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } finally { + router.refresh(); + } + }); + } + return ( - - - - {t("siteLabelsTab")} - - {t("siteLabelsDescription")} - - - - <> - - +
+ {optimisticLabels.map((label) => ( + + ))} + + - - - + + + + + +
); } diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index ec8d7f270..64a80b26a 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -21,12 +21,13 @@ import { SelectTrigger, SelectValue } from "./ui/select"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import type { AxiosResponse } from "axios"; +import { toast } from "@app/hooks/useToast"; -type SelectedLabel = { +export type SelectedLabel = { name: string; color: string; labelId: number; @@ -35,8 +36,7 @@ type SelectedLabel = { export type LabelsSelectorProps = { orgId: string; selectedLabels: SelectedLabel[]; - onSelectionChange: (sites: SelectedLabel[]) => void; - onCreateLabel: (newlabel: SelectedLabel) => Promise; + toggleLabel: (newlabel: SelectedLabel, action: "detach" | "attach") => void; }; const LABEL_COLORS = { @@ -52,8 +52,7 @@ const LABEL_COLORS = { export function LabelsSelector({ orgId, selectedLabels, - onSelectionChange, - onCreateLabel + toggleLabel }: LabelsSelectorProps) { const t = useTranslations(); const [labelSearchQuery, setlabelsSearchQuery] = useState(""); @@ -94,17 +93,28 @@ export function LabelsSelector({ async function createLabel(_: any, formData: FormData) { const name = formData.get("name")?.toString(); const color = formData.get("color")?.toString(); - const res = await api.post>( - `/org/${orgId}/labels`, - { name, color } - ); + try { + const res = await api.post< + AxiosResponse + >(`/org/${orgId}/labels`, { name, color }); - const { label } = res.data.data; - await onCreateLabel({ - labelId: label.labelId, - name: label.name, - color: label.color - }); + const { label } = res.data.data; + + toggleLabel( + { + labelId: label.labelId, + name: label.name, + color: label.color + }, + "attach" + ); + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } setlabelsSearchQuery(""); } @@ -185,18 +195,18 @@ export function LabelsSelector({ key={label.labelId} value={`${label.labelId}`} onSelect={() => { - if (selectedIds.has(label.labelId)) { - onSelectionChange( - selectedLabels.filter( - (l) => l.labelId !== label.labelId - ) - ); - } else { - onSelectionChange([ - ...selectedLabels, - label - ]); - } + toggleLabel( + label, + selectedIds.has(label.labelId) + ? "detach" + : "attach" + ); + // } else { + // onSelectionChange([ + // ...selectedLabels, + // label + // ]); + // } }} >