import type { Collapsible } from '@chakra-ui/react'; import { Card, Text, Icon, SkeletonCircle, SkeletonText, Span, Tabs, Timeline, Tag, HStack, Stack, Separator, Wrap} from '@chakra-ui/react'; import { HiCheck } from "react-icons/hi" import dayjs from "dayjs"; import React from "react"; import { BiWrench } from "react-icons/bi"; import { HiMiniMagnifyingGlass } from "react-icons/hi2"; import { IoMdCodeDownload } from "react-icons/io"; import { TbDatabaseEdit } from "react-icons/tb"; import type {PlayApiCommonDetailed} from "../../core/Api"; import { DEAD_QUEUE, QUEUE_STATUS_COMPLETED, QUEUE_STATUS_FAILED, QUEUE_STATUS_QUEUED, type ComponentType, type JsonPlayObject, type LifecycleStep, type PlayMatchResult, type ScrobbleResult } from "../../core/Atomic"; import { sortByNewestDate } from "../../core/PlayUtils"; import { capitalizeWords } from "../../core/StringUtils"; import { shortTodayAwareFormat } from "../../core/TimeUtils"; import { activityTransformHasIssue, timelineIconProps, TimelineItemSummaryText } from "../utils/ComponentUtils"; import { ChakraCodeBlockShort } from "./CodeBlock"; import { ErrorAlert } from "./ErrorAlert"; import { MSErrorBoundary } from "./ErrorBoundary"; import { CheckIcon, ExclamationTriangleIcon, TimelineIndicatorIconQueued, XIcon } from "./icons/ChakraIcons"; import { MSCollapsible } from "./MSCollapsible"; import { PlayData } from "./PlayData"; import { ScrobbleActionResult } from "./ScrobbleActionResult"; import { ScrobbleMatchResult } from "./ScrobbleMatchResult"; import { TimelineErrorIcon } from "./timeline/TimelineIcon"; import { diffElements, TransformSteps } from "./TransformSteps"; import { Muted } from "./Typography"; import type { PlayEventPlayStateChange, PlayEventQueueStateChange } from '../../core/PlayEvent'; import { FaInfo } from "react-icons/fa6"; interface ActivityTimelineProps { activity?: PlayApiCommonDetailed collapsibleOpen?: boolean, componentType?: ComponentType componentName?: string } const indicatorProps: Collapsible.TriggerProps = {};//{alignItems: "anchor-center"}; const TimelineLoading = () => ( ) const NewItem = (props: Pick) => { const { activity: { play, input, seenAt, } = {}, collapsibleOpen, componentType } = props; const { meta: { source, } = {}, } = play; const { play: original, data: ogInput } = input || {}; return ( {componentType === 'source' ? 'Discovered' : 'Recieved'} new Play from {capitalizeWords(source)} at {shortTodayAwareFormat(dayjs(seenAt))} } defaultOpen={collapsibleOpen} timeline disableUntil="md"> Play {ogInput !== undefined ? Source Data : null} {ogInput !== undefined ? ( ) : null} ) } const TransformsItem = (props: Pick & { steps: LifecycleStep[], original: JsonPlayObject }) => { const { steps = [], collapsibleOpen, original } = props; let transformVerb: string = 'Transformed Play'; const transformIssue = activityTransformHasIssue(steps); let transformResult: React.JSX.Element | undefined; if (transformIssue === 'error') { transformVerb = 'Transforming Play'; transformResult = resulted in an error; } else if (transformIssue === 'warn') { transformVerb = 'Transforming Play'; transformResult = resulted in warnings; } return ( {transformVerb} using configured Rules for {steps[0].hook} {transformResult}} unmountOnExit defaultOpen={collapsibleOpen} timeline> ) } const ScrobbleMatchItem = (props: Pick & { match: PlayMatchResult, componentName?: string }) => { const { match, match: { closestMatchedPlay: { meta } = {} } = {}, componentName = 'service', collapsibleOpen } = props; let fromSource: React.JSX.Element | undefined; if(match.match && meta !== undefined) { fromSource = from {meta.parsedFrom === 'history' ? capitalizeWords(componentName) : 'MS Database'} } return ( Found {match.match ? a duplicate Scrobble : 'no duplicate Scrobbles'}{fromSource}} defaultOpen={collapsibleOpen} disableUntil="md" unmountOnExit timeline> ) } const ScrobbleResponseItem = (props: Pick & { scrobble: ScrobbleResult, componentName?: string }) => { const { scrobble: { payload, error, warnings = [] } = {}, scrobble, collapsibleOpen, componentName = 'downstream service' } = props; let scrobbleSummary: React.JSX.Element; const scrobbleIconProps: Record = { color: 'green.focusRing' }; if (payload !== undefined) { if (error !== undefined) { scrobbleSummary = Scrobble attempt to {capitalizeWords(componentName)} resulted in an error. } else if (warnings.length > 0) { scrobbleSummary = Scrobbled to {capitalizeWords(componentName)} but response has warnings. } else { scrobbleSummary = Scrobbled to {capitalizeWords(componentName)} successfully.; } } return ( {error !== undefined ? : ( )} ) } const RetryChip = (props: { count: number }) => ( Attempt: {props.count} ) const BoolChip = (props: { text: string, check: boolean }) => ( {props.text} {props.check ? : } ) const DupeChip = (props: { check: boolean }) => ; const TransformChip = (props: { check: boolean }) => const CacheChip = (props: { check: boolean }) => const QueueTimelineItem = (props: {queueState: PlayEventQueueStateChange, collapsibleOpen: boolean}) => { const { queueState: { data: { queueStatus, queueName, error, retries, context, }, createdAt, } = {}, collapsibleOpen, } = props; const { dupeCheck, transform, useCache, reason } = context ?? {}; let indicator: React.JSX.Element, text: React.JSX.Element, title: React.JSX.Element; const contextHints: React.JSX.Element[] = []; const tags: React.JSX.Element[] = []; if(retries !== undefined && retries > 0) { tags.push(); } if(dupeCheck !== undefined) { tags.push() } if(transform !== undefined) { tags.push() } if(useCache !== undefined) { tags.push() } if(tags.length > 0) { contextHints.push({tags}) } if(reason !== undefined) { contextHints.push(Reason - {reason}); } switch(queueStatus) { case QUEUE_STATUS_QUEUED: indicator = ; text = {queueName === DEAD_QUEUE ? 'Dead ' : ''}Queued at {shortTodayAwareFormat(dayjs(createdAt))}; break; case QUEUE_STATUS_COMPLETED: indicator = ; text = {queueName === DEAD_QUEUE ? 'Dead ' : ''}Queue finished processing at {shortTodayAwareFormat(dayjs(createdAt))}; break; case QUEUE_STATUS_FAILED: indicator = ; text = {queueName === DEAD_QUEUE ? 'Dead ' : ''}Queue failed at {shortTodayAwareFormat(dayjs(createdAt))}; } if(error !== undefined && error !== null && typeof error === 'object' && Object.keys(error).length > 0) { title = ( ) } else { title = text; } return ( {indicator} {title} {contextHints.length > 0 ? }>{contextHints} : undefined} ); } const StateChangeItem = (props: {event: PlayEventPlayStateChange, collapsibleOpen: boolean}) => { const { event: { data: { state, error, reason }, createdAt, } = {}, collapsibleOpen, } = props; let color: string; switch (state) { case 'discarded': case 'duped': color = 'orange'; break; case 'discovered': case 'scrobbled': color = 'green'; break; case 'failed': color = 'red'; break; case 'queued': color = 'gray'; break; } const text = State was changed to {capitalizeWords(state)} at {shortTodayAwareFormat(dayjs(createdAt))}; let content: React.JSX.Element; if(error !== null && error !== undefined) { content = ( {reason !== undefined && reason !== null ? {reason} : null} ) } else { content = (<> {text} {reason !== undefined && reason !== null ? {reason} : null} ); } return ( {content} ); } export const ActivityTimeline = (props: ActivityTimelineProps) => { if(props.activity === undefined) { return ; } const { activity:{ input, events = [], } = {}, collapsibleOpen, componentType, componentName } = props; const { play: original, } = input || {}; events.sort((a, b) => sortByNewestDate(b.createdAt, a.createdAt)); const timelineElements: React.JSX.Element[] = [ ]; let lastTransformedPlay = original; for(const event of events) { switch(event.eventName) { case 'transform': { //const d: TransformStepsTimelineData = {id: 'transform-steps', dt: dayjs(event.data[0].createdAt), steps: event.data, original: lastTransformedPlay}; timelineElements.push(); const [__, finalPlay] = diffElements(lastTransformedPlay, event.data); lastTransformedPlay = finalPlay; } break; case 'queueStateChange': { timelineElements.push(); } break; case 'dupeCheck': { timelineElements.push(); } break; case 'scrobbleResult': timelineElements.push(); break; case 'playStateChange': timelineElements.push() } } return ( {timelineElements} ) }