feat!: release v2 (#6545)

Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
This commit is contained in:
3286 files changed
+284386 -139176

No files matched your search

@@ -1,20 +1,25 @@
"use client";
import { useState } from "react";
import { Center, Divider, Group, Pagination, SegmentedControl, Stack, Text } from "@mantine/core";
import { useEffect, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { getQueryKey } from "@trpc/react-query";
import { Button, Divider, Group, Pagination, Paper, ScrollArea, Select, SimpleGrid, Stack, Text } from "@mantine/core";
import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { views } from ".";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state";
import { WidgetQueryLoadingState } from "../common/query-state-indicator";
import { useWidgetRuntimeQueries } from "../runtime-hooks";
import { HealthCheckStatus } from "./health-check-status";
import { QueuePanel } from "./panels/queue.panel";
import { StatisticsPanel } from "./panels/statistics.panel";
import { WorkersPanel } from "./panels/workers.panel";
import { views } from "./views";
type View = (typeof views)[number];
@@ -26,30 +31,112 @@ const viewIcons = {
statistics: IconReportAnalytics,
} satisfies Record<View, TablerIcon>;
type TranscodingControlMode = "buttons" | "select";
const narrowWidthLayout = { minimumWidth: 0, advancedColumns: 1, controlMode: "select" } as const;
const widthBreakpoints = [
{ minimumWidth: 1100, advancedColumns: 3, controlMode: "buttons" },
{ minimumWidth: 700, advancedColumns: 2, controlMode: "buttons" },
{ minimumWidth: 560, advancedColumns: 1, controlMode: "buttons" },
narrowWidthLayout,
] as const satisfies readonly {
minimumWidth: number;
advancedColumns: number;
controlMode: TranscodingControlMode;
}[];
const getTranscodingWidthLayout = (width: number) =>
widthBreakpoints.find((candidate) => width >= candidate.minimumWidth) ?? narrowWidthLayout;
export default function MediaTranscodingWidget({
integrationIds,
options,
width,
height,
displayMode,
widgetRuntimeRef,
}: WidgetComponentProps<"mediaTranscoding">) {
const [queuePage, setQueuePage] = useState(1);
const queuePageSize = 10;
const isAdvanced = displayMode === "advanced";
const queuePageSize = getQueuePageSize(height, isAdvanced);
const [queuePagination, setQueuePagination] = useState<QueuePaginationState>(() => ({
page: 1,
pageSize: queuePageSize,
isAdvanced,
}));
const requestPagination = resolveQueuePagination(queuePagination, queuePageSize, isAdvanced, null);
const input = {
integrationId: integrationIds[0] ?? "",
pageSize: queuePageSize,
page: queuePage,
page: requestPagination.page,
};
const { data: transcodingData } = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input);
useWidgetRuntimeQueries(widgetRuntimeRef, [
getQueryKey(clientApi.widget.mediaTranscoding.getDataAsync, input, "query"),
]);
const transcodingQuery = clientApi.widget.mediaTranscoding.getDataAsync.useQuery(input);
const transcodingData = getUsableWidgetQueryData(transcodingQuery);
const [view, setView] = useState<View>(options.defaultView);
const t = useI18n("widget.mediaTranscoding");
const totalQueuePages = transcodingData
? Math.max(1, Math.ceil(transcodingData.data.queue.totalCount / queuePageSize))
: null;
const resolvedPagination = resolveQueuePagination(queuePagination, queuePageSize, isAdvanced, totalQueuePages);
const queuePage = resolvedPagination.page;
useEffect(() => {
setQueuePagination((current) => {
const next = resolveQueuePagination(current, queuePageSize, isAdvanced, totalQueuePages);
return current.page === next.page && current.pageSize === next.pageSize && current.isAdvanced === next.isAdvanced
? current
: next;
});
}, [isAdvanced, queuePageSize, totalQueuePages]);
const handleQueuePageChange = (page: number) => {
setQueuePagination({ page, pageSize: queuePageSize, isAdvanced });
};
const widthLayout = useMemo(() => getTranscodingWidthLayout(width), [width]);
if (isInitialWidgetQueryPending(transcodingQuery)) return <WidgetQueryLoadingState />;
if (!transcodingData) return <WidgetEmptyState />;
const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize);
const isTiny = width < 256;
const queuePageCount = totalQueuePages ?? 1;
const isTiny = !isAdvanced && (width < 280 || height < 140);
const footerLayout = getTranscodingFooterLayout(width, height);
if (isAdvanced) {
return (
<Stack gap="xs" h="100%" p="xs" pos="relative">
<ScrollArea h="100%" style={{ flex: 1 }}>
<SimpleGrid cols={widthLayout.advancedColumns} spacing="sm">
<AdvancedPanel title={t("tab.workers")} icon={IconCpu2}>
<WorkersPanel workers={transcodingData.data.workers} isTiny={false} />
</AdvancedPanel>
<AdvancedPanel title={t("tab.queue")} icon={IconClipboardList}>
<QueuePanel queue={transcodingData.data.queue} />
</AdvancedPanel>
<AdvancedPanel title={t("tab.statistics")} icon={IconReportAnalytics}>
<StatisticsPanel statistics={transcodingData.data.statistics} />
</AdvancedPanel>
</SimpleGrid>
</ScrollArea>
<Group gap="xs" justify={queuePageCount > 1 ? "space-between" : "flex-end"} wrap="nowrap">
{queuePageCount > 1 && (
<QueuePaginationControls
total={queuePageCount}
value={queuePage}
onChange={handleQueuePageChange}
layout={footerLayout}
/>
)}
<HealthCheckStatus statistics={transcodingData.data.statistics} />
</Group>
</Stack>
);
}
return (
<Stack gap={4} h="100%">
<Stack gap={4} h="100%" pos="relative">
{view === "workers" ? (
<WorkersPanel workers={transcodingData.data.workers} isTiny={isTiny} />
) : view === "queue" ? (
@@ -58,52 +145,65 @@ export default function MediaTranscodingWidget({
<StatisticsPanel statistics={transcodingData.data.statistics} />
)}
<Divider />
<Group gap="xs" mb={4} ms={4} me={8}>
<SegmentedControl
data={views.map((value) => {
const Icon = viewIcons[value];
return {
label: (
<Center style={{ gap: 4 }}>
<Icon size={12} />
{!isTiny && (
<Text span size="xs">
{t(`tab.${value}`)}
</Text>
)}
</Center>
),
<Group gap="xs" mb={4} ms={4} me={8} wrap="nowrap">
{widthLayout.controlMode === "buttons" ? (
<Button.Group style={{ flex: 1, minWidth: 0 }}>
{views.map((value) => {
const Icon = viewIcons[value];
return (
<Button
key={value}
size="compact-xs"
variant={value === view ? "filled" : "default"}
leftSection={<Icon size="var(--mantine-font-size-xs)" />}
aria-pressed={value === view}
style={{ flex: 1 }}
onClick={() => setView(value)}
>
{t(`tab.${value}`)}
</Button>
);
})}
</Button.Group>
) : (
<Select
size="xs"
value={view}
data={views.map((value) => ({
value,
};
})}
value={view}
onChange={(value) => {
const nextView = viewBySegmentValue[value];
if (nextView) {
setView(nextView);
}
}}
size="xs"
/>
label: t(`tab.${value}`),
}))}
aria-label={t("option.defaultView.label")}
style={{ flex: 1, minWidth: 0 }}
onChange={(value) => {
if (!value) return;
<Group gap="xs" ml="auto">
{view === "queue" && (
const nextView = viewBySegmentValue[value];
if (nextView) {
setView(nextView);
}
}}
/>
)}
<Group gap="xs" ml="auto" wrap="nowrap">
{view === "queue" && queuePageCount > 1 && (
<>
<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: String(transcodingData.data.queue.startIndex + 1),
end: String(transcodingData.data.queue.endIndex + 1),
total: String(transcodingData.data.queue.totalCount),
})}
</Text>
<QueuePaginationControls
total={queuePageCount}
value={queuePage}
onChange={handleQueuePageChange}
layout={footerLayout}
/>
{footerLayout.showPageRange && (
<Text size="xs" style={{ whiteSpace: "nowrap" }}>
{t("currentIndex", {
start: String(transcodingData.data.queue.startIndex + 1),
end: String(transcodingData.data.queue.endIndex + 1),
total: String(transcodingData.data.queue.totalCount),
})}
</Text>
)}
</>
)}
@@ -113,3 +213,66 @@ export default function MediaTranscodingWidget({
</Stack>
);
}
export const getQueuePageSize = (height: number, isAdvanced: boolean): number => {
if (isAdvanced) return 25;
return Math.max(3, Math.min(15, Math.floor((height - 84) / 28)));
};
export const getTranscodingFooterLayout = (width: number, height: number) => ({
showTabLabels: width >= 420 && height >= 160,
showPageEdges: width >= 360,
showPageItems: width >= 600,
showPageRange: width >= 760,
});
interface QueuePaginationControlsProps {
total: number;
value: number;
onChange: (page: number) => void;
layout: ReturnType<typeof getTranscodingFooterLayout>;
}
const QueuePaginationControls = ({ total, value, onChange, layout }: QueuePaginationControlsProps) => (
<Pagination.Root total={total} value={value} onChange={onChange} size="xs">
<Group gap={2} justify="center" wrap="nowrap">
{layout.showPageEdges && <Pagination.First disabled={value === 1} />}
<Pagination.Previous disabled={value === 1} />
{layout.showPageItems && <Pagination.Items />}
<Pagination.Next disabled={value === total} />
{layout.showPageEdges && <Pagination.Last disabled={value === total} />}
</Group>
</Pagination.Root>
);
interface QueuePaginationState {
page: number;
pageSize: number;
isAdvanced: boolean;
}
export const resolveQueuePagination = (
state: QueuePaginationState,
pageSize: number,
isAdvanced: boolean,
totalPages: number | null,
): QueuePaginationState => ({
page:
state.pageSize !== pageSize || state.isAdvanced !== isAdvanced
? 1
: Math.max(1, totalPages === null ? state.page : Math.min(state.page, totalPages)),
pageSize,
isAdvanced,
});
const AdvancedPanel = ({ title, icon: Icon, children }: { title: string; icon: TablerIcon; children: ReactNode }) => (
<Paper withBorder radius="sm" p="xs" mih={280} style={{ display: "flex", flexDirection: "column" }}>
<Group gap="xs" mb="xs">
<Icon size="var(--mantine-font-size-md)" style={{ flexShrink: 0 }} />
<Text size="sm" fw={600}>
{title}
</Text>
</Group>
{children}
</Paper>
);
@@ -1,11 +1,13 @@
import type { MantineColor } from "@mantine/core";
import { Divider, Group, HoverCard, Indicator, RingProgress, Stack, Text } from "@mantine/core";
import { ActionIcon, Divider, Group, Indicator, Popover, RingProgress, Stack, Text } from "@mantine/core";
import { useColorScheme } from "@mantine/hooks";
import { IconHeartbeat } from "@tabler/icons-react";
import type { TdarrStatistics } from "@homarr/integrations";
import { useI18n } from "@homarr/translation/client";
import actionTargetClasses from "../common/action-target.module.css";
interface HealthCheckStatusProps {
statistics: TdarrStatistics;
}
@@ -13,6 +15,7 @@ interface HealthCheckStatusProps {
export function HealthCheckStatus(props: HealthCheckStatusProps) {
const colorScheme = useColorScheme();
const t = useI18n("widget.mediaTranscoding.healthCheck");
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
const indicatorColor = props.statistics.failedHealthCheckCount
? "red"
@@ -21,21 +24,23 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
: "green";
return (
<HoverCard position="bottom" width={250} shadow="sm">
<HoverCard.Target>
<Indicator color={textColor(indicatorColor, colorScheme)} size={6} display="flex">
<IconHeartbeat size={16} />
</Indicator>
</HoverCard.Target>
<HoverCard.Dropdown
<Popover position="bottom" width={250} shadow="sm" withArrow>
<Popover.Target>
<ActionIcon className={actionTargetClasses.root} variant="subtle" size="sm" aria-label={tCommon("healthCheck")}>
<Indicator color={textColor(indicatorColor, colorScheme)} size={6} display="flex">
<IconHeartbeat size="var(--mantine-font-size-md)" />
</Indicator>
</ActionIcon>
</Popover.Target>
<Popover.Dropdown
style={{
backgroundColor: "light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8))",
}}
>
<Stack gap="sm" align="center">
<Group gap="xs">
<IconHeartbeat size={18} />
<Text size="sm">{t("title")}</Text>
<IconHeartbeat size="var(--mantine-font-size-lg)" />
<Text size="sm">{tCommon("healthCheck")}</Text>
</Group>
<Divider
style={{
@@ -70,8 +75,8 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
</Stack>
</Group>
</Stack>
</HoverCard.Dropdown>
</HoverCard>
</Popover.Dropdown>
</Popover>
);
}
@@ -2,15 +2,20 @@ import { IconTransform } from "@tabler/icons-react";
import { z } from "zod/v4";
import { capitalize } from "@homarr/common";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { getWidgetIntegrationConfig } from "@homarr/definitions";
import { createWidgetDefinition } from "../definition";
import { createWidgetDefinition, matchesWidgetRuntimeQuery } from "../definition";
import { optionsBuilder } from "../options";
import { views } from "./views";
export const views = ["workers", "queue", "statistics"] as const;
export { views };
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
icon: IconTransform,
supportsAdvancedFocus: true,
queryKey: [["widget", "mediaTranscoding", "getDataAsync"]],
queryMatcher: matchesWidgetRuntimeQuery,
...getWidgetIntegrationConfig("mediaTranscoding"),
refetchInterval: null,
createOptions() {
return optionsBuilder.from((factory) => ({
@@ -21,5 +26,4 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaTran
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
}));
},
supportedIntegrations: getIntegrationKindsByCategory("mediaTranscoding"),
}).withDynamicImport(() => import("./component"));
@@ -0,0 +1,52 @@
import { describe, expect, test } from "vitest";
import { getQueuePageSize, getTranscodingFooterLayout, resolveQueuePagination } from "./component";
import { toRingProgressSections } from "./panels/statistics.panel";
describe("media transcoding layout", () => {
test("keeps compact queues bounded to the available height", () => {
expect(getQueuePageSize(140, false)).toBe(3);
expect(getQueuePageSize(500, false)).toBe(14);
expect(getQueuePageSize(2000, false)).toBe(15);
});
test("loads a bounded deeper queue in advanced mode", () => {
expect(getQueuePageSize(200, true)).toBe(25);
});
test("reveals footer detail only as space becomes available", () => {
expect(getTranscodingFooterLayout(280, 220)).toEqual({
showTabLabels: false,
showPageEdges: false,
showPageItems: false,
showPageRange: false,
});
expect(getTranscodingFooterLayout(600, 220)).toEqual({
showTabLabels: true,
showPageEdges: true,
showPageItems: true,
showPageRange: false,
});
expect(getTranscodingFooterLayout(760, 220).showPageRange).toBe(true);
expect(getTranscodingFooterLayout(760, 120).showTabLabels).toBe(false);
});
test("resets the queue when display mode or page size changes", () => {
const compact = { page: 7, pageSize: 10, isAdvanced: false };
expect(resolveQueuePagination(compact, 25, true, 8)).toEqual({ page: 1, pageSize: 25, isAdvanced: true });
expect(resolveQueuePagination(compact, 12, false, 8)).toEqual({ page: 1, pageSize: 12, isAdvanced: false });
});
test("clamps the queue page when the result count shrinks", () => {
expect(resolveQueuePagination({ page: 7, pageSize: 10, isAdvanced: false }, 10, false, 3)).toEqual({
page: 3,
pageSize: 10,
isAdvanced: false,
});
});
test("does not create NaN chart sections for empty statistics", () => {
expect(toRingProgressSections([{ name: "none", value: 0 }])).toEqual([]);
});
});
@@ -14,8 +14,8 @@ import {
} from "@mantine/core";
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
import { formatBytes } from "@homarr/common";
import type { TdarrQueue } from "@homarr/integrations";
import { useByteFormatter } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client";
interface QueuePanelProps {
@@ -24,13 +24,15 @@ interface QueuePanelProps {
export function QueuePanel(props: QueuePanelProps) {
const { queue } = props;
const { formatBytes } = useByteFormatter();
const t = useI18n("widget.mediaTranscoding.panel.queue");
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
if (queue.array.length === 0) {
return (
<Center style={{ flex: "1" }}>
<Title order={6}>{t("empty")}</Title>
<Title order={6}>{tCommon("empty")}</Title>
</Center>
);
}
@@ -42,7 +44,7 @@ export function QueuePanel(props: QueuePanelProps) {
<TableTr>
<TableTh ta="start" py={4}>
<Text size="xs" fw="bold">
{t("table.file")}
{tCommon("file")}
</Text>
</TableTh>
<TableTh ta="start" py={4}>
@@ -58,12 +60,12 @@ export function QueuePanel(props: QueuePanelProps) {
<TableTd py={2}>
<Group gap={4} wrap="nowrap">
{item.type === "transcode" ? (
<Tooltip label={t("table.transcode")}>
<IconTransform size={12} />
<Tooltip label={tCommon("transcode")}>
<IconTransform size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
</Tooltip>
) : (
<Tooltip label={t("table.healthCheck")}>
<IconHeartbeat size={12} />
<Tooltip label={tCommon("healthCheck")}>
<IconHeartbeat size="var(--mantine-font-size-xs)" style={{ flexShrink: 0 }} />
</Tooltip>
)}
<Text lineClamp={1} size="xs">
@@ -3,8 +3,8 @@ import { Card, Center, Group, RingProgress, ScrollArea, Stack, Text, Title, Tool
import { IconDatabaseHeart, IconFileDescription, IconHeartbeat, IconTransform } from "@tabler/icons-react";
import { useRequiredBoard } from "@homarr/boards/context";
import { formatBytes } from "@homarr/common";
import type { TdarrPieSegment, TdarrStatistics } from "@homarr/integrations";
import { useByteFormatter } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
@@ -16,6 +16,8 @@ interface StatisticsPanelProps {
export function StatisticsPanel(props: StatisticsPanelProps) {
const t = useI18n("widget.mediaTranscoding.panel.statistics");
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
const { formatBytes } = useByteFormatter();
const allLibs = props.statistics;
@@ -23,7 +25,7 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
if (!(allLibs.totalFileCount > 0)) {
return (
<Center style={{ flex: "1" }}>
<Title order={6}>{t("empty")}</Title>
<Title order={6}>{tCommon("empty")}</Title>
</Center>
);
}
@@ -31,10 +33,14 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
return (
<ScrollArea h="100%">
<Group wrap="wrap" justify="center" p={4} w="100%" gap="xs">
<StatisticItem icon={IconTransform} label={t("transcodesCount")} value={props.statistics.totalTranscodeCount} />
<StatisticItem
icon={IconTransform}
label={tCommon("transcodes")}
value={props.statistics.totalTranscodeCount}
/>
<StatisticItem
icon={IconHeartbeat}
label={t("healthChecksCount")}
label={tCommon("healthChecks")}
value={props.statistics.totalHealthCheckCount}
/>
<StatisticItem icon={IconFileDescription} label={t("filesCount")} value={props.statistics.totalFileCount} />
@@ -45,8 +51,8 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
/>
</Group>
<Group justify="center" wrap="wrap" grow>
<StatisticRingProgress items={allLibs.transcodeStatus} label={t("transcodes")} />
<StatisticRingProgress items={allLibs.healthCheckStatus} label={t("healthChecks")} />
<StatisticRingProgress items={allLibs.transcodeStatus} label={tCommon("transcodes")} />
<StatisticRingProgress items={allLibs.healthCheckStatus} label={tCommon("healthChecks")} />
<StatisticRingProgress items={allLibs.videoCodecs} label={t("videoCodecs")} />
<StatisticRingProgress items={allLibs.videoContainers} label={t("videoContainers")} />
<StatisticRingProgress items={allLibs.videoResolutions} label={t("videoResolutions")} />
@@ -71,8 +77,9 @@ const StatisticRingProgress = ({ items, label }: StatisticRingProgressProps) =>
);
};
function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps["sections"] {
export function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps["sections"] {
const total = segments.reduce((prev, curr) => prev + curr.value, 0);
if (total <= 0) return [];
return segments.map((segment, index) => ({
value: (segment.value * 100) / total,
tooltip: `${segment.name}: ${segment.value}`,
@@ -91,9 +98,9 @@ function StatisticItem(props: StatisticItemProps) {
const board = useRequiredBoard();
return (
<Tooltip label={props.label}>
<Card p={0} radius={board.itemRadius} miw={48} flex={1}>
<Card p={0} radius={board.itemRadius} miw={48} flex={1} bg="transparent">
<Group justify="center" align="center" gap="xs" w="100%" wrap="nowrap">
<props.icon size={16} style={{ minWidth: 16 }} />
<props.icon size="var(--mantine-font-size-md)" style={{ flexShrink: 0 }} />
<Text size="md">{props.value}</Text>
</Group>
</Card>
@@ -25,11 +25,12 @@ interface WorkersPanelProps {
export function WorkersPanel(props: WorkersPanelProps) {
const t = useI18n("widget.mediaTranscoding.panel.workers");
const tCommon = useI18n("widget.mediaTranscoding.panel.common");
if (props.workers.length === 0) {
return (
<Center style={{ flex: "1" }}>
<Title order={6}>{t("empty")}</Title>
<Title order={6}>{tCommon("empty")}</Title>
</Center>
);
}
@@ -41,7 +42,7 @@ export function WorkersPanel(props: WorkersPanelProps) {
<TableTr>
<TableTh ta="start" py={4}>
<Text size="xs" fw="bold">
{t("table.file")}
{tCommon("file")}
</Text>
</TableTh>
<TableTh ta="start" py={4} w={50}>
@@ -65,12 +66,12 @@ export function WorkersPanel(props: WorkersPanelProps) {
<Group gap="xs" wrap="nowrap">
<div>
{worker.jobType === "transcode" ? (
<Tooltip label={t("table.transcode")}>
<IconTransform size={14} />
<Tooltip label={tCommon("transcode")}>
<IconTransform size="var(--mantine-font-size-sm)" style={{ flexShrink: 0 }} />
</Tooltip>
) : (
<Tooltip label={t("table.healthCheck")}>
<IconHeartbeat size={14} />
<Tooltip label={tCommon("healthCheck")}>
<IconHeartbeat size="var(--mantine-font-size-sm)" style={{ flexShrink: 0 }} />
</Tooltip>
)}
</div>
@@ -0,0 +1 @@
export const views = ["workers", "queue", "statistics"] as const;