From 3d6e86f258f139b80402895fee490fa5c3a382ac Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 7 Jul 2026 15:21:29 +0000 Subject: [PATCH] feat(ui): Add queue states to timeline --- src/client/components/ActivityTimeline.tsx | 142 ++++++++++++++++-- .../components/ScrobbleActionResult.tsx | 13 +- src/client/components/ScrobbleMatchResult.tsx | 15 +- src/client/components/icons/ChakraIcons.tsx | 11 +- .../msComponent/MSComponentDetailed.tsx | 2 +- 5 files changed, 146 insertions(+), 37 deletions(-) diff --git a/src/client/components/ActivityTimeline.tsx b/src/client/components/ActivityTimeline.tsx index 3576a523..28bbf6d4 100644 --- a/src/client/components/ActivityTimeline.tsx +++ b/src/client/components/ActivityTimeline.tsx @@ -1,27 +1,26 @@ -import React, { ComponentProps, useState, Fragment } from "react" -import { Accordion, Timeline, Icon, Span, Stack, Heading, Card, Box, Tabs, Skeleton, SkeletonCircle, SkeletonText, HTMLChakraProps } from '@chakra-ui/react'; -import { ComponentType, ErrorLike, JsonPlayObject, PlayActivity } from "../../core/Atomic"; +import React, { Fragment } from "react" +import { Timeline, Icon, Span, Card, Tabs, SkeletonCircle, SkeletonText, HTMLChakraProps } from '@chakra-ui/react'; +import { CLIENT_DEAD_QUEUE, CLIENT_INGRESS_QUEUE, ComponentType, QUEUE_STATUS_COMPLETED, QUEUE_STATUS_FAILED, QUEUE_STATUS_QUEUED } from "../../core/Atomic"; import { PlayData } from "./PlayData"; import { ErrorAlert } from "./ErrorAlert"; import { IoMdCodeDownload } from "react-icons/io"; import { BiWrench } from "react-icons/bi"; -import { IoMusicalNoteOutline } from "react-icons/io5"; import { HiMiniMagnifyingGlass } from "react-icons/hi2"; import { TbDatabaseEdit } from "react-icons/tb"; import { capitalize } from "../../core/StringUtils"; -import { shortTodayAwareFormat, todayAwareFormat } from "../../core/TimeUtils"; -import dayjs from "dayjs"; -import { ChakraCodeBlockShort, ChakraPlainBlockShort } from "./CodeBlock"; +import { shortTodayAwareFormat } from "../../core/TimeUtils"; +import dayjs, { Dayjs } from "dayjs"; +import { ChakraCodeBlockShort } from "./CodeBlock"; import { TransformSteps } from "./TransformSteps"; import { ScrobbleMatchResult } from "./ScrobbleMatchResult"; import { ScrobbleActionResult } from "./ScrobbleActionResult"; -import { ExpandCollapse } from "./ExpandCollapse"; import { MSCollapsible } from "./MSCollapsible"; import { TimelineErrorIcon } from "./timeline/TimelineIcon"; import { Muted } from "./Typography"; -import { PlayApiCommonDetailed } from "../../core/Api"; +import { PlayApiCommonDetailed, QueueStateApi } from "../../core/Api"; import { MSErrorBoundary } from "./ErrorBoundary"; import { activityTransformHasIssue, timelineTextFormatting } from "../utils/ComponentUtils"; +import { CheckIcon, TimelineIndicatorIconQueued } from "./icons/ChakraIcons"; export interface ActivityDetailProps { @@ -53,6 +52,97 @@ const TimelineLoading = () => { ); } +const QueueTimelineItem = (props: {queueState: QueueStateApi, collapsibleOpen: boolean, /*isBefore?: Dayjs, isAfter?: Dayjs*/}) => { + const { + queueState, + collapsibleOpen, + } = props; + if(queueState.queueStatus === QUEUE_STATUS_QUEUED) { + return ( + + + + + + + + + + {queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queued at {shortTodayAwareFormat(dayjs(queueState.updatedAt))} + + + + ); + } + + if(queueState.queueStatus === QUEUE_STATUS_COMPLETED) { + return ( + + + + + + + + + + {queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queue finished processing at {shortTodayAwareFormat(dayjs(queueState.updatedAt))} + + + + ); + } + + if(queueState.queueStatus === QUEUE_STATUS_FAILED) { + let titleContent: React.JSX.Element; + const titleProps: HTMLChakraProps<"span"> = queueState.error === undefined ? timelineTextFormatting : {}; + if(queueState.error === undefined) { + titleContent = <>{queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queue failed at {shortTodayAwareFormat(dayjs(queueState.updatedAt))}; + } else { + titleContent = ( + {queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queue failed at {shortTodayAwareFormat(dayjs(queueState.updatedAt))}} + defaultOpen={collapsibleOpen} + disableUntil="md" + timeline> + + + ) + } + return ( + + + + + + + + + + {titleContent} + + + + ); + } +} + +const TimeFiltered = (props: {datetime: Dayjs, isBefore?: Dayjs, isAfter?: Dayjs, children: React.ReactNode}) => { + const { + isBefore, + isAfter, + datetime, + children + } = props; + if(isBefore !== undefined && !datetime.isBefore(isBefore)) { + return null; + } + if(isAfter !== undefined && !datetime.isAfter(isAfter)) { + return null; + } + + return children; +} + export const ActivityTimeline = (props: ActivityDetailProps) => { if(props.activity === undefined) { @@ -63,7 +153,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { activity:{ play, input, - seenAt + seenAt, + queueStates, } = {}, collapsibleOpen, componentType @@ -82,15 +173,15 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { error, warnings = [] } = {}, - scrobble + scrobble, } = play; const { play: original, data: ogInput } = input || {}; - let scrobbleSummary: React.JSX.Element, - scrobbleIconProps: Record = { + let scrobbleSummary: React.JSX.Element; + const scrobbleIconProps: Record = { color: 'green.focusRing' }; if (payload !== undefined) { @@ -116,6 +207,25 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { transformResult = resulted in warnings; } + let queueItem: React.JSX.Element | undefined, + queueDateTime: Dayjs | undefined, + deadItem: React.JSX.Element | undefined, + deadDateTime: Dayjs | undefined; + const ingressQueue = queueStates.find(x => x.queueName === CLIENT_INGRESS_QUEUE); + if(ingressQueue !== undefined) { + queueDateTime = dayjs(ingressQueue.updatedAt); + queueItem = + } + const deadqueue = queueStates.find(x => x.queueName === CLIENT_DEAD_QUEUE); + if(deadqueue !== undefined) { + deadDateTime = dayjs(deadqueue.updatedAt); + deadItem = + } + + const stepsDateTime = steps.length > 0 ? dayjs(steps[0].createdAt) : undefined; + // TODO add actual timestamp to scrobble action so we can make this more accurate + const scrobbleDateTime = match !== undefined ? dayjs(match.createdAt) : undefined; + return ( @@ -157,6 +267,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { + {queueItem && stepsDateTime && {queueItem}} + {deadItem && stepsDateTime && {deadItem}} {steps.length > 0 ? ( @@ -199,6 +311,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { )} + {queueItem && {queueItem}} + {deadItem && {deadItem}} {match !== undefined ? ( @@ -255,6 +369,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { ) : null} + {queueItem && {queueItem}} + {deadItem && {deadItem}} ) diff --git a/src/client/components/ScrobbleActionResult.tsx b/src/client/components/ScrobbleActionResult.tsx index 88b1c0e7..c7074ee0 100644 --- a/src/client/components/ScrobbleActionResult.tsx +++ b/src/client/components/ScrobbleActionResult.tsx @@ -1,16 +1,12 @@ -import { ComponentProps, Fragment } from "react" -import { Timeline, Icon, Span, Stack, Heading, Tabs, DataList, Alert, List, HStack } from '@chakra-ui/react'; -import { PlayData } from "./PlayData"; +import React, { Fragment } from "react" +import { Timeline, Icon, Span, Stack, Alert, List, HStack } from '@chakra-ui/react'; import { ErrorAlert } from "./ErrorAlert"; -import { BiWrench } from "react-icons/bi"; -import { IoMusicalNoteOutline } from "react-icons/io5"; import { HiOutlineCloudUpload, HiOutlineCloudDownload } from "react-icons/hi"; -import { LuCheck, LuCircleX, LuX } from "react-icons/lu"; -import { ChakraCodeBlockShort, ChakraPlainBlock, ChakraPlainBlockShort } from "./CodeBlock"; -import { formatNumber, jdiff } from "../../core/DataUtils"; +import { ChakraCodeBlockShort } from "./CodeBlock"; import { capitalize } from "../../core/StringUtils"; import { MSCollapsible, MSCollapsibleExternalProps } from "./MSCollapsible"; import { TimelineErrorIcon } from "./timeline/TimelineIcon"; +import { ScrobbleResult } from "../../core/Atomic"; export interface ScrobbleActionResultProps extends MSCollapsibleExternalProps { result: ScrobbleResult, @@ -25,7 +21,6 @@ export const ScrobbleActionResult = (props: ScrobbleActionResultProps) => { warnings = [], payload, response, - mergedScrobble } = {}, scrobbler, collapsibleOpen diff --git a/src/client/components/ScrobbleMatchResult.tsx b/src/client/components/ScrobbleMatchResult.tsx index e455df46..81efdc2d 100644 --- a/src/client/components/ScrobbleMatchResult.tsx +++ b/src/client/components/ScrobbleMatchResult.tsx @@ -1,13 +1,8 @@ -import { ComponentProps, Fragment } from "react" -import { Timeline, Icon, Span, Stack, Heading, Tabs, DataList, List } from '@chakra-ui/react'; -import { JsonPlayObject, LifecycleStep, PlayMatchResult } from "../../core/Atomic"; +import { Icon, Tabs, DataList, List } from '@chakra-ui/react'; +import { PlayMatchResult } from "../../core/Atomic"; import { PlayData } from "./PlayData"; -import { ErrorAlert } from "./ErrorAlert"; -import { BiWrench } from "react-icons/bi"; -import { IoMusicalNoteOutline } from "react-icons/io5"; -import { LuCheck, LuCircleX, LuX } from "react-icons/lu"; -import { ChakraCodeBlockShort, ChakraPlainBlock, ChakraPlainBlockShort } from "./CodeBlock"; -import { formatNumber, jdiff } from "../../core/DataUtils"; +import { LuCheck, LuX } from "react-icons/lu"; +import { formatNumber } from "../../core/DataUtils"; export interface ScrobbleMatchResultProps { match: PlayMatchResult @@ -49,7 +44,7 @@ export const ScrobbleMatchResult = (props: ScrobbleMatchResultProps) => { {match.breakdowns.map((x, index) => { - if (x.includes('Time Detail')) { + if (x !== null && x !== undefined && x.includes('Time Detail')) { const sub = x.substring(15).split('|'); return ( diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx index 04a2d388..16f5192c 100644 --- a/src/client/components/icons/ChakraIcons.tsx +++ b/src/client/components/icons/ChakraIcons.tsx @@ -23,7 +23,8 @@ import { LuEyeClosed, LuCalendar, LuRefreshCw, - LuCopy + LuCopy, + LuClock } from "react-icons/lu" import { VscDebugRestart } from 'react-icons/vsc'; import { MdOutlineFiberNew } from "react-icons/md"; @@ -99,8 +100,8 @@ export const MenuButton = makeIconButton(MenuIcon); export const XIcon = LuX; export const XButton = makeIconButton(XIcon); -export const CheckIcon = LuCheck; -export const CheckButton = makeIconButton(CheckIcon); +export const CheckIcon = makeChakraIcon(LuCheck); +export const CheckButton = makeIconButton(LuCheck); export const ExternalLinkIcon = LuExternalLink; export const ExternalLinkButton = makeIconButton(ExternalLinkIcon); @@ -175,4 +176,6 @@ export const CopyIconButton = makeIconButton(LuCopy); export const ExclamationCircleIcon = makeChakraIcon(LuCircleAlert); -export const ExclamationTriangleIcon = makeChakraIcon(LuTriangleAlert); \ No newline at end of file +export const ExclamationTriangleIcon = makeChakraIcon(LuTriangleAlert); + +export const TimelineIndicatorIconQueued = makeChakraIcon(LuClock); \ No newline at end of file diff --git a/src/client/components/msComponent/MSComponentDetailed.tsx b/src/client/components/msComponent/MSComponentDetailed.tsx index 127f7798..7ec5d09e 100644 --- a/src/client/components/msComponent/MSComponentDetailed.tsx +++ b/src/client/components/msComponent/MSComponentDetailed.tsx @@ -5,7 +5,7 @@ import { TextMuted } from "../TextMuted.js"; import { isClientType } from "../../../backend/common/infrastructure/Atomic.js"; import { capitalize } from "../../../core/StringUtils.js"; import { ShortDateDisplay } from "../DateDisplay.js"; -import { CheckIcon, ChevronLeftButton, ChevronRightButton, EllipsisButton, EyeButton, EyeClosedIcon, EyeIcon, IdleIcon, PowerButton, PowerIcon, PowerOffButton, PowerOffIcon, RetryIcon, XIcon } from "../icons/ChakraIcons.js"; +import { ChevronLeftButton, EllipsisButton, EyeButton, EyeClosedIcon, EyeIcon, IdleIcon, PowerButton, PowerIcon, PowerOffButton, PowerOffIcon, RetryIcon } from "../icons/ChakraIcons.js"; import { ChakraPlayer, ChakraPlayerFetchable, PlayersContainer, PlayersContainerFetchable } from "../chakraPlayer/Player.js"; import { InfoTip, ToggleTip, Tooltip } from "../ToggleTip.js"; import { QueryFunctionContext, queryOptions, useQuery, useQueryClient } from '@tanstack/react-query'; -- 2.51.2