import { Box, HStack, SegmentGroup, Separator, Span, Stack, Text, FloatingPanel, Portal, IconButton } from '@chakra-ui/react';
import {
useWindowSize,
} from '@react-hook/window-size';
import { useSSE } from "@flamefrontend/sse-runtime-react";
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as AnsiImport from "ansi-to-react";
import { FixedSizeList } from "fixed-size-list";
import ky from 'ky';
import React, { useCallback, useEffect, useRef, useState, type ComponentProps } from 'react';
import type {LogLevelStandalone, LogOutputConfig} from '../../core/Atomic';
import { tanQueries } from '../queries';
import { ChakraClipDynamic } from './ChakraClipboard';
import { TerminalButton, XButton } from './icons/ChakraIcons';
import { LuGripHorizontal, LuMinus } from 'react-icons/lu';
import { Ripple } from './icons/AnimatedIcons';
import { MSErrorBoundary } from './ErrorBoundary';
import { useLocalStorage } from 'usehooks-ts'
// @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, ref?: React.Ref}) => {
return
{props.logs.map(x => )}
}
export const LogsFetchable = (props: {settings?: LogOutputConfig, streamable?: boolean}) => {
const {
settings: {
limit = 50,
level = 'trace'
} = {},
streamable = true
} = props;
const client = useQueryClient();
const [logLevel, setLogLevel] = useState(level);
const [logLimit, setLogLimit] = useState(limit);
const [logList, setLogList] = useState([]);
const { isPending, isError, data, error } = useQuery({
...tanQueries.logs.list(logLevel, logLimit),
staleTime: Infinity
});
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));
}
}
}
});
const mutateLogSettings = useMutation({
mutationFn: (settings: Partial) => ky.put('api/logs', {json: settings}),
onSuccess: (sentData, variables) => {
if(variables.level !== undefined) {
setLogLevel(variables.level);
}
if(variables.limit !== undefined) {
setLogLimit(variables.limit);
}
client.invalidateQueries({
queryKey: ['logs', { level: variables.level ?? logLevel, limit: variables.limit ?? logLimit }],
refetchType: 'all'
})
}
});
const levelGroup = {
mutateLogSettings.mutate({level: val.value as LogLevelStandalone});
}}>
const limitGroup = {
mutateLogSettings.mutate({limit: Number.parseInt(val.value)});
}}>
useEffect(() => {
if(data !== undefined) {
list = createFixedList(logLimit, 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]);
const logRef = useRef(null);
const getLogCopyText = useCallback(() =>{
const content = logRef.current.innerText;
return content.replaceAll(/\n\[/g, '[');
},[logRef]);
return (
Level: {levelGroup}
Limit: {limitGroup}
);
}
interface Rectangle {
x: number; // left edge
y: number; // top edge
width: number;
height: number;
}
const isAnyCornerOutside = (rectangleA: Rectangle, rectangleB: Rectangle): boolean => {
const corners = [
{ x: rectangleA.x, y: rectangleA.y }, // top-left
{ x: rectangleA.x + rectangleA.width, y: rectangleA.y }, // top-right
{ x: rectangleA.x, y: rectangleA.y + rectangleA.height }, // bottom-left
{ x: rectangleA.x + rectangleA.width, y: rectangleA.y + rectangleA.height }, // bottom-right
];
const bLeft = rectangleB.x;
const bRight = rectangleB.x + rectangleB.width;
const bTop = rectangleB.y;
const bBottom = rectangleB.y + rectangleB.height;
return corners.some(
(corner) =>
corner.x < bLeft ||
corner.x > bRight ||
corner.y < bTop ||
corner.y > bBottom
);
};
const defaultPosition = (width: number, height: number): ComponentProps['position'] => ({x: width * 0.03, y: height * 0.65});
const defaultSize = (width: number, height: number): ComponentProps['size'] => ({ width: width * 0.95, height: height * 0.3 });
export const FloatingLogs = (props: {streamable?: boolean}) => {
const [width, height] = useWindowSize();
// remember logs state across refreshes
// keep logs open if it was last opened, in the last position/size *if still within viewport bounds*
const [logsOpen, setLogsOpen] = useLocalStorage('logsOpen', false);
const [logsPosition, setLogsPosition] = useLocalStorage['position']>('logsPosition', defaultPosition(width, height));
const [logsSize, setLogsSize] = useLocalStorage['size']>('logsSize', defaultSize(width, height));
// when logs are opened
// check if any part of the logs window is outside the bounds of the viewport
// and if it is then reset size/position to default so that user can't accidentally make logs irretrievable
useEffect(() => {
if(logsOpen) {
const logRectangle: Rectangle = {x: logsPosition.x, y: logsPosition.y, width: logsSize.width, height: logsSize.height};
const windowRectangle: Rectangle = {x: 0, y: 0, width, height};
if(isAnyCornerOutside(logRectangle, windowRectangle)) {
console.log('resetting logs height/size due to boundary conflict');
setLogsPosition(defaultPosition(width, height));
setLogsSize(defaultSize(width, height));
}
}
},[logsOpen]);
return (
setLogsOpen(details.open)}
position={logsPosition}
onPositionChange={(details) => setLogsPosition(details.position)}
size={logsSize}
onSizeChange={(details) => setLogsSize(details.size)}
persistRect
closeOnEscape
lazyMount
>
Logs
);
}