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">
<h2 align="center"><a href="https://fossorial.io"><img alt="pangolin" src="public/logo//word_mark.png" width="400" /></a></h2>
[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.fossorial.io/)
[![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)
<h2>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
</picture>
</h2>
</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">
_Your own self-hosted zero trust tunnel._
@ -30,6 +28,12 @@ _Your own self-hosted zero trust tunnel._
Contact Us
</a>
</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>
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:pg:push": "npx tsx server/db/pg/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",
"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",

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;
} catch (error) {
if (axios.isAxiosError(error)) {

View file

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

View file

@ -60,7 +60,8 @@ import {
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionFooter,
SettingsSectionForm
SettingsSectionForm,
SettingsSectionGrid
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { useRouter } from "next/navigation";
@ -73,6 +74,7 @@ import {
CollapsibleTrigger
} from "@app/components/ui/collapsible";
import { ContainersSelector } from "@app/components/ContainersSelector";
import { FaDocker } from "react-icons/fa";
const addTargetSchema = z.object({
ip: z.string().refine(isTargetValid),
@ -559,115 +561,6 @@ export default function ReverseProxyTargets(props: {
return (
<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>
<SettingsSectionHeader>
<SettingsSectionTitle>
@ -775,8 +668,7 @@ export default function ReverseProxyTargets(props: {
<FormControl>
<Input id="ip" {...field} />
</FormControl>
<FormMessage />
{site && site.type == 'newt' && (
{site && site.type == "newt" && (
<ContainersSelector
site={site}
onContainerSelect={(
@ -796,6 +688,7 @@ export default function ReverseProxyTargets(props: {
}}
/>
)}
<FormMessage />
</FormItem>
)}
/>
@ -891,59 +784,175 @@ export default function ReverseProxyTargets(props: {
</SettingsSection>
{resource.http && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Additional Proxy Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how your resource handles proxy settings
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveProxySettings
)}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
Custom Host Header
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The host header to set when
proxying requests. Leave
empty to use the default.
</FormDescription>
<FormMessage />
</FormItem>
<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
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={proxySettingsLoading}
form="proxy-settings-form"
>
Save Proxy Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
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>
<SettingsSectionHeader>
<SettingsSectionTitle>
Additional Proxy Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how your resource handles proxy
settings
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveProxySettings
)}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
Custom Host Header
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The host header to set
when proxying requests.
Leave empty to use the
default.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={proxySettingsLoading}
form="proxy-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsSectionGrid>
)}
</SettingsContainer>
);

View file

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

View file

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