diff --git a/src/client/components/msComponent/MSComponentDetailed.tsx b/src/client/components/msComponent/MSComponentDetailed.tsx
index 5293b692..1df302ee 100644
--- a/src/client/components/msComponent/MSComponentDetailed.tsx
+++ b/src/client/components/msComponent/MSComponentDetailed.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, type ComponentProps, useEffect } from "react"
import { Portal, Group, Span, Menu, Box, Heading, Skeleton, Wrap, HStack, Stack, Flex, Text, Card, Button, CloseButton, SkeletonText, type BadgeProps, type MenuItemProps, createOverlay, Dialog, type MenuSelectionDetails } from '@chakra-ui/react';
-import { COMPONENT_STATE, type ComponentClientApiJson, type ComponentCommonApiJson, type ComponentsApiJson, type ComponentState, type ComponentStateBody, isComponentClientApiJson, isComponentSourceApiJson, type MsSseEvent, type MsSseEventPayload } from "../../../core/Api.js";
+import { COMPONENT_STATE, type ComponentCommonApiJson, type ComponentsApiJson, type ComponentState, type ComponentStateBody, isComponentSourceApiJson, type MsSseEvent, type MsSseEventPayload } from "../../../core/Api.js";
import { capitalize } from "../../../core/StringUtils.js";
import { ChevronLeftButton, EllipsisButton, ExternalLinkIcon, EyeButton, EyeClosedIcon, EyeIcon, IdleIcon, PowerButton, PowerIcon, type PowerOffButton, PowerOffIcon, RetryButton, RetryIcon, UnlockButton, UnlockIconRaw } from "../icons/ChakraIcons.js";
import { PlayersContainer, PlayersContainerFetchable } from "../chakraPlayer/Player.js";
@@ -13,7 +13,7 @@ import {
useSSEAnyEvent
} from "@flamefrontend/sse-runtime-react";
import { Link } from "react-router";
-import { CountLiveIndicator, DateIndicator, DeadLetterIndicator, QueuedIndicator } from "./Stats.js";
+import { CountIndicatorStreamable, DateIndicatorStreamable, DeadLetterIndicatorStreamable, QueuedIndicatorStreamable } from "./Stats.js";
import { ListContainerFilterable } from "../playActivity/ActivityList.js";
import { useParams } from "react-router-dom";
import { ComponentStateBadge } from "../Badges.js";
@@ -64,10 +64,10 @@ export const MSComponentStats = (props: { data?: ComponentCommonApiJson, live?:
}
return (
-
-
-
-
+
+
+
+
)
}
diff --git a/src/client/components/msComponent/MSComponentSummary.tsx b/src/client/components/msComponent/MSComponentSummary.tsx
index 95c9137f..776c8f51 100644
--- a/src/client/components/msComponent/MSComponentSummary.tsx
+++ b/src/client/components/msComponent/MSComponentSummary.tsx
@@ -12,7 +12,7 @@ import {
useSSEContext,
useSSEAnyEvent
} from "@flamefrontend/sse-runtime-react";
-import { CountLiveIndicator, DeadLetterIndicator, QueuedIndicator } from "./Stats.js";
+import { CountIndicatorStreamable, DeadLetterIndicatorStreamable, QueuedIndicatorStreamable } from "./Stats.js";
import { ComponentStateBadge } from "../Badges.js";
import { MSErrorBoundary } from "../ErrorBoundary.js";
@@ -75,21 +75,19 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson, fetcha
//colorPalette={data.mode === 'client' ? 'purple' : 'pink'}
// color={data.mode === 'client' ? 'purple' : 'pink'}
-const QuickStatsSource = (props: { data: ComponentCommonApiJson, streamable?: boolean }) => {
- return (
+const QuickStatsSource = (props: { data: ComponentCommonApiJson, streamable?: boolean }) => (
-
+
-
+
-
+
)
-}
export const MSComponentSummaryFetchable = (props: {componentId: number, data: ComponentCommonApiJson}) => {
const {
diff --git a/src/client/components/msComponent/Stats.tsx b/src/client/components/msComponent/Stats.tsx
index 5d6b03e3..d1b5487e 100644
--- a/src/client/components/msComponent/Stats.tsx
+++ b/src/client/components/msComponent/Stats.tsx
@@ -1,7 +1,8 @@
-import { type ComponentProps, useState, useCallback } from "react"
-import { Badge, Stat, HStack } from '@chakra-ui/react';
+import { type ComponentProps, useState, useCallback, type ReactNode, useEffect } from "react"
+import { Badge, Stat, HStack, type BadgeProps } from '@chakra-ui/react';
import type {ComponentClientApiJson, ComponentCommonApiJson, MsSseEvent} from "../../../core/Api.js";
import { TextMuted } from "../TextMuted.js";
+import type { IconBaseProps } from "react-icons/lib";
import { DownArrowIcon, UpArrowIcon } from "../icons/ChakraIcons.js";
import { useTimeout } from 'react-use-timeout';
import {
@@ -11,267 +12,210 @@ import {
import dayjs from "dayjs";
import { shortTodayAwareFormat } from "../../../core/TimeUtils.js";
-export const CountLiveIndicator = (props: {
- data: Pick & { tracksDiscovered?: number, tracksScrobbled?: number },
- recent?: number
+type Color = BadgeProps['colorPalette'] & IconBaseProps['color'];
+
+export const Indicator = (props: {
+ current?: number
+ currentText: string | ReactNode
recentTimeout?: number
- streamable?: boolean
+ total?: number
+ totalText?: string | ReactNode
+ helpText?: string | ReactNode
+ directionColors?: [Color, Color]
as?: 'text' | 'stat'
} & ComponentProps) => {
const {
- data,
- recent: recentProp = 0,
- recentTimeout: timeoutProp = 10000,
- streamable,
as = 'stat',
+ current = 0,
+ currentText,
+ recentTimeout: timeoutProp = 10000,
+ directionColors = ['green', 'red'],
+ total,
+ totalText,
+ helpText,
...rest
} = props;
- const sessionCount = props.data.mode === 'source' ? props.data.tracksDiscovered : props.data.tracksScrobbled;
-
- const [total, setTotal] = useState(props.data.countLive);
- const [current, setCurrent] = useState(sessionCount);
- const [recent, setRecent] = useState(recentProp);
+ const [lastCurrent, setLastCurrent] = useState(current);
+ const [recent, setRecent] = useState(0);
+ const [recentDirection, setRecentDirection] = useState<'up' | 'down'>('up');
const resetRecent = useCallback(() => {
- setRecent(props.recent ?? 0);
+ setRecent(0);
}, [setRecent]);
const recentTimeout = useTimeout(resetRecent, timeoutProp ?? 10000);
- if (props.streamable) {
- const client = useSSEContext();
- useSSEAnyEvent(client, (payload) => {
- if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === props.data.id) {
- switch (payload.type) {
- case 'scrobble':
- case 'discovered':
- recentTimeout.stop();
- setTotal(total + 1);
- setCurrent(current + 1);
- setRecent(recent + 1);
- recentTimeout.start();
- break;
- }
+ if (lastCurrent !== current) {
+ recentTimeout.stop();
+ if (lastCurrent > current) {
+ if (recentDirection === 'up') {
+ setRecent(1);
+ setRecentDirection('down');
+ } else {
+ setRecent(recent + 1);
}
- });
+ } else {
+ if (recentDirection === 'down') {
+ setRecent(1);
+ setRecentDirection('up');
+ } else {
+ setRecent(recent + 1);
+ }
+ }
+ setLastCurrent(current);
+ recentTimeout.start();
}
if (as === 'stat') {
return (
-
- {props.data.mode === 'source' ? 'Discovered' : 'Scrobbled'}
+
+ {currentText}
- {/* {props.data.mode === 'source' ? 'Discovered' : 'Scrobbled'} since start and (Total) */}
- {current} ({total})
- {recent !== 0 ?
-
+ {current} {total !== undefined && <>({total})>}
+ {recent !== 0 ?
+ {recentDirection === 'up' ? : }
{recent}
: null}
- Since Start and (Total)
+ {helpText !== undefined && totalText !== undefined && {helpText ?? <>{props.currentText} and ({totalText})>}}
);
}
return (
- {current} ({total}) {recent !== 0 ?
-
+ {current} {total !== undefined && <>({total})>} {recent !== 0 ?
+ {recentDirection === 'up' ? : }
{recent}
- : null} {props.data.mode === 'source' ? 'Discovered' : 'Scrobbled'}
+ : null} {currentText}
);
-}
+};
-export const QueuedIndicator = (props: {
- data: Pick,
- recent?: number
- recentTimeout?: number
- streamable?: boolean
- as?: 'text' | 'stat'
-} & ComponentProps) => {
+export const CountIndicatorStatic = (props: Omit, 'totalText' | 'helpText' | 'directionColors' | 'currentText'> & Pick) => (
+
+)
+export const CountIndicatorStreamable = (props: { data: Pick & { tracksDiscovered?: number, tracksScrobbled?: number } }
+ & Omit, 'mode'>) => {
const {
data,
- recent: recentProp = 0,
- recentTimeout: timeoutProp = 10000,
- streamable,
- as = 'stat',
...rest
} = props;
+ const [statsData, setStatsData] = useState({current: data.tracksDiscovered ?? data.tracksScrobbled, total: data.countLive});
+
+ const client = useSSEContext();
+ useSSEAnyEvent(client, (payload) => {
+ if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === data.id) {
+ switch (payload.type) {
+ case 'scrobble':
+ case 'discovered':
+ setStatsData({current: statsData.current + 1, total: statsData.total + 1});
+ break;
+ }
+ }
+ });
- const [current, setCurrent] = useState(props.data.queued);
- const [recent, setRecent] = useState(recentProp);
- const [recentDirection, setRecentDirection] = useState<'up' | 'down'>('up');
- const resetRecent = useCallback(() => {
- setRecent(0);
+ return
+}
- }, [setRecent]);
- const recentTimeout = useTimeout(resetRecent, timeoutProp);
+export const QueuedIndicatorStatic = (props: Omit, 'currentText' | 'totalText' | 'helpText' | 'directionColors'>) => (
+
+);
- if (props.streamable) {
- const client = useSSEContext();
- useSSEAnyEvent(client, (payload) => {
- if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === props.data.id) {
- recentTimeout.stop();
- switch (payload.type) {
- case 'playQueued':
- setCurrent(current + 1);
- if (recentDirection === 'down') {
- setRecent(1);
- } else {
- setRecent(recent + 1);
- }
- setRecentDirection('up');
- break;
- case 'playDequeued':
- setCurrent(current - 1);
- if (recentDirection === 'up') {
- setRecent(1);
- } else {
- setRecent(recent + 1);
- }
- setRecentDirection('down');
- break;
- }
- recentTimeout.start();
+export const QueuedIndicatorStreamable = (props: { data: Pick }
+ & ComponentProps) => {
+ const {
+ data,
+ ...rest
+ } = props;
+ const [statsData, setStatsData] = useState({current: data.queued});
+
+ const client = useSSEContext();
+ useSSEAnyEvent(client, (payload) => {
+ if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === data.id) {
+ switch (payload.type) {
+ case 'playQueued':
+ setStatsData({current: statsData.current + 1});
+ break;
+ case 'playDequeued':
+ setStatsData({current: statsData.current - 1});
+ break;
}
- });
- }
-
- if(as === 'stat') {
- return (
-
- Queued
-
- {current}
- {recent !== 0 ?
- {recentDirection === 'up' ? : }
- {recent}
- : null}
-
-
- );
- }
+ }
+ });
- return (
- {current} {recent !== 0 ?
- {recentDirection === 'up' ? : }
- {recent}
- : null} Queued
- );
+ return
}
-export const DeadLetterIndicator = (props: {
- data: Pick,
- recent?: number
- recentTimeout?: number
- streamable?: boolean
- as?: 'text' | 'stat'
-} & ComponentProps ) => {
+export const DeadLetterIndicatorStatic = (props: Omit, 'currentText' | 'totalText' | 'helpText' | 'directionColors'>) => (
+
+);
+export const DeadLetterIndicatorStreamable = (props: { data: Pick }
+ & ComponentProps) => {
const {
data,
- recent: recentProp = 0,
- recentTimeout: timeoutProp = 10000,
- streamable,
- as = 'stat',
...rest
} = props;
+ const [statsData, setStatsData] = useState({current: data.deadLetterPlays, total: data.deadLetterPlaysTotal});
+
+ const client = useSSEContext();
+ useSSEAnyEvent(client, (payload) => {
+ if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === data.id) {
+ switch (payload.type) {
+ case 'deadLetter':
+ setStatsData({current: statsData.current + 1, total: statsData.total + 1});
+ break;
+ case 'deadLetterRemoved':
+ setStatsData({current: statsData.current - 1, total: statsData.total - 1});
+ break;
+ case 'deadLetterDequeued':
+ setStatsData({current: statsData.current - 1, total: statsData.total});
+ break;
+ case 'deadQueued':
+ setStatsData({current: statsData.current + 1, total: statsData.total});
+ break;
+ }
+ }
+ });
- const [current, setCurrent] = useState(props.data.deadLetterPlays);
- const [total, setTotal] = useState(props.data.deadLetterPlaysTotal);
- const [recent, setRecent] = useState(recentProp);
- const [recentDirection, setRecentDirection] = useState<'up' | 'down'>('up');
- const resetRecent = useCallback(() => {
- setRecent(0);
+ return
+}
- }, [setRecent]);
- const recentTimeout = useTimeout(resetRecent, timeoutProp ?? 10000);
+export const DateIndicatorStreamable = (props: Omit, 'streamedDate'>) => {
+
+ const {
+ data: {
+ id,
+ lastActiveAt,
+ lastReadyAt,
+ state
+ } = {},
+ } = props;
+
+ const useActive = state < 5
+ const [current, setCurrent] = useState(useActive ? lastActiveAt : lastReadyAt);
- if (streamable) {
const client = useSSEContext();
useSSEAnyEvent(client, (payload) => {
- if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === props.data.id) {
- recentTimeout.stop();
- switch (payload.type) {
- case 'deadLetter':
- recentTimeout.stop();
- setCurrent(current + 1);
- setTotal(total + 1);
- if (recentDirection === 'down') {
- setRecent(1);
- } else {
- setRecent(recent + 1);
- }
- setRecentDirection('up');
- break;
- case 'deadLetterRemoved':
- setCurrent(current - 1);
- setTotal(total - 1);
- if (recentDirection === 'down') {
- setRecent(recent + 1);
- } else {
- setRecent(1);
- }
- setRecentDirection('down');
- break;
- case 'deadLetterDequeued':
- setCurrent(current - 1);
- if (recentDirection === 'down') {
- setRecent(recent + 1);
- } else {
- setRecent(1);
- }
- setRecentDirection('down');
- break;
- case 'deadQueued':
- setCurrent(current + 1);
- if (recentDirection === 'down') {
- setRecent(1);
- } else {
- setRecent(recent + 1);
- }
- setRecentDirection('up');
- break;
- }
- recentTimeout.start();
+ if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === id) {
+ // TODO update state from event
+ setCurrent(dayjs().toISOString());
}
});
- }
-
- if(as === 'stat') {
- return (
-
- Dead
-
- {current} ({total})
- {recent !== 0 ?
- {recentDirection === 'up' ? : }
- {recent}
- : null}
-
- Queuable and (Total)
-
- );
- }
- return (
- {current} ({total}) {recent !== 0 ?
- {recentDirection === 'up' ? : }
- {recent}
- : null} Dead
- );
+ return
}
export const DateIndicator = (props: {
data: Pick,
streamable?: boolean
as?: 'text' | 'stat'
+ streamedDate?: string
} & ComponentProps) => {
const {
data: {
- id,
lastActiveAt,
lastReadyAt,
state
@@ -282,17 +226,9 @@ export const DateIndicator = (props: {
} = props;
const useActive = state < 5;
-
- const [current, setCurrent] = useState(useActive ? lastActiveAt : lastReadyAt);
-
- if (props.streamable) {
- const client = useSSEContext();
- useSSEAnyEvent(client, (payload) => {
- if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === props.data.id) {
- // TODO update state from event
- setCurrent(dayjs().toISOString());
- }
- });
+ let usedDate: string = props.streamedDate;
+ if(usedDate === undefined) {
+ usedDate = useActive ? lastActiveAt : lastReadyAt;
}
if(as === 'stat') {
@@ -300,13 +236,13 @@ export const DateIndicator = (props: {
{useActive? 'Last Active At' : 'Last Ready At'}
- {shortTodayAwareFormat(dayjs(current))}
+ {shortTodayAwareFormat(dayjs(usedDate))}
);
}
return (
- {shortTodayAwareFormat(dayjs(current))} {useActive? 'Last Active At' : 'Last Ready At'}
+ {shortTodayAwareFormat(dayjs(usedDate))} {useActive? 'Last Active At' : 'Last Ready At'}
);
}
\ No newline at end of file
diff --git a/src/stories/component/ComponentStatDead.stories.tsx b/src/stories/component/ComponentStatDead.stories.tsx
index 22db8c30..bcb08c44 100644
--- a/src/stories/component/ComponentStatDead.stories.tsx
+++ b/src/stories/component/ComponentStatDead.stories.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { sse } from 'msw';
import { Container } from '@chakra-ui/react';
-import { DeadLetterIndicator } from "../../client/components/msComponent/Stats.js";
+import { DeadLetterIndicatorStreamable } from "../../client/components/msComponent/Stats.js";
import { sseProviderOptions } from "../../client/AppNext.js";
import {Provider} from "../../client/components/Provider.js";
import { SSEProvider } from "@flamefrontend/sse-runtime-react";
@@ -13,7 +13,7 @@ import { faker } from "@faker-js/faker";
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.meta({
title: 'Component/Details/Stat Dead',
- component: DeadLetterIndicator,
+ component: DeadLetterIndicatorStreamable,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'padded',
@@ -31,7 +31,7 @@ const meta = preview.meta({
// streamable: false,
// },
render: function Render(args) {
- return ()
+ return ()
},
decorators: [
(Story) => ( options={sseProviderOptions}>),
@@ -39,17 +39,15 @@ decorators: [
// 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
});
-const randomQueue = () => faker.helpers.arrayElement(['deadLetter']);
+const randomQueue = () => faker.helpers.arrayElement(['deadLetter','deadLetterRemoved','deadLetterDequeued','deadQueued']);
export const StatsQueued = meta.story({
args: {
data: {
- deadLetterScrobbles: 3,
- deadLetterScrobblesTotal: 5,
- mode: 'client',
+ deadLetterPlays: 3,
+ deadLetterPlaysTotal: 5,
id: 1,
- },
- streamable: true
+ }
},
parameters: {
msw: {
diff --git a/src/stories/component/ComponentStatLive.stories.tsx b/src/stories/component/ComponentStatLive.stories.tsx
index 38d94d29..ec95df94 100644
--- a/src/stories/component/ComponentStatLive.stories.tsx
+++ b/src/stories/component/ComponentStatLive.stories.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { sse } from 'msw';
import { Container } from '@chakra-ui/react';
-import { CountLiveIndicator } from "../../client/components/msComponent/Stats.js";
+import { CountIndicatorStreamable } from "../../client/components/msComponent/Stats.js";
import { sseProviderOptions } from "../../client/AppNext.js";
import {Provider} from "../../client/components/Provider.js";
import { SSEProvider } from "@flamefrontend/sse-runtime-react";
@@ -12,7 +12,7 @@ import type {MsSseEvent} from "../../core/Api.js";
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.meta({
title: 'Component/Details/Stats Count',
- component: CountLiveIndicator,
+ component: CountIndicatorStreamable,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'padded',
@@ -30,7 +30,7 @@ const meta = preview.meta({
// streamable: false,
// },
render: function Render(args) {
- return ()
+ return ()
},
decorators: [
(Story) => ( options={sseProviderOptions}>),
@@ -57,7 +57,6 @@ export const StatsCountLive = meta.story({
mode: 'source',
id: 1
},
- streamable: true
},
parameters: {
msw: {
@@ -79,7 +78,6 @@ export const StatsCountLiveReset = meta.story({
mode: 'source',
id: 1,
},
- streamable: true,
recentTimeout: 2000
},
parameters: {
diff --git a/src/stories/component/ComponentStatQueued.stories.tsx b/src/stories/component/ComponentStatQueued.stories.tsx
index e7d2570b..189c6e23 100644
--- a/src/stories/component/ComponentStatQueued.stories.tsx
+++ b/src/stories/component/ComponentStatQueued.stories.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { sse } from 'msw';
import { Container } from '@chakra-ui/react';
-import { QueuedIndicator } from "../../client/components/msComponent/Stats.js";
+import { QueuedIndicatorStreamable } from "../../client/components/msComponent/Stats.js";
import { sseProviderOptions } from "../../client/AppNext.js";
import {Provider} from "../../client/components/Provider.js";
import { SSEProvider } from "@flamefrontend/sse-runtime-react";
@@ -13,7 +13,7 @@ import { faker } from "@faker-js/faker";
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.meta({
title: 'Component/Details/Stat Queued',
- component: QueuedIndicator,
+ component: QueuedIndicatorStreamable,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'padded',
@@ -31,7 +31,7 @@ const meta = preview.meta({
// streamable: false,
// },
render: function Render(args) {
- return ()
+ return ()
},
decorators: [
(Story) => ( options={sseProviderOptions}>),
@@ -39,16 +39,14 @@ decorators: [
// 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
});
-const randomQueue = () => faker.helpers.arrayElement(['scrobbleQueued', 'scrobbleDequeued']);
+const randomQueue = () => faker.helpers.arrayElement(['playQueued', 'playDequeued']);
export const StatsQueued = meta.story({
args: {
data: {
queued: 3,
- mode: 'client',
id: 1,
},
- streamable: true
},
parameters: {
msw: {