feat(ui): Add placeholder primary actions from component state
Some checks are pending
Publish Docker image to Dockerhub / test (push) Waiting to run
Publish Docker image to Dockerhub / Build OCI Images (push) Blocked by required conditions
Publish Docker image to Dockerhub / Build OCI Images-1 (push) Blocked by required conditions
Publish Docker image to Dockerhub / Merge OCI Images and Push (push) Blocked by required conditions

This commit is contained in:
FoxxMD 2026-07-01 19:49:08 +00:00
parent 66fc7a2b09
commit 91ba24e165
3 changed files with 123 additions and 24 deletions

View file

@ -1,5 +1,5 @@
import { Badge } from "@chakra-ui/react";
import { ComponentProps, useState, useCallback, useEffect } from "react";
import { Badge, Separator, HStack } from "@chakra-ui/react";
import React, { ComponentProps, useState, useCallback, useEffect } from "react";
import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, MsSseEventPayload, PlayApiCommon } from "../../core/Api";
import { capitalize } from "../../core/StringUtils";
import { PlayerState } from "../../backend/common/infrastructure/config/source/mpd";
@ -60,12 +60,22 @@ export const NewBadge = (props: ComponentProps<typeof Badge> & { expires?: Secon
return null;
}
export const ComponentStateBadge = (props: ComponentProps<typeof Badge> & { data: Pick<ComponentCommonApiJson, 'state'>, componentId?: number, live?: boolean }) => {
export const ComponentStateBadge = (props: ComponentProps<typeof Badge> & {
data: Pick<ComponentCommonApiJson, 'state'>,
componentId?: number,
live?: boolean,
separator?: boolean | React.JSX.Element,
suffix?: React.JSX.Element
}) => {
const { data, ...rest } = props;
const { data, suffix, separator, ...rest } = props;
const [componentState, setComponentState] = useState(data.state);
useEffect(() =>{
setComponentState(data.state);
},[data.state, setComponentState]);
if(props.componentId !== undefined && props.live) {
const client = useSSEContext<MsSseEvent>();
const connection = useSSEEvent(client, 'componentUpdate', (payload) => {
@ -99,5 +109,14 @@ export const ComponentStateBadge = (props: ComponentProps<typeof Badge> & { data
break;
}
return <Badge variant="surface" colorPalette={badgeColor} {...rest}>{componentStateToFriendly(componentState)}</Badge>
let sep: React.JSX.Element | undefined;
if(suffix !== undefined) {
if(separator === true) {
sep = <Separator orientation="vertical" borderColor="var(--chakra-colors-color-palette-muted)" height="5"/>;
} else if(separator !== false) {
sep = separator;
}
}
return <Badge variant="surface" colorPalette={badgeColor} {...rest}><HStack>{componentStateToFriendly(componentState)}{sep}{suffix}</HStack></Badge>
}

View file

@ -1,4 +1,23 @@
import { LuChevronRight, LuChevronLeft, LuActivity, LuGithub, LuTerminal, LuAlignJustify, LuX, LuCheck, LuExternalLink, LuArrowUp, LuArrowDown, LuEllipsis, LuArrowBigRight, LuBug } from "react-icons/lu"
import {
LuChevronRight,
LuChevronLeft,
LuActivity,
LuGithub,
LuTerminal,
LuAlignJustify,
LuX,
LuCheck,
LuExternalLink,
LuArrowUp,
LuArrowDown,
LuEllipsis,
LuArrowBigRight,
LuBug,
LuPower,
LuPowerOff,
LuEye,
LuEyeClosed
} from "react-icons/lu"
import { VscDebugRestart } from 'react-icons/vsc';
import { RiZzzFill } from "react-icons/ri";
import { SiGoogledocs } from "react-icons/si";
@ -59,6 +78,9 @@ export const MenuButton = makeIconButton(MenuIcon);
export const XIcon = LuX;
export const XButton = makeIconButton(XIcon);
export const CheckIcon = LuCheck;
export const CheckButton = makeIconButton(CheckIcon);
export const ExternalLinkIcon = LuExternalLink;
export const ExternalLinkButton = makeIconButton(ExternalLinkIcon);
@ -101,4 +123,16 @@ export const DebugCopy = (props: {value: Clipboard.RootProps['value']} & Compone
}
export const RetryIcon = VscDebugRestart;
export const RetryButton = makeIconButton(RetryIcon);
export const RetryButton = makeIconButton(RetryIcon);
export const PowerIcon = LuPower;
export const PowerButton = makeIconButton(PowerIcon);
export const PowerOffIcon = LuPowerOff;
export const PowerOffButton = makeIconButton(PowerOffIcon);
export const EyeIcon = LuEye;
export const EyeButton = makeIconButton(EyeIcon);
export const EyeClosedIcon = LuEyeClosed;
export const EyeClosedButton = makeIconButton(LuEyeClosed);

View file

@ -1,11 +1,11 @@
import React, { ComponentProps, useMemo, forwardRef, Fragment, useEffect, useState, useCallback } from "react"
import { DataList, Badge, Grid, Spacer, Span, ButtonGroup, Button, GridItem, Text, Box, Heading, Skeleton, Wrap, Stat, Separator, HStack, Stack, Flex, Collapsible, Card, LinkOverlay, LinkBox, SkeletonText, IconButtonProps } from '@chakra-ui/react';
import { COMPONENT_STATE, ComponentClientApiJson, ComponentCommonApiJson, ComponentsApiJson, ComponentSourceApiJson, componentStateToFriendly, isComponentClientApiJson, isComponentSourceApiJson, MsSseEvent, MsSseEventPayload } from "../../../core/Api.js";
import React, { ComponentProps, useState } from "react"
import { SegmentGroup, Switch, Span, ButtonGroup, Button, Box, Heading, Skeleton, Wrap, HStack, Stack, Flex, Card, LinkOverlay, SkeletonText, BadgeProps, Badge } from '@chakra-ui/react';
import { COMPONENT_STATE, ComponentClientApiJson, ComponentCommonApiJson, ComponentsApiJson, ComponentSourceApiJson, ComponentState, componentStateToFriendly, isComponentClientApiJson, isComponentSourceApiJson, MsSseEvent, MsSseEventPayload } 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 { ChevronLeftButton, ChevronRightButton, IdleIcon } from "../icons/ChakraIcons.js";
import { CheckIcon, ChevronLeftButton, ChevronRightButton, EyeButton, IdleIcon, PowerButton, PowerOffButton, PowerOffIcon, XIcon } from "../icons/ChakraIcons.js";
import { ChakraPlayer, ChakraPlayerFetchable, PlayersContainer, PlayersContainerFetchable } from "../chakraPlayer/Player.js";
import { InfoTip, ToggleTip, Tooltip } from "../ToggleTip.js";
import { QueryFunctionContext, queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
@ -71,18 +71,69 @@ export const MSComponentStats = (props: { data?: ComponentCommonApiJson, live?:
)
}
const ComponentSettings = () => {
const stateIsStarted = (state: ComponentState): boolean => state <= COMPONENT_STATE.MUTED;
const ComponentSettings = (props: { data: ComponentCommonApiJson }) => {
const [startChecked, setStartChecked] = useState<boolean>(stateIsStarted(props.data.state));
return (
<Stack>
<ButtonGroup size="sm" variant="surface" attached>
<Button disabled colorPalette="green">Start</Button>
<Button disabled colorPalette="yellow">Mute</Button>
<Button disabled colorPalette="red">Stop</Button>
</ButtonGroup>
<Switch.Root
checked={startChecked}
onCheckedChange={(e) => setStartChecked(e.checked)}
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb>
<Switch.ThumbIndicator fallback={<XIcon color="black" />}>
<CheckIcon />
</Switch.ThumbIndicator>
</Switch.Thumb>
</Switch.Control>
<Switch.Label>Start</Switch.Label>
</Switch.Root>
</Stack>
)
}
const primaryActionProps: ComponentProps<typeof PowerOffButton> = {
margin: "1px",
variant: "subtle",
size: 'xs'
}
export const ComponentStateBadgeActionable = (props: Omit<ComponentProps<typeof ComponentStateBadge>, 'suffix'>) => {
const {
componentId,
live,
...rest
} = props;
let suffix: React.JSX.Element | null;
let primaryAction: React.JSX.Element | null;
let badgeProps: BadgeProps = {};
switch(props.data.state) {
case COMPONENT_STATE.RUNNING:
primaryAction = <PowerOffButton {...primaryActionProps}/>
break;
case COMPONENT_STATE.MUTED:
primaryAction = <EyeButton {...primaryActionProps}/>
break;
case COMPONENT_STATE.INITIALIZING:
// no actions while init is occurring
break;
default:
// otherwise generic start action for all non-running states
primaryAction = <PowerButton {...primaryActionProps}/>
}
if(primaryAction !== undefined) {
suffix = primaryAction;
}
if(suffix !== undefined || primaryAction !== undefined) {
//badgeProps.paddingRight = 0;
}
return <ComponentStateBadge size="lg" maxWidth="fit-content" {...badgeProps} separator suffix={suffix} {...rest}/>;
}
export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson, live?: boolean}) => {
let sleepingRender: React.JSX.Element = null;
const {data} = props;
@ -110,7 +161,7 @@ export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson,
<Flex direction="column" style={{whiteSpace: 'break-spaces'}} truncate rowGap="1">
<Flex width="100%" truncate>
<Box marginEnd="auto" truncate><MSComponentName data={props.data}/></Box>
<ComponentStateBadge size="lg" maxWidth="fit-content" data={props.data} />
<ComponentStateBadgeActionable size="lg" maxWidth="fit-content" data={props.data} />
</Flex>
<Wrap>
<Box marginEnd="auto">
@ -119,12 +170,7 @@ export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson,
<HStack truncate>{sleepingRender}{props.data.status}</HStack>
</Wrap>
<Flex justifyContent="flex-end" rowGap="6" flexDirection="row-reverse" wrap="wrap">
<Card.Root bgColor="bg.subtle" size="sm">
<Card.Header>Actions</Card.Header>
<Card.Body>
<ComponentSettings/>
</Card.Body>
</Card.Root>
<ComponentSettings data={props.data}/>
<Box marginEnd="auto"><MSComponentStats {...props}/></Box>
</Flex>
{props.live ? <PlayersContainerFetchable data={props.data}/> : <PlayersContainer data={props.data} live={props.live}/>}