feat(ui): Implement component summary list
Some checks failed
Publish Docker image to Dockerhub / test (push) Has been cancelled
Publish Docker image to Dockerhub / Build OCI Images (push) Has been cancelled
Publish Docker image to Dockerhub / Build OCI Images-1 (push) Has been cancelled
Publish Docker image to Dockerhub / Merge OCI Images and Push (push) Has been cancelled

This commit is contained in:
FoxxMD 2026-06-12 16:03:07 +00:00
parent d4019e6dea
commit 892d035d7a
6 changed files with 194 additions and 11 deletions

View file

@ -0,0 +1,82 @@
import {
Popover as ChakraPopover,
IconButton,
type IconButtonProps,
Portal,
} from "@chakra-ui/react"
import * as React from "react"
import { HiOutlineInformationCircle } from "react-icons/hi"
export interface ToggleTipProps extends ChakraPopover.RootProps {
showArrow?: boolean
portalled?: boolean
portalRef?: React.RefObject<HTMLElement | null>
content?: React.ReactNode
contentProps?: ChakraPopover.ContentProps
}
export const ToggleTip = React.forwardRef<HTMLDivElement, ToggleTipProps>(
function ToggleTip(props, ref) {
const {
showArrow,
children,
portalled = true,
content,
contentProps,
portalRef,
...rest
} = props
return (
<ChakraPopover.Root
{...rest}
positioning={{ ...rest.positioning, gutter: 4 }}
>
<ChakraPopover.Trigger asChild>{children}</ChakraPopover.Trigger>
<Portal disabled={!portalled} container={portalRef}>
<ChakraPopover.Positioner>
<ChakraPopover.Content
width="auto"
px="2"
py="1"
textStyle="xs"
rounded="sm"
ref={ref}
{...contentProps}
>
{showArrow && (
<ChakraPopover.Arrow>
<ChakraPopover.ArrowTip />
</ChakraPopover.Arrow>
)}
{content}
</ChakraPopover.Content>
</ChakraPopover.Positioner>
</Portal>
</ChakraPopover.Root>
)
},
)
export interface InfoTipProps extends Partial<ToggleTipProps> {
buttonProps?: IconButtonProps | undefined
}
export const InfoTip = React.forwardRef<HTMLDivElement, InfoTipProps>(
function InfoTip(props, ref) {
const { children, buttonProps, ...rest } = props
return (
<ToggleTip content={children} {...rest} ref={ref}>
<IconButton
variant="ghost"
aria-label="info"
size="2xs"
colorPalette="gray"
{...buttonProps}
>
<HiOutlineInformationCircle />
</IconButton>
</ToggleTip>
)
},
)

View file

@ -0,0 +1,34 @@
import React, { ComponentProps, useMemo, useState, forwardRef, Fragment } from "react"
import { CheckboxCard, Span, Stack, SegmentGroup, Text, Box, Center, Heading, Button, Separator, HStack, Flex, Badge } from '@chakra-ui/react';
import { ComponentsApiJson } from "../../../core/Api";
import { components } from "storybook/internal/components";
import { MSComponentSummary } from "./MSComponentSummary";
export interface ComponentListProps {
components: ComponentsApiJson[]
}
export const MSComponentList = (props: ComponentListProps) => {
const [shownType, setShownType] = useState("All");
return (
<Stack gap="3">
<Center>
<SegmentGroup.Root value={shownType} onValueChange={(val) => setShownType(val.value)}>
<SegmentGroup.Indicator />
<SegmentGroup.Items items={["All", "Sources", "Clients"]} />
</SegmentGroup.Root>
</Center>
<Stack>
{props.components.filter(x => {
if (shownType === 'All') {
return true;
}
if (shownType === 'Sources') {
return x.mode === 'source';
}
return x.mode === 'client';
}).map(x => <MSComponentSummary data={x} key={x.uid} />)}
</Stack>
</Stack>
)
}

View file

@ -1,12 +1,13 @@
import React, { ComponentProps, useMemo, forwardRef, Fragment } from "react"
import { Accordion, For, Span, Stack, Text, Box, Heading, AbsoluteCenter, Button, Separator, HStack, Flex, Badge, IconButton, Container, Collapsible, Card, LinkOverlay, LinkBox } from '@chakra-ui/react';
import { ComponentCommonApi, ComponentCommonApiJson, isComponentClientApiJson, isComponentSourceApiJson } from "../../../core/Api";
import { TextMuted } from "../TextMuted";
import { isClientType } from "../../../backend/common/infrastructure/Atomic";
import { capitalize } from "../../../core/StringUtils";
import { ShortDateDisplay } from "../DateDisplay";
import { ChevronRightButton } from "../icons/ChakraIcons";
import { ChakraPlayer } from "../chakraPlayer/Player";
import { ComponentCommonApi, ComponentCommonApiJson, isComponentClientApiJson, isComponentSourceApiJson } from "../../../core/Api.js";
import { TextMuted } from "../TextMuted.js";
import { isClientType } from "../../../backend/common/infrastructure/Atomic.js";
import { capitalize } from "../../../core/StringUtils.js";
import { ShortDateDisplay } from "../DateDisplay.js";
import { ChevronRightButton } from "../icons/ChakraIcons.js";
import { ChakraPlayer } from "../chakraPlayer/Player.js";
import { InfoTip } from "../ToggleTip.js";
export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => {
const {
@ -26,7 +27,7 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => {
body = (<Card.Body px="3" py="2" paddingTop="3">
<Stack gap="2">
{
Object.values(players).map((x) => <Container maxW="lg" bg="bg.emphasized" borderWidth="1px" p="2" py="3" rounded="md"><ChakraPlayer data={x}/></Container>)
Object.values(players).map((x) => <Container bg="bg.emphasized" borderWidth="1px" p="2" py="3" rounded="md"><ChakraPlayer data={x}/></Container>)
}
</Stack>
</Card.Body>);
@ -50,7 +51,7 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson }) => {
</Stack>
</Flex>
</LinkBox>
<TextMuted textStyle="md">{isClient ? `(${data.mode}) ` : ''}{capitalize(data.type)}</TextMuted>
<TextMuted textStyle="md"><Badge colorPalette={data.mode === 'client' ? 'purple' : 'pink'} size="sm" variant="subtle">{capitalize(data.mode)}</Badge> {capitalize(data.type)}</TextMuted>
<QuickStatsSource data={data} />
</Card.Header>
{body}
@ -83,10 +84,12 @@ const QuickStatsSource = (props: { data: ComponentCommonApiJson }) => {
<HStack gap="2">
<TextMuted textStyle="sm">{queued} Queued</TextMuted>
<Separator orientation="vertical" height="4" />
<TextMuted textStyle="sm">{deadLetterScrobbles} ({deadLetterScrobblesTotal}) Dead (Total) </TextMuted>
<TextMuted textStyle="sm">{deadLetterScrobbles} ({deadLetterScrobblesTotal}) Dead<InfoTip content="Dead scrobbles that can be automatically retried and (all) dead scrobbles, including those that have hit the retry limit."/></TextMuted>
<HStack gap="2" hideBelow="sm">
<Separator orientation="vertical" height="4" />
<TextMuted textStyle="sm">{countLive} Scrobbled</TextMuted>
</HStack>
</HStack>
</Fragment>
)
}

View file

@ -81,6 +81,8 @@ export type ComponentSourceApiBase = {
export type ComponentSourceApi = ComponentCommonApi & ComponentSourceApiBase;
export type ComponentSourceApiJson = Replace<ComponentSourceApi, PickKeys<ComponentSourceApi, Dayjs>, string>;
export type ComponentsApiJson = ComponentSourceApiJson | ComponentClientApiJson;
export const isComponentSourceApiJson = (data: ComponentCommonApiJson): data is ComponentSourceApiJson => {
return data.mode === 'source';
}

View file

@ -0,0 +1,62 @@
import preview from "../../.storybook/preview.js";
import React from 'react';
import { Container } from '@chakra-ui/react';
import { MSComponentList } from "../client/components/msComponent/MSComponentList.js";
import {Provider} from "../client/components/Provider";
import { generateClientApiJson, generateSourceApiJson, generateSourcePlayerJson } from "../core/tests/utils/apiFixtures.js";
import { generateArray } from "../core/DataUtils.js";
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: 'Examples/ComponentList',
component: MSComponentList,
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'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
// args: {
// data: generatePlayApiCommonDetailed(),
// },
// argTypes: {
// componentType: {
// control: { type: 'select' },
// options: ['source', 'client'],
// }
// },
render: function Render(args) {
return (<MSComponentList {...args} />)
},
decorators: [
(Story) => (<Provider><Container maxWidth="4xl"><Story/></Container></Provider>),
]
// 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 Simple = meta.story({
args: {
components: [generateSourceApiJson(), generateClientApiJson()]
}
});
export const Many = meta.story({
args: {
components: [...generateArray(7, () => generateSourceApiJson()),...generateArray(4, () => generateClientApiJson())]
}
});
export const WithSourcePlayers = meta.story({
args: {
components: [...generateArray(3, () => {
if(faker.datatype.boolean()) {
return generateSourceApiJson({players: {test: generateSourcePlayerJson(undefined, {art: true})}});
}
return generateSourceApiJson();
}),...generateArray(3, () => generateClientApiJson())]
}
});

View file

@ -2,7 +2,7 @@ import preview from "../../.storybook/preview.js";
import React from 'react';
import { Container } from '@chakra-ui/react';
import { MSComponentSummary } from "../client/components/msComponent/MSComponentQuickDisplay";
import { MSComponentSummary } from "../client/components/msComponent/MSComponentSummary.js";
import {Provider} from "../client/components/Provider";
import { generateClientApiJson, generateSourceApiJson, generateSourcePlayerJson } from "../core/tests/utils/apiFixtures.js";