minor visal adjustments to docker container view

This commit is contained in:
miloschwartz 2025-06-05 11:18:22 -04:00
parent ab843b1a43
commit 92135ff9c1
No known key found for this signature in database
7 changed files with 279 additions and 313 deletions

View file

@ -1,15 +1,13 @@
<div align="center"> <div align="center">
<h2 align="center"><a href="https://fossorial.io"><img alt="pangolin" src="public/logo//word_mark.png" width="400" /></a></h2> <h2>
<picture>
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.fossorial.io/) <source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
[![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) <img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) </picture>
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) </h2>
[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
</div> </div>
<h3 align="center">Tunneled Reverse Proxy Server with Access Control</h3> <h4 align="center">Tunneled Reverse Proxy Server with Access Control</h4>
<div align="center"> <div align="center">
_Your own self-hosted zero trust tunnel._ _Your own self-hosted zero trust tunnel._
@ -30,6 +28,12 @@ _Your own self-hosted zero trust tunnel._
Contact Us Contact Us
</a> </a>
</h5> </h5>
[![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin)
![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square)
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)
</div> </div>
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.

View file

@ -16,7 +16,8 @@
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts", "db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
"db:pg:push": "npx tsx server/db/pg/migrate.ts", "db:pg:push": "npx tsx server/db/pg/migrate.ts",
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts", "db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
"db:studio": "drizzle-kit studio", "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
"db:clear-migrations": "rm -rf server/migrations", "db:clear-migrations": "rm -rf server/migrations",
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs", "build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs", "build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",

View file

@ -24,7 +24,7 @@ export async function addPeer(exitNodeId: number, peer: {
} }
}); });
logger.info('Peer added successfully:', response.data.status); logger.info('Peer added successfully:', { peer: response.data.status });
return response.data; return response.data;
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {

View file

@ -31,7 +31,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
Resource Information Resource Information
</AlertTitle> </AlertTitle>
<AlertDescription className="mt-4"> <AlertDescription className="mt-4">
<InfoSections cols={isEnabled ? 5 : 4}> <InfoSections cols={4}>
{resource.http ? ( {resource.http ? (
<> <>
<InfoSection> <InfoSection>

View file

@ -60,7 +60,8 @@ import {
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionFooter, SettingsSectionFooter,
SettingsSectionForm SettingsSectionForm,
SettingsSectionGrid
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@ -73,6 +74,7 @@ import {
CollapsibleTrigger CollapsibleTrigger
} from "@app/components/ui/collapsible"; } from "@app/components/ui/collapsible";
import { ContainersSelector } from "@app/components/ContainersSelector"; import { ContainersSelector } from "@app/components/ContainersSelector";
import { FaDocker } from "react-icons/fa";
const addTargetSchema = z.object({ const addTargetSchema = z.object({
ip: z.string().refine(isTargetValid), ip: z.string().refine(isTargetValid),
@ -559,115 +561,6 @@ export default function ReverseProxyTargets(props: {
return ( return (
<SettingsContainer> <SettingsContainer>
{resource.http && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
HTTPS & TLS Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure TLS settings for your resource
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveTlsSettings
)}
className="space-y-4"
id="tls-settings-form"
>
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
/>
<Collapsible
open={isAdvancedOpen}
onOpenChange={setIsAdvancedOpen}
className="space-y-2"
>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
variant="text"
size="sm"
className="p-0 flex items-center justify-start gap-2 w-full"
>
<h4 className="text-sm font-semibold">
Advanced TLS Settings
</h4>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
</span>
</div>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2">
<FormField
control={
tlsSettingsForm.control
}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
TLS Server Name
(SNI)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The TLS Server Name
to use for SNI.
Leave empty to use
the default.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={httpsTlsLoading}
form="tls-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
)}
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@ -775,8 +668,7 @@ export default function ReverseProxyTargets(props: {
<FormControl> <FormControl>
<Input id="ip" {...field} /> <Input id="ip" {...field} />
</FormControl> </FormControl>
<FormMessage /> {site && site.type == "newt" && (
{site && site.type == 'newt' && (
<ContainersSelector <ContainersSelector
site={site} site={site}
onContainerSelect={( onContainerSelect={(
@ -796,6 +688,7 @@ export default function ReverseProxyTargets(props: {
}} }}
/> />
)} )}
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
@ -891,13 +784,127 @@ export default function ReverseProxyTargets(props: {
</SettingsSection> </SettingsSection>
{resource.http && ( {resource.http && (
<SettingsSectionGrid cols={2}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Secure Connection Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure SSL/TLS settings for your resource
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveTlsSettings
)}
className="space-y-4"
id="tls-settings-form"
>
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(
val
);
}}
/>
</FormControl>
</FormItem>
)}
/>
<Collapsible
open={isAdvancedOpen}
onOpenChange={setIsAdvancedOpen}
className="space-y-2"
>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
variant="text"
size="sm"
className="p-0 flex items-center justify-start gap-2 w-full"
>
<p className="text-sm text-muted-foreground">
Advanced TLS
Settings
</p>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
</span>
</div>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2">
<FormField
control={
tlsSettingsForm.control
}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
TLS Server Name
(SNI)
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormDescription>
The TLS Server
Name to use for
SNI. Leave empty
to use the
default.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={httpsTlsLoading}
form="tls-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
Additional Proxy Settings Additional Proxy Settings
</SettingsSectionTitle> </SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
Configure how your resource handles proxy settings Configure how your resource handles proxy
settings
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
@ -922,9 +929,10 @@ export default function ReverseProxyTargets(props: {
<Input {...field} /> <Input {...field} />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
The host header to set when The host header to set
proxying requests. Leave when proxying requests.
empty to use the default. Leave empty to use the
default.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -940,10 +948,11 @@ export default function ReverseProxyTargets(props: {
loading={proxySettingsLoading} loading={proxySettingsLoading}
form="proxy-settings-form" form="proxy-settings-form"
> >
Save Proxy Settings Save Settings
</Button> </Button>
</SettingsSectionFooter> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
</SettingsSectionGrid>
)} )}
</SettingsContainer> </SettingsContainer>
); );

View file

@ -289,17 +289,17 @@ export default function LicensePage() {
terms corresponding to the terms corresponding to the
tier associated with your tier associated with your
license key. license key.
<br /> {/* <br /> */}
<Link {/* <Link */}
href="https://fossorial.io/license.html" {/* href="https://fossorial.io/license.html" */}
target="_blank" {/* target="_blank" */}
rel="noopener noreferrer" {/* rel="noopener noreferrer" */}
className="text-primary hover:underline" {/* className="text-primary hover:underline" */}
> {/* > */}
View Fossorial {/* View Fossorial */}
Commercial License & {/* Commercial License & */}
Subscription Terms {/* Subscription Terms */}
</Link> {/* </Link> */}
</FormLabel> </FormLabel>
<FormMessage /> <FormMessage />
</div> </div>
@ -503,32 +503,32 @@ export default function LicensePage() {
</div> </div>
)} )}
</div> </div>
<SettingsSectionFooter> {/* <SettingsSectionFooter> */}
{!licenseStatus?.isHostLicensed ? ( {/* {!licenseStatus?.isHostLicensed ? ( */}
<> {/* <> */}
<Button {/* <Button */}
onClick={() => { {/* onClick={() => { */}
setPurchaseMode("license"); {/* setPurchaseMode("license"); */}
setIsPurchaseModalOpen(true); {/* setIsPurchaseModalOpen(true); */}
}} {/* }} */}
> {/* > */}
Purchase License {/* Purchase License */}
</Button> {/* </Button> */}
</> {/* </> */}
) : ( {/* ) : ( */}
<> {/* <> */}
<Button {/* <Button */}
variant="outline" {/* variant="outline" */}
onClick={() => { {/* onClick={() => { */}
setPurchaseMode("additional-sites"); {/* setPurchaseMode("additional-sites"); */}
setIsPurchaseModalOpen(true); {/* setIsPurchaseModalOpen(true); */}
}} {/* }} */}
> {/* > */}
Purchase Additional Sites {/* Purchase Additional Sites */}
</Button> {/* </Button> */}
</> {/* </> */}
)} {/* )} */}
</SettingsSectionFooter> {/* </SettingsSectionFooter> */}
</SettingsSection> </SettingsSection>
</SettingsSectionGrid> </SettingsSectionGrid>
<LicenseKeysDataTable <LicenseKeysDataTable

View file

@ -9,23 +9,15 @@ import {
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Credenza,
DialogContent, CredenzaBody,
DialogDescription, CredenzaClose,
DialogHeader, CredenzaContent,
DialogTitle, CredenzaDescription,
DialogTrigger CredenzaFooter,
} from "@/components/ui/dialog"; CredenzaHeader,
import { CredenzaTitle
Drawer, } from "@/components/Credenza";
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger
} from "@/components/ui/drawer";
import { import {
Table, Table,
TableBody, TableBody,
@ -53,7 +45,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Search, RefreshCw, Filter, Columns } from "lucide-react"; import { Search, RefreshCw, Filter, Columns } from "lucide-react";
import { GetSiteResponse, Container } from "@server/routers/site"; import { GetSiteResponse, Container } from "@server/routers/site";
import { useDockerSocket } from "@app/hooks/useDockerSocket"; import { useDockerSocket } from "@app/hooks/useDockerSocket";
import { useMediaQuery } from "@app/hooks/useMediaQuery"; import { FaDocker } from "react-icons/fa";
// Type definitions based on the JSON structure // Type definitions based on the JSON structure
@ -67,13 +59,11 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
onContainerSelect onContainerSelect
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
const { isAvailable, containers, fetchContainers } = useDockerSocket( const { isAvailable, containers, fetchContainers } = useDockerSocket(site);
site
);
useEffect(() => { useEffect(() => {
console.log("DockerSocket isAvailable:", isAvailable);
if (isAvailable) { if (isAvailable) {
fetchContainers(); fetchContainers();
} }
@ -90,29 +80,25 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
setOpen(false); setOpen(false);
}; };
if (isDesktop) {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <>
<DialogTrigger asChild> <a
<Button
type="button" type="button"
variant="squareOutline" className="text-sm text-primary hover:underline cursor-pointer"
size="icon" onClick={() => setOpen(true)}
className="absolute top-[35%] right-0"
> >
<span className="scale-125">🐋</span> View Docker Containers
</Button> </a>
</DialogTrigger> <Credenza open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-[75vw] max-h-[75vh] flex flex-col"> <CredenzaContent className="max-w-[75vw] max-h-[75vh] flex flex-col">
<DialogHeader> <CredenzaHeader>
<DialogTitle> <CredenzaTitle>Containers in {site.name}</CredenzaTitle>
Containers in <b>{site.name}</b> <CredenzaDescription>
</DialogTitle> Select any container to use as a hostname for this
<DialogDescription> target. Click a port to use select a port.
Select any container (w/ port) to use as target for </CredenzaDescription>
your resource </CredenzaHeader>
</DialogDescription> <CredenzaBody>
</DialogHeader>
<div className="flex-1 overflow-hidden min-h-0"> <div className="flex-1 overflow-hidden min-h-0">
<DockerContainersTable <DockerContainersTable
containers={containers} containers={containers}
@ -120,46 +106,15 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
onRefresh={() => fetchContainers()} onRefresh={() => fetchContainers()}
/> />
</div> </div>
</DialogContent> </CredenzaBody>
</Dialog> <CredenzaFooter>
); <CredenzaClose asChild>
} <Button variant="outline">Close</Button>
</CredenzaClose>
return ( </CredenzaFooter>
<Drawer open={open} onOpenChange={setOpen}> </CredenzaContent>
<DrawerTrigger asChild> </Credenza>
<Button </>
type="button"
variant="squareOutline"
size="icon"
className="absolute top-[35%] right-0"
>
<span className="scale-125">🐋</span>
</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>
Containers in <b>{site.name}</b>
</DrawerTitle>
<DrawerDescription>
Select any container to use as target for your resource
</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<DockerContainersTable
containers={containers}
onContainerSelect={handleContainerSelect}
onRefresh={fetchContainers}
/>
</div>
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
); );
}; };
@ -446,7 +401,7 @@ const DockerContainersTable: FC<{
if (initialFilters.length === 0) { if (initialFilters.length === 0) {
return ( return (
<div className="border rounded-md max-h-[500px] overflow-hidden flex flex-col"> <div className="rounded-md max-h-[500px] overflow-hidden flex flex-col">
<div className="flex-1 flex items-center justify-center py-8"> <div className="flex-1 flex items-center justify-center py-8">
<div className="text-center text-muted-foreground space-y-3"> <div className="text-center text-muted-foreground space-y-3">
{(hideContainersWithoutPorts || {(hideContainersWithoutPorts ||
@ -497,8 +452,8 @@ const DockerContainersTable: FC<{
} }
return ( return (
<div className="border rounded-md max-h-[500px] overflow-hidden flex flex-col"> <div className="rounded-md max-h-[500px] overflow-hidden flex flex-col">
<div className="p-3 border-b bg-background space-y-3"> <div className="p-1 space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@ -639,14 +594,11 @@ const DockerContainersTable: FC<{
</div> </div>
<div className="overflow-auto relative flex-1"> <div className="overflow-auto relative flex-1">
<Table sticky> <Table sticky>
<TableHeader sticky className="bg-background border-b"> <TableHeader sticky className="border-b">
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
<TableHead <TableHead key={header.id}>
key={header.id}
className="bg-background"
>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(