diff --git a/src/client/components/ChakraClipboard.tsx b/src/client/components/ChakraClipboard.tsx index 28738ddd..2273676c 100644 --- a/src/client/components/ChakraClipboard.tsx +++ b/src/client/components/ChakraClipboard.tsx @@ -1,6 +1,7 @@ -import { ComponentProps, useMemo } from 'react'; -import { Clipboard, IconButton } from "@chakra-ui/react" +import { ComponentProps, useMemo, useCallback, useEffect, useState } from 'react'; +import { Clipboard, IconButton, useClipboard } from "@chakra-ui/react" import {safeStringify} from '../../core/StringUtils'; +import { CheckIcon, CopyIcon } from './icons/ChakraIcons'; export const ChakraClip = (props: Omit, 'children' | 'value'> & {value: any}) => { @@ -23,4 +24,48 @@ export const ChakraClip = (props: Omit, 'c ) +} + +const createCopyVal = (value: any) => { + if(typeof value === 'string') { + return value; + } + return safeStringify(value); +} + + +/** + * Copies value from onCopy only when clicked + */ +export const ChakraClipDynamic = (props: Omit, 'children' | 'value'> & {onCopy: () => any}) => { + const { + onCopy, + ...rest + } = props; + + // idk what's going on here exactly but just using clip.setValue does not set clipboard, it requires two clicks to get actual value + // so instead using some useState abuse to re-render component and wait to copy until everything matches + // probably related to this https://github.com/chakra-ui/chakra-ui/issues/6759 + + const [clipVal, setClipVal] = useState(); + const clip = useClipboard({timeout: 1000}); + + useEffect(() => { + if(clipVal !== undefined && clip.value === clipVal) { + //console.log('run copy'); + clip.copy(); + setClipVal(undefined); + } + },[clip.value, clipVal, setClipVal]); + const invokeCopy = useCallback(() => { + const val = createCopyVal(onCopy()); + //console.log(val); + setClipVal(val); + clip.setValue(val); + },[onCopy, clip.setValue]); + return ( + + {!clip.copied ? : } + + ) } \ No newline at end of file diff --git a/src/client/components/LogsNext.tsx b/src/client/components/LogsNext.tsx index b8dd05de..8465567e 100644 --- a/src/client/components/LogsNext.tsx +++ b/src/client/components/LogsNext.tsx @@ -1,4 +1,4 @@ -import React, {PropsWithChildren, useState, useEffect} from 'react'; +import React, {PropsWithChildren, useState, useEffect, useRef, useCallback} from 'react'; import * as AnsiImport from "ansi-to-react"; import { Text, Box, SegmentGroup, Separator, HStack, Stack, Span } from '@chakra-ui/react'; import {FixedSizeList} from "fixed-size-list"; @@ -6,8 +6,9 @@ import {useSSE} from "@flamefrontend/sse-runtime-react"; import { useQueryClient, QueryFunctionContext, useQuery, useMutation } from '@tanstack/react-query' import { LogOutputConfig } from '../../core/Atomic'; import ky from 'ky'; -import { baseUrl } from '../utils'; import { LogLevel } from '@foxxmd/logging'; +import { tanQueries } from '../queries'; +import { ChakraClip, ChakraClipDynamic } from './ChakraClipboard'; // @ts-expect-error Ansi export is built incorrectly const Ansi = AnsiImport.default.default as typeof AnsiImport.default; @@ -35,8 +36,8 @@ const createFixedList = (size, initialList: MinLogInfo[] = []): FixedSizeList}) => { - return +export const Logs = (props: {logs: Readonly, ref?: React.Ref}) => { + return {props.logs.map(x => )} } @@ -55,9 +56,9 @@ export const LogsFetchable = (props: {settings?: LogOutputConfig, streamable?: b const [logLimit, setLogLimit] = useState(limit); const [logList, setLogList] = useState([]); + const { isPending, isError, data, error } = useQuery({ - queryKey: ['logs', { level: logLevel, limit: logLimit }], - queryFn: queryFn, + ...tanQueries.logs.list(logLevel, logLimit), staleTime: Infinity }); @@ -123,14 +124,20 @@ export const LogsFetchable = (props: {settings?: LogOutputConfig, streamable?: b } }, [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} - + + Level: {levelGroup} + Limit: {limitGroup} + + + ); -} - -type LogsQueryKey = ['logs', {level: string, limit: number}]; -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/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index ca9fc721..b5457182 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -20,7 +20,8 @@ import { LuEye, LuEyeClosed, LuCalendar, - LuRefreshCw + LuRefreshCw, + LuCopy } from "react-icons/lu" import { VscDebugRestart } from 'react-icons/vsc'; import { MdOutlineFiberNew } from "react-icons/md"; @@ -159,4 +160,7 @@ export const RefreshButton = makeIconButton(RefreshIcon); export const InsertedIcon = makeChakraIcon(MdOutlineFiberNew); -export const UpdatedIcon = makeChakraIcon(LuCircleArrowUp); \ No newline at end of file +export const UpdatedIcon = makeChakraIcon(LuCircleArrowUp); + +export const CopyIcon = makeChakraIcon(LuCopy); +export const CopyIconButton = makeIconButton(LuCopy); \ No newline at end of file diff --git a/src/client/queries/index.ts b/src/client/queries/index.ts index 0b416f33..431a0dab 100644 --- a/src/client/queries/index.ts +++ b/src/client/queries/index.ts @@ -59,9 +59,20 @@ const players = createQueryKeys('players', { queryKey: ['components', componentId, 'play', platformId], queryFn: (ctx) => ky.get(`components/${componentId}/players/${platformId}`, { baseUrl }).json() }) +}); + +const logs = createQueryKeys('logs', { + list: (level: string, limit: number) => ({ + queryKey: ['logs', {level, limit}], + queryFn: (ctx) => { + return ky.get(`logs`, { + baseUrl: baseUrl + }).json<{data: {line: string, time: number, levelLabel: string, level: number}[]}>(); + } + }) }) -export const tanQueries = mergeQueryKeys(components, activities, players); +export const tanQueries = mergeQueryKeys(components, activities, players, logs); export const useQueryState = (queryKey: Readonly) => { const queryClient = useQueryClient()