Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 61 additions & 22 deletions apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import {
Loader2,
Pause,
Play,
SendIcon,
} from "lucide-react";
import React, { useEffect, useRef } from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { ButtonGroup } from "@/components/ui/button-group";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { LineCountFilter } from "./line-count-filter";
Expand Down Expand Up @@ -76,6 +78,7 @@ export const DockerLogsId: React.FC<Props> = ({
const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [copied, setCopied] = React.useState(false);
const [ws, setWs] = React.useState<WebSocket | null>(null);

const scrollToBottom = () => {
if (autoScroll && scrollRef.current) {
Expand Down Expand Up @@ -160,6 +163,7 @@ export const DockerLogsId: React.FC<Props> = ({
window.location.host
}/docker-container-logs?${params.toString()}`;
const ws = new WebSocket(wsUrl);
setWs(ws);

const resetNoDataTimeout = () => {
if (noDataTimeout) clearTimeout(noDataTimeout);
Expand Down Expand Up @@ -394,29 +398,64 @@ export const DockerLogsId: React.FC<Props> = ({
</div>
</AlertBlock>
)}
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
>
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (
<TerminalLine
key={index}
log={filteredLog}
searchTerm={search}
noTimestamp={!showTimestamp}
<div>
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded-t custom-logs-scrollbar"
>
{filteredLogs.length > 0 ? (
filteredLogs.map((filteredLog: LogLine, index: number) => (
<TerminalLine
key={index}
log={filteredLog}
searchTerm={search}
noTimestamp={!showTimestamp}
/>
))
) : isLoading ? (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
No logs found
</div>
)}
</div>
<form
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const input = e.currentTarget.command as HTMLInputElement;

const command = input.value;

if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (isPaused) return;
if (!command.trim()) return;

ws.send(command);
input.value = "";
}}
>
<ButtonGroup className="w-full">
<Input
className="rounded-t-none rounded-r-none bg-background border border-t-0"
placeholder="Send a command"
name="command"
disabled={!ws || ws.readyState !== WebSocket.OPEN || isPaused}
/>
))
) : isLoading ? (
<div className="flex justify-center items-center h-full text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : (
<div className="flex justify-center items-center h-full text-muted-foreground">
No logs found
</div>
)}
<Button
variant="outline"
size="icon"
type="submit"
className="rounded-t-none border-border border-t-0"
disabled={!ws || ws.readyState !== WebSocket.OPEN || isPaused}
>
<SendIcon className="size-4" />
</Button>
</ButtonGroup>
</form>
</div>
</div>
</div>
Expand Down
83 changes: 83 additions & 0 deletions apps/dokploy/components/ui/button-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"

const buttonGroupVariants = cva(
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)

function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}

function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "div"

return (
<Comp
className={cn(
"bg-muted shadow-xs flex items-center gap-2 rounded-md border px-4 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
className
)}
{...props}
/>
)
}

function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
)
}

export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
13 changes: 12 additions & 1 deletion apps/dokploy/server/wss/docker-container-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ export const setupDockerContainerLogsWebSocketServer = (
rows: 30,
});

const attachCommand = `docker attach ${containerId}`;

const attachPty = spawn(shell, ["-c", attachCommand], {
name: "xterm-256color",
cwd: process.env.HOME,
env: process.env,
encoding: "utf8",
cols: 80,
rows: 30,
});

ptyProcess.onData((data) => {
ws.send(data);
});
Expand All @@ -144,7 +155,7 @@ export const setupDockerContainerLogsWebSocketServer = (
} else {
command = message;
}
ptyProcess.write(command.toString());
attachPty.write(`${command.toString()}\n`);
} catch (error) {
// @ts-ignore
const errorMessage = error?.message as unknown as string;
Expand Down