diff --git a/src/components/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx index 9962c8f93..428f65f6b 100644 --- a/src/components/ClientInfoCard.tsx +++ b/src/components/ClientInfoCard.tsx @@ -27,20 +27,18 @@ export default function SiteInfoCard({}: ClientInfoCardProps) { return ( - + {t("name")} {client.name} - - - {userDisplayName ? t("user") : t("identifier")} - - -
- {userDisplayName || client.niceId} - {userDisplayName && - (client.userType ?? "internal") !== + {userDisplayName ? ( + + {t("user")} + +
+ {userDisplayName} + {(client.userType ?? "internal") !== "internal" && ( )} -
-
-
+
+
+
+ ) : null} {t("status")} diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index be60b7b26..959f16998 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -29,8 +29,7 @@ import { ChevronDown, ChevronsUpDownIcon, Funnel, - MoreHorizontal, - PlusIcon + MoreHorizontal } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; @@ -40,7 +39,6 @@ import { startTransition, useEffect, useMemo, - useOptimistic, useState, useTransition } from "react"; @@ -62,10 +60,10 @@ import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertI 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 { type SelectedLabel } from "./labels-selector"; +import { TableLabelsCell } from "./TableLabelsCell"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; +import { useLocalLabels } from "@app/hooks/useLocalLabels"; export type InternalResourceSiteRow = ResourceSiteRow; @@ -164,12 +162,12 @@ export default function ClientResourcesTable({ const { isPaidUser } = usePaidStatus(); const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); - useEffect(() => { - const interval = setInterval(() => { - router.refresh(); - }, 30_000); - return () => clearInterval(interval); - }, [router]); + // useEffect(() => { + // const interval = setInterval(() => { + // router.refresh(); + // }, 30_000); + // return () => clearInterval(interval); + // }, [router]); const siteIdQ = searchParams.get("siteId"); const siteIdNum = siteIdQ ? parseInt(siteIdQ, 10) : NaN; @@ -700,27 +698,28 @@ function ClientResourceLabelCell({ }: ClientResourceLabelCellProps) { const t = useTranslations(); const api = createApiClient(useEnvContext()); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const router = useRouter(); - - const labels = resource.labels ?? []; - const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + const [localLabels, setLocalLabels] = useLocalLabels( + resource.labels, + resource.id + ); function toggleResourceLabel( label: SelectedLabel, action: "attach" | "detach" ) { - startTransition(async () => { + const previousLabels = localLabels; + + void (async () => { try { if (action === "attach") { - setOptimisticLabels([...optimisticLabels, label]); + setLocalLabels([...previousLabels, label]); await api.put( `/org/${orgId}/label/${label.labelId}/attach`, { siteResourceId: resource.id } ); } else { - setOptimisticLabels( - optimisticLabels.filter( + setLocalLabels( + previousLabels.filter( (lb) => lb.labelId !== label.labelId ) ); @@ -730,54 +729,21 @@ function ClientResourceLabelCell({ ); } } catch (e) { + setLocalLabels(previousLabels); toast({ title: t("error"), description: formatAxiosError(e, t("errorOccurred")), variant: "destructive" }); - } finally { - router.refresh(); } - }); + })(); } - const visibleLabels = optimisticLabels.slice(0, 3); - const overflowLabels = optimisticLabels.slice(3); - return ( -
- - - - - - - - - {visibleLabels.map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - setIsPopoverOpen(true)} - /> -
+ ); } diff --git a/src/components/ExitNodesTable.tsx b/src/components/ExitNodesTable.tsx index 27b44706f..9467ff54b 100644 --- a/src/components/ExitNodesTable.tsx +++ b/src/components/ExitNodesTable.tsx @@ -122,7 +122,7 @@ export default function ExitNodesTable({ }, { accessorKey: "online", - friendlyName: t("online"), + friendlyName: t("status"), header: ({ column }) => { return ( ); diff --git a/src/components/HealthChecksTable.tsx b/src/components/HealthChecksTable.tsx index 4ed766884..b4962ecd1 100644 --- a/src/components/HealthChecksTable.tsx +++ b/src/components/HealthChecksTable.tsx @@ -163,12 +163,12 @@ export default function HealthChecksTable({ }); } - useEffect(() => { - const interval = setInterval(() => { - router.refresh(); - }, 30_000); - return () => clearInterval(interval); - }, [router]); + // useEffect(() => { + // const interval = setInterval(() => { + // router.refresh(); + // }, 30_000); + // return () => clearInterval(interval); + // }, [router]); const handlePaginationChange = (newState: PaginationState) => { searchParams.set("page", (newState.pageIndex + 1).toString()); diff --git a/src/components/MachineClientsTable.tsx b/src/components/MachineClientsTable.tsx index 61fa8a010..793312052 100644 --- a/src/components/MachineClientsTable.tsx +++ b/src/components/MachineClientsTable.tsx @@ -25,7 +25,6 @@ import { ChevronsUpDownIcon, CircleSlash, MoreHorizontal, - PlusIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; @@ -33,20 +32,18 @@ import { useRouter } from "next/navigation"; import { startTransition, useMemo, - useOptimistic, useState, useTransition } from "react"; import { useDebouncedCallback } from "use-debounce"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; -import { LabelBadge } from "./label-badge"; -import { LabelOverflowBadge } from "./label-overflow-badge"; -import { LabelsSelector, type SelectedLabel } from "./labels-selector"; +import { type SelectedLabel } from "./labels-selector"; +import { TableLabelsCell } from "./TableLabelsCell"; import { Badge } from "./ui/badge"; import { ControlledDataTable } from "./ui/controlled-data-table"; -import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; +import { useLocalLabels } from "@app/hooks/useLocalLabels"; export type ClientRow = { id: number; @@ -277,7 +274,7 @@ export default function MachineClientsTable({ }, { accessorKey: "online", - friendlyName: t("online"), + friendlyName: t("status"), header: () => { return ( ); @@ -617,27 +614,25 @@ function MachineClientLabelCell({ }: MachineClientLabelCellProps) { const t = useTranslations(); const api = createApiClient(useEnvContext()); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const router = useRouter(); - - const labels = client.labels ?? []; - const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + const [localLabels, setLocalLabels] = useLocalLabels(client.labels, client.id); function toggleClientLabel( label: SelectedLabel, action: "attach" | "detach" ) { - startTransition(async () => { + const previousLabels = localLabels; + + void (async () => { try { if (action === "attach") { - setOptimisticLabels([...optimisticLabels, label]); + setLocalLabels([...previousLabels, label]); await api.put( `/org/${orgId}/label/${label.labelId}/attach`, { clientId: client.id } ); } else { - setOptimisticLabels( - optimisticLabels.filter( + setLocalLabels( + previousLabels.filter( (lb) => lb.labelId !== label.labelId ) ); @@ -647,54 +642,21 @@ function MachineClientLabelCell({ ); } } catch (e) { + setLocalLabels(previousLabels); toast({ title: t("error"), description: formatAxiosError(e, t("errorOccurred")), variant: "destructive" }); - } finally { - router.refresh(); } - }); + })(); } - const visibleLabels = optimisticLabels.slice(0, 3); - const overflowLabels = optimisticLabels.slice(3); - return ( -
- - - - - - - - - {visibleLabels.map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - setIsPopoverOpen(true)} - /> -
+ ); } diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 1cd80e136..2bc80daff 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -70,10 +70,10 @@ 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"; -import { LabelOverflowBadge } from "./label-overflow-badge"; +import { type SelectedLabel } from "./labels-selector"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; +import { useLocalLabels } from "@app/hooks/useLocalLabels"; +import { TableLabelsCell } from "./TableLabelsCell"; export type TargetHealth = { targetId: number; @@ -171,12 +171,12 @@ export default function ProxyResourcesTable({ }; }, [initialFilterSite, siteIdQ, siteIdNum, t]); - useEffect(() => { - const interval = setInterval(() => { - router.refresh(); - }, 30_000); - return () => clearInterval(interval); - }, [router]); + // useEffect(() => { + // const interval = setInterval(() => { + // router.refresh(); + // }, 30_000); + // return () => clearInterval(interval); + // }, [router]); const refreshData = () => { startTransition(() => { @@ -766,29 +766,29 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) { const api = createApiClient(useEnvContext()); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const router = useRouter(); - - const labels = resource.labels ?? []; - const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + const [localLabels, setLocalLabels] = useLocalLabels( + resource.labels, + resource.id + ); function toggleSiteLabel( label: SelectedLabel, action: "attach" | "detach" ) { - startTransition(async () => { + const previousLabels = localLabels; + + void (async () => { try { if (action === "attach") { - setOptimisticLabels([...optimisticLabels, label]); + setLocalLabels([...previousLabels, label]); await api.put( `/org/${orgId}/label/${label.labelId}/attach`, { resourceId: resource.id } ); } else { - setOptimisticLabels( - optimisticLabels.filter( + setLocalLabels( + previousLabels.filter( (lb) => lb.labelId !== label.labelId ) ); @@ -798,55 +798,22 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) { ); } } catch (e) { + setLocalLabels(previousLabels); toast({ title: t("error"), description: formatAxiosError(e, t("errorOccurred")), variant: "destructive" }); - } finally { - router.refresh(); } - }); + })(); } - const visibleLabels = optimisticLabels.slice(0, 3); - const overflowLabels = optimisticLabels.slice(3); - return ( -
- - - - - - - - - {visibleLabels.map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - setIsPopoverOpen(true)} - /> -
+ ); } diff --git a/src/components/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx index d66912d14..bbb6e7519 100644 --- a/src/components/ResourceInfoBox.tsx +++ b/src/components/ResourceInfoBox.tsx @@ -36,15 +36,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { {/* 4 cols because of the certs */} - - - {t("identifier")} - - - {resource.niceId} - - - + {resource.http ? ( <> diff --git a/src/components/SiteInfoCard.tsx b/src/components/SiteInfoCard.tsx index 4f366731e..21697d697 100644 --- a/src/components/SiteInfoCard.tsx +++ b/src/components/SiteInfoCard.tsx @@ -61,8 +61,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { return ( - - {identifierSection} + {statusSection} diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 08dd66d42..284bf4de8 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -37,7 +37,6 @@ import { ChevronDown, ChevronsUpDownIcon, MoreHorizontal, - PlusIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; @@ -46,7 +45,6 @@ import { startTransition, useEffect, useMemo, - useOptimistic, useState, useTransition } from "react"; @@ -61,11 +59,10 @@ import { 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 { type SelectedLabel } from "./labels-selector"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; +import { useLocalLabels } from "@app/hooks/useLocalLabels"; +import { TableLabelsCell } from "./TableLabelsCell"; export type SiteRow = { id: number; @@ -124,12 +121,12 @@ export default function SitesTable({ const api = createApiClient(useEnvContext()); const t = useTranslations(); - useEffect(() => { - const interval = setInterval(() => { - router.refresh(); - }, 30_000); - return () => clearInterval(interval); - }, []); + // useEffect(() => { + // const interval = setInterval(() => { + // router.refresh(); + // }, 30_000); + // return () => clearInterval(interval); + // }, []); const booleanSearchFilterSchema = z .enum(["true", "false"]) @@ -225,7 +222,7 @@ export default function SitesTable({ }, { accessorKey: "online", - friendlyName: t("online"), + friendlyName: t("status"), header: () => { return ( ); @@ -693,29 +690,26 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { const api = createApiClient(useEnvContext()); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const router = useRouter(); - - const labels = site.labels ?? []; - const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + const [localLabels, setLocalLabels] = useLocalLabels(site.labels, site.id); function toggleSiteLabel( label: SelectedLabel, action: "attach" | "detach" ) { - startTransition(async () => { + const previousLabels = localLabels; + + void (async () => { try { if (action === "attach") { - setOptimisticLabels([...optimisticLabels, label]); + setLocalLabels([...previousLabels, label]); await api.put( `/org/${orgId}/label/${label.labelId}/attach`, { siteId: site.id } ); } else { - setOptimisticLabels( - optimisticLabels.filter( + setLocalLabels( + previousLabels.filter( (lb) => lb.labelId !== label.labelId ) ); @@ -725,54 +719,21 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { ); } } catch (e) { + setLocalLabels(previousLabels); toast({ title: t("error"), description: formatAxiosError(e, t("errorOccurred")), variant: "destructive" }); - } finally { - router.refresh(); } - }); + })(); } - const visibleLabels = optimisticLabels.slice(0, 3); - const overflowLabels = optimisticLabels.slice(3); - return ( -
- - - - - - - - - {visibleLabels.map((label) => ( - setIsPopoverOpen(true)} - {...label} - /> - ))} - setIsPopoverOpen(true)} - /> -
+ ); } diff --git a/src/components/TableLabelsCell.tsx b/src/components/TableLabelsCell.tsx new file mode 100644 index 000000000..4d4f28066 --- /dev/null +++ b/src/components/TableLabelsCell.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover"; +import type { Measurable } from "@radix-ui/rect"; +import { PlusIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRef, useState } from "react"; +import { LabelBadge } from "./label-badge"; +import { LabelOverflowBadge } from "./label-overflow-badge"; +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; +import { Button } from "./ui/button"; +import { + Popover, + PopoverAnchor, + PopoverContent, + PopoverTrigger +} from "./ui/popover"; + +const MAX_VISIBLE_LABELS = 3; + +type TableLabelsCellProps = { + orgId: string; + localLabels: SelectedLabel[]; + toggleLabel: (label: SelectedLabel, action: "attach" | "detach") => void; +}; + +export function TableLabelsCell({ + orgId, + localLabels, + toggleLabel +}: TableLabelsCellProps) { + const t = useTranslations(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const triggerRef = useRef(null); + const frozenAnchorRef = useRef({ + getBoundingClientRect: () => new DOMRect() + }); + + const visibleLabels = localLabels.slice(0, MAX_VISIBLE_LABELS); + const overflowLabels = localLabels.slice(MAX_VISIBLE_LABELS); + + function handleOpenChange(open: boolean) { + if (open && triggerRef.current) { + const rect = triggerRef.current.getBoundingClientRect(); + frozenAnchorRef.current = { + getBoundingClientRect: () => rect + }; + } + setIsPopoverOpen(open); + } + + return ( +
+ + + + + + + + + +
+ {visibleLabels.map((label) => ( + handleOpenChange(true)} + {...label} + /> + ))} + handleOpenChange(true)} + /> +
+
+ ); +} diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 20eeae897..8ee2ddb87 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -405,7 +405,7 @@ export default function UserDevicesTable({ }, { accessorKey: "online", - friendlyName: t("connected"), + friendlyName: t("status"), header: () => { return ( ); diff --git a/src/hooks/useLocalLabels.ts b/src/hooks/useLocalLabels.ts new file mode 100644 index 000000000..8e066d785 --- /dev/null +++ b/src/hooks/useLocalLabels.ts @@ -0,0 +1,21 @@ +import type { SelectedLabel } from "@app/components/labels-selector"; +import { useEffect, useState } from "react"; + +export function useLocalLabels( + serverLabels: SelectedLabel[] | undefined, + entityId: number +) { + const labels = serverLabels ?? []; + const [localLabels, setLocalLabels] = useState(labels); + + const serverLabelIds = labels + .map((label) => label.labelId) + .sort((a, b) => a - b) + .join(","); + + useEffect(() => { + setLocalLabels(serverLabels ?? []); + }, [entityId, serverLabelIds]); + + return [localLabels, setLocalLabels] as const; +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index a8725354c..1b80619c2 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -759,7 +759,13 @@ export const logQueries = { } }), - access: ({ orgId, filters }: { orgId: string; filters: AccessLogFilters }) => + access: ({ + orgId, + filters + }: { + orgId: string; + filters: AccessLogFilters; + }) => queryOptions({ queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const, queryFn: async ({ signal, meta }) => {