mirror of
https://github.com/fosrl/pangolin.git
synced 2025-06-21 04:45:41 +02:00
minor visal adjustments to docker container view
This commit is contained in:
parent
ab843b1a43
commit
92135ff9c1
7 changed files with 279 additions and 313 deletions
22
README.md
22
README.md
|
@ -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>
|
||||||
[](https://docs.fossorial.io/)
|
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="250">
|
||||||

|
</picture>
|
||||||
[](https://discord.gg/HCJR8Xhme4)
|
</h2>
|
||||||
[](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>
|
||||||
|
|
||||||
|
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||||
|

|
||||||
|
[](https://discord.gg/HCJR8Xhme4)
|
||||||
|
[](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.
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue