mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-16 07:40:04 +02:00
add sites to resource launcher panel
This commit is contained in:
@@ -28,7 +28,22 @@ import {
|
||||
} from "@app/components/SidePanel";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@app/components/ui/table";
|
||||
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import {
|
||||
derivePublicAuthState,
|
||||
formatPublicResourceType
|
||||
@@ -36,7 +51,10 @@ import {
|
||||
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
|
||||
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
|
||||
import { launcherQueries } from "@app/lib/queries";
|
||||
import type { LauncherResource } from "@server/routers/launcher/types";
|
||||
import type {
|
||||
LauncherResource,
|
||||
LauncherSiteInfo
|
||||
} from "@server/routers/launcher/types";
|
||||
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
|
||||
import type { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
|
||||
@@ -44,15 +62,18 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
ChevronsUpDown,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
XCircle
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
type LauncherResourcePanelProps = {
|
||||
open: boolean;
|
||||
@@ -60,6 +81,7 @@ type LauncherResourcePanelProps = {
|
||||
resource: LauncherResource | null;
|
||||
orgId: string;
|
||||
isAdmin: boolean;
|
||||
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||
};
|
||||
|
||||
type LauncherResourceDetailResult =
|
||||
@@ -153,6 +175,171 @@ function HealthStatusDisplay({
|
||||
const PUBLIC_AUTH_METHODS_MODES = ["http", "ssh", "rdp", "vnc"];
|
||||
const PUBLIC_AUTH_BADGE_MODES = [...PUBLIC_AUTH_METHODS_MODES, "inference"];
|
||||
|
||||
type SitesSortKey = "name" | "status";
|
||||
type SitesSortOrder = "asc" | "desc";
|
||||
|
||||
function siteStatusSortValue(site: LauncherSiteInfo): number {
|
||||
if (site.type !== "newt" && site.type !== "wireguard") {
|
||||
return -1;
|
||||
}
|
||||
if (typeof site.online !== "boolean") {
|
||||
return -1;
|
||||
}
|
||||
return site.online ? 1 : 0;
|
||||
}
|
||||
|
||||
function SiteStatusCell({ site }: { site: LauncherSiteInfo }) {
|
||||
const t = useTranslations();
|
||||
|
||||
if (site.type !== "newt" && site.type !== "wireguard") {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
if (typeof site.online !== "boolean") {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"size-2 shrink-0 rounded-full",
|
||||
site.online ? "bg-green-500" : "bg-neutral-500"
|
||||
)}
|
||||
/>
|
||||
<span>{site.online ? t("online") : t("offline")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function LauncherResourceSitesSection({
|
||||
orgId,
|
||||
sites,
|
||||
isAdmin,
|
||||
onFilterBySite
|
||||
}: {
|
||||
orgId: string;
|
||||
sites: LauncherSiteInfo[];
|
||||
isAdmin: boolean;
|
||||
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const [sortKey, setSortKey] = useState<SitesSortKey>("name");
|
||||
const [sortOrder, setSortOrder] = useState<SitesSortOrder>("asc");
|
||||
|
||||
if (sites.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function toggleSort(key: SitesSortKey) {
|
||||
if (sortKey === key) {
|
||||
setSortOrder((order) => (order === "asc" ? "desc" : "asc"));
|
||||
return;
|
||||
}
|
||||
setSortKey(key);
|
||||
setSortOrder("asc");
|
||||
}
|
||||
|
||||
const sortedSites = [...sites].sort((a, b) => {
|
||||
const cmp =
|
||||
sortKey === "name"
|
||||
? a.name.localeCompare(b.name, undefined, {
|
||||
sensitivity: "base"
|
||||
})
|
||||
: siteStatusSortValue(a) - siteStatusSortValue(b);
|
||||
return sortOrder === "desc" ? -cmp : cmp;
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>{t("sites")}</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourceLauncherSitesDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-3"
|
||||
onClick={() => toggleSort("name")}
|
||||
>
|
||||
{t("name")}
|
||||
<ChevronsUpDown className="ml-2 size-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 px-3"
|
||||
onClick={() => toggleSort("status")}
|
||||
>
|
||||
{t("status")}
|
||||
<ChevronsUpDown className="ml-2 size-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead className="w-12">
|
||||
<span className="sr-only">{t("actions")}</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedSites.map((site) => (
|
||||
<TableRow key={site.siteId}>
|
||||
<TableCell>{site.name}</TableCell>
|
||||
<TableCell>
|
||||
<SiteStatusCell site={site} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("openMenu")}
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{isAdmin ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/${orgId}/settings/sites/${site.niceId}`}
|
||||
>
|
||||
{t(
|
||||
"resourceLauncherViewSiteAsAdmin"
|
||||
)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
onFilterBySite(site)
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"resourceLauncherFilterBySite"
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthMethodStatusDisplay({ enabled }: { enabled: boolean }) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -235,12 +422,16 @@ function PublicResourceDetails({
|
||||
orgId,
|
||||
launcherResource,
|
||||
resource,
|
||||
authInfo
|
||||
authInfo,
|
||||
isAdmin,
|
||||
onFilterBySite
|
||||
}: {
|
||||
orgId: string;
|
||||
launcherResource: LauncherResource;
|
||||
resource: GetResourceResponse;
|
||||
authInfo: GetResourceAuthInfoResponse;
|
||||
isAdmin: boolean;
|
||||
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const mode = resource.mode || "";
|
||||
@@ -328,6 +519,12 @@ function PublicResourceDetails({
|
||||
</InfoSections>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<LauncherResourceSitesSection
|
||||
orgId={orgId}
|
||||
sites={launcherResource.sites ?? []}
|
||||
isAdmin={isAdmin}
|
||||
onFilterBySite={onFilterBySite}
|
||||
/>
|
||||
{showAuthMethods ? (
|
||||
<PublicResourceAuthMethods authInfo={authInfo} />
|
||||
) : null}
|
||||
@@ -363,11 +560,15 @@ function PublicResourceDetails({
|
||||
function PrivateResourceDetails({
|
||||
orgId,
|
||||
launcherResource,
|
||||
resource
|
||||
resource,
|
||||
isAdmin,
|
||||
onFilterBySite
|
||||
}: {
|
||||
orgId: string;
|
||||
launcherResource: LauncherResource;
|
||||
resource: GetSiteResourceResponse;
|
||||
isAdmin: boolean;
|
||||
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const isInference = resource.mode === "inference";
|
||||
@@ -417,6 +618,12 @@ function PrivateResourceDetails({
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<LauncherResourceSitesSection
|
||||
orgId={orgId}
|
||||
sites={launcherResource.sites ?? []}
|
||||
isAdmin={isAdmin}
|
||||
onFilterBySite={onFilterBySite}
|
||||
/>
|
||||
{isInference ? (
|
||||
<>
|
||||
<LauncherInferenceModelsSection
|
||||
@@ -440,11 +647,15 @@ function PrivateResourceDetails({
|
||||
function LauncherResourcePanelBody({
|
||||
orgId,
|
||||
resource,
|
||||
open
|
||||
open,
|
||||
isAdmin,
|
||||
onFilterBySite
|
||||
}: {
|
||||
orgId: string;
|
||||
resource: LauncherResource;
|
||||
open: boolean;
|
||||
isAdmin: boolean;
|
||||
onFilterBySite: (site: LauncherSiteInfo) => void;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const { data, isPending, isError } = useQuery({
|
||||
@@ -477,6 +688,8 @@ function LauncherResourcePanelBody({
|
||||
launcherResource={resource}
|
||||
resource={detail.data}
|
||||
authInfo={detail.authInfo}
|
||||
isAdmin={isAdmin}
|
||||
onFilterBySite={onFilterBySite}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -486,6 +699,8 @@ function LauncherResourcePanelBody({
|
||||
orgId={orgId}
|
||||
launcherResource={resource}
|
||||
resource={detail.data}
|
||||
isAdmin={isAdmin}
|
||||
onFilterBySite={onFilterBySite}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -495,7 +710,8 @@ export function LauncherResourcePanel({
|
||||
onOpenChange,
|
||||
resource,
|
||||
orgId,
|
||||
isAdmin
|
||||
isAdmin,
|
||||
onFilterBySite
|
||||
}: LauncherResourcePanelProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -511,6 +727,8 @@ export function LauncherResourcePanel({
|
||||
orgId={orgId}
|
||||
resource={resource}
|
||||
open={open}
|
||||
isAdmin={isAdmin}
|
||||
onFilterBySite={onFilterBySite}
|
||||
/>
|
||||
) : null}
|
||||
</SidePanelBody>
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
type LauncherGroup,
|
||||
type LauncherResource,
|
||||
type LauncherScaleInfo,
|
||||
type LauncherSiteInfo,
|
||||
type LauncherViewConfig,
|
||||
type LauncherViewRecord,
|
||||
type ListLauncherResourcesResponse
|
||||
@@ -584,6 +585,14 @@ export default function ResourceLauncher({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleFilterBySite = useCallback(
|
||||
(site: LauncherSiteInfo) => {
|
||||
applyConfigPatch({ siteIds: [site.siteId] });
|
||||
handlePanelOpenChange(false);
|
||||
},
|
||||
[applyConfigPatch, handlePanelOpenChange]
|
||||
);
|
||||
|
||||
const hasHandledAutoOpen = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -795,6 +804,7 @@ export default function ResourceLauncher({
|
||||
resource={selectedResource}
|
||||
orgId={orgId}
|
||||
isAdmin={isAdmin}
|
||||
onFilterBySite={handleFilterBySite}
|
||||
/>
|
||||
|
||||
{activeSavedView ? (
|
||||
|
||||
Reference in New Issue
Block a user