add sites to resource launcher panel

This commit is contained in:
miloschwartz
2026-09-11 14:57:36 -04:00
parent d776478350
commit 110b6d19a5
5 changed files with 422 additions and 94 deletions
+3
View File
@@ -4276,6 +4276,9 @@
"resourceLauncherViewAsAdmin": "View as Admin", "resourceLauncherViewAsAdmin": "View as Admin",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.", "resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details", "resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherSitesDescription": "The resource is accessible via the following sites.",
"resourceLauncherViewSiteAsAdmin": "View Site as Admin",
"resourceLauncherFilterBySite": "Filter by Site",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.", "resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.", "resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required", "resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
+147 -51
View File
@@ -522,6 +522,7 @@ async function fetchLabelsForResources(
type SiteGroupRow = { type SiteGroupRow = {
siteId: number; siteId: number;
name: string; name: string;
niceId: string;
type: string; type: string;
online: boolean; online: boolean;
itemCount: number; itemCount: number;
@@ -556,6 +557,7 @@ async function listSiteGroups(
.select({ .select({
siteId: sites.siteId, siteId: sites.siteId,
name: sites.name, name: sites.name,
niceId: sites.niceId,
type: sites.type, type: sites.type,
online: sites.online, online: sites.online,
itemCount: countDistinct(resources.resourceId) itemCount: countDistinct(resources.resourceId)
@@ -576,7 +578,13 @@ async function listSiteGroups(
const publicRows = await publicQuery const publicRows = await publicQuery
.where(and(...publicConditions)) .where(and(...publicConditions))
.groupBy(sites.siteId, sites.name, sites.type, sites.online); .groupBy(
sites.siteId,
sites.name,
sites.niceId,
sites.type,
sites.online
);
for (const row of publicRows) { for (const row of publicRows) {
const existing = siteCountMap.get(row.siteId); const existing = siteCountMap.get(row.siteId);
@@ -586,6 +594,7 @@ async function listSiteGroups(
siteCountMap.set(row.siteId, { siteCountMap.set(row.siteId, {
siteId: row.siteId, siteId: row.siteId,
name: row.name, name: row.name,
niceId: row.niceId,
type: row.type, type: row.type,
online: row.online, online: row.online,
itemCount: Number(row.itemCount) itemCount: Number(row.itemCount)
@@ -612,6 +621,7 @@ async function listSiteGroups(
.select({ .select({
siteId: sites.siteId, siteId: sites.siteId,
name: sites.name, name: sites.name,
niceId: sites.niceId,
type: sites.type, type: sites.type,
online: sites.online, online: sites.online,
itemCount: countDistinct(siteResources.siteResourceId) itemCount: countDistinct(siteResources.siteResourceId)
@@ -638,7 +648,13 @@ async function listSiteGroups(
const siteRows = await siteResourceQuery const siteRows = await siteResourceQuery
.where(and(...siteConditions)) .where(and(...siteConditions))
.groupBy(sites.siteId, sites.name, sites.type, sites.online); .groupBy(
sites.siteId,
sites.name,
sites.niceId,
sites.type,
sites.online
);
for (const row of siteRows) { for (const row of siteRows) {
const existing = siteCountMap.get(row.siteId); const existing = siteCountMap.get(row.siteId);
@@ -648,6 +664,7 @@ async function listSiteGroups(
siteCountMap.set(row.siteId, { siteCountMap.set(row.siteId, {
siteId: row.siteId, siteId: row.siteId,
name: row.name, name: row.name,
niceId: row.niceId,
type: row.type, type: row.type,
online: row.online, online: row.online,
itemCount: Number(row.itemCount) itemCount: Number(row.itemCount)
@@ -1061,6 +1078,43 @@ export async function listLauncherGroupsForUser(
}; };
} }
function toLauncherSiteInfo(row: {
siteId: number | null;
siteName: string | null;
siteNiceId: string | null;
siteType: string | null;
siteOnline: boolean | null;
}): LauncherSiteInfo | null {
if (
row.siteId == null ||
row.siteName == null ||
row.siteNiceId == null ||
row.siteType == null
) {
return null;
}
return {
siteId: row.siteId,
name: row.siteName,
niceId: row.siteNiceId,
type: row.siteType,
online: row.siteOnline ?? undefined
};
}
function pickPrimarySite(
sites: LauncherSiteInfo[],
siteIdFilter?: number
): LauncherSiteInfo | undefined {
if (sites.length === 0) {
return undefined;
}
if (siteIdFilter != null) {
return sites.find((site) => site.siteId === siteIdFilter) ?? sites[0];
}
return sites[0];
}
async function mapPublicResources( async function mapPublicResources(
orgId: string, orgId: string,
resourceIds: number[], resourceIds: number[],
@@ -1084,6 +1138,7 @@ async function mapPublicResources(
enabled: resources.enabled, enabled: resources.enabled,
siteId: sites.siteId, siteId: sites.siteId,
siteName: sites.name, siteName: sites.name,
siteNiceId: sites.niceId,
siteType: sites.type, siteType: sites.type,
siteOnline: sites.online, siteOnline: sites.online,
exitNodeEndpoint: exitNodes.endpoint exitNodeEndpoint: exitNodes.endpoint
@@ -1097,23 +1152,18 @@ async function mapPublicResources(
inArray(resources.resourceId, resourceIds), inArray(resources.resourceId, resourceIds),
eq(resources.orgId, orgId), eq(resources.orgId, orgId),
eq(resources.enabled, true), eq(resources.enabled, true),
eq(resources.status, "approved"), eq(resources.status, "approved")
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
) )
); );
const seen = new Set<string>(); const byKey = new Map<string, LauncherResource>();
const result: LauncherResource[] = []; const siteIdsByKey = new Map<string, Set<number>>();
for (const row of rows) { for (const row of rows) {
const key = `public:${row.resourceId}`; const key = `public:${row.resourceId}`;
if (seen.has(key)) { let item = byKey.get(key);
continue;
}
seen.add(key);
if (!item) {
const access = formatPublicResourceAccess({ const access = formatPublicResourceAccess({
mode: row.mode, mode: row.mode,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
@@ -1123,7 +1173,7 @@ async function mapPublicResources(
exitNodeEndpoint: row.exitNodeEndpoint exitNodeEndpoint: row.exitNodeEndpoint
}); });
result.push({ item = {
launcherResourceKey: key, launcherResourceKey: key,
resourceType: "public", resourceType: "public",
resourceId: row.resourceId, resourceId: row.resourceId,
@@ -1134,19 +1184,33 @@ async function mapPublicResources(
enabled: row.enabled, enabled: row.enabled,
mode: row.mode, mode: row.mode,
labels: labelMaps.byResourceId.get(row.resourceId) ?? [], labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
site: sites: []
row.siteId != null };
? { byKey.set(key, item);
siteId: row.siteId, siteIdsByKey.set(key, new Set());
name: row.siteName!,
type: row.siteType!,
online: row.siteOnline ?? undefined
}
: undefined
});
} }
return result; const site = toLauncherSiteInfo(row);
if (!site) {
continue;
}
const seenSiteIds = siteIdsByKey.get(key)!;
if (seenSiteIds.has(site.siteId)) {
continue;
}
seenSiteIds.add(site.siteId);
item.sites.push(site);
}
for (const item of byKey.values()) {
item.sites.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
);
item.site = pickPrimarySite(item.sites, siteIdFilter);
}
return Array.from(byKey.values());
} }
async function mapSiteResources( async function mapSiteResources(
@@ -1175,6 +1239,7 @@ async function mapSiteResources(
enabled: siteResources.enabled, enabled: siteResources.enabled,
siteId: sites.siteId, siteId: sites.siteId,
siteName: sites.name, siteName: sites.name,
siteNiceId: sites.niceId,
siteType: sites.type, siteType: sites.type,
siteOnline: sites.online siteOnline: sites.online
}) })
@@ -1189,23 +1254,18 @@ async function mapSiteResources(
inArray(siteResources.siteResourceId, siteResourceIds), inArray(siteResources.siteResourceId, siteResourceIds),
eq(siteResources.orgId, orgId), eq(siteResources.orgId, orgId),
eq(siteResources.enabled, true), eq(siteResources.enabled, true),
eq(siteResources.status, "approved"), eq(siteResources.status, "approved")
siteIdFilter != null
? eq(sites.siteId, siteIdFilter)
: undefined
) )
); );
const seen = new Set<string>(); const byKey = new Map<string, LauncherResource>();
const result: LauncherResource[] = []; const siteIdsByKey = new Map<string, Set<number>>();
for (const row of rows) { for (const row of rows) {
const key = `site:${row.siteResourceId}`; const key = `site:${row.siteResourceId}`;
if (seen.has(key)) { let item = byKey.get(key);
continue;
}
seen.add(key);
if (!item) {
const access = formatSiteResourceAccess({ const access = formatSiteResourceAccess({
mode: row.mode, mode: row.mode,
destination: row.destination, destination: row.destination,
@@ -1217,7 +1277,7 @@ async function mapSiteResources(
aliasAddress: row.aliasAddress aliasAddress: row.aliasAddress
}); });
result.push({ item = {
launcherResourceKey: key, launcherResourceKey: key,
resourceType: "site", resourceType: "site",
resourceId: row.siteResourceId, resourceId: row.siteResourceId,
@@ -1228,20 +1288,35 @@ async function mapSiteResources(
iconUrl: null, iconUrl: null,
enabled: row.enabled, enabled: row.enabled,
mode: row.mode, mode: row.mode,
labels: labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [], labels:
site: labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
row.siteId != null sites: []
? { };
siteId: row.siteId, byKey.set(key, item);
name: row.siteName!, siteIdsByKey.set(key, new Set());
type: row.siteType!,
online: row.siteOnline ?? undefined
}
: undefined
});
} }
return result; const site = toLauncherSiteInfo(row);
if (!site) {
continue;
}
const seenSiteIds = siteIdsByKey.get(key)!;
if (seenSiteIds.has(site.siteId)) {
continue;
}
seenSiteIds.add(site.siteId);
item.sites.push(site);
}
for (const item of byKey.values()) {
item.sites.sort((a, b) =>
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
);
item.site = pickPrimarySite(item.sites, siteIdFilter);
}
return Array.from(byKey.values());
} }
function filterResourcesBySite( function filterResourcesBySite(
@@ -1252,13 +1327,17 @@ function filterResourcesBySite(
return items.filter((item) => item.mode === "inference"); return items.filter((item) => item.mode === "inference");
} }
if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) { if (groupKey === LAUNCHER_NO_SITE_GROUP_KEY) {
return items.filter((item) => !item.site && item.mode !== "inference"); return items.filter(
(item) => item.sites.length === 0 && item.mode !== "inference"
);
} }
const siteId = Number.parseInt(groupKey, 10); const siteId = Number.parseInt(groupKey, 10);
if (!Number.isFinite(siteId)) { if (!Number.isFinite(siteId)) {
return items; return items;
} }
return items.filter((item) => item.site?.siteId === siteId); return items.filter((item) =>
item.sites.some((site) => site.siteId === siteId)
);
} }
function filterResourcesByLabel( function filterResourcesByLabel(
@@ -1499,6 +1578,7 @@ async function collectAccessibleSites(
.select({ .select({
siteId: sites.siteId, siteId: sites.siteId,
name: sites.name, name: sites.name,
niceId: sites.niceId,
type: sites.type, type: sites.type,
online: sites.online, online: sites.online,
itemCount: countDistinct(resources.resourceId) itemCount: countDistinct(resources.resourceId)
@@ -1507,7 +1587,13 @@ async function collectAccessibleSites(
.innerJoin(resources, eq(targets.resourceId, resources.resourceId)) .innerJoin(resources, eq(targets.resourceId, resources.resourceId))
.innerJoin(sites, eq(targets.siteId, sites.siteId)) .innerJoin(sites, eq(targets.siteId, sites.siteId))
.where(and(...publicConditions)) .where(and(...publicConditions))
.groupBy(sites.siteId, sites.name, sites.type, sites.online); .groupBy(
sites.siteId,
sites.name,
sites.niceId,
sites.type,
sites.online
);
for (const row of publicRows) { for (const row of publicRows) {
const existing = siteCountMap.get(row.siteId); const existing = siteCountMap.get(row.siteId);
@@ -1517,6 +1603,7 @@ async function collectAccessibleSites(
siteCountMap.set(row.siteId, { siteCountMap.set(row.siteId, {
siteId: row.siteId, siteId: row.siteId,
name: row.name, name: row.name,
niceId: row.niceId,
type: row.type, type: row.type,
online: row.online, online: row.online,
itemCount: Number(row.itemCount) itemCount: Number(row.itemCount)
@@ -1540,6 +1627,7 @@ async function collectAccessibleSites(
.select({ .select({
siteId: sites.siteId, siteId: sites.siteId,
name: sites.name, name: sites.name,
niceId: sites.niceId,
type: sites.type, type: sites.type,
online: sites.online, online: sites.online,
itemCount: countDistinct(siteResources.siteResourceId) itemCount: countDistinct(siteResources.siteResourceId)
@@ -1551,7 +1639,13 @@ async function collectAccessibleSites(
) )
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId)) .innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
.where(and(...siteConditions)) .where(and(...siteConditions))
.groupBy(sites.siteId, sites.name, sites.type, sites.online); .groupBy(
sites.siteId,
sites.name,
sites.niceId,
sites.type,
sites.online
);
for (const row of siteRows) { for (const row of siteRows) {
const existing = siteCountMap.get(row.siteId); const existing = siteCountMap.get(row.siteId);
@@ -1561,6 +1655,7 @@ async function collectAccessibleSites(
siteCountMap.set(row.siteId, { siteCountMap.set(row.siteId, {
siteId: row.siteId, siteId: row.siteId,
name: row.name, name: row.name,
niceId: row.niceId,
type: row.type, type: row.type,
online: row.online, online: row.online,
itemCount: Number(row.itemCount) itemCount: Number(row.itemCount)
@@ -1675,6 +1770,7 @@ export async function listAccessibleLauncherSitesForUser(
.map((row) => ({ .map((row) => ({
siteId: row.siteId, siteId: row.siteId,
name: row.name, name: row.name,
niceId: row.niceId,
type: row.type, type: row.type,
online: row.online online: row.online
})) }))
+3 -2
View File
@@ -32,6 +32,7 @@ export type LauncherLabel = {
export type LauncherSiteInfo = { export type LauncherSiteInfo = {
siteId: number; siteId: number;
name: string; name: string;
niceId: string;
type: string; type: string;
online?: boolean; online?: boolean;
}; };
@@ -51,6 +52,7 @@ export type LauncherResource = {
mode: string; mode: string;
labels: LauncherLabel[]; labels: LauncherLabel[];
site?: LauncherSiteInfo; site?: LauncherSiteInfo;
sites: LauncherSiteInfo[];
}; };
export type LauncherGroup = { export type LauncherGroup = {
@@ -184,8 +186,7 @@ export function parseIdListParam(value: string | undefined): number[] {
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const; export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
export type LauncherViewSelection = export type LauncherViewSelection =
| { type: "default" } { type: "default" } | { type: "saved"; viewId: number };
| { type: "saved"; viewId: number };
export type LauncherScaleCapabilities = { export type LauncherScaleCapabilities = {
allowSiteGrouping: boolean; allowSiteGrouping: boolean;
@@ -28,7 +28,22 @@ import {
} from "@app/components/SidePanel"; } from "@app/components/SidePanel";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button"; 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 { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
import { cn } from "@app/lib/cn";
import { import {
derivePublicAuthState, derivePublicAuthState,
formatPublicResourceType formatPublicResourceType
@@ -36,7 +51,10 @@ import {
import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref"; import { getLauncherResourceAdminHref } from "@app/lib/launcherResourceAdminHref";
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess"; import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
import { launcherQueries } from "@app/lib/queries"; 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 { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
import type { GetResourceResponse } from "@server/routers/resource/getResource"; import type { GetResourceResponse } from "@server/routers/resource/getResource";
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource"; import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
@@ -44,15 +62,18 @@ import { useQuery } from "@tanstack/react-query";
import { import {
AlertCircle, AlertCircle,
CheckCircle2, CheckCircle2,
ChevronsUpDown,
Clock, Clock,
ExternalLink, ExternalLink,
Loader2, Loader2,
MoreHorizontal,
ShieldCheck, ShieldCheck,
ShieldOff, ShieldOff,
XCircle XCircle
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
type LauncherResourcePanelProps = { type LauncherResourcePanelProps = {
open: boolean; open: boolean;
@@ -60,6 +81,7 @@ type LauncherResourcePanelProps = {
resource: LauncherResource | null; resource: LauncherResource | null;
orgId: string; orgId: string;
isAdmin: boolean; isAdmin: boolean;
onFilterBySite: (site: LauncherSiteInfo) => void;
}; };
type LauncherResourceDetailResult = type LauncherResourceDetailResult =
@@ -153,6 +175,171 @@ function HealthStatusDisplay({
const PUBLIC_AUTH_METHODS_MODES = ["http", "ssh", "rdp", "vnc"]; const PUBLIC_AUTH_METHODS_MODES = ["http", "ssh", "rdp", "vnc"];
const PUBLIC_AUTH_BADGE_MODES = [...PUBLIC_AUTH_METHODS_MODES, "inference"]; 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 }) { function AuthMethodStatusDisplay({ enabled }: { enabled: boolean }) {
const t = useTranslations(); const t = useTranslations();
@@ -235,12 +422,16 @@ function PublicResourceDetails({
orgId, orgId,
launcherResource, launcherResource,
resource, resource,
authInfo authInfo,
isAdmin,
onFilterBySite
}: { }: {
orgId: string; orgId: string;
launcherResource: LauncherResource; launcherResource: LauncherResource;
resource: GetResourceResponse; resource: GetResourceResponse;
authInfo: GetResourceAuthInfoResponse; authInfo: GetResourceAuthInfoResponse;
isAdmin: boolean;
onFilterBySite: (site: LauncherSiteInfo) => void;
}) { }) {
const t = useTranslations(); const t = useTranslations();
const mode = resource.mode || ""; const mode = resource.mode || "";
@@ -328,6 +519,12 @@ function PublicResourceDetails({
</InfoSections> </InfoSections>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
<LauncherResourceSitesSection
orgId={orgId}
sites={launcherResource.sites ?? []}
isAdmin={isAdmin}
onFilterBySite={onFilterBySite}
/>
{showAuthMethods ? ( {showAuthMethods ? (
<PublicResourceAuthMethods authInfo={authInfo} /> <PublicResourceAuthMethods authInfo={authInfo} />
) : null} ) : null}
@@ -363,11 +560,15 @@ function PublicResourceDetails({
function PrivateResourceDetails({ function PrivateResourceDetails({
orgId, orgId,
launcherResource, launcherResource,
resource resource,
isAdmin,
onFilterBySite
}: { }: {
orgId: string; orgId: string;
launcherResource: LauncherResource; launcherResource: LauncherResource;
resource: GetSiteResourceResponse; resource: GetSiteResourceResponse;
isAdmin: boolean;
onFilterBySite: (site: LauncherSiteInfo) => void;
}) { }) {
const t = useTranslations(); const t = useTranslations();
const isInference = resource.mode === "inference"; const isInference = resource.mode === "inference";
@@ -417,6 +618,12 @@ function PrivateResourceDetails({
/> />
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
<LauncherResourceSitesSection
orgId={orgId}
sites={launcherResource.sites ?? []}
isAdmin={isAdmin}
onFilterBySite={onFilterBySite}
/>
{isInference ? ( {isInference ? (
<> <>
<LauncherInferenceModelsSection <LauncherInferenceModelsSection
@@ -440,11 +647,15 @@ function PrivateResourceDetails({
function LauncherResourcePanelBody({ function LauncherResourcePanelBody({
orgId, orgId,
resource, resource,
open open,
isAdmin,
onFilterBySite
}: { }: {
orgId: string; orgId: string;
resource: LauncherResource; resource: LauncherResource;
open: boolean; open: boolean;
isAdmin: boolean;
onFilterBySite: (site: LauncherSiteInfo) => void;
}) { }) {
const t = useTranslations(); const t = useTranslations();
const { data, isPending, isError } = useQuery({ const { data, isPending, isError } = useQuery({
@@ -477,6 +688,8 @@ function LauncherResourcePanelBody({
launcherResource={resource} launcherResource={resource}
resource={detail.data} resource={detail.data}
authInfo={detail.authInfo} authInfo={detail.authInfo}
isAdmin={isAdmin}
onFilterBySite={onFilterBySite}
/> />
); );
} }
@@ -486,6 +699,8 @@ function LauncherResourcePanelBody({
orgId={orgId} orgId={orgId}
launcherResource={resource} launcherResource={resource}
resource={detail.data} resource={detail.data}
isAdmin={isAdmin}
onFilterBySite={onFilterBySite}
/> />
); );
} }
@@ -495,7 +710,8 @@ export function LauncherResourcePanel({
onOpenChange, onOpenChange,
resource, resource,
orgId, orgId,
isAdmin isAdmin,
onFilterBySite
}: LauncherResourcePanelProps) { }: LauncherResourcePanelProps) {
const t = useTranslations(); const t = useTranslations();
@@ -511,6 +727,8 @@ export function LauncherResourcePanel({
orgId={orgId} orgId={orgId}
resource={resource} resource={resource}
open={open} open={open}
isAdmin={isAdmin}
onFilterBySite={onFilterBySite}
/> />
) : null} ) : null}
</SidePanelBody> </SidePanelBody>
@@ -44,6 +44,7 @@ import {
type LauncherGroup, type LauncherGroup,
type LauncherResource, type LauncherResource,
type LauncherScaleInfo, type LauncherScaleInfo,
type LauncherSiteInfo,
type LauncherViewConfig, type LauncherViewConfig,
type LauncherViewRecord, type LauncherViewRecord,
type ListLauncherResourcesResponse 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); const hasHandledAutoOpen = useRef(false);
useEffect(() => { useEffect(() => {
@@ -795,6 +804,7 @@ export default function ResourceLauncher({
resource={selectedResource} resource={selectedResource}
orgId={orgId} orgId={orgId}
isAdmin={isAdmin} isAdmin={isAdmin}
onFilterBySite={handleFilterBySite}
/> />
{activeSavedView ? ( {activeSavedView ? (