mirror of
https://github.com/fosrl/pangolin.git
synced 2025-06-20 20:35:43 +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">
|
||||
<h2 align="center"><a href="https://fossorial.io"><img alt="pangolin" src="public/logo//word_mark.png" width="400" /></a></h2>
|
||||
|
||||
[](https://docs.fossorial.io/)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

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

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