diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx
index 27d541dd..ccb4a527 100644
--- a/src/client/components/ActivityDetail.tsx
+++ b/src/client/components/ActivityDetail.tsx
@@ -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 = ;
+ } else if(timelineIssue === 'warn') {
+ timelineStatusIcon = ;
+ }
+
return (
-
+ {/* */}
+ {error !== undefined && error !== null ? : null}
@@ -257,9 +262,7 @@ export const ActivityDetails = (props: ActivityDetailProps) => {
- Timeline {error !== undefined && error !== null ? (
-
- ) : null}
+ Timeline {timelineStatusIcon}
{
}
}
+ let transformVerb: string = 'Transformed Play';
+
+ const transformIssue = activityTransformHasIssue(props.activity);
+ 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 (
@@ -158,7 +170,7 @@ export const ActivityTimeline = (props: ActivityDetailProps) => {
Transformed Play using configured Rules}
+ indicator={{transformVerb} using configured Rules{transformResult}}
defaultOpen={collapsibleOpen}
timeline>
diff --git a/src/client/components/TransformSteps.tsx b/src/client/components/TransformSteps.tsx
index fdde0c2d..a2864271 100644
--- a/src/client/components/TransformSteps.tsx
+++ b/src/client/components/TransformSteps.tsx
@@ -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";
diff --git a/src/client/components/icons/ChakraIcons.tsx b/src/client/components/icons/ChakraIcons.tsx
index b5457182..04a2d388 100644
--- a/src/client/components/icons/ChakraIcons.tsx
+++ b/src/client/components/icons/ChakraIcons.tsx
@@ -1,6 +1,8 @@
import {
LuChevronRight,
LuChevronLeft,
+ LuCircleAlert,
+ LuTriangleAlert,
LuActivity,
LuGithub,
LuTerminal,
@@ -45,7 +47,13 @@ export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren
);
}
-export const makeChakraIcon = (IconComponent: IconType) => (props: ComponentProps & { iconProps?: IconBaseProps }) =>
+export const makeChakraIcon = (IconComponent: IconType) => (props: ComponentProps & { iconProps?: IconBaseProps }) => {
+ const {
+ iconProps,
+ ...rest
+ } = props;
+ return
+}
export const ChevronRight = LuChevronRight;
export const ChevronRightButton = (props: ComponentProps) => (
@@ -163,4 +171,8 @@ export const InsertedIcon = makeChakraIcon(MdOutlineFiberNew);
export const UpdatedIcon = makeChakraIcon(LuCircleArrowUp);
export const CopyIcon = makeChakraIcon(LuCopy);
-export const CopyIconButton = makeIconButton(LuCopy);
\ No newline at end of file
+export const CopyIconButton = makeIconButton(LuCopy);
+
+export const ExclamationCircleIcon = makeChakraIcon(LuCircleAlert);
+
+export const ExclamationTriangleIcon = makeChakraIcon(LuTriangleAlert);
\ No newline at end of file
diff --git a/src/client/utils/ComponentUtils.tsx b/src/client/utils/ComponentUtils.tsx
index ad617e98..4fcf76d5 100644
--- a/src/client/utils/ComponentUtils.tsx
+++ b/src/client/utils/ComponentUtils.tsx
@@ -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;
}
\ No newline at end of file
diff --git a/src/core/tests/utils/fixtures.ts b/src/core/tests/utils/fixtures.ts
index f021978f..93298006 100644
--- a/src/core/tests/utils/fixtures.ts
+++ b/src/core/tests/utils/fixtures.ts
@@ -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];