Skip to content

Commit 7e792c6

Browse files
authored
Add instant logs (Ctrl + Shift + L) (#857)
* Add instant logs (Ctrl + Shift + L) * scrollbar fix * fix fonts * fix keyboard navigation
1 parent 733ac4d commit 7e792c6

File tree

5 files changed

+201
-4
lines changed

5 files changed

+201
-4
lines changed

app/Http/Controllers/ServerLogController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ public function remote(Server $server): Response
5656
}
5757

5858
#[Get('/json/{site?}', name: 'logs.json')]
59-
public function json(Server $server, ?Site $site = null): ResourceCollection
59+
public function json(Request $request, Server $server, ?Site $site = null): ResourceCollection
6060
{
6161
$this->authorize('viewAny', [ServerLog::class, $server]);
6262

6363
$logs = $server->logs()
6464
->when($site, fn ($query) => $query->where('site_id', $site->id))
6565
->latest()
66-
->simplePaginate(config('web.pagination_size'));
66+
->simplePaginate($request->query('count') ?? config('web.pagination_size'));
6767

6868
return ServerLogResource::collection($logs);
6969
}

resources/css/app.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,32 @@
2626
animation: indeterminate 1s linear infinite;
2727
}
2828
}
29+
30+
::-webkit-scrollbar {
31+
width: 8px; /* scrollbar width */
32+
height: 8px; /* horizontal scrollbar height */
33+
}
34+
35+
::-webkit-scrollbar-track {
36+
background: transparent; /* track background */
37+
}
38+
39+
::-webkit-scrollbar-thumb {
40+
background-color: rgba(100, 100, 100, 0.5); /* thumb color */
41+
border-radius: 10px; /* rounded ends */
42+
border: 2px solid transparent; /* padding around thumb */
43+
background-clip: content-box; /* ensures border doesn’t overlap thumb */
44+
}
45+
46+
::-webkit-scrollbar-thumb:hover {
47+
background-color: rgba(100, 100, 100, 0.7);
48+
}
49+
50+
.scrollbar-hide {
51+
scrollbar-width: none; /* Firefox */
52+
-ms-overflow-style: none; /* IE 10+ */
53+
}
54+
55+
.scrollbar-hide::-webkit-scrollbar {
56+
display: none; /* Safari + Chrome */
57+
}

resources/js/components/instant-terminal.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ export default function InstantTerminal({ server, children }: { server: Server;
235235
const newSession = async () => {
236236
await fetch(route('console.new-session', { server: server.id }), {});
237237
getWorkingDir(user);
238+
clearOutputCallback();
238239
};
239240

240241
const handleSubmit = (e: FormEvent) => {
@@ -310,7 +311,7 @@ export default function InstantTerminal({ server, children }: { server: Server;
310311
<SheetHeader className="bg-muted/50 flex flex-row items-center justify-between border-b px-4 py-2">
311312
<div className="flex items-center gap-2">
312313
<TerminalSquareIcon className="h-4 w-4" />
313-
<SheetTitle className="text-sm font-medium">Terminal - {server.name}</SheetTitle>
314+
<SheetTitle className="text-sm font-medium">Headless Terminal - {server.name}</SheetTitle>
314315
<SheetDescription className="sr-only">Terminal</SheetDescription>
315316
</div>
316317

@@ -418,6 +419,7 @@ export default function InstantTerminal({ server, children }: { server: Server;
418419
<div className="flex items-center gap-2">
419420
<LoaderCircleIcon className="text-muted-foreground h-4 w-4 animate-spin" />
420421
<span className="text-muted-foreground font-mono text-sm">Running command...</span>
422+
<button tabIndex={0} autoFocus className="opacity-0"></button>
421423
</div>
422424
)}
423425
</div>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import DateTime from '@/components/date-time';
2+
import { Button } from '@/components/ui/button';
3+
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
4+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
5+
import { PaginatedData } from '@/types';
6+
import { Server } from '@/types/server';
7+
import { ServerLog } from '@/types/server-log';
8+
import { useQuery } from '@tanstack/react-query';
9+
import { ChevronRightIcon, LogsIcon, RefreshCwIcon, XIcon } from 'lucide-react';
10+
import { ReactNode, useEffect, useState } from 'react';
11+
import { toast } from 'sonner';
12+
13+
interface LogEntry {
14+
id: number;
15+
content: string;
16+
}
17+
18+
export function InstantLogs({ server, children }: { server: Server; children: ReactNode }) {
19+
const [open, setOpen] = useState(false);
20+
const [logEntry, setLogEntry] = useState<LogEntry | null>(null);
21+
const [page, setPage] = useState(1);
22+
const [logs, setLogs] = useState<ServerLog[]>([]);
23+
24+
const query = useQuery<PaginatedData<ServerLog>>({
25+
queryKey: ['instant-logs', server.id, page],
26+
queryFn: async () => {
27+
const response = await fetch(route('logs.json', { server: server.id, count: 15, page: page }));
28+
if (!response.ok) {
29+
toast.error('Failed to fetch logs');
30+
throw new Error('Network response was not ok');
31+
}
32+
const data = response.json();
33+
const logs = (await data).data;
34+
if (page === 1) {
35+
setLogs(logs);
36+
} else {
37+
setLogs((prev) => [...new Set([...prev, ...logs])]);
38+
}
39+
setPage((await data).meta.current_page);
40+
return data;
41+
},
42+
enabled: false,
43+
});
44+
45+
useEffect(() => {
46+
if (open && logs.length === 0) {
47+
query.refetch();
48+
}
49+
}, [open, logs]);
50+
51+
useEffect(() => {
52+
query.refetch();
53+
}, [page]);
54+
55+
const fetchLog = async (logId: number) => {
56+
if (logEntry?.id === logId) {
57+
setLogEntry(null);
58+
return;
59+
}
60+
setLogEntry({
61+
id: logId,
62+
content: 'Loading...',
63+
});
64+
const response = await fetch(route('logs.show', { server: server.id, log: logId }));
65+
if (!response.ok) {
66+
toast.error('Failed to fetch log');
67+
throw new Error('Network response was not ok');
68+
}
69+
const text = await response.text();
70+
setLogEntry({ id: logId, content: text });
71+
};
72+
73+
const loadMore = () => {
74+
setPage((prev) => prev + 1);
75+
};
76+
77+
const reset = () => {
78+
setLogs([]);
79+
setLogEntry(null);
80+
setPage(1);
81+
};
82+
83+
useEffect(() => {
84+
const handleKeydown = (event: KeyboardEvent) => {
85+
if (event.ctrlKey && event.shiftKey && event.key === 'L') {
86+
event.preventDefault();
87+
setOpen(!open);
88+
}
89+
};
90+
91+
document.addEventListener('keydown', handleKeydown);
92+
return () => document.removeEventListener('keydown', handleKeydown);
93+
}, [open, setOpen]);
94+
95+
return (
96+
<Sheet open={open} onOpenChange={setOpen}>
97+
<SheetTrigger asChild>{children}</SheetTrigger>
98+
<SheetContent side="bottom" className="h-3/4" showClose={false}>
99+
<SheetHeader className="bg-muted/50 flex flex-row items-center justify-between border-b px-4 py-2">
100+
<div className="flex items-center gap-2">
101+
<LogsIcon className="h-4 w-4" />
102+
<SheetTitle className="text-sm font-medium">Logs - {server.name}</SheetTitle>
103+
<SheetDescription className="sr-only">Logs</SheetDescription>
104+
</div>
105+
<div className="flex items-center gap-2">
106+
<Tooltip delayDuration={0}>
107+
<TooltipTrigger asChild>
108+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={reset}>
109+
{query.isFetching ? <RefreshCwIcon className="h-3 w-3 animate-spin" /> : <RefreshCwIcon className="h-3 w-3" />}
110+
</Button>
111+
</TooltipTrigger>
112+
<TooltipContent>Reload</TooltipContent>
113+
</Tooltip>
114+
<Tooltip delayDuration={0}>
115+
<TooltipTrigger asChild>
116+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => setOpen(false)}>
117+
<XIcon className="h-3 w-3" />
118+
</Button>
119+
</TooltipTrigger>
120+
<TooltipContent>Close</TooltipContent>
121+
</Tooltip>
122+
</div>
123+
</SheetHeader>
124+
<div className="flex h-full flex-col overflow-y-auto">
125+
{logs.map((log, index) => (
126+
<div key={`log-${log.id}`}>
127+
<Button
128+
variant="ghost"
129+
className="flex w-full items-center justify-between rounded-none border-b px-4 py-2 font-mono text-xs"
130+
onClick={() => fetchLog(log.id)}
131+
tabIndex={index + 1}
132+
autoFocus={index === 0}
133+
>
134+
<div className="flex items-center gap-2">
135+
<ChevronRightIcon className="size-4" />
136+
<div>{log.name}</div>
137+
</div>
138+
<div className="text-muted-foreground text-xs">
139+
<DateTime date={log.created_at} />
140+
</div>
141+
</Button>
142+
{logEntry?.id === log.id && (
143+
<div className="bg-muted/50 max-h-64 overflow-auto border-b px-4 py-2 font-mono text-xs whitespace-pre-wrap">{logEntry.content}</div>
144+
)}
145+
</div>
146+
))}
147+
<Button variant="ghost" onClick={loadMore} tabIndex={logs.length + 1}>
148+
{query.isFetching ? 'Loading...' : 'Load More'}
149+
</Button>
150+
</div>
151+
</SheetContent>
152+
</Sheet>
153+
);
154+
}

resources/js/pages/servers/components/header.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Server } from '@/types/server';
2-
import { CheckIcon, CloudIcon, LoaderCircleIcon, MousePointerClickIcon, SlashIcon, TerminalSquareIcon } from 'lucide-react';
2+
import { CheckIcon, CloudIcon, LoaderCircleIcon, LogsIcon, MousePointerClickIcon, SlashIcon, TerminalSquareIcon } from 'lucide-react';
33
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
44
import ServerActions from '@/pages/servers/components/actions';
55
import { cn } from '@/lib/utils';
@@ -10,6 +10,7 @@ import { useForm } from '@inertiajs/react';
1010
import { useState } from 'react';
1111
import { Button } from '@/components/ui/button';
1212
import InstantTerminal from '@/components/instant-terminal';
13+
import { InstantLogs } from '@/pages/server-logs/components/instant-logs';
1314

1415
export default function ServerHeader({ server, site }: { server: Server; site?: Site }) {
1516
const statusForm = useForm();
@@ -133,6 +134,17 @@ export default function ServerHeader({ server, site }: { server: Server; site?:
133134
</div>
134135
</div>
135136
<div className="flex items-center space-x-1">
137+
<Tooltip>
138+
<InstantLogs server={server}>
139+
<TooltipTrigger asChild>
140+
<Button variant="ghost" size="icon" className="h-8 w-8 p-0">
141+
<LogsIcon className="h-4 w-4" />
142+
</Button>
143+
</TooltipTrigger>
144+
</InstantLogs>
145+
<TooltipContent>Logs (Ctrl + Shift + L)</TooltipContent>
146+
</Tooltip>
147+
136148
<Tooltip>
137149
<InstantTerminal server={server}>
138150
<TooltipTrigger asChild>

0 commit comments

Comments
 (0)