mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-11 01:53:58 +00:00
Merge branch 'rdp-ssh' into dev
This commit is contained in:
146
src/components/BrowserGatewayTargetForm.tsx
Normal file
146
src/components/BrowserGatewayTargetForm.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronsUpDown, ExternalLink } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
MultiSitesSelector,
|
||||
formatMultiSitesSelectorLabel
|
||||
} from "./multi-site-selector";
|
||||
import { SitesSelector, type Selectedsite } from "./site-selector";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
|
||||
type SingleSiteProps = {
|
||||
multiSite?: false;
|
||||
selectedSite: Selectedsite | null;
|
||||
onSiteChange: (site: Selectedsite | null) => void;
|
||||
};
|
||||
|
||||
type MultiSiteProps = {
|
||||
multiSite: true;
|
||||
selectedSites: Selectedsite[];
|
||||
onSitesChange: (sites: Selectedsite[]) => void;
|
||||
};
|
||||
|
||||
export type BrowserGatewayTargetFormProps = {
|
||||
orgId: string;
|
||||
destination: string;
|
||||
defaultPort: number;
|
||||
destinationPort: string;
|
||||
onDestinationChange: (v: string) => void;
|
||||
onDestinationPortChange: (v: string) => void;
|
||||
learnMoreHref?: string;
|
||||
} & (SingleSiteProps | MultiSiteProps);
|
||||
|
||||
export function BrowserGatewayTargetForm(props: BrowserGatewayTargetFormProps) {
|
||||
const t = useTranslations();
|
||||
const [siteOpen, setSiteOpen] = useState(false);
|
||||
|
||||
const siteSelector =
|
||||
props.multiSite === true ? (
|
||||
<Popover open={siteOpen} onOpenChange={setSiteOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{formatMultiSitesSelectorLabel(
|
||||
props.selectedSites,
|
||||
t
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<MultiSitesSelector
|
||||
orgId={props.orgId}
|
||||
selectedSites={props.selectedSites}
|
||||
onSelectionChange={props.onSitesChange}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Popover open={siteOpen} onOpenChange={setSiteOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
<span className="truncate">
|
||||
{props.selectedSite?.name ?? t("siteSelect")}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
|
||||
<SitesSelector
|
||||
orgId={props.orgId}
|
||||
selectedSite={props.selectedSite}
|
||||
onSelectSite={(site) => {
|
||||
props.onSiteChange(site);
|
||||
setSiteOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">
|
||||
{t("sites")}
|
||||
</label>
|
||||
{siteSelector}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">
|
||||
{t("destination")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="192.168.1.1"
|
||||
value={props.destination}
|
||||
onChange={(e) =>
|
||||
props.onDestinationChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">{t("port")}</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={props.defaultPort.toString()}
|
||||
value={props.destinationPort}
|
||||
onChange={(e) =>
|
||||
props.onDestinationPortChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{props.multiSite === true && props.selectedSites.length > 1 && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("bgTargetMultiSiteDisclaimer")}{" "}
|
||||
<a
|
||||
href={
|
||||
props.learnMoreHref ??
|
||||
"https://docs.pangolin.net/manage/resources/public/ssh"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{t("learnMore")}
|
||||
<ExternalLink className="size-3.5 shrink-0" />
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -77,21 +77,22 @@ export type InternalResourceRow = {
|
||||
siteIds: number[];
|
||||
siteNiceIds: string[];
|
||||
// mode: "host" | "cidr" | "port";
|
||||
mode: "host" | "cidr" | "http";
|
||||
mode: "host" | "cidr" | "http" | "ssh";
|
||||
scheme: "http" | "https" | null;
|
||||
ssl: boolean;
|
||||
// protocol: string | null;
|
||||
// proxyPort: number | null;
|
||||
destination: string;
|
||||
httpHttpsPort: number | null;
|
||||
destination: string | null;
|
||||
destinationPort: number | null;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
niceId: string;
|
||||
tcpPortRangeString: string | null;
|
||||
udpPortRangeString: string | null;
|
||||
disableIcmp: boolean;
|
||||
authDaemonMode?: "site" | "remote" | null;
|
||||
authDaemonMode?: "site" | "remote" | "native" | null;
|
||||
authDaemonPort?: number | null;
|
||||
pamMode?: "passthrough" | "push" | null;
|
||||
subdomain?: string | null;
|
||||
domainId?: string | null;
|
||||
fullDomain?: string | null;
|
||||
@@ -106,7 +107,7 @@ function formatDestinationDisplay(row: InternalResourceRow): string {
|
||||
return formatSiteResourceDestinationDisplay({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
httpHttpsPort: row.httpHttpsPort,
|
||||
destinationPort: row.destinationPort,
|
||||
scheme: row.scheme
|
||||
});
|
||||
}
|
||||
@@ -357,6 +358,10 @@ export default function ClientResourcesTable({
|
||||
{
|
||||
value: "http",
|
||||
label: t("editInternalResourceDialogModeHttp")
|
||||
},
|
||||
{
|
||||
value: "ssh",
|
||||
label: t("editInternalResourceDialogModeSsh")
|
||||
}
|
||||
]}
|
||||
selectedValue={searchParams.get("mode") ?? undefined}
|
||||
@@ -372,13 +377,14 @@ export default function ClientResourcesTable({
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
const modeLabels: Record<
|
||||
"host" | "cidr" | "port" | "http",
|
||||
"host" | "cidr" | "port" | "http" | "ssh",
|
||||
string
|
||||
> = {
|
||||
host: t("editInternalResourceDialogModeHost"),
|
||||
cidr: t("editInternalResourceDialogModeCidr"),
|
||||
port: t("editInternalResourceDialogModePort"),
|
||||
http: t("editInternalResourceDialogModeHttp")
|
||||
http: t("editInternalResourceDialogModeHttp"),
|
||||
ssh: t("editInternalResourceDialogModeSsh")
|
||||
};
|
||||
return <span>{modeLabels[resourceRow.mode]}</span>;
|
||||
}
|
||||
|
||||
@@ -47,31 +47,36 @@ export default function CreateInternalResourceDialog({
|
||||
try {
|
||||
let data = { ...values };
|
||||
if (
|
||||
(data.mode === "host" || data.mode === "http") &&
|
||||
(data.mode === "host" ||
|
||||
data.mode === "http" ||
|
||||
data.mode === "ssh") &&
|
||||
isHostname(data.destination)
|
||||
) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
if (!currentAlias) {
|
||||
let aliasValue = data.destination;
|
||||
if (data.destination.toLowerCase() === "localhost") {
|
||||
if (data.destination?.toLowerCase() === "localhost") {
|
||||
aliasValue = `${cleanForFQDN(data.name)}.internal`;
|
||||
}
|
||||
data = { ...data, alias: aliasValue };
|
||||
}
|
||||
}
|
||||
|
||||
// "ssh" mode maps to "host" in the backend with SSH settings
|
||||
const backendMode = data.mode === "ssh" ? "host" : data.mode;
|
||||
|
||||
await api.put<
|
||||
AxiosResponse<{ data: { siteResourceId: number } }>
|
||||
>(`/org/${orgId}/site-resource`, {
|
||||
name: data.name,
|
||||
siteIds: data.siteIds,
|
||||
mode: data.mode,
|
||||
mode: backendMode,
|
||||
destination: data.destination,
|
||||
enabled: true,
|
||||
...(data.mode === "http" && {
|
||||
scheme: data.scheme,
|
||||
ssl: data.ssl ?? false,
|
||||
destinationPort: data.httpHttpsPort ?? undefined,
|
||||
destinationPort: data.destinationPort ?? undefined,
|
||||
domainId: data.httpConfigDomainId
|
||||
? data.httpConfigDomainId
|
||||
: undefined,
|
||||
@@ -94,7 +99,25 @@ export default function CreateInternalResourceDialog({
|
||||
authDaemonPort: data.authDaemonPort
|
||||
})
|
||||
}),
|
||||
...((data.mode === "host" || data.mode == "cidr") && {
|
||||
...(data.mode === "ssh" && {
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: undefined,
|
||||
pamMode: data.pamMode ?? undefined,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" &&
|
||||
data.authDaemonPort != null && {
|
||||
authDaemonPort: data.authDaemonPort
|
||||
})
|
||||
}),
|
||||
...((data.mode === "host" ||
|
||||
data.mode === "ssh" ||
|
||||
data.mode === "cidr") && {
|
||||
tcpPortRangeString: data.tcpPortRangeString,
|
||||
udpPortRangeString: data.udpPortRangeString,
|
||||
disableIcmp: data.disableIcmp ?? false
|
||||
|
||||
@@ -51,29 +51,34 @@ export default function EditInternalResourceDialog({
|
||||
try {
|
||||
let data = { ...values };
|
||||
if (
|
||||
(data.mode === "host" || data.mode === "http") &&
|
||||
(data.mode === "host" ||
|
||||
data.mode === "http" ||
|
||||
data.mode === "ssh") &&
|
||||
isHostname(data.destination)
|
||||
) {
|
||||
const currentAlias = data.alias?.trim() || "";
|
||||
if (!currentAlias) {
|
||||
let aliasValue = data.destination;
|
||||
if (data.destination.toLowerCase() === "localhost") {
|
||||
if (data.destination?.toLowerCase() === "localhost") {
|
||||
aliasValue = `${cleanForFQDN(data.name)}.internal`;
|
||||
}
|
||||
data = { ...data, alias: aliasValue };
|
||||
}
|
||||
}
|
||||
|
||||
// "ssh" mode maps to "host" in the backend with SSH settings
|
||||
const backendMode = data.mode === "ssh" ? "host" : data.mode;
|
||||
|
||||
await api.post(`/site-resource/${resource.id}`, {
|
||||
name: data.name,
|
||||
siteIds: data.siteIds,
|
||||
mode: data.mode,
|
||||
mode: backendMode,
|
||||
niceId: data.niceId,
|
||||
destination: data.destination,
|
||||
...(data.mode === "http" && {
|
||||
scheme: data.scheme,
|
||||
ssl: data.ssl ?? false,
|
||||
destinationPort: data.httpHttpsPort ?? null,
|
||||
destinationPort: data.destinationPort ?? null,
|
||||
domainId: data.httpConfigDomainId
|
||||
? data.httpConfigDomainId
|
||||
: undefined,
|
||||
@@ -95,7 +100,24 @@ export default function EditInternalResourceDialog({
|
||||
authDaemonPort: data.authDaemonPort || null
|
||||
})
|
||||
}),
|
||||
...((data.mode === "host" || data.mode === "cidr") && {
|
||||
...(data.mode === "ssh" && {
|
||||
alias:
|
||||
data.alias &&
|
||||
typeof data.alias === "string" &&
|
||||
data.alias.trim()
|
||||
? data.alias
|
||||
: null,
|
||||
pamMode: data.pamMode ?? undefined,
|
||||
...(data.authDaemonMode != null && {
|
||||
authDaemonMode: data.authDaemonMode
|
||||
}),
|
||||
...(data.authDaemonMode === "remote" && {
|
||||
authDaemonPort: data.authDaemonPort || null
|
||||
})
|
||||
}),
|
||||
...((data.mode === "host" ||
|
||||
data.mode === "ssh" ||
|
||||
data.mode === "cidr") && {
|
||||
tcpPortRangeString: data.tcpPortRangeString,
|
||||
udpPortRangeString: data.udpPortRangeString,
|
||||
disableIcmp: data.disableIcmp ?? false
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
import type { SidebarNavSection } from "@app/app/navigation";
|
||||
import { OrgSelector } from "@app/components/OrgSelector";
|
||||
import { SidebarNav } from "@app/components/SidebarNav";
|
||||
import SupporterStatus from "@app/components/SupporterStatus";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -258,11 +257,6 @@ export function LayoutSidebar({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{build === "oss" && (
|
||||
<div className="px-4">
|
||||
<SupporterStatus isCollapsed={isSidebarCollapsed} />
|
||||
</div>
|
||||
)}
|
||||
{build === "saas" && (
|
||||
<div className="px-4">
|
||||
<SidebarSupportButton
|
||||
|
||||
@@ -49,7 +49,7 @@ type Resource = {
|
||||
domain: string;
|
||||
enabled: boolean;
|
||||
protected: boolean;
|
||||
protocol: string;
|
||||
mode: string; // "http", "tcp", "udp", "rdp", "vnc", "ssh"
|
||||
// Auth method fields
|
||||
sso?: boolean;
|
||||
password?: boolean;
|
||||
@@ -64,7 +64,6 @@ type SiteResource = {
|
||||
name: string;
|
||||
destination: string;
|
||||
mode: string;
|
||||
protocol: string | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
enabled: boolean;
|
||||
@@ -882,21 +881,6 @@ export default function MemberResourcesPortal({
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
{siteResource.protocol && (
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
"protocol"
|
||||
)}
|
||||
:
|
||||
</span>
|
||||
<span className="ml-2 text-muted-foreground uppercase">
|
||||
{
|
||||
siteResource.protocol
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{t(
|
||||
@@ -954,7 +938,7 @@ export default function MemberResourcesPortal({
|
||||
siteResource.fullDomain ? (
|
||||
/* HTTP mode - show as clickable link */
|
||||
<CopyToClipboard
|
||||
text={`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`}
|
||||
text={`${siteResource.ssl ? "https" : (siteResource.mode ?? "http")}://${siteResource.fullDomain}`}
|
||||
isLink={true}
|
||||
/>
|
||||
) : siteResource.alias ? (
|
||||
@@ -1037,7 +1021,7 @@ export default function MemberResourcesPortal({
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`${siteResource.ssl ? "https" : (siteResource.protocol ?? "http")}://${siteResource.fullDomain}`,
|
||||
`${siteResource.ssl ? "https" : (siteResource.mode ?? "http")}://${siteResource.fullDomain}`,
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { Laptop, LogOut, Moon, Sun, Smartphone, Trash2 } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { build } from "@server/build";
|
||||
import { useState } from "react";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import Disable2FaForm from "./Disable2FaForm";
|
||||
@@ -27,7 +26,6 @@ import SecurityKeyForm from "./SecurityKeyForm";
|
||||
import Enable2FaDialog from "./Enable2FaDialog";
|
||||
import ChangePasswordDialog from "./ChangePasswordDialog";
|
||||
import ViewDevicesDialog from "./ViewDevicesDialog";
|
||||
import SupporterStatus from "./SupporterStatus";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import LocaleSwitcher from "@app/components/LocaleSwitcher";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -90,9 +90,8 @@ export type ResourceRow = {
|
||||
name: string;
|
||||
orgId: string;
|
||||
domain: string;
|
||||
mode: string | null;
|
||||
authState: string;
|
||||
http: boolean;
|
||||
protocol: string;
|
||||
proxyPort: number | null;
|
||||
enabled: boolean;
|
||||
domainId?: string;
|
||||
@@ -366,11 +365,11 @@ export default function ProxyResourcesTable({
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<span>
|
||||
{resourceRow.http
|
||||
{resourceRow.mode == "http"
|
||||
? resourceRow.ssl
|
||||
? "HTTPS"
|
||||
: "HTTP"
|
||||
: resourceRow.protocol.toUpperCase()}
|
||||
: resourceRow.mode?.toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -413,6 +412,9 @@ export default function ProxyResourcesTable({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
if (resourceRow.mode !== "http") {
|
||||
return <span>-</span>;
|
||||
}
|
||||
return (
|
||||
<TargetStatusCell
|
||||
targets={resourceRow.targets}
|
||||
@@ -441,6 +443,9 @@ export default function ProxyResourcesTable({
|
||||
header: () => <span className="p-3">{t("uptime30d")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
if (resourceRow.mode !== "http") {
|
||||
return <span>-</span>;
|
||||
}
|
||||
return (
|
||||
<UptimeMiniBar resourceId={resourceRow.id} days={30} />
|
||||
);
|
||||
@@ -453,7 +458,11 @@ export default function ProxyResourcesTable({
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
|
||||
if (!resourceRow.http) {
|
||||
if (
|
||||
!["http", "ssh", "rdp", "vnc"].includes(
|
||||
resourceRow.mode || ""
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<CopyToClipboard
|
||||
@@ -941,7 +950,7 @@ function ResourceEnabledForm({
|
||||
resource,
|
||||
onToggleResourceEnabled
|
||||
}: ResourceEnabledFormProps) {
|
||||
const enabled = resource.http
|
||||
const enabled = ["http", "ssh", "rdp", "vnc"].includes(resource.mode || "")
|
||||
? !!resource.domainId && resource.enabled
|
||||
: resource.enabled;
|
||||
const [optimisticEnabled, setOptimisticEnabled] = useOptimistic(enabled);
|
||||
@@ -959,7 +968,10 @@ function ResourceEnabledForm({
|
||||
<Switch
|
||||
checked={optimisticEnabled}
|
||||
disabled={
|
||||
(resource.http && !resource.domainId) ||
|
||||
(["http", "ssh", "rdp", "vnc"].includes(
|
||||
resource.mode || ""
|
||||
) &&
|
||||
!resource.domainId) ||
|
||||
optimisticEnabled !== enabled
|
||||
}
|
||||
name="enabled"
|
||||
|
||||
@@ -4,11 +4,9 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
Eye,
|
||||
EyeOff,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock
|
||||
XCircle
|
||||
} from "lucide-react";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
@@ -32,12 +30,43 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
|
||||
const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
|
||||
|
||||
const showCertificate = !!(
|
||||
["http", "ssh", "rdp", "vnc"].includes(resource.mode) &&
|
||||
resource.domainId &&
|
||||
resource.fullDomain &&
|
||||
build != "oss"
|
||||
);
|
||||
const showType = !!(
|
||||
["http", "ssh", "rdp", "vnc"].includes(resource.mode) && resource.mode
|
||||
);
|
||||
const showHealth =
|
||||
!["ssh", "rdp", "vnc"].includes(resource.mode || "") &&
|
||||
!!resource.health &&
|
||||
resource.health !== "unknown";
|
||||
const showVisibility = !resource.enabled;
|
||||
|
||||
const numSections = [
|
||||
true, // URL or Protocol
|
||||
true, // Authentication or Port
|
||||
showType,
|
||||
showCertificate,
|
||||
showHealth,
|
||||
showVisibility
|
||||
].filter(Boolean).length;
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
{/* 4 cols because of the certs */}
|
||||
<InfoSections cols={resource.http && build != "oss" ? 5 : 4}>
|
||||
{resource.http ? (
|
||||
<InfoSections cols={numSections}>
|
||||
{/* <InfoSection>
|
||||
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span className="inline-flex items-center">
|
||||
{resource.niceId}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection> */}
|
||||
{["http", "ssh", "rdp", "vnc"].includes(resource.mode) ? (
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>URL</InfoSectionTitle>
|
||||
@@ -54,6 +83,18 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{showType && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("type")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span className="inline-flex items-center">
|
||||
{resource.mode!.toUpperCase()}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("authentication")}
|
||||
@@ -76,24 +117,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{/* {isEnabled && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Socket</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isAvailable ? (
|
||||
<span className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Online</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-neutral-500 rounded-full"></div>
|
||||
<span>Offline</span>
|
||||
</span>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)} */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -103,7 +126,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span className="inline-flex items-center">
|
||||
{resource.protocol.toUpperCase()}
|
||||
{resource.mode?.toUpperCase()}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
@@ -141,74 +164,69 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
{/* </InfoSectionContent> */}
|
||||
{/* </InfoSection> */}
|
||||
{/* Certificate Status Column */}
|
||||
{resource.http &&
|
||||
resource.domainId &&
|
||||
resource.fullDomain &&
|
||||
build != "oss" && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("certificateStatus", {
|
||||
defaultValue: "Certificate"
|
||||
})}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CertificateStatus
|
||||
orgId={resource.orgId}
|
||||
domainId={resource.domainId}
|
||||
fullDomain={resource.fullDomain}
|
||||
autoFetch={true}
|
||||
showLabel={false}
|
||||
polling={true}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("health")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{resource.health === "healthy" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 flex-shrink-0 text-green-500" />
|
||||
<span>{t("resourcesTableHealthy")}</span>
|
||||
</div>
|
||||
)}
|
||||
{resource.health === "degraded" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 flex-shrink-0 text-yellow-500" />
|
||||
<span>{t("resourcesTableDegraded")}</span>
|
||||
</div>
|
||||
)}
|
||||
{resource.health === "unhealthy" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<XCircle className="w-4 h-4 flex-shrink-0 text-destructive" />
|
||||
<span>{t("resourcesTableUnhealthy")}</span>
|
||||
</div>
|
||||
)}
|
||||
{(!resource.health ||
|
||||
resource.health === "unknown") && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t("resourcesTableUnknown")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{resource.enabled ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Eye className="w-4 h-4 flex-shrink-0 text-green-500" />
|
||||
<span>{t("enabled")}</span>
|
||||
</div>
|
||||
) : (
|
||||
{showCertificate && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("certificateStatus", {
|
||||
defaultValue: "Certificate"
|
||||
})}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CertificateStatus
|
||||
orgId={resource.orgId}
|
||||
domainId={resource.domainId!}
|
||||
fullDomain={resource.fullDomain!}
|
||||
autoFetch={true}
|
||||
showLabel={false}
|
||||
polling={true}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
{showHealth && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>{t("health")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{resource.health === "healthy" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 flex-shrink-0 text-green-500" />
|
||||
<span>
|
||||
{t("resourcesTableHealthy")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{resource.health === "degraded" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="w-4 h-4 flex-shrink-0 text-yellow-500" />
|
||||
<span>
|
||||
{t("resourcesTableDegraded")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{resource.health === "unhealthy" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<XCircle className="w-4 h-4 flex-shrink-0 text-destructive" />
|
||||
<span>
|
||||
{t("resourcesTableUnhealthy")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
{showVisibility && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("visibility")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="flex items-center space-x-2">
|
||||
<EyeOff className="w-4 h-4 flex-shrink-0 text-neutral-500" />
|
||||
<span>{t("disabled")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -22,13 +22,24 @@ export function SettingsSectionHeader({
|
||||
|
||||
export function SettingsSectionForm({
|
||||
children,
|
||||
className
|
||||
className,
|
||||
variant = "compact"
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant?: "half" | "compact";
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("max-w-xl space-y-4", className)}>{children}</div>
|
||||
<div
|
||||
className={cn(
|
||||
variant === "half"
|
||||
? "max-w-3xl space-y-4"
|
||||
: "max-w-xl space-y-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,13 +44,13 @@ function isSafeUrlForLink(href: string): boolean {
|
||||
const OVERVIEW_META_CLASS = "w-full min-w-0 text-muted-foreground text-sm";
|
||||
|
||||
function publicProtocolLabel(r: PublicResourceRow): string {
|
||||
if (r.http) {
|
||||
if (r.mode == "http") {
|
||||
return r.ssl ? "HTTPS" : "HTTP";
|
||||
}
|
||||
const p = (r.protocol || "").toLowerCase();
|
||||
const p = (r.mode || "").toLowerCase();
|
||||
if (p === "tcp") return "TCP";
|
||||
if (p === "udp") return "UDP";
|
||||
return (r.protocol || "—").toUpperCase();
|
||||
return (r.mode || "—").toUpperCase();
|
||||
}
|
||||
|
||||
function PublicResourceMeta({ resource: r }: { resource: PublicResourceRow }) {
|
||||
@@ -68,12 +68,13 @@ function PrivateResourceMeta({ row }: { row: SiteResourceRow }) {
|
||||
const modeLabel: Record<SiteResourceRow["mode"], string> = {
|
||||
host: t("editInternalResourceDialogModeHost"),
|
||||
cidr: t("editInternalResourceDialogModeCidr"),
|
||||
http: t("editInternalResourceDialogModeHttp")
|
||||
http: t("editInternalResourceDialogModeHttp"),
|
||||
ssh: t("editInternalResourceDialogModeSsh")
|
||||
};
|
||||
const dest = formatSiteResourceDestinationDisplay({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
httpHttpsPort: row.destinationPort ?? null,
|
||||
destinationPort: row.destinationPort ?? null,
|
||||
scheme: row.scheme
|
||||
});
|
||||
return (
|
||||
@@ -90,7 +91,7 @@ function PrivateResourceMeta({ row }: { row: SiteResourceRow }) {
|
||||
|
||||
function PublicAccessMethod({ resource: r }: { resource: PublicResourceRow }) {
|
||||
const t = useTranslations();
|
||||
if (!r.http) {
|
||||
if (!["http", "ssh", "rdp", "vnc"].includes(r.mode || "")) {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
text={r.proxyPort?.toString() ?? ""}
|
||||
@@ -149,7 +150,7 @@ function PrivateAccessMethod({ row }: { row: SiteResourceRow }) {
|
||||
const dest = formatSiteResourceDestinationDisplay({
|
||||
mode: row.mode,
|
||||
destination: row.destination,
|
||||
httpHttpsPort: row.destinationPort,
|
||||
destinationPort: row.destinationPort,
|
||||
scheme: row.scheme
|
||||
});
|
||||
return (
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Separator } from "@app/components/ui/separator";
|
||||
// THIS IS DEPRECATED AND IS NO LONGER SHOWED TO THE USER WITH THE DISCONTINUATION
|
||||
// OF THE SUPPORTER PROGRAM. IT MAY BE REMOVED IN A FUTURE UPDATE.
|
||||
|
||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||
import { useState, useTransition } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@app/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -44,7 +40,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ValidateSupporterKeyResponse } from "@server/routers/supporterKey";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -228,7 +223,10 @@ export default function SupporterStatus({
|
||||
|
||||
<div className="my-4 p-4 border border-blue-500/50 bg-blue-500/10 rounded-lg">
|
||||
<p className="text-sm">
|
||||
<strong>Business & Enterprise Users:</strong> For larger organizations or teams requiring advanced features, consider our self-serve enterprise license and Enterprise Edition.{" "}
|
||||
<strong>Business & Enterprise Users:</strong>{" "}
|
||||
For larger organizations or teams requiring
|
||||
advanced features, consider our self-serve
|
||||
enterprise license and Enterprise Edition.{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/pricing#Self-Hosted"
|
||||
target="_blank"
|
||||
|
||||
@@ -41,23 +41,23 @@ export function MultiSelectTagInput<T extends TagValue>({
|
||||
variant: "outline"
|
||||
}),
|
||||
"justify-between w-full inline-flex",
|
||||
"text-muted-foreground pl-1.5 cursor-text h-auto py-1",
|
||||
"text-muted-foreground pl-1.5 cursor-text h-9 py-0",
|
||||
"hover:bg-transparent hover:text-muted-foreground",
|
||||
props.disabled && "pointer-events-none opacity-50"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1",
|
||||
"overflow-x-auto flex-wrap h-auto"
|
||||
"inline-flex items-center gap-1 min-w-0 flex-1",
|
||||
"overflow-x-auto flex-nowrap h-full"
|
||||
)}
|
||||
>
|
||||
{props.value.map((option) => (
|
||||
<span
|
||||
key={option.id}
|
||||
className={cn(
|
||||
"bg-muted-foreground/10 font-normal text-foreground rounded-sm",
|
||||
"py-1 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
|
||||
"bg-muted-foreground/10 font-normal text-foreground rounded-sm flex-none",
|
||||
"py-0.5 pl-1.5 pr-0.5 text-xs inline-flex items-center gap-0.5"
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user