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:
Manuel
2024-04-04 18:07:23 +02:00
committed by GitHub
parent 2fb0535260
commit c82915c6dc
15 changed files with 235 additions and 3 deletions

View File

@@ -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}

View File

@@ -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",
},
],
},
{

View 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>
);
}

View File

@@ -0,0 +1,3 @@
.outerTerminal > div {
height: 100%;
}

View 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>
);
}

View 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))";