diff --git a/src/client/components/ActivityDetail.tsx b/src/client/components/ActivityDetail.tsx
new file mode 100644
index 00000000..3ae60853
--- /dev/null
+++ b/src/client/components/ActivityDetail.tsx
@@ -0,0 +1,47 @@
+import { ComponentProps } from "react"
+import { Accordion, For, Span, Stack, Text, Box, AbsoluteCenter, Button, Separator, HStack, Flex, Badge, IconButton, Container, Icon } from '@chakra-ui/react';
+import { ErrorLike, PlayActivity } from "../../core/Atomic";
+import { PlayData } from "./PlayData";
+import { ErrorAlert } from "./ErrorAlert";
+import { AiOutlineExclamationCircle } from "react-icons/ai";
+
+export interface ActivityDetailProps {
+ activity: PlayActivity
+}
+
+export const ActivityDetails = (props: ActivityDetailProps) => {
+ const {
+ activity,
+ activity: {
+ error
+ }
+ } = props;
+ return (
+
+
+
+
+ Play Data
+
+
+
+
+
+
+
+
+
+
+ Timeline {error !== undefined ? (
+
+ ) : null}
+
+
+
+ {error !== undefined ? : null}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/client/components/ChakraClipboard.tsx b/src/client/components/ChakraClipboard.tsx
index 6f32d6cb..28738ddd 100644
--- a/src/client/components/ChakraClipboard.tsx
+++ b/src/client/components/ChakraClipboard.tsx
@@ -1,17 +1,21 @@
-import { useMemo } from 'react';
+import { ComponentProps, useMemo } from 'react';
import { Clipboard, IconButton } from "@chakra-ui/react"
import {safeStringify} from '../../core/StringUtils';
-export const ChakraClip = (props: {value: any}) => {
+export const ChakraClip = (props: Omit, 'children' | 'value'> & {value: any}) => {
+ const {
+ value,
+ ...rest
+ } = props;
const clipVal = useMemo(() => {
- if(typeof props.value === 'string') {
- return props.value;
+ if(typeof value === 'string') {
+ return value;
}
- return safeStringify(props.value);
- },[props.value])
+ return safeStringify(value);
+ },[value])
return (
-
+
diff --git a/src/client/components/CodeBlock.tsx b/src/client/components/CodeBlock.tsx
index 1b0cb110..9aa00acd 100644
--- a/src/client/components/CodeBlock.tsx
+++ b/src/client/components/CodeBlock.tsx
@@ -1,12 +1,13 @@
import type { HighlighterGeneric } from "shiki"
import { createShikiAdapter, CodeBlock, IconButton, ClientOnly, ScrollArea } from "@chakra-ui/react"
import { useColorMode } from "./Color-Mode";
+import { ComponentProps } from "react";
const shikiAdapter = createShikiAdapter>({
async load() {
const { createHighlighter } = await import("shiki")
return createHighlighter({
- langs: ["json"],
+ langs: ["json", "plaintext"],
themes: ["github-dark", "github-light"],
})
},
@@ -16,31 +17,62 @@ const shikiAdapter = createShikiAdapter>({
},
});
-export interface ChakraCodeBlockProps {
+export type ChakraCodeBlockProps = Omit, 'children'> & {
code: string
language?: string
title?: string
maxHeight?: string
+ maxLines?: number
+ collapsedMaxHeight?: string
}
export const ChakraCodeBlock = (props: ChakraCodeBlockProps) => {
+ const {
+ maxHeight = '70vh',
+ maxLines,
+ collapsedMaxHeight = '320px',
+ language = 'json',
+ ...rest
+ } = props;
+
+ const contentProps: ComponentProps = maxLines === undefined ? { maxHeight, overflowY: 'auto' } : { css: {"--code-block-max-height": collapsedMaxHeight}};
+
return (
Loading...}>
{() => (
-
+
{props.title ?? ' '}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
)}
diff --git a/src/client/components/ErrorAlert.tsx b/src/client/components/ErrorAlert.tsx
new file mode 100644
index 00000000..7bc334c1
--- /dev/null
+++ b/src/client/components/ErrorAlert.tsx
@@ -0,0 +1,62 @@
+import { Alert, Accordion, Stack, Text, Box } from '@chakra-ui/react';
+import { Fragment } from 'react';
+import { ErrorLike } from '../../core/Atomic';
+import { ChakraCodeBlock } from './CodeBlock';
+import { ChakraClip } from './ChakraClipboard';
+
+export interface ErrorAlertProps {
+ error: ErrorLike
+}
+
+export const ErrorAlert = (props: ErrorAlertProps) => {
+
+ let causes: ErrorData[] = [];
+ if(props.error.cause !== undefined && typeof props.error.cause === 'object') {
+ causes = walkError(props.error.cause as ErrorLike);
+ }
+
+ return (
+
+
+
+
+ {props.error.name ?? 'Error'}
+
+
+ {props.error.message}
+ {props.error.stack !== undefined ? : null}
+ {causes.map(x => (
+
+ Caused By: {x.name ?? ''}{x.code !== undefined ? ` (${x.code}) ` : ''}{x.message}
+ {x.stack !== undefined ? : null}
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+interface ErrorData {
+ name?: string
+ code?: string
+ message?: string
+ stack?: string
+}
+
+const walkError = (err: ErrorLike, errors: ErrorData[] = []): ErrorData[] => {
+ const thisErr: ErrorData = {
+ name: err.name,
+ code: 'code' in err ? err.code : undefined,
+ message: err.message,
+ stack: err.stack
+ };
+ errors.push(thisErr);
+ if(err.cause !== undefined && typeof err.cause === 'object') {
+ return walkError(err, errors);
+ }
+ return errors;
+}
\ No newline at end of file
diff --git a/src/client/components/List.tsx b/src/client/components/List.tsx
index 864554cb..dfb126cb 100644
--- a/src/client/components/List.tsx
+++ b/src/client/components/List.tsx
@@ -1,15 +1,12 @@
import { Accordion, For, Span, Stack, Text, Box, AbsoluteCenter, Button, Separator, HStack, Flex, Badge, IconButton, Container } from '@chakra-ui/react';
-import { JsonPlayObject } from '../../core/Atomic';
+import { JsonPlayObject, PlayActivity } from '../../core/Atomic';
import { ShortDateDisplay } from './DateDisplay';
import { TextMuted } from './TextMuted';
import { capitalize } from '../../core/StringUtils';
import { ComponentProps } from "react"
import { VscDebugRestart } from "react-icons/vsc";
-import { PlayInfo, PlayInfoContainer } from './PlayInfo';
-export interface PlayActivity {
- play: JsonPlayObject
- status: string
-}
+import { PlayData, PlayInfoContainer } from './PlayData';
+import { ActivityDetails } from './ActivityDetail';
export interface ActivityLogProps {
data: PlayActivity[]
}
@@ -58,32 +55,7 @@ export const CList = (props: ActivityLogProps) => {
-
-
-
-
-
- Play Info
-
-
-
-
-
-
-
-
-
-
- Timeline
-
-
-
- test
-
-
-
-
-
+
diff --git a/src/client/components/PlayInfo.tsx b/src/client/components/PlayData.tsx
similarity index 94%
rename from src/client/components/PlayInfo.tsx
rename to src/client/components/PlayData.tsx
index b269e9ff..c7aede08 100644
--- a/src/client/components/PlayInfo.tsx
+++ b/src/client/components/PlayData.tsx
@@ -1,14 +1,14 @@
import React, { Fragment, useMemo, useState } from 'react';
import { EmptyState, DataList, HStack, Tag, Wrap, Box, Flex, SegmentGroup, Stack, Text, Separator, IconButton, Container, SimpleGrid } from "@chakra-ui/react"
import { LuCode, LuText } from "react-icons/lu"
-import { JsonPlayObject } from '../../core/Atomic';
-import { shortTodayAwareFormat } from '../../core/TimeUtils';
+import { JsonPlayObject } from '../../core/Atomic.js';
+import { shortTodayAwareFormat } from '../../core/TimeUtils.js';
import dayjs from 'dayjs';
-import { ChakraCodeBlock } from './CodeBlock';
-import { safeStringify } from '../../core/StringUtils';
-import { TextMuted } from './TextMuted';
+import { ChakraCodeBlock } from './CodeBlock.js';
+import { safeStringify } from '../../core/StringUtils.js';
+import { TextMuted } from './TextMuted.js';
-const EmptyPlay = () => {
+const EmptyPlayData = () => {
return (
@@ -29,7 +29,7 @@ export interface PlayInfoProps {
showDates?: false | 'all' | 'played' | 'seen'
}
-export const PlayInfo = (props?: PlayInfoProps) => {
+export const PlayData = (props?: PlayInfoProps) => {
const {
play,
final,
@@ -39,7 +39,7 @@ export const PlayInfo = (props?: PlayInfoProps) => {
} = props ?? {};
if (play === undefined) {
- return
+ return
}
const [compareVal, setCompareVal] = useState('Original')
@@ -180,5 +180,5 @@ export const PlayInfo = (props?: PlayInfoProps) => {
}
export const PlayInfoContainer = (props?: PlayInfoProps) => {
- return
+ return
}
\ No newline at end of file
diff --git a/src/core/Atomic.ts b/src/core/Atomic.ts
index 935c608c..f4081572 100644
--- a/src/core/Atomic.ts
+++ b/src/core/Atomic.ts
@@ -290,6 +290,8 @@ export interface PlayObjectLifecycleless {
// lifecycle: PlayLifecycle
// }
+export type ErrorLike = Error | ErrorObject;
+
export interface PlayLifecycle {
input?: object
original: PlayObjectLifecycleless
@@ -353,6 +355,11 @@ export interface PlayObject extends AmbPlayObject {
// meta: PlayMetaLifecycled
// }
+export interface PlayActivity {
+ play: JsonPlayObject
+ status: string
+ error?: ErrorLike
+}
export interface JsonPlayObject extends AmbPlayObject {
data: JsonPlayData
}
diff --git a/src/stories/ErrorAlert.stories.tsx b/src/stories/ErrorAlert.stories.tsx
new file mode 100644
index 00000000..e4f4135a
--- /dev/null
+++ b/src/stories/ErrorAlert.stories.tsx
@@ -0,0 +1,53 @@
+import preview from "../../.storybook/preview.js";
+import React from 'react';
+import { Container } from '@chakra-ui/react';
+
+import { fn } from 'storybook/test';
+import { ErrorAlert } from "../client/components/ErrorAlert.js";
+import {Provider} from "../client/components/Provider";
+import { ErrorLike } from "../core/Atomic.js";
+
+const stack = "Scrobble Submit Error: Failed to submit to Listenbrainz (listen_type single)\n at ListenbrainzApiClient.submitListen (/app/src/backend/common/vendor/ListenbrainzApiClient.ts:246:19)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async ListenbrainzScrobbler.doScrobble (/app/src/backend/scrobblers/ListenbrainzScrobbler.ts:87:28)\n at async ListenbrainzScrobbler.scrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:679:28)\n at async ListenbrainzScrobbler.processDeadLetterScrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:920:39)\n at async ListenbrainzScrobbler.processDeadLetterQueue (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:894:43)\n at async PromisePoolExecutor.handler (/app/src/backend/tasks/heartbeatClients.ts:35:21)\n at async PromisePoolExecutor.waitForActiveTaskToFinish (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:375:9)\n at async PromisePoolExecutor.waitForProcessingSlot (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:368:13)\n at async PromisePoolExecutor.process (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:354:13)";
+
+const errorExample: ErrorLike = {
+ showStopper: false,
+ name: "Scrobble Submit Error",
+ message: "Failed to submit to Listenbrainz (listen_type single)",
+ stack: `${stack}`,
+ cause: {
+ errno: -104,
+ code: "ECONNRESET",
+ syscall: "read",
+ name: "Error",
+ message: "read ECONNRESET",
+ stack: "Error: read ECONNRESET\n at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\n at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17)"
+ }
+}
+
+type PropsAndCustomArgs = React.ComponentProps & {
+};
+// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
+const meta = preview.type<{args: PropsAndCustomArgs}>().meta({
+ title: 'Examples/ErrorAlert',
+ component: ErrorAlert,
+ parameters: {
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
+ layout: 'padded',
+ },
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
+ tags: ['autodocs'],
+decorators: [
+ (Story) => (),
+ ],
+args: {
+ error: errorExample,
+ },
+ // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
+});
+
+// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
+export const ErrorAlertStory = meta.story({
+ render: function Render(args) {
+ return ()
+ }
+});
\ No newline at end of file
diff --git a/src/stories/List.stories.tsx b/src/stories/List.stories.tsx
index 47731765..29cfb7b7 100644
--- a/src/stories/List.stories.tsx
+++ b/src/stories/List.stories.tsx
@@ -2,16 +2,33 @@ import preview from "../../.storybook/preview.js";
import React from 'react';
import { fn } from 'storybook/test';
-import { CList, ListContainer } from "../client/components/List";
+import { Container } from '@chakra-ui/react';
+import { CList } from "../client/components/List";
import {Provider} from "../client/components/Provider";
import { generateJsonPlays } from "../backend/tests/utils/PlayTestUtils.js";
+import { ErrorLike } from "../core/Atomic.js";
+const stack = "Scrobble Submit Error: Failed to submit to Listenbrainz (listen_type single)\n at ListenbrainzApiClient.submitListen (/app/src/backend/common/vendor/ListenbrainzApiClient.ts:246:19)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async ListenbrainzScrobbler.doScrobble (/app/src/backend/scrobblers/ListenbrainzScrobbler.ts:87:28)\n at async ListenbrainzScrobbler.scrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:679:28)\n at async ListenbrainzScrobbler.processDeadLetterScrobble (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:920:39)\n at async ListenbrainzScrobbler.processDeadLetterQueue (/app/src/backend/scrobblers/AbstractScrobbleClient.ts:894:43)\n at async PromisePoolExecutor.handler (/app/src/backend/tasks/heartbeatClients.ts:35:21)\n at async PromisePoolExecutor.waitForActiveTaskToFinish (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:375:9)\n at async PromisePoolExecutor.waitForProcessingSlot (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:368:13)\n at async PromisePoolExecutor.process (/app/node_modules/@supercharge/promise-pool/dist/promise-pool-executor.js:354:13)";
+const errorExample: ErrorLike = {
+ showStopper: false,
+ name: "Scrobble Submit Error",
+ message: "Failed to submit to Listenbrainz (listen_type single)",
+ stack: `${stack}`,
+ cause: {
+ errno: -104,
+ code: "ECONNRESET",
+ syscall: "read",
+ name: "Error",
+ message: "read ECONNRESET",
+ stack: "Error: read ECONNRESET\n at TLSWrap.onStreamRead (node:internal/stream_base_commons:218:20)\n at TLSWrap.callbackTrampoline (node:internal/async_hooks:130:17)"
+ }
+}
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.meta({
title: 'List',
- component: ListContainer,
+ component: CList,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'padded',
@@ -20,10 +37,20 @@ const meta = preview.meta({
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
- data: (generateJsonPlays(3, undefined, {source: 'Spotify'})).map((x, index) => ({play: x, status: index === 0 ? 'queued' : index === 1 ? 'scrobbled' : index === 2 ? 'error' : 'unknown'})),
+ data: (generateJsonPlays(3, undefined, {source: 'Spotify'})).map((x, index) => {
+ const mod = (index + 1) % 3;
+ switch(mod) {
+ case 1:
+ return {play: x, status: 'queued'}
+ case 2:
+ return {play: x, status: 'scrobbled'}
+ case 0:
+ return {play: x, status: 'error', error: errorExample};
+ }
+ }),
},
decorators: [
- (Story) => (),
+ (Story) => (),
]
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
});
diff --git a/src/stories/PlayInfo.stories.tsx b/src/stories/PlayInfo.stories.tsx
index 7eb600b4..6ee1aed4 100644
--- a/src/stories/PlayInfo.stories.tsx
+++ b/src/stories/PlayInfo.stories.tsx
@@ -2,12 +2,12 @@ import preview from "../../.storybook/preview.js";
import React from 'react';
import { fn } from 'storybook/test';
-import { PlayInfo, PlayInfoContainer } from "../client/components/PlayInfo";
+import { PlayData, PlayInfoContainer } from "../client/components/PlayData.js";
import {Provider} from "../client/components/Provider";
import { generateArtists, generateJsonPlay, generatePlay } from "../backend/tests/utils/PlayTestUtils"
import clone from "clone";
-type PropsAndCustomArgs = React.ComponentProps & {
+type PropsAndCustomArgs = React.ComponentProps & {
includeAlbumArtists?: boolean;
defaultFinal?: boolean
};