feat(ui): Add queue states to timeline
Some checks are pending
Publish Docker image to Dockerhub / test (push) Waiting to run
Publish Docker image to Dockerhub / Build OCI Images (push) Blocked by required conditions
Publish Docker image to Dockerhub / Build OCI Images-1 (push) Blocked by required conditions
Publish Docker image to Dockerhub / Merge OCI Images and Push (push) Blocked by required conditions

This commit is contained in:
FoxxMD 2026-07-07 15:21:29 +00:00
parent e6130eb5eb
commit 3d6e86f258
5 changed files with 146 additions and 37 deletions

View file

@ -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 (
<Timeline.Item>
<Timeline.Connector>
<Timeline.Separator />
<Timeline.Indicator>
<TimelineIndicatorIconQueued fontSize="lg"/>
</Timeline.Indicator>
</Timeline.Connector>
<Timeline.Content gap="4">
<Timeline.Title {...timelineTextFormatting}>
{queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queued <Muted>at</Muted> {shortTodayAwareFormat(dayjs(queueState.updatedAt))}
</Timeline.Title>
</Timeline.Content>
</Timeline.Item>
);
}
if(queueState.queueStatus === QUEUE_STATUS_COMPLETED) {
return (
<Timeline.Item>
<Timeline.Connector>
<Timeline.Separator />
<Timeline.Indicator>
<CheckIcon fontSize="lg"/>
</Timeline.Indicator>
</Timeline.Connector>
<Timeline.Content gap="4">
<Timeline.Title {...timelineTextFormatting}>
{queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queue finished processing <Muted>at</Muted> {shortTodayAwareFormat(dayjs(queueState.updatedAt))}
</Timeline.Title>
</Timeline.Content>
</Timeline.Item>
);
}
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 <Muted>at</Muted> {shortTodayAwareFormat(dayjs(queueState.updatedAt))}</>;
} else {
titleContent = (
<MSCollapsible indicator={<Fragment>{queueState.queueName === CLIENT_DEAD_QUEUE ? 'Dead ' : ''}Queue failed <Muted>at</Muted> {shortTodayAwareFormat(dayjs(queueState.updatedAt))}</Fragment>}
defaultOpen={collapsibleOpen}
disableUntil="md"
timeline>
<ErrorAlert error={queueState.error} />
</MSCollapsible>
)
}
return (
<Timeline.Item>
<Timeline.Connector>
<Timeline.Separator />
<Timeline.Indicator>
<CheckIcon fontSize="lg"/>
</Timeline.Indicator>
</Timeline.Connector>
<Timeline.Content gap="4">
<Timeline.Title {...titleProps}>
{titleContent}
</Timeline.Title>
</Timeline.Content>
</Timeline.Item>
);
}
}
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<string, any> = {
let scrobbleSummary: React.JSX.Element;
const scrobbleIconProps: Record<string, any> = {
color: 'green.focusRing'
};
if (payload !== undefined) {
@ -116,6 +207,25 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
transformResult = <Span> resulted in <Span color="orange.solid">warnings</Span></Span>;
}
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 = <QueueTimelineItem queueState={ingressQueue} collapsibleOpen={collapsibleOpen}/>
}
const deadqueue = queueStates.find(x => x.queueName === CLIENT_DEAD_QUEUE);
if(deadqueue !== undefined) {
deadDateTime = dayjs(deadqueue.updatedAt);
deadItem = <QueueTimelineItem queueState={deadqueue} collapsibleOpen={collapsibleOpen}/>
}
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 (
<MSErrorBoundary>
<Timeline.Root variant="subtle" size="lg">
@ -157,6 +267,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
</Timeline.Title>
</Timeline.Content>
</Timeline.Item>
{queueItem && stepsDateTime && <TimeFiltered datetime={queueDateTime} isBefore={stepsDateTime}>{queueItem}</TimeFiltered>}
{deadItem && stepsDateTime && <TimeFiltered datetime={deadDateTime} isBefore={stepsDateTime}>{deadItem}</TimeFiltered>}
{steps.length > 0 ? (
<Timeline.Item>
<Timeline.Connector>
@ -199,6 +311,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
</Timeline.Content>
</Timeline.Item>
)}
{queueItem && <TimeFiltered datetime={queueDateTime} isAfter={stepsDateTime} isBefore={scrobbleDateTime}>{queueItem}</TimeFiltered>}
{deadItem && <TimeFiltered datetime={deadDateTime} isAfter={stepsDateTime} isBefore={scrobbleDateTime}>{deadItem}</TimeFiltered>}
{match !== undefined ? (
<Timeline.Item>
<Timeline.Connector>
@ -255,6 +369,8 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
</Timeline.Content>
</Timeline.Item>
) : null}
{queueItem && <TimeFiltered datetime={queueDateTime} isAfter={scrobbleDateTime}>{queueItem}</TimeFiltered>}
{deadItem && <TimeFiltered datetime={deadDateTime} isAfter={scrobbleDateTime}>{deadItem}</TimeFiltered>}
</Timeline.Root>
</MSErrorBoundary>
)

View file

@ -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<string>,
@ -25,7 +21,6 @@ export const ScrobbleActionResult = (props: ScrobbleActionResultProps) => {
warnings = [],
payload,
response,
mergedScrobble
} = {},
scrobbler,
collapsibleOpen

View file

@ -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<string>
@ -49,7 +44,7 @@ export const ScrobbleMatchResult = (props: ScrobbleMatchResultProps) => {
<DataList.ItemValue>
<List.Root>
{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 (
<List.Root ps="5">

View file

@ -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);
export const ExclamationTriangleIcon = makeChakraIcon(LuTriangleAlert);
export const TimelineIndicatorIconQueued = makeChakraIcon(LuClock);

View file

@ -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';