feat: add real time logger page (#276)
* feat: add real time logger * feat: add subscription for logging * feat: use timestamp and level in xterm, migrate to new xterm package * feat: improve design on log page * fit: remove xterm fit addon * fix: dispose terminal correctly * style: format code * refactor: add jsdoc for redis-transport * fix: redis connection not possible sometimes * feat: make terminal full size * fix: deepsource issues * fix: lint issue --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { Box, LoadingOverlay, Stack } from "@homarr/ui";
|
||||
import { BoardCategorySection } from "~/components/board/sections/category-section";
|
||||
import { BoardEmptySection } from "~/components/board/sections/empty-section";
|
||||
import { BoardBackgroundVideo } from "~/components/layout/background";
|
||||
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
||||
import { useIsBoardReady, useRequiredBoard } from "./_context";
|
||||
|
||||
let boardName: string | null = null;
|
||||
@@ -58,7 +59,7 @@ export const ClientBoard = () => {
|
||||
visible={!isReady}
|
||||
transitionProps={{ duration: 500 }}
|
||||
loaderProps={{ size: "lg" }}
|
||||
h="calc(100dvh - var(--app-shell-header-offset, 0px) - var(--app-shell-padding) - var(--app-shell-footer-offset, 0px) - var(--app-shell-padding))"
|
||||
h={fullHeightWithoutHeaderAndFooter}
|
||||
/>
|
||||
<Stack
|
||||
ref={ref}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
IconHome,
|
||||
IconInfoSmall,
|
||||
IconLayoutDashboard,
|
||||
IconLogs,
|
||||
IconMailForward,
|
||||
IconQuestionMark,
|
||||
IconTool,
|
||||
@@ -61,6 +62,11 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
|
||||
icon: IconBrandDocker,
|
||||
href: "/manage/tools/docker",
|
||||
},
|
||||
{
|
||||
label: t("items.tools.items.logs"),
|
||||
icon: IconLogs,
|
||||
href: "/manage/tools/logs",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
34
apps/nextjs/src/app/[locale]/manage/tools/logs/page.tsx
Normal file
34
apps/nextjs/src/app/[locale]/manage/tools/logs/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { Box } from "@homarr/ui";
|
||||
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const t = await getScopedI18n("management");
|
||||
const metaTitle = `${t("metaTitle")} • Homarr`;
|
||||
|
||||
return {
|
||||
title: metaTitle,
|
||||
};
|
||||
}
|
||||
|
||||
const ClientSideTerminalComponent = dynamic(() => import("./terminal"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function LogsManagementPage() {
|
||||
return (
|
||||
<Box
|
||||
style={{ borderRadius: 6 }}
|
||||
h={fullHeightWithoutHeaderAndFooter}
|
||||
p="md"
|
||||
bg="black"
|
||||
>
|
||||
<ClientSideTerminalComponent />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.outerTerminal > div {
|
||||
height: 100%;
|
||||
}
|
||||
66
apps/nextjs/src/app/[locale]/manage/tools/logs/terminal.tsx
Normal file
66
apps/nextjs/src/app/[locale]/manage/tools/logs/terminal.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { CanvasAddon } from "@xterm/addon-canvas";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { Box } from "@homarr/ui";
|
||||
|
||||
import classes from "./terminal.module.css";
|
||||
|
||||
export default function TerminalComponent() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const terminalRef = useRef<Terminal>();
|
||||
clientApi.log.subscribe.useSubscription(undefined, {
|
||||
onData(data) {
|
||||
terminalRef.current?.writeln(
|
||||
`${data.timestamp} ${data.level} ${data.message}`,
|
||||
);
|
||||
terminalRef.current?.refresh(0, terminalRef.current.rows - 1);
|
||||
},
|
||||
onError(err) {
|
||||
// This makes sense as logging might cause an infinite loop
|
||||
alert(err);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
const canvasAddon = new CanvasAddon();
|
||||
|
||||
terminalRef.current = new Terminal({
|
||||
cursorBlink: false,
|
||||
disableStdin: true,
|
||||
convertEol: true,
|
||||
});
|
||||
terminalRef.current.open(ref.current);
|
||||
terminalRef.current.loadAddon(canvasAddon);
|
||||
|
||||
// This is a hack to make sure the terminal is rendered before we try to fit it
|
||||
// You can blame @Meierschlumpf for this
|
||||
setTimeout(() => {
|
||||
const fitAddon = new FitAddon();
|
||||
terminalRef.current?.loadAddon(fitAddon);
|
||||
fitAddon.fit();
|
||||
});
|
||||
|
||||
return () => {
|
||||
terminalRef.current?.dispose();
|
||||
canvasAddon.dispose();
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
id="terminal"
|
||||
className={classes.outerTerminal}
|
||||
h="100%"
|
||||
></Box>
|
||||
);
|
||||
}
|
||||
2
apps/nextjs/src/constants.ts
Normal file
2
apps/nextjs/src/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const fullHeightWithoutHeaderAndFooter =
|
||||
"calc(100dvh - var(--app-shell-header-offset, 0px) - var(--app-shell-padding) - var(--app-shell-footer-offset, 0px) - var(--app-shell-padding))";
|
||||
Reference in New Issue
Block a user