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
+ // ]);
+ // }
}}
>