feat: Add quick filters for integration and status + minor UI improvement from feedback (#1641)
This commit is contained in:
@@ -10,10 +10,12 @@ import {
|
|||||||
AvatarGroup,
|
AvatarGroup,
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
|
Chip,
|
||||||
Divider,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Modal,
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
|
Popover,
|
||||||
Progress,
|
Progress,
|
||||||
Space,
|
Space,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -26,6 +28,7 @@ import type { IconProps } from "@tabler/icons-react";
|
|||||||
import {
|
import {
|
||||||
IconAlertTriangle,
|
IconAlertTriangle,
|
||||||
IconCirclesRelation,
|
IconCirclesRelation,
|
||||||
|
IconFilter,
|
||||||
IconInfinity,
|
IconInfinity,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconPlayerPause,
|
IconPlayerPause,
|
||||||
@@ -46,6 +49,11 @@ import { useScopedI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
|
|
||||||
|
interface QuickFilter {
|
||||||
|
integrationKinds: string[];
|
||||||
|
statuses: ExtendedDownloadClientItem["state"][];
|
||||||
|
}
|
||||||
|
|
||||||
//Ratio table for relative width between columns
|
//Ratio table for relative width between columns
|
||||||
const columnsRatios: Record<keyof ExtendedDownloadClientItem, number> = {
|
const columnsRatios: Record<keyof ExtendedDownloadClientItem, number> = {
|
||||||
actions: 2,
|
actions: 2,
|
||||||
@@ -108,6 +116,18 @@ export default function DownloadClientsWidget({
|
|||||||
const [clickedIndex, setClickedIndex] = useState<number>(0);
|
const [clickedIndex, setClickedIndex] = useState<number>(0);
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
|
||||||
|
//User quick settings for filters
|
||||||
|
const [quickFilters, setQuickFilters] = useState<QuickFilter>({ integrationKinds: [], statuses: [] });
|
||||||
|
const availableStatuses = useMemo<QuickFilter["statuses"]>(() => {
|
||||||
|
//Redefine list of available statuses from current items
|
||||||
|
const statuses = Array.from(new Set(currentItems.flatMap(({ data }) => data.items.map(({ state }) => state))));
|
||||||
|
//Reset user filters accordingly to remove unavailable statuses
|
||||||
|
setQuickFilters(({ integrationKinds: names, statuses: prevStatuses }) => {
|
||||||
|
return { integrationKinds: names, statuses: prevStatuses.filter((status) => statuses.includes(status)) };
|
||||||
|
});
|
||||||
|
return statuses;
|
||||||
|
}, [currentItems]);
|
||||||
|
|
||||||
//Get API mutation functions
|
//Get API mutation functions
|
||||||
const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation();
|
const { mutate: mutateResumeItem } = clientApi.widget.downloads.resumeItem.useMutation();
|
||||||
const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation();
|
const { mutate: mutatePauseItem } = clientApi.widget.downloads.pauseItem.useMutation();
|
||||||
@@ -164,6 +184,13 @@ export default function DownloadClientsWidget({
|
|||||||
progress !== 1)) ||
|
progress !== 1)) ||
|
||||||
(type === "usenet" && ((progress === 1 && options.showCompletedUsenet) || progress !== 1)),
|
(type === "usenet" && ((progress === 1 && options.showCompletedUsenet) || progress !== 1)),
|
||||||
)
|
)
|
||||||
|
//Filter following user quick setting
|
||||||
|
.filter(
|
||||||
|
({ state }) =>
|
||||||
|
(quickFilters.integrationKinds.length === 0 ||
|
||||||
|
quickFilters.integrationKinds.includes(pair.integration.name)) &&
|
||||||
|
(quickFilters.statuses.length === 0 || quickFilters.statuses.includes(state)),
|
||||||
|
)
|
||||||
//Add extrapolated data and actions if user is allowed interaction
|
//Add extrapolated data and actions if user is allowed interaction
|
||||||
.map((item): ExtendedDownloadClientItem => {
|
.map((item): ExtendedDownloadClientItem => {
|
||||||
const received = Math.floor(item.size * item.progress);
|
const received = Math.floor(item.size * item.progress);
|
||||||
@@ -199,6 +226,7 @@ export default function DownloadClientsWidget({
|
|||||||
options.filterIsWhitelist,
|
options.filterIsWhitelist,
|
||||||
options.showCompletedTorrent,
|
options.showCompletedTorrent,
|
||||||
options.showCompletedUsenet,
|
options.showCompletedUsenet,
|
||||||
|
quickFilters,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -660,7 +688,13 @@ export default function DownloadClientsWidget({
|
|||||||
<Text>{(globalTraffic.up / globalTraffic.down).toFixed(2)}</Text>
|
<Text>{(globalTraffic.up / globalTraffic.down).toFixed(2)}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
<ClientsControl clients={clients} style={editStyle} />
|
<ClientsControl
|
||||||
|
clients={clients}
|
||||||
|
style={editStyle}
|
||||||
|
filters={quickFilters}
|
||||||
|
setFilters={setQuickFilters}
|
||||||
|
availableStatuses={availableStatuses}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<ItemInfoModal items={data} currentIndex={clickedIndex} opened={opened} onClose={close} />
|
<ItemInfoModal items={data} currentIndex={clickedIndex} opened={opened} onClose={close} />
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -748,10 +782,13 @@ const NormalizedLine = ({
|
|||||||
|
|
||||||
interface ClientsControlProps {
|
interface ClientsControlProps {
|
||||||
clients: ExtendedClientStatus[];
|
clients: ExtendedClientStatus[];
|
||||||
|
filters: QuickFilter;
|
||||||
|
setFilters: (filters: QuickFilter) => void;
|
||||||
|
availableStatuses: QuickFilter["statuses"];
|
||||||
style?: MantineStyleProp;
|
style?: MantineStyleProp;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientsControl = ({ clients, style }: ClientsControlProps) => {
|
const ClientsControl = ({ clients, filters, setFilters, availableStatuses, style }: ClientsControlProps) => {
|
||||||
const integrationsStatuses = clients.reduce(
|
const integrationsStatuses = clients.reduce(
|
||||||
(acc, { status, integration: { id }, interact }) =>
|
(acc, { status, integration: { id }, interact }) =>
|
||||||
status && interact ? (acc[status.paused ? "paused" : "active"].push(id), acc) : acc,
|
status && interact ? (acc[status.paused ? "paused" : "active"].push(id), acc) : acc,
|
||||||
@@ -762,13 +799,61 @@ const ClientsControl = ({ clients, style }: ClientsControlProps) => {
|
|||||||
clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0),
|
clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0),
|
||||||
"/s",
|
"/s",
|
||||||
);
|
);
|
||||||
|
const chipStyle = {
|
||||||
|
"--chip-fz": "var(--button-fz)",
|
||||||
|
"--chip-size": "calc(var(--ratio-width) * 0.9)",
|
||||||
|
"--chip-icon-size": "calc(var(--chip-fz)*2/3)",
|
||||||
|
"--chip-padding": "var(--chip-fz)",
|
||||||
|
"--chip-checked-padding": "var(--chip-icon-size)",
|
||||||
|
"--chip-spacing": "var(--space-size)",
|
||||||
|
};
|
||||||
const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation();
|
const { mutate: mutateResumeQueue } = clientApi.widget.downloads.resume.useMutation();
|
||||||
const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation();
|
const { mutate: mutatePauseQueue } = clientApi.widget.downloads.pause.useMutation();
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const t = useScopedI18n("widget.downloads");
|
const t = useScopedI18n("widget.downloads");
|
||||||
return (
|
return (
|
||||||
<Group gap="var(--space-size)" style={style}>
|
<Group gap="var(--space-size)" style={style}>
|
||||||
<AvatarGroup spacing="calc(var(--space-size)*2)">
|
<Popover withinPortal={false} offset={0}>
|
||||||
|
<Popover.Target>
|
||||||
|
<ActionIcon size="var(--button-size)" radius={999} variant="light">
|
||||||
|
<IconFilter style={actionIconIconStyle} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown
|
||||||
|
w="calc(var(--ratio-width)*4)"
|
||||||
|
p="var(--space-size)"
|
||||||
|
bg="var(--background-color)"
|
||||||
|
style={{ "--popover-border-color": "var(--border-color)" }}
|
||||||
|
>
|
||||||
|
<Stack gap="var(--space-size)" align="center" pb="var(--space-size)">
|
||||||
|
<Text fw="700">{t("items.integration.columnTitle")}</Text>
|
||||||
|
<Chip.Group
|
||||||
|
multiple
|
||||||
|
value={filters.integrationKinds}
|
||||||
|
onChange={(names) => setFilters({ ...filters, integrationKinds: names })}
|
||||||
|
>
|
||||||
|
{clients.map(({ integration }) => (
|
||||||
|
<Chip style={chipStyle} key={integration.id} value={integration.name}>
|
||||||
|
{integration.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</Chip.Group>
|
||||||
|
<Text fw="700">{t("items.state.columnTitle")}</Text>
|
||||||
|
<Chip.Group
|
||||||
|
multiple
|
||||||
|
value={filters.statuses}
|
||||||
|
onChange={(statuses) => setFilters({ ...filters, statuses: statuses as typeof filters.statuses })}
|
||||||
|
>
|
||||||
|
{availableStatuses.map((status) => (
|
||||||
|
<Chip style={chipStyle} key={status} value={status}>
|
||||||
|
{t(`states.${status}`)}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</Chip.Group>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
<AvatarGroup mx="calc(var(--space-size)*2)" spacing="calc(var(--space-size)*2)">
|
||||||
{clients.map((client) => (
|
{clients.map((client) => (
|
||||||
<ClientAvatar key={client.integration.id} client={client} />
|
<ClientAvatar key={client.integration.id} client={client} />
|
||||||
))}
|
))}
|
||||||
@@ -787,7 +872,7 @@ const ClientsControl = ({ clients, style }: ClientsControlProps) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="light"
|
||||||
radius={999}
|
radius={999}
|
||||||
h="var(--button-size)"
|
h="var(--button-size)"
|
||||||
px="calc(var(--space-size)*2)"
|
px="calc(var(--space-size)*2)"
|
||||||
@@ -897,7 +982,8 @@ const ClientAvatar = ({ client }: ClientAvatarProps) => {
|
|||||||
src={getIconUrl(client.integration.kind)}
|
src={getIconUrl(client.integration.kind)}
|
||||||
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
|
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
|
||||||
size="var(--image-size)"
|
size="var(--image-size)"
|
||||||
bd={client.status ? 0 : "calc(var(--space-size)*0.5) solid var(--mantine-color-red-filled)"}
|
p="calc(var(--space-size)*0.5)"
|
||||||
|
bd={`calc(var(--space-size)*0.5) solid ${client.status ? "transparent" : "var(--mantine-color-red-filled)"}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user