feat: modernize widget experience
This commit is contained in:
1 parent
6331c88911
commit
1fa345edd1
165 files changed
+3698
-1649
No files matched your search
@@ -2,7 +2,9 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { getQueryKey } from "@trpc/react-query";
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Divider,
|
||||
Group,
|
||||
@@ -24,7 +26,9 @@ import type { TablerIcon } from "@homarr/ui";
|
||||
import { views } from ".";
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { setWidgetRuntimeQueries } from "../definition";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { WidgetQueryErrorIndicator } 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";
|
||||
@@ -46,7 +50,7 @@ export default function MediaTranscodingWidget({
|
||||
width,
|
||||
height,
|
||||
displayMode,
|
||||
widgetStateRef,
|
||||
widgetRuntimeRef,
|
||||
}: WidgetComponentProps<"mediaTranscoding">) {
|
||||
const isAdvanced = displayMode === "advanced";
|
||||
const queuePageSize = getQueuePageSize(height, isAdvanced);
|
||||
@@ -61,8 +65,11 @@ export default function MediaTranscodingWidget({
|
||||
pageSize: queuePageSize,
|
||||
page: requestPagination.page,
|
||||
};
|
||||
setWidgetRuntimeQueries(widgetStateRef, [{ path: ["widget", "mediaTranscoding", "getDataAsync"], input }]);
|
||||
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");
|
||||
@@ -89,10 +96,17 @@ export default function MediaTranscodingWidget({
|
||||
|
||||
const queuePageCount = totalQueuePages ?? 1;
|
||||
const isTiny = !isAdvanced && (width < 280 || height < 140);
|
||||
const footerLayout = getTranscodingFooterLayout(width, height);
|
||||
const queryIndicator = (
|
||||
<Box pos="absolute" top={4} right={8} style={{ zIndex: 2 }}>
|
||||
<WidgetQueryErrorIndicator error={transcodingQuery.error} label={t("name")} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (isAdvanced) {
|
||||
return (
|
||||
<Stack gap="xs" h="100%" p="xs">
|
||||
<Stack gap="xs" h="100%" p="xs" pos="relative">
|
||||
{queryIndicator}
|
||||
<ScrollArea h="100%" style={{ flex: 1 }}>
|
||||
<SimpleGrid cols={width >= 1100 ? 3 : width >= 700 ? 2 : 1} spacing="sm">
|
||||
<AdvancedPanel title={t("tab.workers")} icon={IconCpu2}>
|
||||
@@ -106,8 +120,15 @@ export default function MediaTranscodingWidget({
|
||||
</AdvancedPanel>
|
||||
</SimpleGrid>
|
||||
</ScrollArea>
|
||||
<Group gap="xs" justify="space-between" wrap="nowrap">
|
||||
<Pagination total={queuePageCount} value={queuePage} onChange={handleQueuePageChange} size="xs" />
|
||||
<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>
|
||||
@@ -115,7 +136,8 @@ export default function MediaTranscodingWidget({
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4} h="100%">
|
||||
<Stack gap={4} h="100%" pos="relative">
|
||||
{queryIndicator}
|
||||
{view === "workers" ? (
|
||||
<WorkersPanel workers={transcodingData.data.workers} isTiny={isTiny} />
|
||||
) : view === "queue" ? (
|
||||
@@ -124,7 +146,7 @@ export default function MediaTranscodingWidget({
|
||||
<StatisticsPanel statistics={transcodingData.data.statistics} />
|
||||
)}
|
||||
<Divider />
|
||||
<Group gap="xs" mb={4} ms={4} me={8}>
|
||||
<Group gap="xs" mb={4} ms={4} me={8} wrap="nowrap">
|
||||
<SegmentedControl
|
||||
data={views.map((value) => {
|
||||
const Icon = viewIcons[value];
|
||||
@@ -132,12 +154,12 @@ export default function MediaTranscodingWidget({
|
||||
label: (
|
||||
<Center style={{ gap: 4 }}>
|
||||
<Icon size={12} />
|
||||
{isTiny ? (
|
||||
<VisuallyHidden>{t(`tab.${value}`)}</VisuallyHidden>
|
||||
) : (
|
||||
{footerLayout.showTabLabels ? (
|
||||
<Text span size="xs">
|
||||
{t(`tab.${value}`)}
|
||||
</Text>
|
||||
) : (
|
||||
<VisuallyHidden>{t(`tab.${value}`)}</VisuallyHidden>
|
||||
)}
|
||||
</Center>
|
||||
),
|
||||
@@ -152,26 +174,27 @@ export default function MediaTranscodingWidget({
|
||||
}
|
||||
}}
|
||||
size="xs"
|
||||
style={{ minWidth: 0, flexShrink: footerLayout.showTabLabels ? 1 : 0 }}
|
||||
/>
|
||||
|
||||
<Group gap="xs" ml="auto">
|
||||
{view === "queue" && (
|
||||
<Group gap="xs" ml="auto" wrap="nowrap">
|
||||
{view === "queue" && queuePageCount > 1 && (
|
||||
<>
|
||||
<Pagination.Root total={queuePageCount} value={queuePage} onChange={handleQueuePageChange} size="xs">
|
||||
<Group gap={2} justify="center">
|
||||
{!isTiny && <Pagination.First disabled={queuePage === 1} />}
|
||||
<Pagination.Previous disabled={queuePage === 1} />
|
||||
<Pagination.Next disabled={queuePage === queuePageCount} />
|
||||
{!isTiny && <Pagination.Last disabled={queuePage === queuePageCount} />}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -187,6 +210,32 @@ export const getQueuePageSize = (height: number, isAdvanced: boolean): number =>
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { getQueuePageSize, resolveQueuePagination } from "./component";
|
||||
import { getQueuePageSize, getTranscodingFooterLayout, resolveQueuePagination } from "./component";
|
||||
import { toRingProgressSections } from "./panels/statistics.panel";
|
||||
|
||||
describe("media transcoding layout", () => {
|
||||
@@ -14,6 +14,23 @@ describe("media transcoding layout", () => {
|
||||
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 };
|
||||
|
||||
|
||||
Reference in new issue
Block a user