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:
@@ -44,8 +44,11 @@
|
||||
"@trpc/next": "next",
|
||||
"@trpc/react-query": "next",
|
||||
"@trpc/server": "next",
|
||||
"@xterm/addon-canvas": "^0.6.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"chroma-js": "^2.4.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"dotenv": "^16.4.5",
|
||||
"jotai": "^2.7.2",
|
||||
"next": "^14.1.4",
|
||||
"postcss-preset-mantine": "^1.13.0",
|
||||
@@ -54,7 +57,7 @@
|
||||
"sass": "^1.72.0",
|
||||
"superjson": "2.2.1",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"dotenv": "^16.4.5"
|
||||
"xterm-addon-fit": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -66,6 +69,7 @@
|
||||
"@types/chroma-js": "2.4.4",
|
||||
"dotenv-cli": "^7.4.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"dotenv-cli": "^7.4.1",
|
||||
"eslint": "^8.57.0",
|
||||
"prettier": "^3.2.5",
|
||||
"tsx": "^4.7.1",
|
||||
|
||||
@@ -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