mirror of
https://github.com/idanoo/autobrr
synced 2025-07-25 09:49:13 +00:00
enhancement(web): ui overhaul (#1155)
* Various WebUI changes and fixes. * feat(tooltip): make tooltip display upwards * fix(tooltip): place tooltip to the right * fix(web): add missing ml-px to SwitchGroup header current: https://i.imgur.com/2WXstPV.png new: https://i.imgur.com/QGQ49mP.png * fix(web): collapse sections * fix(web): improve freeleech section * fix(web): rename action to action_components Renamed the 'action' folder to 'action_components' to resolve import issues due to case sensitivity. * fix(web): align CollapsibleSection Old Advanced tab: https://i.imgur.com/MXaJ5eJ.png New Advanced tab: https://i.imgur.com/4nPJJRw.png Music tab for comparison: https://i.imgur.com/I59X7ot.png * fix(web): remove invalid CSS class * revert: vertical padding on switchgroup added py-0 on the freeleech part instead * feat(settings): add back log files * fix(settings): irc channels and font sizes * fix(components): radio select roundness * fix(styling): various minor changes * fix(filters): remove jitter fields --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com> Co-authored-by: soup <soup@r4tio.dev> Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
parent
a274d9ddce
commit
e842a7bd42
84 changed files with 4378 additions and 4361 deletions
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
function ActionSettings() {
|
||||
return (
|
||||
<div className="lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Manage actions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Port
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>empty</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionSettings;
|
|
@ -16,6 +16,8 @@ import { APIClient } from "@api/APIClient";
|
|||
import { useToggle } from "@hooks/hooks";
|
||||
import { classNames } from "@utils";
|
||||
import { EmptySimple } from "@components/emptystates";
|
||||
import { Section } from "./_components";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export const apiKeys = {
|
||||
all: ["api_keys"] as const,
|
||||
|
@ -37,55 +39,44 @@ function APISettings() {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="pb-6 py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<APIKeyAddForm isOpen={addFormIsOpen} toggle={toggleAddForm} />
|
||||
<Section
|
||||
title="API keys"
|
||||
description="Manage your autobrr API keys here."
|
||||
rightSide={
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
onClick={toggleAddForm}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 mr-1" />
|
||||
Add new
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<APIKeyAddForm isOpen={addFormIsOpen} toggle={toggleAddForm} />
|
||||
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
API keys
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage API keys.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
onClick={toggleAddForm}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{data && data.length > 0 ? (
|
||||
<ul className="min-w-full relative">
|
||||
<li className="hidden sm:grid grid-cols-12 gap-4 mb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Name
|
||||
</div>
|
||||
<div className="col-span-8 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Key
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{data && data.length > 0 ? (
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="hidden sm:grid grid-cols-12 gap-4 mb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-5 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Name
|
||||
</div>
|
||||
<div className="col-span-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Key
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((k, idx) => <APIListItem key={idx} apikey={k} />)}
|
||||
</ol>
|
||||
</section>
|
||||
) : (
|
||||
<EmptySimple
|
||||
title="No API keys"
|
||||
subtitle=""
|
||||
buttonAction={toggleAddForm}
|
||||
buttonText="Create API key"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{data.map((k, idx) => <APIListItem key={idx} apikey={k} />)}
|
||||
</ul>
|
||||
) : (
|
||||
<EmptySimple
|
||||
title="No API keys"
|
||||
subtitle=""
|
||||
buttonAction={toggleAddForm}
|
||||
buttonText="Create API key"
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -131,7 +122,7 @@ function APIListItem({ apikey }: ApiKeyItemProps) {
|
|||
/>
|
||||
|
||||
<div className="sm:grid grid-cols-12 gap-4 items-center py-2">
|
||||
<div className="col-span-5 px-2 sm:px-6 py-2 sm:py-0 truncate block sm:text-sm text-md font-medium text-gray-900 dark:text-white">
|
||||
<div className="col-span-3 px-2 sm:px-6 py-2 sm:py-0 truncate block sm:text-sm text-md font-medium text-gray-900 dark:text-white">
|
||||
<div className="flex justify-between">
|
||||
<div className="pl-1 py-2">{apikey.name}</div>
|
||||
<div>
|
||||
|
@ -151,7 +142,7 @@ function APIListItem({ apikey }: ApiKeyItemProps) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-6 flex items-center text-sm font-medium text-gray-900 dark:text-white">
|
||||
<div className="col-span-8 flex items-center text-sm font-medium text-gray-900 dark:text-white">
|
||||
<KeyField value={apikey.key} />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -7,79 +7,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import { SettingsContext } from "@utils/Context";
|
||||
import { GithubRelease } from "@app/types/Update";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { ExternalLink } from "@components/ExternalLink";
|
||||
|
||||
interface RowItemProps {
|
||||
label: string;
|
||||
value?: string;
|
||||
title?: string;
|
||||
emptyText?: string;
|
||||
newUpdate?: GithubRelease;
|
||||
}
|
||||
|
||||
const RowItem = ({ label, value, title, emptyText }: RowItemProps) => {
|
||||
return (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-900 dark:text-white text-sm" title={title}>{label}</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-gray-300 text-sm sm:mt-0 sm:col-span-3 break-all truncate">
|
||||
{value ? <span className="px-1.5 py-1 bg-gray-200 dark:bg-gray-700 rounded shadow">{value}</span> : emptyText}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// interface RowItemNumberProps {
|
||||
// label: string;
|
||||
// value?: string | number;
|
||||
// title?: string;
|
||||
// unit?: string;
|
||||
// }
|
||||
|
||||
// const RowItemNumber = ({ label, value, title, unit }: RowItemNumberProps) => {
|
||||
// return (
|
||||
// <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
// <dt className="font-medium text-gray-500 dark:text-white" title={title}>{label}:</dt>
|
||||
// <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">
|
||||
// <span className="px-1 py-0.5 bg-gray-700 rounded shadow">{value}</span>
|
||||
// {unit &&
|
||||
// <span className="ml-1 text-sm text-gray-800 dark:text-gray-400">{unit}</span>
|
||||
// }
|
||||
// </dd>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
const RowItemVersion = ({ label, value, title, newUpdate }: RowItemProps) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-900 dark:text-white text-sm" title={title}>{label}</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-gray-300 text-sm sm:mt-0 sm:col-span-2 break-all truncate">
|
||||
<span className="px-1.5 py-1 bg-gray-200 dark:bg-gray-700 rounded shadow">{value}</span>
|
||||
{newUpdate && newUpdate.html_url && (
|
||||
<ExternalLink
|
||||
href={newUpdate.html_url}
|
||||
className="ml-2 inline-flex items-center rounded-md bg-green-100 px-2.5 py-0.5 text-sm font-medium text-green-800"
|
||||
>
|
||||
{newUpdate.name} available!
|
||||
</ExternalLink>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { Section, RowItem } from "./_components";
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const { isLoading, data } = useQuery({
|
||||
const { data } = useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: APIClient.config.get,
|
||||
retry: false,
|
||||
|
@ -105,8 +43,9 @@ function ApplicationSettings() {
|
|||
}
|
||||
});
|
||||
|
||||
const toggleCheckUpdateMutation = useMutation((value: boolean) => APIClient.config.update({ check_for_updates: value }).then(() => value), {
|
||||
onSuccess: (value: boolean) => {
|
||||
const toggleCheckUpdateMutation = useMutation({
|
||||
mutationFn: (value: boolean) => APIClient.config.update({ check_for_updates: value }).then(() => value),
|
||||
onSuccess: (_, value: boolean) => {
|
||||
toast.custom(t => <Toast type="success" body={`${value ? "You will now be notified of new updates." : "You will no longer be notified of new updates."}`} t={t} />);
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
checkUpdateMutation.mutate();
|
||||
|
@ -114,20 +53,16 @@ function ApplicationSettings() {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Application settings. Change in config.toml and restart to take effect.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||
{!isLoading && data && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<Section
|
||||
title="Application"
|
||||
description="Application settings. Change in config.toml and restart to take effect."
|
||||
>
|
||||
<div className="-mx-4 divide-y divide-gray-150 dark:divide-gray-750">
|
||||
<form className="mt-6 mb-4" action="#" method="POST">
|
||||
{data && (
|
||||
<div className="grid grid-cols-12 gap-2 sm:gap-6 px-4 sm:px-6">
|
||||
<div className="col-span-12 sm:col-span-4">
|
||||
<label htmlFor="host" className="block ml-px text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
|
@ -136,12 +71,12 @@ function ApplicationSettings() {
|
|||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
className="mt-1 block w-full sm:text-sm rounded-md border-gray-300 dark:border-gray-750 bg-gray-100 dark:bg-gray-825 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<div className="col-span-12 sm:col-span-4">
|
||||
<label htmlFor="port" className="block ml-px text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
|
@ -150,12 +85,12 @@ function ApplicationSettings() {
|
|||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
className="mt-1 block w-full sm:text-sm rounded-md border-gray-300 dark:border-gray-750 bg-gray-100 dark:bg-gray-825 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<div className="col-span-12 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block ml-px text-xs font-bold text-gray-700 dark:text-white uppercase tracking-wide">
|
||||
Base url
|
||||
</label>
|
||||
<input
|
||||
|
@ -164,64 +99,68 @@ function ApplicationSettings() {
|
|||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
className="mt-1 block w-full sm:text-sm rounded-md border-gray-300 dark:border-gray-750 bg-gray-100 dark:bg-gray-825 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<RowItemVersion label="Version" value={data?.version} newUpdate={updateData ?? undefined} />
|
||||
{data?.commit && <RowItem label="Commit" value={data.commit} />}
|
||||
{data?.date && <RowItem label="Build date" value={data.date} />}
|
||||
<RowItem label="Application" value={data?.application} />
|
||||
<RowItem label="Config path" value={data?.config_dir} />
|
||||
<RowItem label="Database" value={data?.database} />
|
||||
</dl>
|
||||
<RowItem
|
||||
label="Version"
|
||||
value={data?.version}
|
||||
rightSide={
|
||||
updateData && updateData.html_url ? (
|
||||
<ExternalLink
|
||||
href={updateData.html_url}
|
||||
className="ml-2 inline-flex items-center rounded-md bg-green-100 px-2.5 py-0.5 text-sm font-medium text-green-800"
|
||||
>
|
||||
{updateData.name} available!
|
||||
</ExternalLink>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
{data?.commit && <RowItem label="Commit" value={data.commit} />}
|
||||
{data?.date && <RowItem label="Build date" value={data.date} />}
|
||||
<RowItem label="Application" value={data?.application} />
|
||||
<RowItem label="Config path" value={data?.config_dir} />
|
||||
<RowItem label="Database" value={data?.database} />
|
||||
<div className="py-0.5">
|
||||
<Checkbox
|
||||
label="WebUI Debug mode"
|
||||
value={settings.debug}
|
||||
className="p-4 sm:px-6"
|
||||
setValue={
|
||||
(newValue: boolean) => setSettings((prevState) => ({
|
||||
...prevState,
|
||||
debug: newValue
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="WebUI Debug mode"
|
||||
value={settings.debug}
|
||||
setValue={
|
||||
(newValue: boolean) => setSettings((prevState) => ({
|
||||
...prevState,
|
||||
debug: newValue
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Check for updates"
|
||||
description="Get notified of new updates."
|
||||
value={data?.check_for_updates ?? true}
|
||||
setValue={(newValue: boolean) => {
|
||||
toggleCheckUpdateMutation.mutate(newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme."
|
||||
value={settings.darkTheme}
|
||||
setValue={
|
||||
(newValue: boolean) => setSettings((prevState) => ({
|
||||
...prevState,
|
||||
darkTheme: newValue
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
<Checkbox
|
||||
label="Check for updates"
|
||||
description="Get notified of new updates."
|
||||
value={data?.check_for_updates ?? true}
|
||||
className="p-4 sm:px-6"
|
||||
setValue={(newValue: boolean) => {
|
||||
toggleCheckUpdateMutation.mutate(newValue);
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme."
|
||||
value={settings.darkTheme}
|
||||
className="p-4 sm:px-6"
|
||||
setValue={
|
||||
(newValue: boolean) => setSettings((prevState) => ({
|
||||
...prevState,
|
||||
darkTheme: newValue
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,17 +4,19 @@
|
|||
*/
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { useToggle } from "@hooks/hooks";
|
||||
import { classNames } from "@utils";
|
||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "@forms";
|
||||
import { EmptySimple } from "@components/emptystates";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { DownloadClientTypeNameMap } from "@domain/constants";
|
||||
import { ActionTypeNameMap } from "@domain/constants";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
|
||||
import { Section } from "./_components";
|
||||
|
||||
export const clientKeys = {
|
||||
all: ["download_clients"] as const,
|
||||
|
@ -25,8 +27,7 @@ export const clientKeys = {
|
|||
};
|
||||
|
||||
interface DLSettingsItemProps {
|
||||
client: DownloadClient;
|
||||
idx: number;
|
||||
client: DownloadClient;
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
|
@ -87,7 +88,7 @@ function useSort(items: ListItemProps["clients"][], config?: SortConfig) {
|
|||
return { items: sortedItems, requestSort, sortConfig, getSortIndicator };
|
||||
}
|
||||
|
||||
function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) {
|
||||
function ListItem({ client }: DLSettingsItemProps) {
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -108,35 +109,24 @@ function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<li key={client.name}>
|
||||
<li>
|
||||
<div className="grid grid-cols-12 items-center py-2">
|
||||
<DownloadClientUpdateForm
|
||||
client={client}
|
||||
isOpen={updateClientIsOpen}
|
||||
toggle={toggleUpdateClient}
|
||||
/>
|
||||
<div className="col-span-2 sm:col-span-1 px-6 flex items-center sm:px-6">
|
||||
<Switch
|
||||
checked={client.enabled}
|
||||
onChange={onToggleMutation}
|
||||
className={classNames(
|
||||
client.enabled ? "bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
client.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<div className="col-span-2 sm:col-span-1 pl-1 sm:pl-5 flex items-center">
|
||||
<Checkbox
|
||||
value={client.enabled}
|
||||
setValue={onToggleMutation}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-8 sm:col-span-4 lg:col-span-4 pl-10 sm:pl-12 pr-6 py-3 block flex-col text-sm font-medium text-gray-900 dark:text-white truncate" title={client.name}>{client.name}</div>
|
||||
<div className="hidden sm:block col-span-4 pr-6 py-3 text-left items-center whitespace-nowrap text-sm text-gray-600 dark:text-gray-400 truncate" title={client.host}>{client.host}</div>
|
||||
<div className="hidden sm:block col-span-2 py-3 text-left items-center text-sm text-gray-600 dark:text-gray-400">
|
||||
{ActionTypeNameMap[client.type]}
|
||||
</div>
|
||||
<div className="col-span-8 sm:col-span-4 lg:col-span-4 pl-12 pr-6 py-3 block flex-col text-sm font-medium text-gray-900 dark:text-white truncate" title={client.name}>{client.name}</div>
|
||||
<div className="hidden sm:block col-span-4 pr-6 py-3 text-left items-center whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 truncate" title={client.host}>{client.host}</div>
|
||||
<div className="hidden sm:block col-span-2 py-3 text-left items-center text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</div>
|
||||
<div className="col-span-1 pl-0.5 whitespace-nowrap text-center text-sm font-medium">
|
||||
<span className="text-blue-600 dark:text-gray-300 hover:text-blue-900 cursor-pointer" onClick={toggleUpdateClient}>
|
||||
Edit
|
||||
|
@ -163,65 +153,57 @@ function DownloadClientSettings() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="lg:col-span-9">
|
||||
<Section
|
||||
title="Download Clients"
|
||||
description="Manage download clients."
|
||||
rightSide={
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
|
||||
onClick={toggleAddClient}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 mr-1" />
|
||||
Add new
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
||||
|
||||
<div className="py-6 px-2 lg:pb-8">
|
||||
<div className="px-4 -ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage download clients.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
|
||||
onClick={toggleAddClient}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6 px-4">
|
||||
{sortedClients.items.length > 0
|
||||
? <section className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-sm">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("enabled")}>
|
||||
Enabled <span className="sort-indicator">{sortedClients.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-6 sm:col-span-4 lg:col-span-4 pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("name")}
|
||||
>
|
||||
Name <span className="sort-indicator">{sortedClients.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden sm:flex col-span-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("host")}
|
||||
>
|
||||
Host <span className="sort-indicator">{sortedClients.getSortIndicator("host")}</span>
|
||||
</div>
|
||||
<div className="hidden sm:flex col-span-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("type")}
|
||||
>
|
||||
Type <span className="sort-indicator">{sortedClients.getSortIndicator("type")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedClients.items.map((client, idx) => (
|
||||
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No download clients" subtitle="" buttonText="Add new client" buttonAction={toggleAddClient} />
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{sortedClients.items.length > 0 ? (
|
||||
<ul className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex col-span-2 sm:col-span-1 pl-0 sm:pl-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("enabled")}>
|
||||
Enabled <span className="sort-indicator">{sortedClients.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-6 sm:col-span-4 lg:col-span-4 pl-10 sm:pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("name")}
|
||||
>
|
||||
Name <span className="sort-indicator">{sortedClients.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden sm:flex col-span-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("host")}
|
||||
>
|
||||
Host <span className="sort-indicator">{sortedClients.getSortIndicator("host")}</span>
|
||||
</div>
|
||||
<div className="hidden sm:flex col-span-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedClients.requestSort("type")}
|
||||
>
|
||||
Type <span className="sort-indicator">{sortedClients.getSortIndicator("type")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedClients.items.map((client) => (
|
||||
<ListItem key={client.id} client={client} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<EmptySimple title="No download clients" subtitle="" buttonText="Add new client" buttonAction={toggleAddClient} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Fragment, useRef, useState, useMemo } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import {
|
||||
ArrowsRightLeftIcon,
|
||||
|
@ -25,6 +25,8 @@ import { EmptySimple } from "@components/emptystates";
|
|||
import { ImplementationBadges } from "./Indexer";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||
import { ExternalLink } from "@components/ExternalLink";
|
||||
import { Section } from "./_components";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
|
||||
export const feedKeys = {
|
||||
all: ["feeds"] as const,
|
||||
|
@ -99,55 +101,47 @@ function FeedSettings() {
|
|||
const sortedFeeds = useSort(data || []);
|
||||
|
||||
return (
|
||||
<div className="lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage RSS, Newznab, and Torznab feeds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("enabled")}>
|
||||
Enabled <span className="sort-indicator">{sortedFeeds.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-5 pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("name")}>
|
||||
Name <span className="sort-indicator">{sortedFeeds.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-1 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("type")}>
|
||||
Type <span className="sort-indicator">{sortedFeeds.getSortIndicator("type")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-2 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("last_run")}>
|
||||
Last run <span className="sort-indicator">{sortedFeeds.getSortIndicator("last_run")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-2 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("next_run")}>
|
||||
Next run <span className="sort-indicator">{sortedFeeds.getSortIndicator("next_run")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedFeeds.items.map((feed) => (
|
||||
<ListItem key={feed.id} feed={feed} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
|
||||
</div>
|
||||
</div>
|
||||
<Section
|
||||
title="Feeds"
|
||||
description="Manage RSS, Newznab, and Torznab feeds."
|
||||
>
|
||||
{data && data.length > 0 ? (
|
||||
<ul className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="flex col-span-2 sm:col-span-1 pl-0 sm:pl-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("enabled")}>
|
||||
Enabled <span className="sort-indicator">{sortedFeeds.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-5 pl-10 sm:pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("name")}>
|
||||
Name <span className="sort-indicator">{sortedFeeds.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-1 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("type")}>
|
||||
Type <span className="sort-indicator">{sortedFeeds.getSortIndicator("type")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-2 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("last_run")}>
|
||||
Last run <span className="sort-indicator">{sortedFeeds.getSortIndicator("last_run")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-2 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedFeeds.requestSort("next_run")}>
|
||||
Next run <span className="sort-indicator">{sortedFeeds.getSortIndicator("next_run")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedFeeds.items.map((feed) => (
|
||||
<ListItem key={feed.id} feed={feed} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<EmptySimple title="No feeds" subtitle="Setup via indexers" />
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -181,26 +175,13 @@ function ListItem({ feed }: ListItemProps) {
|
|||
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed} />
|
||||
|
||||
<div className="grid grid-cols-12 items-center">
|
||||
<div className="col-span-2 sm:col-span-1 px-6 flex items-center">
|
||||
<Switch
|
||||
checked={feed.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
feed.enabled ? "bg-blue-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
feed.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<div className="col-span-2 sm:col-span-1 pl-1 sm:pl-5 flex items-center">
|
||||
<Checkbox
|
||||
value={feed.enabled}
|
||||
setValue={toggleActive}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-8 sm:col-span-5 pl-12 py-3 flex flex-col text-sm font-medium text-gray-900 dark:text-white">
|
||||
<div className="col-span-8 sm:col-span-5 pl-10 sm:pl-12 py-3 flex flex-col text-sm font-medium text-gray-900 dark:text-white">
|
||||
<span>{feed.name}</span>
|
||||
<span className="text-gray-900 dark:text-gray-500 text-xs">
|
||||
{feed.indexer}
|
||||
|
@ -308,7 +289,7 @@ const FeedItemDropdown = ({
|
|||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-825 divide-y divide-gray-200 dark:divide-gray-750 rounded-md shadow-lg border border-gray-250 dark:border-gray-750 focus:outline-none z-10"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
|
@ -352,26 +333,18 @@ const FeedItemDropdown = ({
|
|||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<ExternalLink
|
||||
href={`${baseUrl()}api/feeds/${feed.id}/latest`}
|
||||
className={classNames(
|
||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||
)}
|
||||
>
|
||||
<DocumentTextIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
View latest run
|
||||
</ExternalLink>
|
||||
)}
|
||||
<ExternalLink
|
||||
href={`${baseUrl()}api/feeds/${feed.id}/latest`}
|
||||
className="font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm text-gray-900 dark:text-gray-300 hover:bg-blue-600 hover:text-white"
|
||||
>
|
||||
<DocumentTextIcon
|
||||
className="w-5 h-5 mr-2 text-blue-500 group-hover:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
View latest run
|
||||
</ExternalLink>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
import { useState, useMemo } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { IndexerAddForm, IndexerUpdateForm } from "@forms";
|
||||
import { useToggle } from "@hooks/hooks";
|
||||
import { classNames } from "@utils";
|
||||
import { EmptySimple } from "@components/emptystates";
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { EmptySimple } from "@components/emptystates";
|
||||
import { IndexerAddForm, IndexerUpdateForm } from "@forms";
|
||||
import { componentMapType } from "@forms/settings/DownloadClientForms";
|
||||
|
||||
import { Section } from "./_components";
|
||||
|
||||
export const indexerKeys = {
|
||||
all: ["indexers"] as const,
|
||||
lists: () => [...indexerKeys.all, "list"] as const,
|
||||
|
@ -85,7 +87,7 @@ const ImplementationBadgeIRC = () => (
|
|||
);
|
||||
|
||||
const ImplementationBadgeTorznab = () => (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-amber-900">
|
||||
Torznab
|
||||
</span>
|
||||
);
|
||||
|
@ -131,6 +133,10 @@ const ListItem = ({ indexer }: ListItemProps) => {
|
|||
updateMutation.mutate(newState);
|
||||
};
|
||||
|
||||
if (!indexer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li>
|
||||
<div className="grid grid-cols-12 items-center py-1.5">
|
||||
|
@ -139,25 +145,8 @@ const ListItem = ({ indexer }: ListItemProps) => {
|
|||
toggle={toggleUpdate}
|
||||
indexer={indexer}
|
||||
/>
|
||||
<div className="col-span-2 sm:col-span-1 flex px-6 items-center sm:px-6">
|
||||
<Switch
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
checked={indexer.enabled ?? false}
|
||||
onChange={onToggleMutation}
|
||||
className={classNames(
|
||||
indexer.enabled ? "bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Enable</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
indexer.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<div className="col-span-2 sm:col-span-1 flex pl-1 sm:pl-5 items-center">
|
||||
<Checkbox value={indexer.enabled ?? false} setValue={onToggleMutation} />
|
||||
</div>
|
||||
<div className="col-span-7 sm:col-span-8 pl-12 sm:pr-6 py-3 block flex-col text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{indexer.name}
|
||||
|
@ -194,71 +183,64 @@ function IndexerSettings() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="lg:col-span-9">
|
||||
<Section
|
||||
title="Indexers"
|
||||
description={
|
||||
<>
|
||||
Indexer settings for IRC, RSS, Newznab, and Torznab based indexers.<br />
|
||||
Generic RSS/Newznab/Torznab feeds can be added here by selecting one of the <span className="font-bold">Generic</span> indexers.
|
||||
</>
|
||||
}
|
||||
rightSide={
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddIndexer}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 mr-1" />
|
||||
Add new
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Indexers
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Indexer settings for IRC, RSS, Newznab, and Torznab based indexers.<br />
|
||||
Generic feeds can be added here by selecting the Generic indexer.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddIndexer}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
{data && data.length > 0 ? (
|
||||
<section className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedIndexers.requestSort("enabled")}
|
||||
>
|
||||
Enabled <span className="sort-indicator">{sortedIndexers.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-7 sm:col-span-8 pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedIndexers.requestSort("name")}
|
||||
>
|
||||
Name <span className="sort-indicator">{sortedIndexers.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-1 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedIndexers.requestSort("implementation")}
|
||||
>
|
||||
Implementation <span className="sort-indicator">{sortedIndexers.getSortIndicator("implementation")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedIndexers.items.map((indexer) => (
|
||||
<ListItem indexer={indexer} key={indexer.id} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
) : (
|
||||
<EmptySimple
|
||||
title="No indexers"
|
||||
subtitle=""
|
||||
buttonText="Add new indexer"
|
||||
buttonAction={toggleAddIndexer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{sortedIndexers.items.length ? (
|
||||
<ul className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
className="flex col-span-2 sm:col-span-1 pl-0 sm:pl-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-800 hover:dark:text-gray-250 transition-colors uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedIndexers.requestSort("enabled")}
|
||||
>
|
||||
Enabled <span className="sort-indicator">{sortedIndexers.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="col-span-7 sm:col-span-8 pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-800 hover:dark:text-gray-250 transition-colors uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedIndexers.requestSort("name")}
|
||||
>
|
||||
Name <span className="sort-indicator">{sortedIndexers.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div
|
||||
className="hidden md:flex col-span-1 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-800 hover:dark:text-gray-250 transition-colors uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedIndexers.requestSort("implementation")}
|
||||
>
|
||||
Implementation <span className="sort-indicator">{sortedIndexers.getSortIndicator("implementation")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedIndexers.items.map((indexer) => (
|
||||
<ListItem indexer={indexer} key={indexer.id} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<EmptySimple
|
||||
title="No indexers"
|
||||
subtitle=""
|
||||
buttonText="Add new indexer"
|
||||
buttonAction={toggleAddIndexer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import { Fragment, useRef, useState, useMemo, useEffect, MouseEvent } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
import { LockClosedIcon, LockOpenIcon, PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import {
|
||||
ArrowsPointingInIcon,
|
||||
|
@ -29,6 +29,8 @@ import { SettingsContext } from "@utils/Context";
|
|||
import { Checkbox } from "@components/Checkbox";
|
||||
// import { useForm } from "react-hook-form";
|
||||
|
||||
import { Section } from "./_components";
|
||||
|
||||
export const ircKeys = {
|
||||
all: ["irc_networks"] as const,
|
||||
lists: () => [...ircKeys.all, "list"] as const,
|
||||
|
@ -106,112 +108,99 @@ const IrcSettings = () => {
|
|||
const sortedNetworks = useSort(data || []);
|
||||
|
||||
return (
|
||||
<div className="text-sm lg:col-span-9">
|
||||
<Section
|
||||
title="IRC"
|
||||
description="IRC networks and channels. Click on a network to view channel status."
|
||||
rightSide={
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNetwork}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-500"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 mr-1" />
|
||||
Add new
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
||||
|
||||
<div className="py-6 px-4 md:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap md:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
IRC
|
||||
</h3>
|
||||
<p className="mt-1 text-gray-500 dark:text-gray-400">
|
||||
IRC networks and channels. Click on a network to view channel
|
||||
status.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNetwork}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
<div className="flex justify-between flex-col md:flex-row px-1">
|
||||
<ul className="flex flex-col md:flex-row md:gap-2 pb-4 md:pb-0 md:divide-x md:divide-gray-200 md:dark:divide-gray-700">
|
||||
<li className="flex items-center">
|
||||
<span
|
||||
className="mr-2 flex h-4 w-4 relative"
|
||||
title="Network healthy"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="inline-flex absolute rounded-full h-4 w-4 bg-green-500" />
|
||||
</span>
|
||||
<span className="text-sm text-gray-800 dark:text-gray-500">Network healthy</span>
|
||||
</li>
|
||||
|
||||
<div className="flex justify-between flex-col md:flex-row mt-10 px-1">
|
||||
<ol className="flex flex-col md:flex-row md:gap-2 pb-4 md:pb-0 md:divide-x md:divide-gray-200 md:dark:divide-gray-700">
|
||||
<li className="flex items-center">
|
||||
<span
|
||||
className="mr-2 flex h-4 w-4 relative"
|
||||
title="Network healthy"
|
||||
>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="inline-flex absolute rounded-full h-4 w-4 bg-green-500" />
|
||||
</span>
|
||||
<span className="text-gray-800 dark:text-gray-500">Network healthy</span>
|
||||
</li>
|
||||
<li className="flex items-center md:pl-2">
|
||||
<span
|
||||
className="mr-2 flex h-4 w-4 rounded-full opacity-75 bg-yellow-400 over:text-yellow-600"
|
||||
title="Network unhealthy"
|
||||
/>
|
||||
<span className="text-sm text-gray-800 dark:text-gray-500">Network unhealthy</span>
|
||||
</li>
|
||||
|
||||
<li className="flex items-center md:pl-2">
|
||||
<span
|
||||
className="mr-2 flex h-4 w-4 rounded-full opacity-75 bg-yellow-400 over:text-yellow-600"
|
||||
title="Network unhealthy"
|
||||
/>
|
||||
<span className="text-gray-800 dark:text-gray-500">Network unhealthy</span>
|
||||
</li>
|
||||
|
||||
<li className="flex items-center md:pl-2">
|
||||
<span
|
||||
className="mr-2 flex h-4 w-4 rounded-full opacity-75 bg-gray-500"
|
||||
title="Network disabled"
|
||||
>
|
||||
</span>
|
||||
<span className="text-gray-800 dark:text-gray-500">Network disabled</span>
|
||||
</li>
|
||||
</ol>
|
||||
<div className="flex gap-x-2">
|
||||
<button
|
||||
className="flex items-center text-gray-800 dark:text-gray-400 p-1 px-2 rounded shadow bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
onClick={toggleExpand}
|
||||
title={expandNetworks ? "collapse" : "expand"}
|
||||
<li className="flex items-center md:pl-2">
|
||||
<span
|
||||
className="mr-2 flex h-4 w-4 rounded-full opacity-75 bg-gray-500"
|
||||
title="Network disabled"
|
||||
>
|
||||
{expandNetworks
|
||||
? <span className="flex items-center">Collapse <ArrowsPointingInIcon className="ml-1 w-4 h-4"/></span>
|
||||
: <span className="flex items-center">Expand <ArrowsPointingOutIcon className="ml-1 w-4 h-4"/></span>
|
||||
}</button>
|
||||
<IRCLogsDropdown/>
|
||||
</div>
|
||||
</span>
|
||||
<span className="text-sm text-gray-800 dark:text-gray-500">Network disabled</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex gap-x-2">
|
||||
<button
|
||||
className="flex items-center text-gray-800 dark:text-gray-400 p-1 px-2 rounded shadow bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
onClick={toggleExpand}
|
||||
title={expandNetworks ? "collapse" : "expand"}
|
||||
>
|
||||
{expandNetworks
|
||||
? <span className="flex items-center text-sm">Collapse <ArrowsPointingInIcon className="ml-1 w-4 h-4" /></span>
|
||||
: <span className="flex items-center text-sm">Expand <ArrowsPointingOutIcon className="ml-1 w-4 h-4" /></span>
|
||||
}</button>
|
||||
<IRCLogsDropdown />
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ? (
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow md:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex col-span-2 md:col-span-1 px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("enabled")}>
|
||||
Enabled <span className="sort-indicator">{sortedNetworks.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div className="col-span-10 md:col-span-3 px-8 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("name")}>
|
||||
Network <span className="sort-indicator">{sortedNetworks.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("server")}>
|
||||
Server <span className="sort-indicator">{sortedNetworks.getSortIndicator("server")}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex col-span-3 px-5 lg:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("nick")}>
|
||||
Nick <span className="sort-indicator">{sortedNetworks.getSortIndicator("nick")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{data &&
|
||||
sortedNetworks.items.map((network) => (
|
||||
<ListItem key={network.id} expanded={expandNetworks} network={network} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
) : (
|
||||
<EmptySimple
|
||||
title="No networks"
|
||||
subtitle="Normally set up via Indexers"
|
||||
buttonText="Add new network"
|
||||
buttonAction={toggleAddNetwork}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ? (
|
||||
<ul className="mt-6 min-w-full relative">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex col-span-2 md:col-span-1 pl-0 sm:px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("enabled")}>
|
||||
Enabled <span className="sort-indicator">{sortedNetworks.getSortIndicator("enabled")}</span>
|
||||
</div>
|
||||
<div className="col-span-10 md:col-span-3 px-8 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("name")}>
|
||||
Network <span className="sort-indicator">{sortedNetworks.getSortIndicator("name")}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("server")}>
|
||||
Server <span className="sort-indicator">{sortedNetworks.getSortIndicator("server")}</span>
|
||||
</div>
|
||||
<div className="hidden md:flex col-span-3 px-5 lg:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => sortedNetworks.requestSort("nick")}>
|
||||
Nick <span className="sort-indicator">{sortedNetworks.getSortIndicator("nick")}</span>
|
||||
</div>
|
||||
</li>
|
||||
{sortedNetworks.items.map((network) => (
|
||||
<ListItem key={network.id} expanded={expandNetworks} network={network} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<EmptySimple
|
||||
title="No networks"
|
||||
subtitle="Normally set up via Indexers"
|
||||
buttonText="Add new network"
|
||||
buttonAction={toggleAddNetwork}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -245,7 +234,7 @@ const ListItem = ({ network, expanded }: ListItemProps) => {
|
|||
<li>
|
||||
<div
|
||||
className={classNames(
|
||||
"grid grid-cols-12 gap-2 lg:gap-4 items-center py-2 cursor-pointer",
|
||||
"grid grid-cols-12 gap-2 lg:gap-4 items-center mt-0.5 py-2.5 cursor-pointer first:rounded-t-md last:rounded-b-md transition",
|
||||
network.enabled && !network.healthy ? "bg-red-50 dark:bg-red-900 hover:bg-red-100 dark:hover:bg-red-800" : "hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
|
@ -261,25 +250,11 @@ const ListItem = ({ network, expanded }: ListItemProps) => {
|
|||
toggle={toggleUpdate}
|
||||
network={network}
|
||||
/>
|
||||
<div className="col-span-2 md:col-span-1 flex pl-5 text-gray-500 dark:text-gray-400">
|
||||
<Switch
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
checked={network.enabled}
|
||||
onChange={onToggleMutation}
|
||||
className={classNames(
|
||||
network.enabled ? "bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"items-center relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Enable</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
network.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<div className="col-span-2 md:col-span-1 flex pl-1 sm:pl-5 text-gray-500 dark:text-gray-400">
|
||||
<Checkbox
|
||||
value={network.enabled}
|
||||
setValue={onToggleMutation}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-8 xs:col-span-3 md:col-span-3 items-center pl-8 font-medium text-gray-900 dark:text-white cursor-pointer">
|
||||
<div className="flex">
|
||||
|
@ -305,7 +280,7 @@ const ListItem = ({ network, expanded }: ListItemProps) => {
|
|||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
)}
|
||||
</span>
|
||||
<div className="block truncate">
|
||||
<div className="block text-sm truncate">
|
||||
{network.name}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -330,13 +305,13 @@ const ListItem = ({ network, expanded }: ListItemProps) => {
|
|||
)} />
|
||||
)}
|
||||
</div>
|
||||
<p className="block truncate">
|
||||
<p className="block text-sm truncate">
|
||||
{network.server}:{network.port}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden md:flex col-span-3 items-center md:pl-6 text-gray-500 dark:text-gray-400">
|
||||
<div className="block truncate">
|
||||
<div className="block text-sm truncate">
|
||||
{network.nick}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -345,25 +320,25 @@ const ListItem = ({ network, expanded }: ListItemProps) => {
|
|||
</div>
|
||||
</div>
|
||||
{(edit || expanded) && (
|
||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-775">
|
||||
<div className="min-w-full">
|
||||
{network.channels.length > 0 ? (
|
||||
<ol>
|
||||
<ul>
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<div className="col-span-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Channel
|
||||
</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<div className="col-span-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Monitoring since
|
||||
</div>
|
||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<div className="col-span-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Last announce
|
||||
</div>
|
||||
</li>
|
||||
{network.channels.map((c) => (
|
||||
<ChannelItem key={`${network.id}.${c.id}`} network={network} channel={c} />
|
||||
))}
|
||||
</ol>
|
||||
</ul>
|
||||
) : (
|
||||
<div className="flex text-center justify-center py-4 dark:text-gray-500">
|
||||
<p>No channels!</p>
|
||||
|
@ -387,12 +362,12 @@ const ChannelItem = ({ network, channel }: ChannelItemProps) => {
|
|||
return (
|
||||
<li
|
||||
className={classNames(
|
||||
"mb-2 text-gray-500 dark:text-gray-400",
|
||||
viewChannel ? "bg-gray-200 dark:bg-gray-800 rounded-md" : ""
|
||||
"mb-2 text-gray-500 dark:text-gray-400 hover:cursor-pointer rounded-md",
|
||||
viewChannel ? "bg-gray-200 dark:bg-gray-800 rounded-md" : "hover:bg-gray-300 dark:hover:bg-gray-800"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="grid grid-cols-12 gap-4 items-center py-4 hover:bg-gray-300 dark:hover:bg-gray-800 hover:cursor-pointer rounded-md"
|
||||
className="grid grid-cols-12 gap-4 items-center py-4 "
|
||||
onClick={toggleView}
|
||||
>
|
||||
<div className="col-span-4 flex items-center md:px-6">
|
||||
|
@ -403,14 +378,14 @@ const ChannelItem = ({ network, channel }: ChannelItemProps) => {
|
|||
className="mr-3 flex h-3 w-3 relative"
|
||||
title="monitoring"
|
||||
>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400"/>
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
)
|
||||
) : (
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500"/>
|
||||
<span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
)}
|
||||
{channel.name}
|
||||
</span>
|
||||
|
@ -432,7 +407,7 @@ const ChannelItem = ({ network, channel }: ChannelItemProps) => {
|
|||
</div>
|
||||
</div>
|
||||
{viewChannel && (
|
||||
<Events network={network} channel={channel.name}/>
|
||||
<Events network={network} channel={channel.name} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
|
@ -459,7 +434,7 @@ const ListItemDropdown = ({
|
|||
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
|
||||
|
||||
toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t}/>);
|
||||
toast.custom((t) => <Toast type="success" body={`Network ${network.name} was deleted`} t={t} />);
|
||||
|
||||
toggleDeleteModal();
|
||||
}
|
||||
|
@ -471,7 +446,7 @@ const ListItemDropdown = ({
|
|||
queryClient.invalidateQueries({ queryKey: ircKeys.lists() });
|
||||
queryClient.invalidateQueries({ queryKey: ircKeys.detail(network.id) });
|
||||
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was successfully restarted`} t={t}/>);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was successfully restarted`} t={t} />);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -514,7 +489,7 @@ const ListItemDropdown = ({
|
|||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 w-32 md:w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-825 divide-y divide-gray-200 dark:divide-gray-750 rounded-md shadow-lg border border-gray-250 dark:border-gray-750 focus:outline-none z-10"
|
||||
>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
|
@ -623,7 +598,6 @@ interface EventsProps {
|
|||
}
|
||||
|
||||
export const Events = ({ network, channel }: EventsProps) => {
|
||||
|
||||
const [logs, setLogs] = useState<IrcEvent[]>([]);
|
||||
const [settings] = SettingsContext.use();
|
||||
|
||||
|
@ -700,8 +674,8 @@ export const Events = ({ network, channel }: EventsProps) => {
|
|||
onClick={toggleFullscreen}
|
||||
>
|
||||
{isFullscreen
|
||||
? <span className="flex items-center"><ArrowsPointingInIcon className="w-5 h-5"/></span>
|
||||
: <span className="flex items-center"><ArrowsPointingOutIcon className="w-5 h-5"/></span>}
|
||||
? <span className="flex items-center"><ArrowsPointingInIcon className="w-5 h-5" /></span>
|
||||
: <span className="flex items-center"><ArrowsPointingOutIcon className="w-5 h-5" /></span>}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
@ -759,7 +733,7 @@ const IRCLogsDropdown = () => {
|
|||
return (
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center text-gray-800 dark:text-gray-400 p-1 px-2 rounded shadow bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600">
|
||||
<span className="flex items-center">Options <Cog6ToothIcon className="ml-1 w-4 h-4"/></span>
|
||||
<span className="flex items-center">Options <Cog6ToothIcon className="ml-1 w-4 h-4" /></span>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
|
@ -771,7 +745,7 @@ const IRCLogsDropdown = () => {
|
|||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute z-10 right-0 mt-2 px-3 py-2 bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
className="absolute z-10 right-0 mt-2 px-3 py-2 bg-white dark:bg-gray-825 divide-y divide-gray-200 dark:divide-gray-750 rounded-md shadow-lg border border-gray-750 focus:outline-none"
|
||||
>
|
||||
<Menu.Item>
|
||||
{() => (
|
||||
|
|
|
@ -5,126 +5,55 @@
|
|||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import { Link } from "react-router-dom";
|
||||
import Select from "react-select";
|
||||
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { GithubRelease } from "@app/types/Update";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import { LogLevelOptions, SelectOption } from "@domain/constants";
|
||||
|
||||
import { Section, RowItem } from "./_components";
|
||||
import * as common from "@components/inputs/common";
|
||||
import { LogFiles } from "@screens/Logs";
|
||||
|
||||
interface RowItemProps {
|
||||
label: string;
|
||||
value?: string;
|
||||
title?: string;
|
||||
emptyText?: string;
|
||||
newUpdate?: GithubRelease;
|
||||
}
|
||||
|
||||
const RowItem = ({ label, value, title, emptyText }: RowItemProps) => {
|
||||
return (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-900 dark:text-white text-sm" title={title}>{label}</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white text-sm sm:mt-0 sm:col-span-2 break-all truncate">
|
||||
<span className="px-1.5 py-1 bg-gray-200 dark:bg-gray-700 rounded shadow">{value ? value : emptyText}</span>
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
type SelectWrapperProps = {
|
||||
id: string;
|
||||
value: unknown;
|
||||
onChange: any;
|
||||
options: unknown[];
|
||||
};
|
||||
|
||||
interface RowItemNumberProps {
|
||||
label: string;
|
||||
value?: string | number;
|
||||
title?: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
const RowItemNumber = ({ label, value, title, unit }: RowItemNumberProps) => {
|
||||
return (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-900 dark:text-white text-sm" title={title}>{label}</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white text-sm sm:mt-0 sm:col-span-2 break-all truncate">
|
||||
<span className="px-1.5 py-1 bg-gray-200 dark:bg-gray-700 rounded shadow truncate">{value}</span>
|
||||
{unit &&
|
||||
<span className="ml-1 text-sm text-gray-700 dark:text-gray-400">{unit}</span>
|
||||
}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Input = (props: InputProps) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Control = (props: ControlProps) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Option = (props: OptionProps) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RowItemSelect = ({ id, title, label, value, options, onChange }: any) => {
|
||||
return (
|
||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||
<dt className="font-medium text-gray-500 dark:text-white" title={title}>{label}:</dt>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2 break-all">
|
||||
<Select
|
||||
id={id}
|
||||
components={{ Input, Control, Menu, Option }}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
color: "unset"
|
||||
})
|
||||
}}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
spacing: {
|
||||
...theme.spacing,
|
||||
controlHeight: 30,
|
||||
baseUnit: 2
|
||||
}
|
||||
})}
|
||||
value={value && options.find((o: any) => o.value == value)}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const SelectWrapper = ({ id, value, onChange, options }: SelectWrapperProps) => (
|
||||
<Select
|
||||
id={id}
|
||||
components={{
|
||||
Input: common.SelectInput,
|
||||
Control: common.SelectControl,
|
||||
Menu: common.SelectMenu,
|
||||
Option: common.SelectOption,
|
||||
IndicatorSeparator: common.IndicatorSeparator,
|
||||
DropdownIndicator: common.DropdownIndicator
|
||||
}}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
color: "unset"
|
||||
})
|
||||
}}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
spacing: {
|
||||
...theme.spacing,
|
||||
controlHeight: 30,
|
||||
baseUnit: 2
|
||||
}
|
||||
})}
|
||||
value={value && options.find((o: any) => o.value == value)}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
|
||||
function LogSettings() {
|
||||
const { isLoading, data } = useQuery({
|
||||
|
@ -140,58 +69,58 @@ function LogSettings() {
|
|||
const setLogLevelUpdateMutation = useMutation({
|
||||
mutationFn: (value: string) => APIClient.config.update({ log_level: value }),
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t}/>);
|
||||
toast.custom((t) => <Toast type="success" body={"Config successfully updated!"} t={t} />);
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["config"] });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Logs</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Set level, size etc.
|
||||
</p>
|
||||
</div>
|
||||
<Section
|
||||
title="Logs"
|
||||
description={
|
||||
<>
|
||||
Configure log level, log size rotation, etc. You can download your old log files
|
||||
{" "}
|
||||
<Link
|
||||
to="/logs"
|
||||
className="text-gray-700 dark:text-gray-200 underline font-semibold underline-offset-2 decoration-blue-500 decoration hover:text-black hover:dark:text-gray-100"
|
||||
>
|
||||
on the Logs page
|
||||
</Link>.
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="-mx-4 lg:col-span-9">
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-750">
|
||||
{!isLoading && data && (
|
||||
<form className="divide-y divide-gray-200 dark:divide-gray-750" action="#" method="POST">
|
||||
<RowItem label="Path" value={data?.log_path} title="Set in config.toml" emptyText="Not set!"/>
|
||||
<RowItem
|
||||
className="sm:col-span-1"
|
||||
label="Level"
|
||||
title="Log level"
|
||||
value={
|
||||
<SelectWrapper
|
||||
id="log_level"
|
||||
value={data?.log_level}
|
||||
options={LogLevelOptions}
|
||||
onChange={(value: SelectOption) => setLogLevelUpdateMutation.mutate(value.value)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<RowItem label="Max Size" value={data?.log_max_size} title="Set in config.toml" rightSide="MB"/>
|
||||
<RowItem label="Max Backups" value={data?.log_max_backups} title="Set in config.toml"/>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="px-6 pt-4">
|
||||
<LogFiles/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||
{!isLoading && data && (
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<RowItem label="Path" value={data?.log_path} title="Set in config.toml" emptyText="Not set!" />
|
||||
<RowItemSelect id="log_level" label="Level" value={data?.log_level} title="Log level" options={LogLevelOptions} onChange={(value: SelectOption) => setLogLevelUpdateMutation.mutate(value.value)} />
|
||||
<RowItemNumber label="Max Size" value={data?.log_max_size} title="Set in config.toml" unit="MB" />
|
||||
<RowItemNumber label="Max Backups" value={data?.log_max_backups} title="Set in config.toml" />
|
||||
</dl>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col py-4 px-4 sm:px-6">
|
||||
<LogFiles />
|
||||
</div>
|
||||
|
||||
{/*<div className="mt-4 flex justify-end py-4 px-4 sm:px-6">*/}
|
||||
{/* <button*/}
|
||||
{/* type="button"*/}
|
||||
{/* className="inline-flex justify-center rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"*/}
|
||||
{/* >*/}
|
||||
{/* Cancel*/}
|
||||
{/* </button>*/}
|
||||
{/* <button*/}
|
||||
{/* type="submit"*/}
|
||||
{/* className="ml-5 inline-flex justify-center rounded-md border border-transparent bg-blue-700 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"*/}
|
||||
{/* >*/}
|
||||
{/* Save*/}
|
||||
{/* </button>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,16 +4,17 @@
|
|||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Switch } from "@headlessui/react";
|
||||
|
||||
import { APIClient } from "@api/APIClient";
|
||||
import { EmptySimple } from "@components/emptystates";
|
||||
import { useToggle } from "@hooks/hooks";
|
||||
import { NotificationAddForm, NotificationUpdateForm } from "@forms/settings/NotificationForms";
|
||||
import { classNames } from "@utils";
|
||||
import { componentMapType } from "@forms/settings/DownloadClientForms";
|
||||
import Toast from "@components/notifications/Toast";
|
||||
import toast from "react-hot-toast";
|
||||
import { Section } from "./_components";
|
||||
import { PlusIcon } from "@heroicons/react/24/solid";
|
||||
import { Checkbox } from "@components/Checkbox";
|
||||
|
||||
export const notificationKeys = {
|
||||
all: ["notifications"] as const,
|
||||
|
@ -28,50 +29,42 @@ function NotificationSettings() {
|
|||
const { data } = useQuery({
|
||||
queryKey: notificationKeys.lists(),
|
||||
queryFn: APIClient.notifications.getAll,
|
||||
refetchOnWindowFocus: false }
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="lg:col-span-9">
|
||||
<Section
|
||||
title="Notifications"
|
||||
description="Send notifications on events."
|
||||
rightSide={
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5 mr-1" />
|
||||
Add new
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Send notifications on events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{data && data.length > 0 ? (
|
||||
<ul className="min-w-full">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-2 sm:col-span-1 pl-1 sm:pl-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-6 pl-10 sm:pl-12 pr-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="hidden md:flex col-span-2 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||
<div className="hidden md:flex col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
</li>
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-2 sm:col-span-1 pl-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-6 pl-10 md:pl-12 pr-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="hidden md:flex col-span-2 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||
<div className="hidden md:flex col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((n: ServiceNotification) => (
|
||||
<ListItem key={n.id} notification={n} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No notifications" subtitle="" buttonText="Create new notification" buttonAction={toggleAddNotifications} />}
|
||||
</div>
|
||||
</div>
|
||||
{data.map((n) => <ListItem key={n.id} notification={n} />)}
|
||||
</ul>
|
||||
) : (
|
||||
<EmptySimple title="No notifications" subtitle="" buttonText="Create new notification" buttonAction={toggleAddNotifications} />
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -95,14 +88,14 @@ const TelegramIcon = () => (
|
|||
const PushoverIcon = () => (
|
||||
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" className="mr-2 h-4">
|
||||
<path d="m495.6 319.4 104-13.7-101.3 228.6c17.8-1.4 35.2-7.4 52.3-18.1 17.1-10.7 32.9-24.2 47.2-40.4 14.4-16.2 26.8-34.2 37.3-54.1 10.5-19.8 18-39.4 22.6-58.5 2.7-11.9 4-23.3 3.8-34.2-.2-10.9-3.1-20.5-8.6-28.7s-13.8-14.8-25-19.8-26.3-7.5-45.5-7.5c-22.4 0-44.4 3.6-66 10.9-21.7 7.3-41.7 17.9-60.2 31.8-18.5 13.9-34.5 31.2-48.2 52-13.7 20.8-23.5 44.4-29.4 70.8-2.3 8.7-3.6 15.6-4.1 20.9-.5 5.3-.6 9.6-.3 13 .2 3.4.7 6.1 1.4 7.9.7 1.8 1.3 3.6 1.7 5.5-23.3 0-40.3-4.7-51-14-10.7-9.3-13.3-25.7-7.9-48.9 5.5-24.2 17.9-47.2 37.3-69.1 19.4-21.9 42.4-41.2 69.1-57.8 26.7-16.6 55.9-29.9 87.6-39.7 31.7-9.8 62.6-14.7 92.7-14.7 26.5 0 48.7 3.8 66.7 11.3 18 7.5 32.1 17.5 42.1 29.8s16.3 26.7 18.8 43.1c2.5 16.4 1.7 33.5-2.4 51.3-5 21.4-14.5 43-28.4 64.7-13.9 21.7-31.4 41.3-52.3 58.8-21 17.6-45 31.8-72.2 42.8-27.1 10.9-56 16.4-86.6 16.4h-3.4l-86.9 195H302l193.6-435.4z"
|
||||
clipRule="evenodd" fill="currentColor" fillRule="evenodd"/>
|
||||
clipRule="evenodd" fill="currentColor" fillRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const GotifyIcon = () => (
|
||||
<svg viewBox="0 0 140 140" xmlns="http://www.w3.org/2000/svg" className="mr-2 h-4">
|
||||
<path d="m 114.5,21.4 c -11.7,0 -47.3,5.9 -54.3,7.1 -47.3,8.0 -48.4,9.9 -50.1,12.8 -1.2,2.1 -2.4,4.0 2.6,29.4 2.3,11.5 5.8,26.9 8.8,35.8 1.8,5.4 3.6,8.8 6.9,10.1 0.8,0.3 1.7,0.5 2.7,0.6 0.2,0.0 0.3,0.0 0.5,0.0 12.8,0 89.1,-19.5 89.9,-19.7 1.4,-0.4 4.0,-1.5 5.3,-5.1 1.8,-4.7 1.9,-16.7 0.5,-35.7 -2.1,-28.0 -4.1,-31.0 -4.8,-32.0 -2.0,-3.1 -5.6,-3.3 -6.7,-3.3 -0.4,-0.0 -0.9,-0.0 -1.4,-0.0 z m -1.9,6.6 c -9.3,12.0 -18.9,24.0 -25.9,32.4 -2.3,2.8 -4.3,5.1 -6.0,7.0 -1.7,1.9 -2.9,3.2 -3.8,4.0 l -0.3,0.3 -0.4,-0.1 c -1.0,-0.3 -2.5,-0.9 -4.4,-1.7 -2.3,-1.0 -5.2,-2.3 -8.8,-3.9 C 51.6,60.7 34.4,52.2 18.0,43.6 30.3,39.7 95.0,28.7 112.6,27.9 Z m 5.7,5.0 c 2.0,11.8 4.5,42.6 3.1,54.0 -1.8,-1.4 -10.1,-8.0 -19.8,-15.2 -3.0,-2.3 -5.9,-4.3 -8.4,-6.1 l -0.7,-0.5 0.5,-0.6 C 99.5,56.9 108.0,46.2 118.3,32.9 Z M 16.1,51.1 c 3.0,1.5 14.3,7.4 27.4,13.8 5.3,2.6 9.9,4.8 13.9,6.7 l 0.9,0.4 -0.7,0.8 C 50.3,81.2 40.6,92.8 28.8,107.2 24.5,96.7 17.9,65.0 16.1,51.1 Z m 71.5,19.7 0.6,0.4 c 7.8,5.5 18.1,13.2 27.9,21.0 C 104.9,95.1 53.2,107.9 36.0,110.3 46.6,97.4 57.3,84.7 65.1,75.8 l 0.4,-0.4 0.5,0.2 c 5.7,2.5 9.3,3.7 11.1,3.8 0.1,0.0 0.2,0.0 0.3,0.0 0.6,0 1.0,-0.1 1.4,-0.3 0.6,-0.2 2.0,-0.7 8.3,-7.7 z"
|
||||
clipRule="evenodd" fill="currentColor" fillRule="evenodd"/>
|
||||
clipRule="evenodd" fill="currentColor" fillRule="evenodd" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
@ -143,27 +136,14 @@ function ListItem({ notification }: ListItemProps) {
|
|||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
|
||||
<div className="grid grid-cols-12 items-center py-4">
|
||||
<div className="col-span-2 sm:col-span-1 px-6 flex items-center ">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={onToggleMutation}
|
||||
className={classNames(
|
||||
notification.enabled ? "bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<div className="grid grid-cols-12 items-center py-2">
|
||||
<div className="col-span-2 sm:col-span-1 pl-1 py-0.5 sm:pl-5 flex items-center">
|
||||
<Checkbox
|
||||
value={notification.enabled}
|
||||
setValue={onToggleMutation}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-8 md:col-span-6 pl-10 md:pl-12 pr-2 sm:pr-6 truncate block items-center text-sm font-medium text-gray-900 dark:text-white" title={notification.name}>
|
||||
<div className="col-span-8 md:col-span-6 pl-10 sm:pl-12 pr-2 sm:pr-6 truncate block items-center text-sm font-medium text-gray-900 dark:text-white" title={notification.name}>
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="hidden md:flex col-span-2 items-center">
|
||||
|
@ -179,13 +159,12 @@ function ListItem({ notification }: ListItemProps) {
|
|||
</div>
|
||||
<div className="col-span-1 flex first-letter:px-6 whitespace-nowrap text-right text-sm font-medium">
|
||||
<span
|
||||
className="col-span-1 px-6 text-blue-600 dark:text-gray-300 hover:text-blue-900 dark:hover:text-blue-500 cursor-pointer"
|
||||
className="col-span-1 px-0 sm:px-6 text-blue-600 dark:text-gray-300 hover:text-blue-900 dark:hover:text-blue-500 cursor-pointer"
|
||||
onClick={toggleUpdateForm}
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -60,11 +60,11 @@ const RegexPlayground = () => {
|
|||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="py-6 px-4 sm:p-6">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<h2 className="text-lg leading-4 font-bold text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Regex playground. Experiment with your filters here. WIP.
|
||||
Regex playground. Experiment with your filters here. WIP.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,20 +73,20 @@ const RegexPlayground = () => {
|
|||
htmlFor="input-regex"
|
||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
RegExp filter
|
||||
RegExp filter
|
||||
</label>
|
||||
<input
|
||||
ref={regexRef}
|
||||
id="input-regex"
|
||||
type="text"
|
||||
autoComplete="true"
|
||||
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
className="mt-1 mb-4 block w-full border rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 border-gray-300 dark:border-gray-700 bg-gray-100 dark:bg-gray-815 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
<label
|
||||
htmlFor="input-lines"
|
||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
Lines to match
|
||||
Lines to match
|
||||
</label>
|
||||
<div
|
||||
id="input-lines"
|
||||
|
@ -95,10 +95,10 @@ const RegexPlayground = () => {
|
|||
contentEditable
|
||||
></div>
|
||||
</div>
|
||||
<div className="py-4 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="py-6 px-4 sm:p-6">
|
||||
<div>
|
||||
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Matches
|
||||
Matches
|
||||
</h3>
|
||||
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
|
||||
{output}
|
||||
|
|
|
@ -12,40 +12,30 @@ import Toast from "@components/notifications/Toast";
|
|||
import { releaseKeys } from "@screens/releases/ReleaseTable";
|
||||
import { useToggle } from "@hooks/hooks";
|
||||
import { DeleteModal } from "@components/modals";
|
||||
import { Section } from "./_components";
|
||||
|
||||
function ReleaseSettings() {
|
||||
return (
|
||||
<div className="lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
const ReleaseSettings = () => (
|
||||
<Section
|
||||
title="Releases"
|
||||
description="Manage release history."
|
||||
>
|
||||
<div className="border border-red-500 rounded">
|
||||
<div className="py-6 px-4 sm:p-6">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
Releases
|
||||
</h2>
|
||||
<h2 className="text-lg leading-4 font-bold text-gray-900 dark:text-white">Danger zone</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage release history.
|
||||
This will clear release history in your database
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-6 px-4">
|
||||
<div className="border border-red-500 rounded">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger zone</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
This will clear release history in your database
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<DeleteReleases />
|
||||
</div>
|
||||
</div>
|
||||
<div className="py-6 px-4 sm:p-6">
|
||||
<DeleteReleases />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</Section>
|
||||
);
|
||||
|
||||
|
||||
const getDurationLabel = (durationValue: number): string => {
|
||||
const durationOptions: Record<number, string> = {
|
||||
|
@ -98,7 +88,7 @@ function DeleteReleases() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center rounded-md">
|
||||
<div className="flex flex-col sm:flex-row gap-2 justify-between items-center rounded-md">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
isLoading={deleteOlderMutation.isLoading}
|
||||
|
@ -113,7 +103,7 @@ function DeleteReleases() {
|
|||
<p className="text-sm font-medium text-gray-900 dark:text-white">Delete</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Delete releases older than select duration</p>
|
||||
</label>
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<select
|
||||
name="duration"
|
||||
id="duration"
|
||||
|
@ -139,7 +129,7 @@ function DeleteReleases() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={toggleDeleteModal}
|
||||
className="ml-2 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-red-700 hover:text-red-800 dark:text-white bg-red-200 dark:bg-red-700 hover:bg-red-300 dark:hover:bg-red-800 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-red-600"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-red-700 hover:text-red-800 dark:text-white bg-red-200 dark:bg-red-700 hover:bg-red-300 dark:hover:bg-red-800 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-red-600"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
|
|
84
web/src/screens/settings/_components.tsx
Normal file
84
web/src/screens/settings/_components.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
import { classNames } from "@utils";
|
||||
|
||||
type SectionProps = {
|
||||
title: string;
|
||||
description: string | React.ReactNode;
|
||||
rightSide?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Section = ({
|
||||
title,
|
||||
description,
|
||||
rightSide,
|
||||
children
|
||||
}: SectionProps) => (
|
||||
<div className="pb-6 px-4 lg:col-span-9">
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-6 mb-4",
|
||||
rightSide
|
||||
? "flex justify-between items-start flex-wrap sm:flex-nowrap gap-2"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<div className="sm:px-2">
|
||||
<h2 className="text-lg leading-4 font-bold text-gray-900 dark:text-white">{title}</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
{rightSide ?? null}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface RowItemProps {
|
||||
label: string;
|
||||
value?: string | React.ReactNode;
|
||||
title?: string;
|
||||
emptyText?: string;
|
||||
rightSide?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RowItem = ({
|
||||
label,
|
||||
value,
|
||||
title,
|
||||
emptyText,
|
||||
rightSide,
|
||||
className = "sm:col-span-3"
|
||||
}: RowItemProps) => (
|
||||
<div className="p-4 sm:px-6 sm:grid sm:grid-cols-4 sm:gap-4">
|
||||
<div className="font-medium text-gray-900 dark:text-white text-sm self-center" title={title}>
|
||||
{label}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
"mt-1 text-gray-900 dark:text-gray-300 text-sm break-all sm:mt-0"
|
||||
)}
|
||||
>
|
||||
{value
|
||||
? (
|
||||
<>
|
||||
{typeof (value) === "string" ? (
|
||||
<span className="px-1.5 py-1 bg-gray-200 dark:bg-gray-700 rounded shadow text-ellipsis leading-7">
|
||||
{value}
|
||||
</span>
|
||||
) : value}
|
||||
{rightSide ?? null}
|
||||
</>
|
||||
)
|
||||
: (emptyText ?? null)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue