fix(media-transcoding): improve responsive styles (#2550)
* fix(media-transcoding): improve responsive styles * fix: typecheck issue
This commit is contained in:
@@ -1890,10 +1890,10 @@
|
|||||||
"statistics": {
|
"statistics": {
|
||||||
"empty": "Empty",
|
"empty": "Empty",
|
||||||
"transcodes": "Transcodes",
|
"transcodes": "Transcodes",
|
||||||
"transcodesCount": "Transcodes: {value}",
|
"transcodesCount": "Transcodes",
|
||||||
"healthChecksCount": "Health checks: {value}",
|
"healthChecksCount": "Health checks",
|
||||||
"filesCount": "Files: {value}",
|
"filesCount": "Files",
|
||||||
"savedSpace": "Saved space: {value}",
|
"savedSpace": "Saved space",
|
||||||
"healthChecks": "Health checks",
|
"healthChecks": "Health checks",
|
||||||
"videoCodecs": "Codecs",
|
"videoCodecs": "Codecs",
|
||||||
"videoContainers": "Containers",
|
"videoContainers": "Containers",
|
||||||
|
|||||||
@@ -2,20 +2,32 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Center, Divider, Group, Pagination, SegmentedControl, Stack, Text } from "@mantine/core";
|
import { Center, Divider, Group, Pagination, SegmentedControl, Stack, Text } from "@mantine/core";
|
||||||
|
import type { TablerIcon } from "@tabler/icons-react";
|
||||||
import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react";
|
import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import { views } from ".";
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
import { HealthCheckStatus } from "./health-check-status";
|
import { HealthCheckStatus } from "./health-check-status";
|
||||||
import { QueuePanel } from "./panels/queue.panel";
|
import { QueuePanel } from "./panels/queue.panel";
|
||||||
import { StatisticsPanel } from "./panels/statistics.panel";
|
import { StatisticsPanel } from "./panels/statistics.panel";
|
||||||
import { WorkersPanel } from "./panels/workers.panel";
|
import { WorkersPanel } from "./panels/workers.panel";
|
||||||
|
|
||||||
type Views = "workers" | "queue" | "statistics";
|
type View = (typeof views)[number];
|
||||||
|
|
||||||
export default function MediaTranscodingWidget({ integrationIds, options }: WidgetComponentProps<"mediaTranscoding">) {
|
const viewIcons = {
|
||||||
|
workers: IconCpu2,
|
||||||
|
queue: IconClipboardList,
|
||||||
|
statistics: IconReportAnalytics,
|
||||||
|
} satisfies Record<View, TablerIcon>;
|
||||||
|
|
||||||
|
export default function MediaTranscodingWidget({
|
||||||
|
integrationIds,
|
||||||
|
options,
|
||||||
|
width,
|
||||||
|
}: WidgetComponentProps<"mediaTranscoding">) {
|
||||||
const [queuePage, setQueuePage] = useState(1);
|
const [queuePage, setQueuePage] = useState(1);
|
||||||
const queuePageSize = 10;
|
const queuePageSize = 10;
|
||||||
const [transcodingData] = clientApi.widget.mediaTranscoding.getDataAsync.useSuspenseQuery(
|
const [transcodingData] = clientApi.widget.mediaTranscoding.getDataAsync.useSuspenseQuery(
|
||||||
@@ -31,15 +43,16 @@ export default function MediaTranscodingWidget({ integrationIds, options }: Widg
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [view, setView] = useState<Views>(options.defaultView);
|
const [view, setView] = useState<View>(options.defaultView);
|
||||||
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
|
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
|
||||||
|
|
||||||
const t = useI18n("widget.mediaTranscoding");
|
const t = useI18n("widget.mediaTranscoding");
|
||||||
|
const isTiny = width < 256;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={4} h="100%">
|
<Stack gap={4} h="100%">
|
||||||
{view === "workers" ? (
|
{view === "workers" ? (
|
||||||
<WorkersPanel workers={transcodingData.data.workers} />
|
<WorkersPanel workers={transcodingData.data.workers} isTiny={isTiny} />
|
||||||
) : view === "queue" ? (
|
) : view === "queue" ? (
|
||||||
<QueuePanel queue={transcodingData.data.queue} />
|
<QueuePanel queue={transcodingData.data.queue} />
|
||||||
) : (
|
) : (
|
||||||
@@ -48,65 +61,48 @@ export default function MediaTranscodingWidget({ integrationIds, options }: Widg
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Group gap="xs" mb={4} ms={4} me={8}>
|
<Group gap="xs" mb={4} ms={4} me={8}>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={[
|
data={views.map((value) => {
|
||||||
{
|
const Icon = viewIcons[value];
|
||||||
|
return {
|
||||||
label: (
|
label: (
|
||||||
<Center>
|
<Center style={{ gap: 4 }}>
|
||||||
<IconCpu2 size={18} />
|
<Icon size={12} />
|
||||||
<Text size="xs" ml={8}>
|
{!isTiny && (
|
||||||
{t("tab.workers")}
|
<Text span size="xs">
|
||||||
</Text>
|
{t(`tab.${value}`)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Center>
|
</Center>
|
||||||
),
|
),
|
||||||
value: "workers",
|
value,
|
||||||
},
|
};
|
||||||
{
|
})}
|
||||||
label: (
|
|
||||||
<Center>
|
|
||||||
<IconClipboardList size={18} />
|
|
||||||
<Text size="xs" ml={8}>
|
|
||||||
{t("tab.queue")}
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
),
|
|
||||||
value: "queue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: (
|
|
||||||
<Center>
|
|
||||||
<IconReportAnalytics size={18} />
|
|
||||||
<Text size="xs" ml={8}>
|
|
||||||
{t("tab.statistics")}
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
),
|
|
||||||
value: "statistics",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={view}
|
value={view}
|
||||||
onChange={(value) => setView(value as Views)}
|
onChange={(value) => setView(value as View)}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
{view === "queue" && (
|
|
||||||
<>
|
|
||||||
<Pagination.Root total={totalQueuePages} value={queuePage} onChange={setQueuePage} size="sm">
|
|
||||||
<Group gap={5} justify="center">
|
|
||||||
<Pagination.First disabled={transcodingData.data.queue.startIndex === 1} />
|
|
||||||
<Pagination.Previous disabled={transcodingData.data.queue.startIndex === 1} />
|
|
||||||
<Pagination.Next disabled={transcodingData.data.queue.startIndex === totalQueuePages} />
|
|
||||||
<Pagination.Last disabled={transcodingData.data.queue.startIndex === totalQueuePages} />
|
|
||||||
</Group>
|
|
||||||
</Pagination.Root>
|
|
||||||
<Text size="xs">
|
|
||||||
{t("currentIndex", {
|
|
||||||
start: transcodingData.data.queue.startIndex + 1,
|
|
||||||
end: transcodingData.data.queue.endIndex + 1,
|
|
||||||
total: transcodingData.data.queue.totalCount,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Group gap="xs" ml="auto">
|
<Group gap="xs" ml="auto">
|
||||||
|
{view === "queue" && (
|
||||||
|
<>
|
||||||
|
<Pagination.Root total={totalQueuePages} value={queuePage} onChange={setQueuePage} size="xs">
|
||||||
|
<Group gap={2} justify="center">
|
||||||
|
{!isTiny && <Pagination.First disabled={transcodingData.data.queue.startIndex === 1} />}
|
||||||
|
<Pagination.Previous disabled={transcodingData.data.queue.startIndex === 1} />
|
||||||
|
<Pagination.Next disabled={transcodingData.data.queue.startIndex === totalQueuePages} />
|
||||||
|
{!isTiny && <Pagination.Last disabled={transcodingData.data.queue.startIndex === totalQueuePages} />}
|
||||||
|
</Group>
|
||||||
|
</Pagination.Root>
|
||||||
|
<Text size="xs">
|
||||||
|
{t("currentIndex", {
|
||||||
|
start: transcodingData.data.queue.startIndex + 1,
|
||||||
|
end: transcodingData.data.queue.endIndex + 1,
|
||||||
|
total: transcodingData.data.queue.totalCount,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<HealthCheckStatus statistics={transcodingData.data.statistics} />
|
<HealthCheckStatus statistics={transcodingData.data.statistics} />
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
|
|||||||
return (
|
return (
|
||||||
<HoverCard position="bottom" width={250} shadow="sm">
|
<HoverCard position="bottom" width={250} shadow="sm">
|
||||||
<HoverCard.Target>
|
<HoverCard.Target>
|
||||||
<Indicator color={textColor(indicatorColor, colorScheme)} size={8} display="flex">
|
<Indicator color={textColor(indicatorColor, colorScheme)} size={6} display="flex">
|
||||||
<IconHeartbeat size={20} />
|
<IconHeartbeat size={16} />
|
||||||
</Indicator>
|
</Indicator>
|
||||||
</HoverCard.Target>
|
</HoverCard.Target>
|
||||||
<HoverCard.Dropdown bg={colorScheme === "light" ? "gray.2" : "dark.8"}>
|
<HoverCard.Dropdown bg={colorScheme === "light" ? "gray.2" : "dark.8"}>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { IconTransform } from "@tabler/icons-react";
|
import { IconTransform } from "@tabler/icons-react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { capitalize } from "@homarr/common";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
import { optionsBuilder } from "../options";
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
|
export const views = ["workers", "queue", "statistics"] as const;
|
||||||
|
|
||||||
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
|
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
|
||||||
icon: IconTransform,
|
icon: IconTransform,
|
||||||
createOptions() {
|
createOptions() {
|
||||||
return optionsBuilder.from((factory) => ({
|
return optionsBuilder.from((factory) => ({
|
||||||
defaultView: factory.select({
|
defaultView: factory.select({
|
||||||
defaultValue: "statistics",
|
defaultValue: "statistics",
|
||||||
options: [
|
options: views.map((view) => ({ label: capitalize(view), value: view })),
|
||||||
{ label: "Workers", value: "workers" },
|
|
||||||
{ label: "Queue", value: "queue" },
|
|
||||||
{ label: "Statistics", value: "statistics" },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Center, Group, ScrollArea, Table, Text, Title, Tooltip } from "@mantine/core";
|
import { Center, Group, ScrollArea, Table, TableTd, TableTh, TableTr, Text, Title, Tooltip } from "@mantine/core";
|
||||||
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { humanFileSize } from "@homarr/common";
|
import { humanFileSize } from "@homarr/common";
|
||||||
@@ -17,7 +17,7 @@ export function QueuePanel(props: QueuePanelProps) {
|
|||||||
if (queue.array.length === 0) {
|
if (queue.array.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ flex: "1" }}>
|
<Center style={{ flex: "1" }}>
|
||||||
<Title order={3}>{t("empty")}</Title>
|
<Title order={6}>{t("empty")}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -26,36 +26,42 @@ export function QueuePanel(props: QueuePanelProps) {
|
|||||||
<ScrollArea style={{ flex: "1" }}>
|
<ScrollArea style={{ flex: "1" }}>
|
||||||
<Table style={{ tableLayout: "fixed" }}>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<tr>
|
<TableTr>
|
||||||
<th>{t("table.file")}</th>
|
<TableTh ta="start" py={4}>
|
||||||
<th style={{ width: 80 }}>{t("table.size")}</th>
|
<Text size="xs" fw="bold">
|
||||||
</tr>
|
{t("table.file")}
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh ta="start" py={4}>
|
||||||
|
<Text size="xs" fw="bold">
|
||||||
|
{t("table.size")}
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
</TableTr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{queue.array.map((item) => (
|
{queue.array.map((item) => (
|
||||||
<tr key={item.id}>
|
<TableTr key={item.id}>
|
||||||
<td>
|
<TableTd py={2}>
|
||||||
<Group gap="xs" wrap="nowrap">
|
<Group gap={4} wrap="nowrap">
|
||||||
<div>
|
{item.type === "transcode" ? (
|
||||||
{item.type === "transcode" ? (
|
<Tooltip label={t("table.transcode")}>
|
||||||
<Tooltip label={t("table.transcode")}>
|
<IconTransform size={12} />
|
||||||
<IconTransform size={14} />
|
</Tooltip>
|
||||||
</Tooltip>
|
) : (
|
||||||
) : (
|
<Tooltip label={t("table.healthCheck")}>
|
||||||
<Tooltip label={t("table.healthCheck")}>
|
<IconHeartbeat size={12} />
|
||||||
<IconHeartbeat size={14} />
|
</Tooltip>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Text lineClamp={1} size="xs">
|
<Text lineClamp={1} size="xs">
|
||||||
{item.filePath.split("\\").pop()?.split("/").pop() ?? item.filePath}
|
{item.filePath.split("\\").pop()?.split("/").pop() ?? item.filePath}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</td>
|
</TableTd>
|
||||||
<td>
|
<TableTd py={2}>
|
||||||
<Text size="xs">{humanFileSize(item.fileSize)}</Text>
|
<Text size="xs">{humanFileSize(item.fileSize)}</Text>
|
||||||
</td>
|
</TableTd>
|
||||||
</tr>
|
</TableTr>
|
||||||
))}
|
))}
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type react from "react";
|
|
||||||
import type { MantineColor, RingProgressProps } from "@mantine/core";
|
import type { MantineColor, RingProgressProps } from "@mantine/core";
|
||||||
import { Box, Center, Grid, Group, RingProgress, Stack, Text, Title, useMantineColorScheme } from "@mantine/core";
|
import { Card, Center, Group, RingProgress, ScrollArea, Stack, Text, Title, Tooltip } from "@mantine/core";
|
||||||
import { IconDatabaseHeart, IconFileDescription, IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
import { IconDatabaseHeart, IconFileDescription, IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import { humanFileSize } from "@homarr/common";
|
import { humanFileSize } from "@homarr/common";
|
||||||
import type { TdarrPieSegment, TdarrStatistics } from "@homarr/integrations";
|
import type { TdarrPieSegment, TdarrStatistics } from "@homarr/integrations";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import type { TablerIcon } from "@homarr/ui";
|
||||||
|
|
||||||
const PIE_COLORS: MantineColor[] = ["cyan", "grape", "gray", "orange", "pink"];
|
const PIE_COLORS: MantineColor[] = ["cyan", "grape", "gray", "orange", "pink"];
|
||||||
|
|
||||||
@@ -21,90 +22,54 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
|
|||||||
if (!allLibs) {
|
if (!allLibs) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ flex: "1" }}>
|
<Center style={{ flex: "1" }}>
|
||||||
<Title order={3}>{t("empty")}</Title>
|
<Title order={6}>{t("empty")}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ flex: "1" }} gap="xs">
|
<ScrollArea h="100%">
|
||||||
<Group
|
<Group wrap="wrap" justify="center" p={4} w="100%" gap="xs">
|
||||||
style={{
|
<StatisticItem icon={IconTransform} label={t("transcodesCount")} value={props.statistics.totalTranscodeCount} />
|
||||||
flex: 1,
|
<StatisticItem
|
||||||
}}
|
icon={IconHeartbeat}
|
||||||
justify="apart"
|
label={t("healthChecksCount")}
|
||||||
align="center"
|
value={props.statistics.totalHealthCheckCount}
|
||||||
wrap="nowrap"
|
/>
|
||||||
>
|
<StatisticItem icon={IconFileDescription} label={t("filesCount")} value={props.statistics.totalFileCount} />
|
||||||
<Stack align="center" gap={0}>
|
<StatisticItem
|
||||||
<RingProgress size={120} sections={toRingProgressSections(allLibs.transcodeStatus)} />
|
icon={IconDatabaseHeart}
|
||||||
<Text size="xs">{t("transcodes")}</Text>
|
label={t("savedSpace")}
|
||||||
</Stack>
|
value={humanFileSize(Math.floor(allLibs.savedSpace))}
|
||||||
<Grid gutter="xs">
|
/>
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StatBox
|
|
||||||
icon={<IconTransform size={18} />}
|
|
||||||
label={t("transcodesCount", {
|
|
||||||
value: props.statistics.totalTranscodeCount,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StatBox
|
|
||||||
icon={<IconHeartbeat size={18} />}
|
|
||||||
label={t("healthChecksCount", {
|
|
||||||
value: props.statistics.totalHealthCheckCount,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StatBox
|
|
||||||
icon={<IconFileDescription size={18} />}
|
|
||||||
label={t("filesCount", {
|
|
||||||
value: props.statistics.totalFileCount,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<StatBox
|
|
||||||
icon={<IconDatabaseHeart size={18} />}
|
|
||||||
label={t("savedSpace", {
|
|
||||||
value: humanFileSize(Math.floor(allLibs.savedSpace)),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
<Stack align="center" gap={0}>
|
|
||||||
<RingProgress size={120} sections={toRingProgressSections(allLibs.healthCheckStatus)} />
|
|
||||||
<Text size="xs">{t("healthChecks")}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Group
|
<Group justify="center" wrap="wrap" grow>
|
||||||
style={{
|
<StatisticRingProgress items={allLibs.transcodeStatus} label={t("transcodes")} />
|
||||||
flex: 1,
|
<StatisticRingProgress items={allLibs.healthCheckStatus} label={t("healthChecks")} />
|
||||||
}}
|
<StatisticRingProgress items={allLibs.videoCodecs} label={t("videoCodecs")} />
|
||||||
justify="space-between"
|
<StatisticRingProgress items={allLibs.videoContainers} label={t("videoContainers")} />
|
||||||
align="center"
|
<StatisticRingProgress items={allLibs.videoResolutions} label={t("videoResolutions")} />
|
||||||
wrap="nowrap"
|
|
||||||
w="100%"
|
|
||||||
>
|
|
||||||
<Stack align="center" gap={0}>
|
|
||||||
<RingProgress size={120} sections={toRingProgressSections(allLibs.videoCodecs)} />
|
|
||||||
<Text size="xs">{t("videoCodecs")}</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack align="center" gap={0}>
|
|
||||||
<RingProgress size={120} sections={toRingProgressSections(allLibs.videoContainers)} />
|
|
||||||
<Text size="xs">{t("videoContainers")}</Text>
|
|
||||||
</Stack>
|
|
||||||
<Stack align="center" gap={0}>
|
|
||||||
<RingProgress size={120} sections={toRingProgressSections(allLibs.videoResolutions)} />
|
|
||||||
<Text size="xs">{t("videoResolutions")}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface StatisticRingProgressProps {
|
||||||
|
items: TdarrPieSegment[];
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatisticRingProgress = ({ items, label }: StatisticRingProgressProps) => {
|
||||||
|
return (
|
||||||
|
<Stack align="center" gap={0} miw={60}>
|
||||||
|
<Text size="10px" ta="center" style={{ whiteSpace: "nowrap" }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<RingProgress size={60} thickness={6} sections={toRingProgressSections(items)} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps["sections"] {
|
function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps["sections"] {
|
||||||
const total = segments.reduce((prev, curr) => prev + curr.value, 0);
|
const total = segments.reduce((prev, curr) => prev + curr.value, 0);
|
||||||
return segments.map((segment, index) => ({
|
return segments.map((segment, index) => ({
|
||||||
@@ -115,26 +80,22 @@ function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps[
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatBoxProps {
|
interface StatisticItemProps {
|
||||||
icon: react.ReactNode;
|
icon: TablerIcon;
|
||||||
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatBox(props: StatBoxProps) {
|
function StatisticItem(props: StatisticItemProps) {
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const board = useRequiredBoard();
|
||||||
return (
|
return (
|
||||||
<Box
|
<Tooltip label={props.label}>
|
||||||
style={(theme) => ({
|
<Card p={0} withBorder radius={board.itemRadius} miw={48} flex={1}>
|
||||||
padding: theme.spacing.xs,
|
<Group justify="center" align="center" gap="xs" w="100%" wrap="nowrap">
|
||||||
border: "1px solid",
|
<props.icon size={16} style={{ minWidth: 16 }} />
|
||||||
borderRadius: theme.radius.md,
|
<Text size="md">{props.value}</Text>
|
||||||
borderColor: colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1],
|
</Group>
|
||||||
})}
|
</Card>
|
||||||
>
|
</Tooltip>
|
||||||
<Stack gap="xs" align="center">
|
|
||||||
{props.icon}
|
|
||||||
<Text size="xs">{props.label}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { Center, Group, Progress, ScrollArea, Table, Text, Title, Tooltip } from "@mantine/core";
|
import {
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Progress,
|
||||||
|
ScrollArea,
|
||||||
|
Table,
|
||||||
|
TableTd,
|
||||||
|
TableTh,
|
||||||
|
TableTr,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mantine/core";
|
||||||
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
||||||
|
|
||||||
import type { TdarrWorker } from "@homarr/integrations";
|
import type { TdarrWorker } from "@homarr/integrations";
|
||||||
@@ -6,6 +18,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
interface WorkersPanelProps {
|
interface WorkersPanelProps {
|
||||||
workers: TdarrWorker[];
|
workers: TdarrWorker[];
|
||||||
|
isTiny: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkersPanel(props: WorkersPanelProps) {
|
export function WorkersPanel(props: WorkersPanelProps) {
|
||||||
@@ -14,7 +27,7 @@ export function WorkersPanel(props: WorkersPanelProps) {
|
|||||||
if (props.workers.length === 0) {
|
if (props.workers.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ flex: "1" }}>
|
<Center style={{ flex: "1" }}>
|
||||||
<Title order={3}>{t("empty")}</Title>
|
<Title order={6}>{t("empty")}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -23,52 +36,71 @@ export function WorkersPanel(props: WorkersPanelProps) {
|
|||||||
<ScrollArea style={{ flex: "1" }}>
|
<ScrollArea style={{ flex: "1" }}>
|
||||||
<Table style={{ tableLayout: "fixed" }}>
|
<Table style={{ tableLayout: "fixed" }}>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<tr>
|
<TableTr>
|
||||||
<th>{t("table.file")}</th>
|
<TableTh ta="start" py={4}>
|
||||||
<th style={{ width: 60 }}>{t("table.eta")}</th>
|
<Text size="xs" fw="bold">
|
||||||
<th style={{ width: 175 }}>{t("table.progress")}</th>
|
{t("table.file")}
|
||||||
</tr>
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh ta="start" py={4} w={50}>
|
||||||
|
<Text size="xs" fw="bold">
|
||||||
|
{t("table.eta")}
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
<TableTh ta="start" py={4}>
|
||||||
|
<Text size="xs" fw="bold">
|
||||||
|
{t("table.progress")}
|
||||||
|
</Text>
|
||||||
|
</TableTh>
|
||||||
|
</TableTr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{props.workers.map((worker) => (
|
{props.workers.map((worker) => {
|
||||||
<tr key={worker.id}>
|
const fileName = worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath;
|
||||||
<td>
|
return (
|
||||||
<Group gap="xs" wrap="nowrap">
|
<TableTr key={worker.id}>
|
||||||
<div>
|
<TableTd py={2}>
|
||||||
{worker.jobType === "transcode" ? (
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Tooltip label={t("table.transcode")}>
|
<div>
|
||||||
<IconTransform size={14} />
|
{worker.jobType === "transcode" ? (
|
||||||
</Tooltip>
|
<Tooltip label={t("table.transcode")}>
|
||||||
) : (
|
<IconTransform size={14} />
|
||||||
<Tooltip label={t("table.healthCheck")}>
|
</Tooltip>
|
||||||
<IconHeartbeat size={14} />
|
) : (
|
||||||
</Tooltip>
|
<Tooltip label={t("table.healthCheck")}>
|
||||||
|
<IconHeartbeat size={14} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Text lineClamp={1} size="xs" title={fileName}>
|
||||||
|
{fileName}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd py={2}>
|
||||||
|
<Text size="xs">{worker.ETA.startsWith("0:") ? worker.ETA.substring(2) : worker.ETA}</Text>
|
||||||
|
</TableTd>
|
||||||
|
<TableTd py={2}>
|
||||||
|
<Group wrap="nowrap" gap="xs">
|
||||||
|
{!props.isTiny && (
|
||||||
|
<>
|
||||||
|
<Text size="xs">{worker.step}</Text>
|
||||||
|
<Progress
|
||||||
|
value={worker.percentage}
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
<Text size="xs">{Math.round(worker.percentage)}%</Text>
|
||||||
<Text lineClamp={1} size="xs">
|
</Group>
|
||||||
{worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath}
|
</TableTd>
|
||||||
</Text>
|
</TableTr>
|
||||||
</Group>
|
);
|
||||||
</td>
|
})}
|
||||||
<td>
|
|
||||||
<Text size="xs">{worker.ETA.startsWith("0:") ? worker.ETA.substring(2) : worker.ETA}</Text>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Group wrap="nowrap" gap="xs">
|
|
||||||
<Text size="xs">{worker.step}</Text>
|
|
||||||
<Progress
|
|
||||||
value={worker.percentage}
|
|
||||||
size="lg"
|
|
||||||
radius="xl"
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text size="xs">{Math.round(worker.percentage)}%</Text>
|
|
||||||
</Group>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</Table.Tbody>
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
Reference in New Issue
Block a user