autobrr/web/src/components/data-table/Cells.tsx
soup 0391629862
chore(license): update copyright year in headers (#1929)
* chore: update copyright year in license headers

* Revert "chore: update copyright year in license headers"

This reverts commit 3e58129c431b9a491089ce36b908f9bb6ba38ed3.

* chore: update copyright year in license headers

* fix: sort go imports

* fix: add missing license headers
2025-01-06 22:23:19 +01:00

326 lines
11 KiB
TypeScript

/*
* Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import * as React from "react";
import { formatDistanceToNowStrict } from "date-fns";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { CellContext } from "@tanstack/react-table";
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/solid";
import {
ClockIcon,
XMarkIcon,
NoSymbolIcon,
ArrowDownTrayIcon,
ArrowTopRightOnSquareIcon, DocumentTextIcon
} from "@heroicons/react/24/outline";
import { APIClient } from "@api/APIClient";
import { FilterKeys } from "@api/query_keys";
import { classNames, humanFileSize, simplifyDate } from "@utils";
import { ExternalLink } from "../ExternalLink";
import { toast } from "@components/hot-toast";
import Toast from "@components/notifications/Toast";
import { RingResizeSpinner } from "@components/Icons";
import { Tooltip } from "@components/tooltips/Tooltip";
export const NameCell = (props: CellContext<Release, unknown>) => (
<div
className={classNames(
"flex justify-between items-center py-2 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
"max-w-[82px] sm:max-w-[130px] md:max-w-[260px] lg:max-w-[500px] xl:max-w-[760px]"
)}
>
<div className="flex flex-col truncate">
<span className="truncate">
{String(props.cell.getValue())}
</span>
<div className="text-xs truncate">
<span className="text-xs text-gray-500 dark:text-gray-400">Category:</span> {props.row.original.category}
<span
className="text-xs text-gray-500 dark:text-gray-400"> Size:</span> {humanFileSize(props.row.original.size)}
<span
className="text-xs text-gray-500 dark:text-gray-400"> Misc:</span> {`${props.row.original.resolution} ${props.row.original.source} ${props.row.original.codec ?? ""} ${props.row.original.container}`}
</div>
</div>
</div>
);
export const LinksCell = (props: CellContext<Release, unknown>) => {
return (
<div className="flex space-x-2 text-blue-400 dark:text-blue-500">
<div>
<Tooltip
requiresClick
label={<DocumentTextIcon
className="h-5 w-5 cursor-pointer text-blue-400 hover:text-blue-500 dark:text-blue-500 dark:hover:text-blue-600"
aria-hidden={true}/>}
title="Details"
>
<div className="mb-1">
<CellLine title="Release">{props.row.original.name}</CellLine>
<CellLine title="Indexer">{props.row.original.indexer.identifier}</CellLine>
<CellLine title="Protocol">{props.row.original.protocol}</CellLine>
<CellLine title="Implementation">{props.row.original.implementation}</CellLine>
<CellLine title="Announce Type">{props.row.original.announce_type}</CellLine>
<CellLine title="Category">{props.row.original.category}</CellLine>
<CellLine title="Uploader">{props.row.original.uploader}</CellLine>
<CellLine title="Size">{humanFileSize(props.row.original.size)}</CellLine>
<CellLine title="Title">{props.row.original.title}</CellLine>
{props.row.original.year > 0 && <CellLine title="Year">{props.row.original.year.toString()}</CellLine>}
{props.row.original.season > 0 &&
<CellLine title="Season">{props.row.original.season.toString()}</CellLine>}
{props.row.original.episode > 0 &&
<CellLine title="Episode">{props.row.original.episode.toString()}</CellLine>}
<CellLine title="Resolution">{props.row.original.resolution}</CellLine>
<CellLine title="Source">{props.row.original.source}</CellLine>
<CellLine title="Codec">{props.row.original.codec}</CellLine>
<CellLine title="HDR">{props.row.original.hdr}</CellLine>
<CellLine title="Group">{props.row.original.group}</CellLine>
<CellLine title="Container">{props.row.original.container}</CellLine>
<CellLine title="Origin">{props.row.original.origin}</CellLine>
</div>
</Tooltip>
</div>
{props.row.original.download_url && (
<ExternalLink href={props.row.original.download_url}>
<ArrowDownTrayIcon
title="Download torrent file"
className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600"
aria-hidden="true"
/>
</ExternalLink>
)}
{props.row.original.info_url && (
<ExternalLink href={props.row.original.info_url}>
<ArrowTopRightOnSquareIcon
title="Visit torrentinfo url"
className="h-5 w-5 hover:text-blue-500 dark:hover:text-blue-600"
aria-hidden="true"
/>
</ExternalLink>
)}
</div>
);
};
export const AgeCell = ({cell}: CellContext<Release, unknown>) => (
<div className="text-sm text-gray-500" title={simplifyDate(cell.getValue() as string)}>
{formatDistanceToNowStrict(new Date(cell.getValue() as string), {addSuffix: false})}
</div>
);
export const IndexerCell = (props: CellContext<Release, unknown>) => (
<div
className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
"max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"
)}
>
<Tooltip
requiresClick
label={props.row.original.indexer.name ? props.row.original.indexer.name : props.row.original.indexer.identifier}
maxWidth="max-w-[90vw]"
>
<span className="whitespace-pre-wrap break-words">
{props.row.original.indexer.name ? props.row.original.indexer.name : props.row.original.indexer.identifier}
</span>
</Tooltip>
</div>
);
export const TitleCell = ({cell}: CellContext<Release, string>) => (
<div
className={classNames(
"py-3 text-sm font-medium box-content text-gray-900 dark:text-gray-300",
"max-w-[96px] sm:max-w-[216px] md:max-w-[360px] lg:max-w-[640px] xl:max-w-[840px]"
)}
>
<Tooltip
requiresClick
label={cell.getValue()}
maxWidth="max-w-[90vw]"
>
<span className="whitespace-pre-wrap break-words">
{cell.getValue()}
</span>
</Tooltip>
</div>
);
interface RetryActionButtonProps {
status: ReleaseActionStatus;
}
interface RetryAction {
releaseId: number;
actionId: number;
}
const RetryActionButton = ({ status }: RetryActionButtonProps) => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (vars: RetryAction) => APIClient.release.replayAction(vars.releaseId, vars.actionId),
onSuccess: () => {
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries({ queryKey: FilterKeys.lists() });
toast.custom((t) => (
<Toast type="success" body={`${status?.action} replayed`} t={t} />
));
}
});
const replayAction = () => {
console.log("replay action");
mutation.mutate({ releaseId: status.release_id,actionId: status.id });
};
return (
<button className="flex items-center px-1.5 py-1 ml-2 rounded transition border-gray-500 bg-gray-250 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600" onClick={replayAction}>
<span className="mr-1.5">Retry</span>
{mutation.isPending
? <RingResizeSpinner className="text-blue-500 w-4 h-4 iconHeight" aria-hidden="true" />
: <ArrowPathIcon className="h-4 w-4" />
}
</button>
);
};
interface StatusCellMapEntry {
colors: string;
icon: React.ReactElement;
textFormatter: (status: ReleaseActionStatus) => React.ReactElement;
}
const StatusCellMap: Record<string, StatusCellMapEntry> = {
"PUSH_ERROR": {
colors: "bg-red-100 text-red-800 hover:bg-red-275",
icon: <XMarkIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (status: ReleaseActionStatus) => (
<>
<span>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-red-500">
error
</span>
{": "}
{status.action}
</span>
<div>
{status.action_id > 0 && <RetryActionButton status={status} />}
</div>
</>
)
},
"PUSH_REJECTED": {
colors: "bg-blue-100 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
icon: <NoSymbolIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (status: ReleaseActionStatus) => (
<>
<span>
Action
{" "}
<span
className="font-bold underline underline-offset-2 decoration-2 decoration-sky-500"
>
rejected
</span>
{": "}
{status.action}
</span>
<div>
{status.action_id > 0 && <RetryActionButton status={status} />}
</div>
</>
)
},
"PUSH_APPROVED": {
colors: "bg-green-175 text-green-900 hover:bg-green-300",
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (status: ReleaseActionStatus) => (
<>
<span>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-green-500">
approved
</span>
{": "}
{status.action}
</span>
{/*<div>*/}
{/* {status.action_id > 0 && <RetryActionButton status={status} />}*/}
{/*</div>*/}
</>
)
},
"PENDING": {
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />,
textFormatter: (status: ReleaseActionStatus) => (
<>
<span>
Action
{" "}
<span className="font-bold underline underline-offset-2 decoration-2 decoration-yellow-500">
pending
</span>
{": "}
{status.action}
</span>
<div>
{status.action_id > 0 && <RetryActionButton status={status} />}
</div>
</>
)
}
};
const CellLine = ({ title, children }: { title: string; children?: string; }) => {
if (!children)
return null;
return (
<div className="mt-0.5">
<span className="font-bold">{title}: </span>
<span className="whitespace-pre-wrap break-words leading-5">{children}</span>
</div>
);
};
export const ReleaseStatusCell = ({ row }: CellContext<Release, unknown>) => (
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
{row.original.action_status.map((v, idx) => (
<div
key={idx}
className={classNames(
StatusCellMap[v.status].colors,
"mr-1 inline-flex items-center rounded text-xs cursor-pointer"
)}
>
<Tooltip
requiresClick
label={StatusCellMap[v.status].icon}
title={StatusCellMap[v.status].textFormatter(v)}
>
<div className="mb-1">
<CellLine title="Type">{v.type}</CellLine>
<CellLine title="Client">{v.client}</CellLine>
<CellLine title="Filter">{v.filter}</CellLine>
<CellLine title="Time">{simplifyDate(v.timestamp)}</CellLine>
{v.rejections.length ? (
<CellLine title="Rejected">
{v.rejections.toString()}
</CellLine>
) : null}
</div>
</Tooltip>
</div>
))}
</div>
);