remove extra sites query

This commit is contained in:
miloschwartz
2026-04-12 14:58:55 -07:00
parent b5e239d1ad
commit 0cbcc0c29c
6 changed files with 97 additions and 133 deletions

View File

@@ -76,6 +76,7 @@ export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
siteName: string; siteName: string;
siteNiceId: string; siteNiceId: string;
siteAddress: string | null; siteAddress: string | null;
siteOnline: boolean;
})[]; })[];
}>; }>;
@@ -106,7 +107,8 @@ function querySiteResourcesBase() {
fullDomain: siteResources.fullDomain, fullDomain: siteResources.fullDomain,
siteName: sites.name, siteName: sites.name,
siteNiceId: sites.niceId, siteNiceId: sites.niceId,
siteAddress: sites.address siteAddress: sites.address,
siteOnline: sites.online
}) })
.from(siteResources) .from(siteResources)
.innerJoin(sites, eq(siteResources.siteId, sites.siteId)); .innerJoin(sites, eq(siteResources.siteId, sites.siteId));

View File

@@ -73,7 +73,8 @@ export default async function ClientResourcesPage(
{ {
siteId: siteResource.siteId, siteId: siteResource.siteId,
siteName: siteResource.siteName, siteName: siteResource.siteName,
siteNiceId: siteResource.siteNiceId siteNiceId: siteResource.siteNiceId,
online: siteResource.siteOnline
} }
], ],
siteName: siteResource.siteName, siteName: siteResource.siteName,

View File

@@ -20,7 +20,7 @@ import {
ArrowDown01Icon, ArrowDown01Icon,
ArrowUp10Icon, ArrowUp10Icon,
ArrowUpDown, ArrowUpDown,
ArrowUpRight, ChevronDown,
ChevronsUpDownIcon, ChevronsUpDownIcon,
MoreHorizontal MoreHorizontal
} from "lucide-react"; } from "lucide-react";
@@ -38,16 +38,13 @@ import { ControlledDataTable } from "./ui/controlled-data-table";
import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { ColumnFilterButton } from "./ColumnFilterButton"; import { ColumnFilterButton } from "./ColumnFilterButton";
import { import { cn } from "@app/lib/cn";
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
export type InternalResourceSiteRow = { export type InternalResourceSiteRow = {
siteId: number; siteId: number;
siteName: string; siteName: string;
siteNiceId: string; siteNiceId: string;
online: boolean;
}; };
export type InternalResourceRow = { export type InternalResourceRow = {
@@ -113,99 +110,106 @@ function isSafeUrlForLink(href: string): boolean {
} }
} }
const MAX_SITE_LINKS = 3; type AggregateSitesStatus = "allOnline" | "partial" | "allOffline";
function ClientResourceSiteLinks({ function aggregateSitesStatus(
resourceSites: InternalResourceSiteRow[]
): AggregateSitesStatus {
if (resourceSites.length === 0) {
return "allOffline";
}
const onlineCount = resourceSites.filter((rs) => rs.online).length;
if (onlineCount === resourceSites.length) return "allOnline";
if (onlineCount > 0) return "partial";
return "allOffline";
}
function aggregateStatusDotClass(status: AggregateSitesStatus): string {
switch (status) {
case "allOnline":
return "bg-green-500";
case "partial":
return "bg-yellow-500";
case "allOffline":
default:
return "bg-gray-500";
}
}
function ClientResourceSitesStatusCell({
orgId, orgId,
sites resourceSites
}: { }: {
orgId: string; orgId: string;
sites: InternalResourceSiteRow[]; resourceSites: InternalResourceSiteRow[];
}) { }) {
if (sites.length === 0) { const t = useTranslations();
if (resourceSites.length === 0) {
return <span>-</span>; return <span>-</span>;
} }
const visible = sites.slice(0, MAX_SITE_LINKS);
const overflow = sites.slice(MAX_SITE_LINKS); const aggregate = aggregateSitesStatus(resourceSites);
const countLabel = t("multiSitesSelectorSitesCount", {
count: resourceSites.length
});
return ( return (
<div className="flex flex-wrap items-center gap-1"> <DropdownMenu>
{visible.map((site) => ( <DropdownMenuTrigger asChild>
<Link
key={site.siteId}
href={`/${orgId}/settings/sites/${site.siteNiceId}`}
>
<Button <Button
variant="outline" variant="ghost"
size="sm" size="sm"
className="w-full gap-1" className="flex h-8 items-center gap-2 px-0 font-normal"
> >
<span className="max-w-[10rem] truncate"> <div
{site.siteName} className={cn(
</span> "h-2 w-2 shrink-0 rounded-full",
<ArrowUpRight className="h-3 w-3 shrink-0" /> aggregateStatusDotClass(aggregate)
)}
/>
<span className="text-sm tabular-nums">{countLabel}</span>
<ChevronDown className="h-3 w-3 shrink-0" />
</Button> </Button>
</Link> </DropdownMenuTrigger>
))} <DropdownMenuContent align="start" className="min-w-56">
{overflow.length > 0 ? ( {resourceSites.map((site) => {
<OverflowSitesPopover orgId={orgId} sites={overflow} /> const isOnline = site.online;
) : null}
</div>
);
}
function OverflowSitesPopover({
orgId,
sites
}: {
orgId: string;
sites: InternalResourceSiteRow[];
}) {
const [open, setOpen] = useState(false);
return ( return (
<Popover open={open} onOpenChange={setOpen}> <DropdownMenuItem key={site.siteId} asChild>
<PopoverTrigger asChild>
<Button
type="button"
size="sm"
variant="outline"
className="gap-1 px-2 font-normal"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
+{sites.length}
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
side="top"
className="w-auto max-w-xs p-2"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
<ul className="flex flex-col gap-1.5 text-sm">
{sites.map((site) => (
<li key={site.siteId}>
<Link <Link
href={`/${orgId}/settings/sites/${site.siteNiceId}`} href={`/${orgId}/settings/sites/${site.siteNiceId}`}
className="flex cursor-pointer items-center justify-between gap-4"
> >
<Button <div className="flex min-w-0 items-center gap-2">
variant="outline" <div
size="sm" className={cn(
className="w-full justify-start gap-1" "h-2 w-2 shrink-0 rounded-full",
> isOnline
? "bg-green-500"
: "bg-gray-500"
)}
/>
<span className="truncate"> <span className="truncate">
{site.siteName} {site.siteName}
</span> </span>
<ArrowUpRight className="ml-auto h-3 w-3 shrink-0" /> </div>
</Button> <span
className={cn(
"shrink-0 capitalize",
isOnline
? "text-green-600"
: "text-muted-foreground"
)}
>
{isOnline ? t("online") : t("offline")}
</span>
</Link> </Link>
</li> </DropdownMenuItem>
))} );
</ul> })}
</PopoverContent> </DropdownMenuContent>
</Popover> </DropdownMenu>
); );
} }
@@ -243,8 +247,6 @@ export default function ClientResourcesTable({
useState<InternalResourceRow | null>(); useState<InternalResourceRow | null>();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const { data: sites = [] } = useQuery(orgQueries.sites({ orgId }));
const [isRefreshing, startTransition] = useTransition(); const [isRefreshing, startTransition] = useTransition();
const refreshData = () => { const refreshData = () => {
@@ -339,9 +341,9 @@ export default function ClientResourcesTable({
cell: ({ row }) => { cell: ({ row }) => {
const resourceRow = row.original; const resourceRow = row.original;
return ( return (
<ClientResourceSiteLinks <ClientResourceSitesStatusCell
orgId={resourceRow.orgId} orgId={resourceRow.orgId}
sites={resourceRow.sites} resourceSites={resourceRow.sites}
/> />
); );
} }
@@ -599,7 +601,6 @@ export default function ClientResourcesTable({
setOpen={setIsEditDialogOpen} setOpen={setIsEditDialogOpen}
resource={editingResource} resource={editingResource}
orgId={orgId} orgId={orgId}
sites={sites}
onSuccess={() => { onSuccess={() => {
// Delay refresh to allow modal to close smoothly // Delay refresh to allow modal to close smoothly
setTimeout(() => { setTimeout(() => {
@@ -614,7 +615,6 @@ export default function ClientResourcesTable({
open={isCreateDialogOpen} open={isCreateDialogOpen}
setOpen={setIsCreateDialogOpen} setOpen={setIsCreateDialogOpen}
orgId={orgId} orgId={orgId}
sites={sites}
onSuccess={() => { onSuccess={() => {
// Delay refresh to allow modal to close smoothly // Delay refresh to allow modal to close smoothly
setTimeout(() => { setTimeout(() => {

View File

@@ -31,7 +31,6 @@ type CreateInternalResourceDialogProps = {
open: boolean; open: boolean;
setOpen: (val: boolean) => void; setOpen: (val: boolean) => void;
orgId: string; orgId: string;
sites: Site[];
onSuccess?: () => void; onSuccess?: () => void;
}; };
@@ -39,7 +38,6 @@ export default function CreateInternalResourceDialog({
open, open,
setOpen, setOpen,
orgId, orgId,
sites,
onSuccess onSuccess
}: CreateInternalResourceDialogProps) { }: CreateInternalResourceDialogProps) {
const t = useTranslations(); const t = useTranslations();
@@ -155,7 +153,6 @@ export default function CreateInternalResourceDialog({
<InternalResourceForm <InternalResourceForm
variant="create" variant="create"
open={open} open={open}
sites={sites}
orgId={orgId} orgId={orgId}
formId="create-internal-resource-form" formId="create-internal-resource-form"
onSubmit={handleSubmit} onSubmit={handleSubmit}

View File

@@ -34,7 +34,6 @@ type EditInternalResourceDialogProps = {
setOpen: (val: boolean) => void; setOpen: (val: boolean) => void;
resource: InternalResourceData; resource: InternalResourceData;
orgId: string; orgId: string;
sites: Site[];
onSuccess?: () => void; onSuccess?: () => void;
}; };
@@ -43,7 +42,6 @@ export default function EditInternalResourceDialog({
setOpen, setOpen,
resource, resource,
orgId, orgId,
sites,
onSuccess onSuccess
}: EditInternalResourceDialogProps) { }: EditInternalResourceDialogProps) {
const t = useTranslations(); const t = useTranslations();
@@ -174,7 +172,6 @@ export default function EditInternalResourceDialog({
variant="edit" variant="edit"
open={open} open={open}
resource={resource} resource={resource}
sites={sites}
orgId={orgId} orgId={orgId}
siteResourceId={resource.id} siteResourceId={resource.id}
formId="edit-internal-resource-form" formId="edit-internal-resource-form"

View File

@@ -159,18 +159,7 @@ const tagSchema = z.object({ id: z.string(), text: z.string() });
function buildSelectedSitesForResource( function buildSelectedSitesForResource(
resource: InternalResourceData, resource: InternalResourceData,
catalog: Site[]
): Selectedsite[] { ): Selectedsite[] {
const fromCatalog = catalog.find((s) => s.siteId === resource.siteId);
if (fromCatalog) {
return [
{
name: fromCatalog.name,
siteId: fromCatalog.siteId,
type: fromCatalog.type
}
];
}
return [ return [
{ {
name: resource.siteName, name: resource.siteName,
@@ -207,7 +196,6 @@ type InternalResourceFormProps = {
variant: "create" | "edit"; variant: "create" | "edit";
resource?: InternalResourceData; resource?: InternalResourceData;
open?: boolean; open?: boolean;
sites: Site[];
orgId: string; orgId: string;
siteResourceId?: number; siteResourceId?: number;
formId: string; formId: string;
@@ -218,7 +206,6 @@ export function InternalResourceForm({
variant, variant,
resource, resource,
open, open,
sites,
orgId, orgId,
siteResourceId, siteResourceId,
formId, formId,
@@ -375,8 +362,6 @@ export function InternalResourceForm({
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
const availableSites = sites.filter((s) => s.type === "newt");
const rolesQuery = useQuery(orgQueries.roles({ orgId })); const rolesQuery = useQuery(orgQueries.roles({ orgId }));
const usersQuery = useQuery(orgQueries.users({ orgId })); const usersQuery = useQuery(orgQueries.users({ orgId }));
const clientsQuery = useQuery(orgQueries.machineClients({ orgId })); const clientsQuery = useQuery(orgQueries.machineClients({ orgId }));
@@ -517,7 +502,7 @@ export function InternalResourceForm({
} }
: { : {
name: "", name: "",
siteIds: availableSites[0] ? [availableSites[0].siteId] : [], siteIds: [],
mode: "host", mode: "host",
destination: "", destination: "",
alias: null, alias: null,
@@ -539,15 +524,7 @@ export function InternalResourceForm({
const [selectedSites, setSelectedSites] = useState<Selectedsite[]>(() => const [selectedSites, setSelectedSites] = useState<Selectedsite[]>(() =>
variant === "edit" && resource variant === "edit" && resource
? buildSelectedSitesForResource(resource, sites) ? buildSelectedSitesForResource(resource)
: availableSites[0]
? [
{
name: availableSites[0].name,
siteId: availableSites[0].siteId,
type: availableSites[0].type
}
]
: [] : []
); );
@@ -580,7 +557,7 @@ export function InternalResourceForm({
if (variant === "create" && open) { if (variant === "create" && open) {
form.reset({ form.reset({
name: "", name: "",
siteIds: availableSites[0] ? [availableSites[0].siteId] : [], siteIds: [],
mode: "host", mode: "host",
destination: "", destination: "",
alias: null, alias: null,
@@ -599,23 +576,13 @@ export function InternalResourceForm({
users: [], users: [],
clients: [] clients: []
}); });
setSelectedSites( setSelectedSites([]);
availableSites[0]
? [
{
name: availableSites[0].name,
siteId: availableSites[0].siteId,
type: availableSites[0].type
}
]
: []
);
setTcpPortMode("all"); setTcpPortMode("all");
setUdpPortMode("all"); setUdpPortMode("all");
setTcpCustomPorts(""); setTcpCustomPorts("");
setUdpCustomPorts(""); setUdpCustomPorts("");
} }
}, [variant, open, form, sites]); }, [variant, open, form]);
// Reset when edit dialog opens / resource changes // Reset when edit dialog opens / resource changes
useEffect(() => { useEffect(() => {
@@ -644,7 +611,7 @@ export function InternalResourceForm({
clients: [] clients: []
}); });
setSelectedSites( setSelectedSites(
buildSelectedSitesForResource(resource, sites) buildSelectedSitesForResource(resource)
); );
setTcpPortMode( setTcpPortMode(
getPortModeFromString(resource.tcpPortRangeString) getPortModeFromString(resource.tcpPortRangeString)
@@ -667,7 +634,7 @@ export function InternalResourceForm({
previousResourceId.current = resource.id; previousResourceId.current = resource.id;
} }
} }
}, [variant, resource, form, sites]); }, [variant, resource, form]);
// When edit dialog closes, clear previousResourceId so next open (for any resource) resets from fresh data // When edit dialog closes, clear previousResourceId so next open (for any resource) resets from fresh data
useEffect(() => { useEffect(() => {