mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-13 13:26:36 +00:00
remove extra sites query
This commit is contained in:
@@ -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));
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user