mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-11 21:41:41 +02:00
add sites to resource launcher panel
This commit is contained in:
@@ -4276,6 +4276,9 @@
|
||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
|
||||
"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.",
|
||||
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
|
||||
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
|
||||
|
||||
@@ -522,6 +522,7 @@ async function fetchLabelsForResources(
|
||||
type SiteGroupRow = {
|
||||
siteId: number;
|
||||
name: string;
|
||||
niceId: string;
|
||||
type: string;
|
||||
online: boolean;
|
||||
itemCount: number;
|
||||
@@ -556,6 +557,7 @@ async function listSiteGroups(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(resources.resourceId)
|
||||
@@ -576,7 +578,13 @@ async function listSiteGroups(
|
||||
|
||||
const publicRows = await publicQuery
|
||||
.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) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -586,6 +594,7 @@ async function listSiteGroups(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -612,6 +621,7 @@ async function listSiteGroups(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(siteResources.siteResourceId)
|
||||
@@ -638,7 +648,13 @@ async function listSiteGroups(
|
||||
|
||||
const siteRows = await siteResourceQuery
|
||||
.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) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -648,6 +664,7 @@ async function listSiteGroups(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
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(
|
||||
orgId: string,
|
||||
resourceIds: number[],
|
||||
@@ -1084,6 +1138,7 @@ async function mapPublicResources(
|
||||
enabled: resources.enabled,
|
||||
siteId: sites.siteId,
|
||||
siteName: sites.name,
|
||||
siteNiceId: sites.niceId,
|
||||
siteType: sites.type,
|
||||
siteOnline: sites.online,
|
||||
exitNodeEndpoint: exitNodes.endpoint
|
||||
@@ -1097,56 +1152,65 @@ async function mapPublicResources(
|
||||
inArray(resources.resourceId, resourceIds),
|
||||
eq(resources.orgId, orgId),
|
||||
eq(resources.enabled, true),
|
||||
eq(resources.status, "approved"),
|
||||
siteIdFilter != null
|
||||
? eq(sites.siteId, siteIdFilter)
|
||||
: undefined
|
||||
eq(resources.status, "approved")
|
||||
)
|
||||
);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const result: LauncherResource[] = [];
|
||||
const byKey = new Map<string, LauncherResource>();
|
||||
const siteIdsByKey = new Map<string, Set<number>>();
|
||||
|
||||
for (const row of rows) {
|
||||
const key = `public:${row.resourceId}`;
|
||||
if (seen.has(key)) {
|
||||
let item = byKey.get(key);
|
||||
|
||||
if (!item) {
|
||||
const access = formatPublicResourceAccess({
|
||||
mode: row.mode,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
proxyPort: row.proxyPort,
|
||||
wildcard: row.wildcard,
|
||||
exitNodeEndpoint: row.exitNodeEndpoint
|
||||
});
|
||||
|
||||
item = {
|
||||
launcherResourceKey: key,
|
||||
resourceType: "public",
|
||||
resourceId: row.resourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
|
||||
sites: []
|
||||
};
|
||||
byKey.set(key, item);
|
||||
siteIdsByKey.set(key, new Set());
|
||||
}
|
||||
|
||||
const site = toLauncherSiteInfo(row);
|
||||
if (!site) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
|
||||
const access = formatPublicResourceAccess({
|
||||
mode: row.mode,
|
||||
fullDomain: row.fullDomain,
|
||||
ssl: row.ssl,
|
||||
proxyPort: row.proxyPort,
|
||||
wildcard: row.wildcard,
|
||||
exitNodeEndpoint: row.exitNodeEndpoint
|
||||
});
|
||||
|
||||
result.push({
|
||||
launcherResourceKey: key,
|
||||
resourceType: "public",
|
||||
resourceId: row.resourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels: labelMaps.byResourceId.get(row.resourceId) ?? [],
|
||||
site:
|
||||
row.siteId != null
|
||||
? {
|
||||
siteId: row.siteId,
|
||||
name: row.siteName!,
|
||||
type: row.siteType!,
|
||||
online: row.siteOnline ?? undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const seenSiteIds = siteIdsByKey.get(key)!;
|
||||
if (seenSiteIds.has(site.siteId)) {
|
||||
continue;
|
||||
}
|
||||
seenSiteIds.add(site.siteId);
|
||||
item.sites.push(site);
|
||||
}
|
||||
|
||||
return result;
|
||||
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(
|
||||
@@ -1175,6 +1239,7 @@ async function mapSiteResources(
|
||||
enabled: siteResources.enabled,
|
||||
siteId: sites.siteId,
|
||||
siteName: sites.name,
|
||||
siteNiceId: sites.niceId,
|
||||
siteType: sites.type,
|
||||
siteOnline: sites.online
|
||||
})
|
||||
@@ -1189,59 +1254,69 @@ async function mapSiteResources(
|
||||
inArray(siteResources.siteResourceId, siteResourceIds),
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
eq(siteResources.status, "approved"),
|
||||
siteIdFilter != null
|
||||
? eq(sites.siteId, siteIdFilter)
|
||||
: undefined
|
||||
eq(siteResources.status, "approved")
|
||||
)
|
||||
);
|
||||
|
||||
const seen = new Set<string>();
|
||||
const result: LauncherResource[] = [];
|
||||
const byKey = new Map<string, LauncherResource>();
|
||||
const siteIdsByKey = new Map<string, Set<number>>();
|
||||
|
||||
for (const row of rows) {
|
||||
const key = `site:${row.siteResourceId}`;
|
||||
if (seen.has(key)) {
|
||||
let item = byKey.get(key);
|
||||
|
||||
if (!item) {
|
||||
const access = formatSiteResourceAccess({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
destinationPort: row.destinationPort,
|
||||
scheme: row.scheme,
|
||||
ssl: row.ssl,
|
||||
fullDomain: row.fullDomain,
|
||||
alias: row.alias,
|
||||
aliasAddress: row.aliasAddress
|
||||
});
|
||||
|
||||
item = {
|
||||
launcherResourceKey: key,
|
||||
resourceType: "site",
|
||||
resourceId: row.siteResourceId,
|
||||
siteResourceId: row.siteResourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels:
|
||||
labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
|
||||
sites: []
|
||||
};
|
||||
byKey.set(key, item);
|
||||
siteIdsByKey.set(key, new Set());
|
||||
}
|
||||
|
||||
const site = toLauncherSiteInfo(row);
|
||||
if (!site) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
|
||||
const access = formatSiteResourceAccess({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
destinationPort: row.destinationPort,
|
||||
scheme: row.scheme,
|
||||
ssl: row.ssl,
|
||||
fullDomain: row.fullDomain,
|
||||
alias: row.alias,
|
||||
aliasAddress: row.aliasAddress
|
||||
});
|
||||
|
||||
result.push({
|
||||
launcherResourceKey: key,
|
||||
resourceType: "site",
|
||||
resourceId: row.siteResourceId,
|
||||
siteResourceId: row.siteResourceId,
|
||||
niceId: row.niceId,
|
||||
name: row.name,
|
||||
...access,
|
||||
iconUrl: null,
|
||||
enabled: row.enabled,
|
||||
mode: row.mode,
|
||||
labels: labelMaps.bySiteResourceId.get(row.siteResourceId) ?? [],
|
||||
site:
|
||||
row.siteId != null
|
||||
? {
|
||||
siteId: row.siteId,
|
||||
name: row.siteName!,
|
||||
type: row.siteType!,
|
||||
online: row.siteOnline ?? undefined
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
const seenSiteIds = siteIdsByKey.get(key)!;
|
||||
if (seenSiteIds.has(site.siteId)) {
|
||||
continue;
|
||||
}
|
||||
seenSiteIds.add(site.siteId);
|
||||
item.sites.push(site);
|
||||
}
|
||||
|
||||
return result;
|
||||
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(
|
||||
@@ -1252,13 +1327,17 @@ function filterResourcesBySite(
|
||||
return items.filter((item) => item.mode === "inference");
|
||||
}
|
||||
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);
|
||||
if (!Number.isFinite(siteId)) {
|
||||
return items;
|
||||
}
|
||||
return items.filter((item) => item.site?.siteId === siteId);
|
||||
return items.filter((item) =>
|
||||
item.sites.some((site) => site.siteId === siteId)
|
||||
);
|
||||
}
|
||||
|
||||
function filterResourcesByLabel(
|
||||
@@ -1499,6 +1578,7 @@ async function collectAccessibleSites(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(resources.resourceId)
|
||||
@@ -1507,7 +1587,13 @@ async function collectAccessibleSites(
|
||||
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||
.innerJoin(sites, eq(targets.siteId, sites.siteId))
|
||||
.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) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -1517,6 +1603,7 @@ async function collectAccessibleSites(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -1540,6 +1627,7 @@ async function collectAccessibleSites(
|
||||
.select({
|
||||
siteId: sites.siteId,
|
||||
name: sites.name,
|
||||
niceId: sites.niceId,
|
||||
type: sites.type,
|
||||
online: sites.online,
|
||||
itemCount: countDistinct(siteResources.siteResourceId)
|
||||
@@ -1551,7 +1639,13 @@ async function collectAccessibleSites(
|
||||
)
|
||||
.innerJoin(sites, eq(siteNetworks.siteId, sites.siteId))
|
||||
.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) {
|
||||
const existing = siteCountMap.get(row.siteId);
|
||||
@@ -1561,6 +1655,7 @@ async function collectAccessibleSites(
|
||||
siteCountMap.set(row.siteId, {
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online,
|
||||
itemCount: Number(row.itemCount)
|
||||
@@ -1675,6 +1770,7 @@ export async function listAccessibleLauncherSitesForUser(
|
||||
.map((row) => ({
|
||||
siteId: row.siteId,
|
||||
name: row.name,
|
||||
niceId: row.niceId,
|
||||
type: row.type,
|
||||
online: row.online
|
||||
}))
|
||||
|
||||
@@ -32,6 +32,7 @@ export type LauncherLabel = {
|
||||
export type LauncherSiteInfo = {
|
||||
siteId: number;
|
||||
name: string;
|
||||
niceId: string;
|
||||
type: string;
|
||||
online?: boolean;
|
||||
};
|
||||
@@ -51,6 +52,7 @@ export type LauncherResource = {
|
||||
mode: string;
|
||||
labels: LauncherLabel[];
|
||||
site?: LauncherSiteInfo;
|
||||
sites: LauncherSiteInfo[];
|
||||
};
|
||||
|
||||
export type LauncherGroup = {
|
||||
@@ -184,8 +186,7 @@ export function parseIdListParam(value: string | undefined): number[] {
|
||||
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||
|
||||
export type LauncherViewSelection =
|
||||
| { type: "default" }
|
||||
| { type: "saved"; viewId: number };
|
||||
{ type: "default" } | { type: "saved"; viewId: number };
|
||||
|
||||
export type LauncherScaleCapabilities = {
|
||||
allowSiteGrouping: boolean;
|
||||
|
||||
@@ -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