mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-07-09 17:28:28 +00:00
feat(ui): Better error/warning indicators for timeline
This commit is contained in:
parent
97b6042768
commit
e6130eb5eb
6 changed files with 102 additions and 26 deletions
|
|
@ -1,27 +1,23 @@
|
|||
import React, { ComponentProps, useState, Fragment, useEffect } from "react"
|
||||
import { Accordion, For, Span, Code, Alert, Stack, Text, Box, AbsoluteCenter, Button, Clipboard, Separator, HStack, Flex, Badge, IconButton, Container, Icon, useAccordionItemContext, Skeleton, SkeletonText, Collapsible, BadgeProps } from '@chakra-ui/react';
|
||||
import { CLIENT_DEAD_QUEUE, ComponentType, QUEUE_NAMES, Second } from "../../core/Atomic";
|
||||
import React, { useState, Fragment, useEffect } from "react"
|
||||
import { Accordion, Span, Code, Alert, Stack, Box, Separator, HStack, Flex, useAccordionItemContext, Skeleton, SkeletonText, Collapsible, BadgeProps } from '@chakra-ui/react';
|
||||
import { CLIENT_DEAD_QUEUE, ComponentType, Second } from "../../core/Atomic";
|
||||
import { PlayData } from "./PlayData";
|
||||
import { ErrorAlert } from "./ErrorAlert";
|
||||
import { AiOutlineExclamationCircle } from "react-icons/ai";
|
||||
import { ActivityTimeline } from "./ActivityTimeline";
|
||||
import { ExpandCollapse } from "./ExpandCollapse";
|
||||
import { MsSseEvent, PlayApiCommon, PlayApiCommonDetailed, SortPlaysBy, SortPlaysByProps } from "../../core/Api";
|
||||
import { InfiniteData, QueryFunctionContext, QueryOptions, queryOptions, SuspenseQueriesOptions, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import ky from 'ky';
|
||||
import { baseUrl } from "../utils";
|
||||
import { MsSseEvent, PlayApiCommonDetailed, SortPlaysByProps } from "../../core/Api";
|
||||
import { InfiniteData, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { ShortDateDisplay } from "./DateDisplay";
|
||||
import { TextMuted } from "./TextMuted";
|
||||
import { VscDebugRestart } from "react-icons/vsc";
|
||||
import { EphemeralElement, PlayStateBadge } from "./Badges";
|
||||
import { MarkOptional } from "ts-essentials";
|
||||
import { QueryPlaysOpts, QueryPlaysOptsJson } from "../../backend/common/database/drizzle/repositories/PlayRepository";
|
||||
import { QueryPlaysOptsJson } from "../../backend/common/database/drizzle/repositories/PlayRepository";
|
||||
import { tanQueries } from "../queries";
|
||||
import { PaginatedResponse } from "../../backend/common/database/drizzle/repositories/BaseRepository";
|
||||
import { LuChevronRight } from "react-icons/lu";
|
||||
import { useSSEContext, useSSEEvent } from "@flamefrontend/sse-runtime-react";
|
||||
import { DebugCopy, InsertedIcon, RetryButton, UpdatedIcon } from "./icons/ChakraIcons";
|
||||
import dayjs from "dayjs";
|
||||
import { DebugCopy, ExclamationCircleIcon, ExclamationTriangleIcon, InsertedIcon, RetryButton, UpdatedIcon } from "./icons/ChakraIcons";
|
||||
import { activityTimelineHasIssue } from "../utils/ComponentUtils";
|
||||
|
||||
type UseActivityQueryOptions = {
|
||||
msQuery?: QueryPlaysOptsJson
|
||||
|
|
@ -238,9 +234,18 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
|
|||
|
||||
const [collapsibleOpen, setCollapsibleOpen] = useState(undefined);
|
||||
|
||||
let timelineStatusIcon: React.JSX.Element | undefined;
|
||||
const timelineIssue = activityTimelineHasIssue(activity);
|
||||
if(timelineIssue === 'error') {
|
||||
timelineStatusIcon = <ExclamationCircleIcon size="sm" color="red.focusRing"/>;
|
||||
} else if(timelineIssue === 'warn') {
|
||||
timelineStatusIcon = <ExclamationTriangleIcon size="sm" color="yellow.focusRing"/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="2">
|
||||
<ActivityErrorSummary activity={props.activity}/>
|
||||
{/* <ActivityErrorSummary activity={props.activity}/> */}
|
||||
{error !== undefined && error !== null ? <ErrorAlert error={error}/> : null}
|
||||
<Accordion.Root width="full" variant="enclosed" collapsible multiple>
|
||||
<Accordion.Item value="info">
|
||||
<Accordion.ItemTrigger>
|
||||
|
|
@ -257,9 +262,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
|
|||
<Flex justify="flex-start">
|
||||
<Accordion.ItemTrigger>
|
||||
<Accordion.ItemIndicator />
|
||||
Timeline {error !== undefined && error !== null ? (<Icon size="sm" color="red.focusRing">
|
||||
<AiOutlineExclamationCircle />
|
||||
</Icon>) : null}
|
||||
Timeline {timelineStatusIcon}
|
||||
</Accordion.ItemTrigger>
|
||||
<Stack style={{
|
||||
paddingBlock: "var(--accordion-padding-y)",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ComponentProps, useState, Fragment } from "react"
|
||||
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 { PlayData } from "./PlayData";
|
||||
|
|
@ -21,7 +21,7 @@ import { TimelineErrorIcon } from "./timeline/TimelineIcon";
|
|||
import { Muted } from "./Typography";
|
||||
import { PlayApiCommonDetailed } from "../../core/Api";
|
||||
import { MSErrorBoundary } from "./ErrorBoundary";
|
||||
import { timelineTextFormatting } from "../utils/ComponentUtils";
|
||||
import { activityTransformHasIssue, timelineTextFormatting } from "../utils/ComponentUtils";
|
||||
|
||||
|
||||
export interface ActivityDetailProps {
|
||||
|
|
@ -104,6 +104,18 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
let transformVerb: string = 'Transformed Play';
|
||||
|
||||
const transformIssue = activityTransformHasIssue(props.activity);
|
||||
let transformResult: React.JSX.Element | undefined;
|
||||
if(transformIssue === 'error') {
|
||||
transformVerb = 'Transforming Play';
|
||||
transformResult = <Span> resulted in <Span color="red.solid">an error</Span></Span>;
|
||||
} else if(transformIssue === 'warn') {
|
||||
transformVerb = 'Transforming Play';
|
||||
transformResult = <Span> resulted in <Span color="orange.solid">warnings</Span></Span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MSErrorBoundary>
|
||||
<Timeline.Root variant="subtle" size="lg">
|
||||
|
|
@ -158,7 +170,7 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
|
|||
<Timeline.Content gap="4">
|
||||
<Timeline.Title>
|
||||
<MSCollapsible
|
||||
indicator={<Span {...timelineTextFormatting}>Transformed Play <Span color="fg.muted">using configured Rules</Span></Span>}
|
||||
indicator={<Span {...timelineTextFormatting}>{transformVerb} <Span color="fg.muted">using configured Rules</Span>{transformResult}</Span>}
|
||||
defaultOpen={collapsibleOpen}
|
||||
timeline>
|
||||
<Card.Root bgColor="bg.muted" size="sm">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ComponentProps, Fragment, useMemo } from "react"
|
||||
import React, { ComponentProps, Fragment, useMemo } from "react"
|
||||
import { Timeline, Icon, Span, Stack, Heading, Box, Text } from '@chakra-ui/react';
|
||||
import { JsonPlayObject, LifecycleStep } from "../../core/Atomic";
|
||||
import { PlayData } from "./PlayData";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {
|
||||
LuChevronRight,
|
||||
LuChevronLeft,
|
||||
LuCircleAlert,
|
||||
LuTriangleAlert,
|
||||
LuActivity,
|
||||
LuGithub,
|
||||
LuTerminal,
|
||||
|
|
@ -45,7 +47,13 @@ export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren<Comp
|
|||
</IconButton>
|
||||
);
|
||||
}
|
||||
export const makeChakraIcon = (IconComponent: IconType) => (props: ComponentProps<typeof Icon> & { iconProps?: IconBaseProps }) => <Icon {...props}><IconComponent {...props.iconProps}/></Icon>
|
||||
export const makeChakraIcon = (IconComponent: IconType) => (props: ComponentProps<typeof Icon> & { iconProps?: IconBaseProps }) => {
|
||||
const {
|
||||
iconProps,
|
||||
...rest
|
||||
} = props;
|
||||
return <Icon {...rest}><IconComponent {...iconProps}/></Icon>
|
||||
}
|
||||
|
||||
export const ChevronRight = LuChevronRight;
|
||||
export const ChevronRightButton = (props: ComponentProps<typeof IconButton>) => (
|
||||
|
|
@ -163,4 +171,8 @@ export const InsertedIcon = makeChakraIcon(MdOutlineFiberNew);
|
|||
export const UpdatedIcon = makeChakraIcon(LuCircleArrowUp);
|
||||
|
||||
export const CopyIcon = makeChakraIcon(LuCopy);
|
||||
export const CopyIconButton = makeIconButton(LuCopy);
|
||||
export const CopyIconButton = makeIconButton(LuCopy);
|
||||
|
||||
export const ExclamationCircleIcon = makeChakraIcon(LuCircleAlert);
|
||||
|
||||
export const ExclamationTriangleIcon = makeChakraIcon(LuTriangleAlert);
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import { Card, HTMLChakraProps } from '@chakra-ui/react';
|
||||
import { PlayApiCommon, PlayApiCommonDetailed } from '../../core/Api';
|
||||
import { QUEUE_STATUS_COMPLETED, QUEUE_STATUS_FAILED } from '../../core/Atomic';
|
||||
|
||||
export const cardHeaderSeparator: Card.HeaderProps = {
|
||||
borderBottomWidth: "1px",
|
||||
|
|
@ -8,4 +10,51 @@ export const cardHeaderSeparator: Card.HeaderProps = {
|
|||
export const timelineTextFormatting: HTMLChakraProps<"span"> = {
|
||||
textAlign: "left",
|
||||
textWrap: "balance"
|
||||
}
|
||||
|
||||
export const activityTransformHasIssue = (activity: PlayApiCommon): 'warn' | 'error' | undefined => {
|
||||
const {
|
||||
play: {
|
||||
lifecycle = [],
|
||||
} = {},
|
||||
} = activity;
|
||||
for(const step of lifecycle) {
|
||||
if(step.flowKnownState === 'prereq') {
|
||||
return 'warn';
|
||||
}
|
||||
if(step.flowKnownState === 'skip') {
|
||||
continue;
|
||||
}
|
||||
if(step.error !== undefined && step.error !== null && Object.keys(step.error).length > 0) {
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const activityTimelineHasIssue = (activity: PlayApiCommonDetailed): 'warn' | 'error' | undefined => {
|
||||
const {
|
||||
queueStates = [],
|
||||
play: {
|
||||
scrobble: {
|
||||
error: scrobbleError,
|
||||
warnings: scrobbleWarnings = []
|
||||
} = {},
|
||||
} = {},
|
||||
} = activity;
|
||||
const transformIssue = activityTransformHasIssue(activity);
|
||||
if(transformIssue !== undefined) {
|
||||
return transformIssue;
|
||||
}
|
||||
if (scrobbleError !== undefined) {
|
||||
return 'error';
|
||||
}
|
||||
if(scrobbleWarnings.length > 0) {
|
||||
return 'warn';
|
||||
}
|
||||
if (queueStates.some(x => x.queueStatus === QUEUE_STATUS_FAILED) && !queueStates.some(x => x.queueStatus === QUEUE_STATUS_COMPLETED)) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -191,23 +191,23 @@ export const generateLifecycleStep = (play: PlayObject, opts: GenerateLifecycleO
|
|||
if(error === true) {
|
||||
step.flowResult = 'stop';
|
||||
step.flowReason = 'Error encountered while transforming';
|
||||
step.error = new Error('Failed to do something', {cause: new Error('Oops it borked.')});
|
||||
step.error = serializeError(new Error('Failed to do something', {cause: new Error('Oops it borked.')}));
|
||||
} else if(error === 'prereq') {
|
||||
step.flowResult = 'stop';
|
||||
step.flowKnownState = 'prereq';
|
||||
step.flowReason = 'Transform could not be completed due to prerequisite failure';
|
||||
step.error = mergeSimpleError(new StagePrerequisiteError('No matches returned from Musicbrainz API', {shortStack: true, cause: new SimpleError('Results were empty')}));
|
||||
step.error = serializeError(mergeSimpleError(new StagePrerequisiteError('No matches returned from Musicbrainz API', {shortStack: true, cause: new SimpleError('Results were empty')})));
|
||||
} else if(error === 'stop') {
|
||||
step.flowResult = 'stop';
|
||||
} else if(error === 'continuewitherror') {
|
||||
step.flowResult = 'continue';
|
||||
step.flowReason = 'Transform encountered an error but continuing due to onFailure: continue';
|
||||
step.error = mergeSimpleError(new SkipTransformStageError('An error that was an okay to continue with'));
|
||||
step.error = serializeError(mergeSimpleError(new SkipTransformStageError('An error that was an okay to continue with')));
|
||||
} else {
|
||||
step.flowResult = 'continue';
|
||||
step.flowKnownState = 'skip';
|
||||
step.flowReason = `Stage ${name} was skipped`;
|
||||
step.error = mergeSimpleError(new SkipTransformStageError('No desired MBIDs were missing', {shortStack: true}));
|
||||
step.error = serializeError(mergeSimpleError(new SkipTransformStageError('No desired MBIDs were missing', {shortStack: true})));
|
||||
}
|
||||
|
||||
return [step, play];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue