From 6123503ee87cbcac2cad61d1a2eb20549b7cec9c Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 16 Jun 2026 18:12:54 +0000 Subject: [PATCH] feat(ui): Implement logs component --- src/client/components/LogsNext.tsx | 88 ++++++++++++++++++++++++++++++ src/client/components/Provider.tsx | 31 ++++++++++- src/stories/Logs.stories.tsx | 41 ++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/client/components/LogsNext.tsx create mode 100644 src/stories/Logs.stories.tsx diff --git a/src/client/components/LogsNext.tsx b/src/client/components/LogsNext.tsx new file mode 100644 index 00000000..7be8dd4f --- /dev/null +++ b/src/client/components/LogsNext.tsx @@ -0,0 +1,88 @@ +import React, {PropsWithChildren, useState, useEffect} from 'react'; +import * as AnsiImport from "ansi-to-react"; +import { Text, Box } from '@chakra-ui/react'; +import {FixedSizeList} from "fixed-size-list"; +import {useSSE} from "@flamefrontend/sse-runtime-react"; +import { useQueryClient, QueryFunctionContext, useQuery } from '@tanstack/react-query' +import { LogOutputConfig } from '../../core/Atomic'; +import ky from 'ky'; +import { baseUrl } from '../utils'; + +// @ts-expect-error Ansi export is built incorrectly +const Ansi = AnsiImport.default.default as typeof AnsiImport.default; + +interface LogLineProps { + message: string +} + +export const LogLine = (props: LogLineProps) => { + return ( + {props.message} + ) +}; + +interface MinLogInfo { + message: string, + id: string, + level: number + levelLabel: string +} + +const createFixedList = (size, initialList: MinLogInfo[] = []): FixedSizeList => { + return new FixedSizeList(size, initialList); +} + +let list = createFixedList(50); + +export const Logs = (props: {logs: Readonly}) => { + return + {props.logs.map(x => )} + +} + +export const LogsFetchable = (props: {settings?: LogOutputConfig, streamable?: boolean}) => { + const { + settings: { + limit = 50, + level = 'trace' + } = {}, + streamable = true + } = props; + + const [logLevel, setLogLevel] = useState(level); + const [logList, setLogList] = useState([]); + + const { isPending, isError, data, error } = useQuery({ + queryKey: ['logs', { level: logLevel }], + queryFn: queryFn + }); + + const connection = useSSE<{stream: MinLogInfo}>({ + key: ['stream'], + url: "/api/logs/stream", + enabled: streamable, + events: { + stream: (data) => { + if(!isPending) { + list.add(data); + setLogList(Array.from(list.data)); + } + } + } + }); + + useEffect(() => { + if(data !== undefined) { + list = createFixedList(limit, data.data.map((x, index) => ({...x, message: x.line, id: x.time.toString(), levelLabel: x.levelLabel, level: x.level}))); + setLogList(Array.from(list.data)); + } + }, [data, limit,setLogList]); + + return + +} + +type LogsQueryKey = ['logs', {level: string}]; +const queryFn = async (context: QueryFunctionContext) => { + return await ky.get(`logs`, { baseUrl: baseUrl }).json() as {data: {line: string, time: number, levelLabel: string, level: number}[]}; +} \ No newline at end of file diff --git a/src/client/components/Provider.tsx b/src/client/components/Provider.tsx index 0bc77db9..02aee388 100644 --- a/src/client/components/Provider.tsx +++ b/src/client/components/Provider.tsx @@ -13,13 +13,42 @@ import { QueryClientProvider, } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react" const queryClient = new QueryClient() +const customConfig = defineConfig({ + globalCss: { + ".ansi-blue-fg": { + color: "blue.400" + }, + ".ansi-red-fg": { + color: "red.400" + }, + ".ansi-green-fg": { + color: "green.400" + }, + ".ansi-magenta-fg": { + color: "pink.400" + }, + ".ansi-yellow-fg": { + color: "yellow.400" + }, + ".ansi-cyan-fg": { + color: "white" + }, + ".ansi-bright-black-fg": { + color: "gray.500" + } + }, +}) + +export const system = createSystem(defaultConfig, customConfig) + export function Provider(props: ColorModeProviderProps) { return ( - + {/* */} diff --git a/src/stories/Logs.stories.tsx b/src/stories/Logs.stories.tsx new file mode 100644 index 00000000..4e3b4557 --- /dev/null +++ b/src/stories/Logs.stories.tsx @@ -0,0 +1,41 @@ +import preview from "../../.storybook/preview.js"; +import React from 'react'; + +import { Container, Box } from '@chakra-ui/react'; +import { Logs } from "../client/components/LogsNext"; +import {Provider} from "../client/components/Provider"; +import { generateClientApiJson, generateSourceApiJson, generateSourcePlayerJson } from "../core/tests/utils/apiFixtures.js"; + +const traceMessage = "[2026-06-16 13:02:42.674 -0400] \u001b[90mTRACE\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Scrobblers]\u001b[36m \u001b[90m[Koito - koito]\u001b[36m \u001b[90m[Now Playing]\u001b[36m Not updating, previous matches current update --BUT-- time since last update (194s) is less than max threshold 238.121s\u001b[39m"; +const debugMessage = "[2026-06-16 13:03:20.150 -0400] \u001b[34mDEBUG\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Sources]\u001b[36m \u001b[90m[Spotify - default]\u001b[36m Temporarily decreasing polling interval to 1.00s due to Player c98a8fb80e-foxx-arch-SingleUser reporting track duration remaining (2.01s) less than normal interval (10.00s)\u001b[39m"; +const verboseMessage = "[2026-06-16 13:03:23.568 -0400] \u001b[35mVERBOSE\u001b[39m: \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Sources]\u001b[36m \u001b[90m[Spotify - default]\u001b[36m \u001b[90m[Player c98a8fb80e-foxx-arch-SingleUser]\u001b[36m New Play: (2KcQh1rHrJ23eaxax1L1PG) Strutman Lane - One of a Kind\u001b[39m"; +const infoMessage = "[2026-06-16 13:03:24.129 -0400] \u001b[32mINFO\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Scrobblers]\u001b[36m \u001b[90m[Koito - koito]\u001b[36m Scrobbled (New) => (Spotify) Couch - Jessie @ 2026-06-16T13:03:23-04:00 (C)\u001b[39m"; +const errorMessage = "[2026-06-16 10:41:13.948 -0400] \u001b[31mERROR\u001b[39m : \u001b[36m\u001b[90m[App]\u001b[36m \u001b[90m[Scrobblers]\u001b[36m \u001b[90m[Lastfm - mylfm]\u001b[36m Scrobble Error (New)\u001b[39m\n playInfo: \"Alice Auer - Unknown @ 2026-06-16T10:06:07-04:00 (C)\"\n payload: {\n \"artist\": \"Alice Auer\",\n \"track\": \"Unknown\",\n \"album\": \"Unknown\",\n \"timestamp\": 1781618767,\n \"mbid\": \"9957324d-3c86-47cd-b844-0c92cf374ec1\",\n \"duration\": 214\n }"; + +const messages = [traceMessage, debugMessage, verboseMessage, infoMessage, errorMessage]; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export +const meta = preview.meta({ + title: 'Examples/Logs', + component: Logs, + parameters: { + // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout + layout: 'padded', + }, + // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs + tags: ['autodocs'], + // More on argTypes: https://storybook.js.org/docs/api/argtypes + args: { + logs: messages.map(x => ({message: x})) + }, + render: function Render(args) { + return () + }, +decorators: [ + (Story) => (), + ] + // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args +}); + +export const LogLevels = meta.story({ +}); \ No newline at end of file -- 2.51.2