refactor(web) add eslint (#222)

* fix(tsconfig.json): changed skipLibCheck to false.
refactor(eslint): moved configuration from package.json to .eslintrc.js and added a typescript plugin for future use

* feat: wip eslint and types

* feat: fix identation

* feat: get rid of last any types
This commit is contained in:
stacksmash76 2022-05-17 06:44:07 +02:00 committed by GitHub
parent 7f06a4c707
commit cb8f280e86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 6797 additions and 6541 deletions

71
web/.eslintrc.js Normal file
View file

@ -0,0 +1,71 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
plugins: [
"@typescript-eslint",
],
// If we ever decide on a code-style, I'll leave this here.
//extends: [
// "airbnb",
// "airbnb/hooks",
// "airbnb-typescript",
//],
rules: {
// Turn off pesky "react not in scope" error while
// we transition to proper ESLint support
"react/react-in-jsx-scope": "off",
// Add a UNIX-style linebreak at the end of each file
"linebreak-style": ["error", "unix"],
// Allow only double quotes and backticks
quotes: ["error", "double"],
// Warn if a line isn't indented with a multiple of 2
indent: ["warn", 2],
// Don't enforce any particular brace style
curly: "off",
// Let's keep these off for now and
// maybe turn these back on sometime in the future
"import/prefer-default-export": "off",
"react/function-component-definition": "off",
"nonblock-statement-body-position": ["warn", "below"]
},
// Conditionally run the following configuration only for TS files.
// Otherwise, this will create inter-op problems with JS files.
overrides: [
{
// Run only .ts and .tsx files
files: ["*.ts", "*.tsx"],
// Define the @typescript-eslint plugin schemas
extends: [
"plugin:@typescript-eslint/recommended",
// Don't require strict type-checking for now, since we have too many
// dubious statements literred in the code.
//"plugin:@typescript-eslint/recommended-requiring-type-checking",
],
parserOptions: {
project: "tsconfig.json",
// This is needed so we can always point to the tsconfig.json
// file relative to the current .eslintrc.js file.
// Generally, a problem occurrs when "npm run lint"
// gets ran from another directory. This fixes it.
tsconfigRootDir: __dirname,
sourceType: "module",
},
// Override JS rules and apply @typescript-eslint rules
// as they might interfere with eachother.
rules: {
quotes: "off",
"@typescript-eslint/quotes": ["error", "double"],
semi: "off",
"@typescript-eslint/semi": ["warn", "always"],
// indent: "off",
indent: ["warn", 2],
"@typescript-eslint/indent": "off",
"@typescript-eslint/comma-dangle": "warn",
"keyword-spacing": "off",
"@typescript-eslint/keyword-spacing": ["error"],
"object-curly-spacing": "off",
"@typescript-eslint/object-curly-spacing": ["warn", "always"],
},
},
],
};

View file

@ -29,7 +29,7 @@
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"lint": "esw src/ --ext \".ts,.tsx,.js,.jsx\" --color",
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx --color",
"lint:watch": "npm run lint -- --watch"
},
"browserslist": {
@ -59,8 +59,8 @@
"@types/react-dom": "17.0.0",
"@types/react-router-dom": "^5.1.7",
"@types/react-table": "^7.7.7",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2",
"eslint": "^8.8.0",
"eslint-plugin-import": "^2.25.4",
@ -71,59 +71,5 @@
"postcss": "^8.4.6",
"tailwindcss": "^3.0.18",
"typescript": "^4.1.2"
},
"eslintConfig": {
"root": true,
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended"
],
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"settings": {
"react": {
"version": "detect"
},
"import/resolver": {
"node": {
"extensions": [
".ts",
".tsx",
".js",
".jsx"
]
}
}
},
"env": {
"browser": true,
"node": true,
"jquery": false
},
"globals": {}
}
}

View file

@ -16,7 +16,7 @@ import Toast from "./components/notifications/Toast";
export const queryClient = new QueryClient({
defaultOptions: {
queries: { useErrorBoundary: true, },
queries: { useErrorBoundary: true },
mutations: {
onError: (error) => {
// Use a format string to convert the error object to a proper string without much hassle.
@ -27,8 +27,8 @@ export const queryClient = new QueryClient({
);
toast.custom((t) => <Toast type="error" body={message} t={t} />);
}
},
},
}
}
});
export function App() {

View file

@ -1,12 +1,14 @@
import {baseUrl, sseBaseUrl} from "../utils";
import {AuthContext} from "../utils/Context";
import {Cookies} from "react-cookie";
import { baseUrl, sseBaseUrl } from "../utils";
import { AuthContext } from "../utils/Context";
import { Cookies } from "react-cookie";
interface ConfigType {
body?: BodyInit | Record<string, unknown> | null;
body?: BodyInit | Record<string, unknown> | unknown | null;
headers?: Record<string, string>;
}
type PostBody = BodyInit | Record<string, unknown> | unknown | null;
export async function HttpClient<T>(
endpoint: string,
method: string,
@ -29,7 +31,7 @@ export async function HttpClient<T>(
// if 401 consider the session expired and force logout
const cookies = new Cookies();
cookies.remove("user_session");
AuthContext.reset()
AuthContext.reset();
return Promise.reject(new Error(response.statusText));
}
@ -56,25 +58,32 @@ export async function HttpClient<T>(
const appClient = {
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
Post: <T>(endpoint: string, data: any) => HttpClient<void | T>(endpoint, "POST", { body: data }),
Put: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PUT", { body: data }),
Patch: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PATCH", { body: data }),
Post: <T>(endpoint: string, data: PostBody) => HttpClient<void | T>(endpoint, "POST", { body: data }),
PostBody: <T>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "POST", { body: data }),
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }),
Patch: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PATCH", { body: data }),
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
}
};
export const APIClient = {
auth: {
login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }),
login: (username: string, password: string) => appClient.Post("api/auth/login", {
username: username,
password: password
}),
logout: () => appClient.Post("api/auth/logout", null),
validate: () => appClient.Get<void>("api/auth/validate"),
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { username: username, password: password }),
canOnboard: () => appClient.Get("api/auth/onboard"),
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", {
username: username,
password: password
}),
canOnboard: () => appClient.Get("api/auth/onboard")
},
actions: {
create: (action: Action) => appClient.Post("api/actions", action),
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null),
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null)
},
config: {
get: () => appClient.Get<Config>("api/config")
@ -84,7 +93,7 @@ export const APIClient = {
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc),
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc)
},
filters: {
getAll: () => appClient.Get<Filter[]>("api/filters"),
@ -93,14 +102,14 @@ export const APIClient = {
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
delete: (id: number) => appClient.Delete(`api/filters/${id}`),
delete: (id: number) => appClient.Delete(`api/filters/${id}`)
},
feeds: {
find: () => appClient.Get<Feed[]>("api/feeds"),
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
delete: (id: number) => appClient.Delete(`api/feeds/${id}`),
delete: (id: number) => appClient.Delete(`api/feeds/${id}`)
},
indexers: {
// returns indexer options for all currently present/enabled indexers
@ -109,15 +118,15 @@ export const APIClient = {
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
// returns all possible indexer definitions
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
create: (indexer: Indexer) => appClient.Post<Indexer>("api/indexer", indexer),
create: (indexer: Indexer) => appClient.PostBody<Indexer>("api/indexer", indexer),
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
delete: (id: number) => appClient.Delete(`api/indexer/${id}`),
delete: (id: number) => appClient.Delete(`api/indexer/${id}`)
},
irc: {
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`)
},
events: {
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
@ -126,7 +135,7 @@ export const APIClient = {
getAll: () => appClient.Get<Notification[]>("api/notification"),
create: (notification: Notification) => appClient.Post("api/notification", notification),
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
delete: (id: number) => appClient.Delete(`api/notification/${id}`),
delete: (id: number) => appClient.Delete(`api/notification/${id}`)
},
release: {
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
@ -148,10 +157,10 @@ export const APIClient = {
params.append("push_status", filter.value);
});
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`)
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`);
},
indexerOptions: () => appClient.Get<string[]>(`api/release/indexers`),
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
delete: () => appClient.Delete(`api/release/all`),
delete: () => appClient.Delete("api/release/all")
}
};

View file

@ -23,11 +23,11 @@ export const Checkbox = ({ label, description, value, setValue }: CheckboxProps)
checked={value}
onChange={setValue}
className={
`${value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'
`${value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700"
} ml-4 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={`${value ? '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`}
className={`${value ? "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>
</Switch.Group>

View file

@ -51,7 +51,8 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
role="alert"
>
<div className="flex items-center">
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
@ -77,11 +78,11 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
resetErrorBoundary();
}}
>
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5" />
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5"/>
Reset page state
</button>
</div>
</div>
</div>
);
}
};

View file

@ -1,33 +1,36 @@
import { classNames } from "../../utils"
import React from "react";
import { classNames } from "../../utils";
interface ButtonProps {
className?: string;
children: any;
[rest: string]: any;
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
}
export const Button = ({ children, className, ...rest }: ButtonProps) => (
export const Button = ({ children, className, disabled, onClick }: ButtonProps) => (
<button
type="button"
className={classNames(
className ?? "",
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50"
)}
{...rest}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
export const PageButton = ({ children, className, ...rest }: ButtonProps) => (
export const PageButton = ({ children, className, disabled, onClick }: ButtonProps) => (
<button
type="button"
className={classNames(
className ?? "",
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
)}
{...rest}
disabled={disabled}
onClick={onClick}
>
{children}
</button>

View file

@ -26,8 +26,6 @@ export const TitleCell = ({ value }: CellProps) => (
interface ReleaseStatusCellProps {
value: ReleaseActionStatus[];
column: any;
row: any;
}
interface StatusCellMapEntry {

View file

@ -1,4 +1,4 @@
import { FC } from "react"
import { FC } from "react";
interface DebugProps {
values: unknown;
@ -10,10 +10,8 @@ const DEBUG: FC<DebugProps> = ({ values }) => {
}
return (
<div className="w-full p-2 flex flex-col mt-6 bg-gray-100 dark:bg-gray-900">
<pre className="overflow-x-auto dark:text-gray-400">
{JSON.stringify(values, undefined, 2)}
</pre>
<div className="w-full p-2 flex flex-col mt-12 mb-12 bg-gray-100 dark:bg-gray-900">
<pre className="dark:text-gray-400">{JSON.stringify(values, null, 2)}</pre>
</div>
);
};

View file

@ -29,12 +29,12 @@ export const EmptySimple = ({
</div>
) : null}
</div>
)
);
interface EmptyListStateProps {
text: string;
buttonText?: string;
buttonOnClick?: any;
buttonOnClick?: () => void;
}
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
@ -51,5 +51,5 @@ export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListSta
</button>
)}
</div>
)
);
}

View file

@ -10,4 +10,4 @@ export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
</div>
)
);

View file

@ -1,14 +1,13 @@
import { Field } from "formik";
import { Field, FieldProps } from "formik";
interface ErrorFieldProps {
name: string;
classNames?: string;
subscribe?: any;
}
const ErrorField = ({ name, classNames }: ErrorFieldProps) => (
<Field name={name} subscribe={{ touched: true, error: true }}>
{({ meta: { touched, error } }: any) =>
{({ meta: { touched, error } }: FieldProps) =>
touched && error ? <span className={classNames}>{error}</span> : null
}
</Field>
@ -41,6 +40,6 @@ const CheckboxField = ({
<p className="text-gray-500">{sublabel}</p>
</div>
</div>
)
);
export { ErrorField, CheckboxField }
export { ErrorField, CheckboxField };

View file

@ -2,6 +2,6 @@ export { ErrorField, CheckboxField } from "./common";
export { TextField, NumberField, PasswordField } from "./input";
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
export { RadioFieldsetWide } from "./radio";
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect} from "./select";
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect } from "./select";
export { SwitchGroup } from "./switch";

View file

@ -1,4 +1,4 @@
import { Field } from "formik";
import { Field, FieldProps } from "formik";
import { classNames } from "../../utils";
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
import { useToggle } from "../../hooks/hooks";
@ -22,12 +22,12 @@ export const TextField = ({
placeholder,
columns,
autoComplete,
hidden,
hidden
}: TextFieldProps) => (
<div
className={classNames(
hidden ? "hidden" : "",
columns ? `col-span-${columns}` : "col-span-12",
columns ? `col-span-${columns}` : "col-span-12"
)}
>
{label && (
@ -38,8 +38,8 @@ export const TextField = ({
<Field name={name}>
{({
field,
meta,
}: any) => (
meta
}: FieldProps) => (
<div>
<input
{...field}
@ -58,7 +58,7 @@ export const TextField = ({
)}
</Field>
</div>
)
);
interface PasswordFieldProps {
name: string;
@ -81,7 +81,7 @@ export const PasswordField = ({
help,
required
}: PasswordFieldProps) => {
const [isVisible, toggleVisibility] = useToggle(false)
const [isVisible, toggleVisibility] = useToggle(false);
return (
<div
@ -97,8 +97,8 @@ export const PasswordField = ({
<Field name={name} defaultValue={defaultValue}>
{({
field,
meta,
}: any) => (
meta
}: FieldProps) => (
<div className="sm:col-span-2 relative">
<input
{...field}
@ -124,8 +124,8 @@ export const PasswordField = ({
)}
</Field>
</div>
)
}
);
};
interface NumberFieldProps {
name: string;
@ -138,7 +138,7 @@ export const NumberField = ({
name,
label,
placeholder,
step,
step
}: NumberFieldProps) => (
<div className="col-span-12 sm:col-span-6">
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
@ -148,8 +148,8 @@ export const NumberField = ({
<Field name={name} type="number">
{({
field,
meta,
}: any) => (
meta
}: FieldProps) => (
<div className="sm:col-span-2">
<input
type="number"

View file

@ -4,7 +4,7 @@ import { classNames } from "../../utils";
import { useToggle } from "../../hooks/hooks";
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
import { Switch } from "@headlessui/react";
import { ErrorField } from "./common"
import { ErrorField } from "./common";
interface TextFieldWideProps {
name: string;
@ -56,7 +56,7 @@ export const TextFieldWide = ({
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div>
)
);
interface PasswordFieldWideProps {
name: string;
@ -77,7 +77,7 @@ export const PasswordFieldWide = ({
required,
defaultVisible
}: PasswordFieldWideProps) => {
const [isVisible, toggleVisibility] = useToggle(defaultVisible)
const [isVisible, toggleVisibility] = useToggle(defaultVisible);
return (
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
@ -114,8 +114,8 @@ export const PasswordFieldWide = ({
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div>
)
}
);
};
interface NumberFieldWideProps {
name: string;
@ -154,7 +154,7 @@ export const NumberFieldWide = ({
id={name}
type="number"
value={field.value ? field.value : defaultValue ?? 0}
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)) }}
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)); }}
className={classNames(
meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500"
@ -213,19 +213,19 @@ export const SwitchGroupWide = ({
value={field.value}
checked={field.checked ?? false}
onChange={value => {
form.setFieldValue(field?.name ?? '', value)
form.setFieldValue(field?.name ?? "", value);
}}
className={classNames(
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
'ml-4 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'
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-500",
"ml-4 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(
field.value ? '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'
field.value ? "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>
@ -233,5 +233,5 @@ export const SwitchGroupWide = ({
</Field>
</Switch.Group>
</ul>
)
);

View file

@ -15,15 +15,20 @@ interface props {
options: radioFieldsetOption[];
}
interface anyObj {
[key: string]: string
}
function RadioFieldsetWide({ name, legend, options }: props) {
const {
values,
setFieldValue,
} = useFormikContext<any>();
setFieldValue
} = useFormikContext<anyObj>();
const onChange = (value: string) => {
setFieldValue(name, value)
}
setFieldValue(name, value);
};
return (
<fieldset>

View file

@ -1,5 +1,5 @@
import { Fragment } from "react";
import { Field } from "formik";
import { Field, FieldProps } from "formik";
import { Transition, Listbox } from "@headlessui/react";
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
import { MultiSelect as RMSC } from "react-multi-select-component";
@ -7,10 +7,17 @@ import { MultiSelect as RMSC } from "react-multi-select-component";
import { classNames, COL_WIDTHS } from "../../utils";
import { SettingsContext } from "../../utils/Context";
export interface MultiSelectOption {
value: string | number;
label: string;
key?: string;
disabled?: boolean;
}
interface MultiSelectProps {
name: string;
label?: string;
options?: [] | any;
options: MultiSelectOption[];
columns?: COL_WIDTHS;
creatable?: boolean;
}
@ -20,14 +27,14 @@ export const MultiSelect = ({
label,
options,
columns,
creatable,
creatable
}: MultiSelectProps) => {
const settingsContext = SettingsContext.useValue();
const handleNewField = (value: string) => ({
value: value.toUpperCase(),
label: value.toUpperCase(),
key: value,
key: value
});
return (
@ -46,21 +53,20 @@ export const MultiSelect = ({
<Field name={name} type="select" multiple={true}>
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<RMSC
{...field}
type="select"
options={[...[...options, ...field.value.map((i: any) => ({ value: i.value ?? i, label: i.label ?? i}))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
options={[...[...options, ...field.value.map((i: MultiSelectOption) => ({ value: i.value ?? i, label: i.label ?? i }))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
labelledBy={name}
isCreatable={creatable}
onCreateOption={handleNewField}
value={field.value && field.value.map((item: any) => ({
value={field.value && field.value.map((item: MultiSelectOption) => ({
value: item.value ? item.value : item,
label: item.label ? item.label : item,
label: item.label ? item.label : item
}))}
onChange={(values: any) => {
const am = values && values.map((i: any) => i.value);
onChange={(values: Array<MultiSelectOption>) => {
const am = values && values.map((i) => i.value);
setFieldValue(field.name, am);
}}
@ -70,13 +76,18 @@ export const MultiSelect = ({
</Field>
</div>
);
};
interface IndexerMultiSelectOption {
id: number;
name: string;
}
export const IndexerMultiSelect = ({
name,
label,
options,
columns,
columns
}: MultiSelectProps) => {
const settingsContext = SettingsContext.useValue();
return (
@ -95,26 +106,26 @@ export const IndexerMultiSelect = ({
<Field name={name} type="select" multiple={true}>
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<RMSC
{...field}
type="select"
options={options}
labelledBy={name}
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))}
onChange={(values: any) => {
const am = values && values.map((i: any) => i.value);
setFieldValue(field.name, am);
value={field.value && field.value.map((item: IndexerMultiSelectOption) => ({
value: item.id, label: item.name
}))}
onChange={(values: MultiSelectOption[]) => {
const item = values && values.map((i) => ({ id: i.value, name: i.label }));
setFieldValue(field.name, item);
}}
className={settingsContext.darkTheme ? "dark" : ""}
itemHeight={50}
/>
)}
</Field>
</div>
);
}
};
interface DownloadClientSelectProps {
name: string;
@ -132,11 +143,11 @@ export function DownloadClientSelect({
<Field name={name} type="select">
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<Listbox
value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)}
onChange={(value) => setFieldValue(field?.name, value)}
>
{({ open }) => (
<>
@ -147,7 +158,7 @@ export function DownloadClientSelect({
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate">
{field.value
? clients.find((c) => c.id === field.value)!.name
? clients.find((c) => c.id === field.value)?.name
: "Choose a client"}
</span>
{/*<span className="block truncate">Choose a client</span>*/}
@ -171,7 +182,7 @@ export function DownloadClientSelect({
>
{clients
.filter((c) => c.type === action.type)
.map((client: any) => (
.map((client) => (
<Listbox.Option
key={client.id}
className={({ active }) => classNames(
@ -244,11 +255,11 @@ export const Select = ({
<Field name={name} type="select">
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<Listbox
value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)}
onChange={(value) => setFieldValue(field?.name, value)}
>
{({ open }) => (
<>
@ -259,7 +270,7 @@ export const Select = ({
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate">
{field.value
? options.find((c) => c.value === field.value)!.label
? options.find((c) => c.value === field.value)?.label
: optionDefaultText
}
</span>
@ -333,7 +344,7 @@ export const Select = ({
</Field>
</div>
);
}
};
export const SelectWide = ({
name,
@ -348,11 +359,11 @@ export const SelectWide = ({
<Field name={name} type="select">
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<Listbox
value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)}
onChange={(value) => setFieldValue(field?.name, value)}
>
{({ open }) => (
<div className="py-4 flex items-center justify-between">
@ -364,7 +375,7 @@ export const SelectWide = ({
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate">
{field.value
? options.find((c) => c.value === field.value)!.label
? options.find((c) => c.value === field.value)?.label
: optionDefaultText
}
</span>
@ -439,4 +450,4 @@ export const SelectWide = ({
</div>
</div>
);
}
};

View file

@ -1,3 +1,4 @@
import React from "react";
import { Field } from "formik";
import type {
FieldInputProps,
@ -9,15 +10,18 @@ import type {
import { Switch as HeadlessSwitch } from "@headlessui/react";
import { classNames } from "../../utils";
type SwitchProps<V = any> = {
label: string
type SwitchProps<V = unknown> = {
label?: string
checked: boolean
value: boolean
disabled?: boolean
onChange: (value: boolean) => void
field?: FieldInputProps<V>
form?: FormikProps<FormikValues>
meta?: FieldMetaProps<V>
}
children: React.ReactNode
className: string
};
export const Switch = ({
label,
@ -25,9 +29,9 @@ export const Switch = ({
disabled = false,
onChange: $onChange,
field,
form,
form
}: SwitchProps) => {
const checked = field?.checked ?? $checked
const checked = field?.checked ?? $checked;
return (
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
@ -38,32 +42,32 @@ export const Switch = ({
disabled={disabled}
checked={checked}
onChange={value => {
form?.setFieldValue(field?.name ?? '', value)
$onChange && $onChange(value)
form?.setFieldValue(field?.name ?? "", value);
$onChange && $onChange(value);
}}
className={classNames(
checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'ml-4 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'
checked ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
"ml-4 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"
)}
>
{({ checked }) => (
<span
aria-hidden="true"
className={classNames(
checked ? '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'
checked ? "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"
)}
/>
)}
</HeadlessSwitch>
</HeadlessSwitch.Group>
)
}
);
};
export type SwitchFormikProps = SwitchProps & FieldProps & React.InputHTMLAttributes<HTMLInputElement>;
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} />
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} children={props.children}/>;
interface SwitchGroupProps {
name: string;
@ -94,26 +98,26 @@ const SwitchGroup = ({
<Field name={name} type="checkbox">
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<Switch
{...field}
type="button"
// type="button"
value={field.value}
checked={field.checked}
checked={field.checked ?? false}
onChange={value => {
setFieldValue(field?.name ?? '', value)
setFieldValue(field?.name ?? "", value);
}}
className={classNames(
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
'ml-4 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'
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200",
"ml-4 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
aria-hidden="true"
className={classNames(
field.value ? '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'
field.value ? "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>
@ -123,4 +127,4 @@ const SwitchGroup = ({
</HeadlessSwitch.Group>
);
export { SwitchGroup }
export { SwitchGroup };

View file

@ -1,12 +1,12 @@
import { Fragment, FC } from "react";
import React, { Fragment, FC } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { ExclamationIcon } from "@heroicons/react/solid";
interface DeleteModalProps {
isOpen: boolean;
buttonRef: any;
toggle: any;
deleteAction: any;
buttonRef: React.MutableRefObject<HTMLElement | null> | undefined;
toggle: () => void;
deleteAction: () => void;
title: string;
text: string;
}
@ -79,7 +79,7 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={toggle}
ref={buttonRef}
// ref={buttonRef}
>
Cancel
</button>
@ -89,4 +89,4 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
</div>
</Dialog>
</Transition.Root>
)
);

View file

@ -1,30 +1,30 @@
import { FC } from 'react'
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
import { toast } from 'react-hot-toast'
import { classNames } from '../../utils'
import { FC } from "react";
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from "@heroicons/react/solid";
import { toast, Toast as Tooast } from "react-hot-toast";
import { classNames } from "../../utils";
type Props = {
type: 'error' | 'success' | 'warning'
type: "error" | "success" | "warning"
body?: string
t?: any;
}
t?: Tooast;
};
const Toast: FC<Props> = ({ type, body, t }) => (
<div className={classNames(
t.visible ? 'animate-enter' : 'animate-leave',
t?.visible ? "animate-enter" : "animate-leave",
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
<div className="p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
{type === 'error' && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
{type === "success" && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
{type === "error" && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
{type === "warning" && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
</div>
<div className="ml-3 w-0 flex-1 pt-0.5">
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
{type === 'success' && "Success"}
{type === 'error' && "Error"}
{type === 'warning' && "Warning"}
{type === "success" && "Success"}
{type === "error" && "Error"}
{type === "warning" && "Warning"}
</p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
</div>
@ -32,7 +32,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
<button
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => {
toast.dismiss(t.id)
toast.dismiss(t?.id);
}}
>
<span className="sr-only">Close</span>
@ -42,6 +42,6 @@ const Toast: FC<Props> = ({ type, body, t }) => (
</div>
</div>
</div>
)
);
export default Toast;

View file

@ -1,4 +1,4 @@
import { Fragment, useRef } from "react";
import React, { Fragment, useRef } from "react";
import { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Form, Formik } from "formik";
@ -10,7 +10,7 @@ import { classNames } from "../../utils";
interface SlideOverProps<DataType> {
title: string;
initialValues: DataType;
validate?: (values?: any) => void;
validate?: (values: DataType) => void;
onSubmit: (values?: DataType) => void;
isOpen: boolean;
toggle: () => void;
@ -30,7 +30,7 @@ function SlideOver<DataType>({
type,
children
}: SlideOverProps<DataType>): React.ReactElement {
const cancelModalButtonRef = useRef(null);
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
return (
@ -140,7 +140,7 @@ function SlideOver<DataType>({
</div>
</Dialog>
</Transition.Root>
)
);
}
export { SlideOver };

View file

@ -1,3 +1,5 @@
import { MultiSelectOption } from "../components/inputs/select";
export const resolutions = [
"2160p",
"1080p",
@ -9,7 +11,7 @@ export const resolutions = [
"480i"
];
export const RESOLUTION_OPTIONS = resolutions.map(r => ({ value: r, label: r, key: r}));
export const RESOLUTION_OPTIONS: MultiSelectOption[] = resolutions.map(r => ({ value: r, label: r, key: r }));
export const codecs = [
"HEVC",
@ -23,7 +25,7 @@ export const codecs = [
"XviD"
];
export const CODECS_OPTIONS = codecs.map(v => ({ value: v, label: v, key: v}));
export const CODECS_OPTIONS: MultiSelectOption[] = codecs.map(v => ({ value: v, label: v, key: v }));
export const sources = [
"BluRay",
@ -46,18 +48,18 @@ export const sources = [
"HDTS",
"HDTV",
"Mixed",
"SiteRip",
"SiteRip"
];
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
export const SOURCES_OPTIONS: MultiSelectOption[] = sources.map(v => ({ value: v, label: v, key: v }));
export const containers = [
"avi",
"mp4",
"mkv",
"mkv"
];
export const CONTAINER_OPTIONS = containers.map(v => ({ value: v, label: v, key: v}));
export const CONTAINER_OPTIONS: MultiSelectOption[] = containers.map(v => ({ value: v, label: v, key: v }));
export const hdr = [
"HDR",
@ -69,18 +71,18 @@ export const hdr = [
"DV HDR10",
"DV HDR10+",
"DoVi",
"Dolby Vision",
"Dolby Vision"
];
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
export const HDR_OPTIONS: MultiSelectOption[] = hdr.map(v => ({ value: v, label: v, key: v }));
export const quality_other = [
"REMUX",
"HYBRID",
"REPACK",
"REPACK"
];
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v}));
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v }));
export const formatMusic = [
"MP3",
@ -89,10 +91,10 @@ export const formatMusic = [
"Ogg",
"AAC",
"AC3",
"DTS",
"DTS"
];
export const FORMATS_OPTIONS = formatMusic.map(r => ({ value: r, label: r, key: r}));
export const FORMATS_OPTIONS: MultiSelectOption[] = formatMusic.map(r => ({ value: r, label: r, key: r }));
export const sourcesMusic = [
"CD",
@ -103,10 +105,10 @@ export const sourcesMusic = [
"DAT",
"Cassette",
"Blu-Ray",
"SACD",
"SACD"
];
export const SOURCES_MUSIC_OPTIONS = sourcesMusic.map(v => ({ value: v, label: v, key: v}));
export const SOURCES_MUSIC_OPTIONS: MultiSelectOption[] = sourcesMusic.map(v => ({ value: v, label: v, key: v }));
export const qualityMusic = [
"192",
@ -118,10 +120,10 @@ export const qualityMusic = [
"V1 (VBR)",
"V0 (VBR)",
"Lossless",
"24bit Lossless",
"24bit Lossless"
];
export const QUALITY_MUSIC_OPTIONS = qualityMusic.map(v => ({ value: v, label: v, key: v}));
export const QUALITY_MUSIC_OPTIONS: MultiSelectOption[] = qualityMusic.map(v => ({ value: v, label: v, key: v }));
export const releaseTypeMusic = [
"Album",
@ -138,19 +140,19 @@ export const releaseTypeMusic = [
"Demo",
"Concert Recording",
"DJ Mix",
"Unknown",
"Unknown"
];
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
export const RELEASE_TYPE_MUSIC_OPTIONS: MultiSelectOption[] = releaseTypeMusic.map(v => ({ value: v, label: v, key: v }));
export const originOptions = [
"P2P",
"Internal",
"SCENE",
"O-SCENE",
"O-SCENE"
];
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v}));
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v }));
export interface RadioFieldsetOption {
label: string;
@ -193,7 +195,7 @@ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
label: "Whisparr",
description: "Send to Whisparr and let it decide",
value: "WHISPARR"
},
}
];
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
@ -203,21 +205,21 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
"RADARR": "Radarr",
"SONARR": "Sonarr",
"LIDARR": "Lidarr",
"WHISPARR": "Whisparr",
"WHISPARR": "Whisparr"
};
export const ActionTypeOptions: RadioFieldsetOption[] = [
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
{label: "Webhook", description: "Run webhook", value: "WEBHOOK"},
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
{label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR"},
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR"},
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR"},
{label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR"},
{ label: "Test", description: "A simple action to test a filter.", value: "TEST" },
{ label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER" },
{ label: "Webhook", description: "Run webhook", value: "WEBHOOK" },
{ label: "Exec", description: "Run a custom command after a filter match", value: "EXEC" },
{ label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT" },
{ label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1" },
{ label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
{ label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" },
{ label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
{ label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR" }
];
export const ActionTypeNameMap = {
@ -231,13 +233,18 @@ export const ActionTypeNameMap = {
"RADARR": "Radarr",
"SONARR": "Sonarr",
"LIDARR": "Lidarr",
"WHISPARR": "Whisparr",
"WHISPARR": "Whisparr"
};
export const PushStatusOptions: any[] = [
export interface OptionBasic {
label: string;
value: string;
}
export const PushStatusOptions: OptionBasic[] = [
{
label: "Rejected",
value: "PUSH_REJECTED",
value: "PUSH_REJECTED"
},
{
label: "Approved",
@ -246,36 +253,36 @@ export const PushStatusOptions: any[] = [
{
label: "Error",
value: "PUSH_ERROR"
},
}
];
export const NotificationTypeOptions: any[] = [
export const NotificationTypeOptions: OptionBasic[] = [
{
label: "Discord",
value: "DISCORD",
},
value: "DISCORD"
}
];
export interface SelectOption {
label: string;
description: string;
value: any;
value: string;
}
export const EventOptions: SelectOption[] = [
{
label: "Push Rejected",
value: "PUSH_REJECTED",
description: "On push rejected for the arrs or download client",
description: "On push rejected for the arrs or download client"
},
{
label: "Push Approved",
value: "PUSH_APPROVED",
description: "On push approved for the arrs or download client",
description: "On push approved for the arrs or download client"
},
{
label: "Push Error",
value: "PUSH_ERROR",
description: "On push error for the arrs or download client",
},
description: "On push error for the arrs or download client"
}
];

View file

@ -46,9 +46,9 @@ import {
UseSortByInstanceProps,
UseSortByOptions,
UseSortByState
} from 'react-table'
} from "react-table";
declare module 'react-table' {
declare module "react-table" {
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
export interface TableOptions<D extends Record<string, unknown>>

View file

@ -3,15 +3,20 @@ import { useMutation } from "react-query";
import { toast } from "react-hot-toast";
import { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import { Field, Form, Formik } from "formik";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import type { FieldProps } from "formik";
import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient";
import DEBUG from "../../components/debug";
import Toast from '../../components/notifications/Toast';
import Toast from "../../components/notifications/Toast";
function FilterAddForm({ isOpen, toggle }: any) {
interface filterAddFormProps {
isOpen: boolean;
toggle: () => void;
}
function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
const mutation = useMutation(
(filter: Filter) => APIClient.filters.create(filter),
{
@ -22,10 +27,16 @@ function FilterAddForm({ isOpen, toggle }: any) {
toggle();
}
}
)
);
const handleSubmit = (data: any) => mutation.mutate(data);
const validate = (values: any) => values.name ? {} : { name: "Required" };
const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
}
return errors;
};
return (
<Transition.Root show={isOpen} as={Fragment}>
@ -53,7 +64,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
codecs: [],
sources: [],
containers: [],
origins: [],
origins: []
}}
onSubmit={handleSubmit}
validate={validate}
@ -97,7 +108,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
<Field name="name">
{({
field,
meta,
meta
}: FieldProps ) => (
<div className="sm:col-span-2">
<input
@ -145,7 +156,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
</div>
</Dialog>
</Transition.Root>
)
);
}
export default FilterAddForm;

View file

@ -1,20 +1,21 @@
import { Fragment, useRef, useState } from "react";
import React, { Fragment, useRef, useState } from "react";
import { useMutation } from "react-query";
import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid";
import { sleep, classNames } from "../../utils";
import { classNames, sleep } from "../../utils";
import { Form, Formik, useFormikContext } from "formik";
import DEBUG from "../../components/debug";
import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeOptions } from "../../domain/constants";
import { toast } from 'react-hot-toast'
import Toast from '../../components/notifications/Toast';
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs/input_wide";
import { RadioFieldsetWide } from "../../components/inputs/radio";
import DownloadClient from "../../screens/settings/DownloadClient";
interface InitialValuesSettings {
basic?: {
@ -46,70 +47,73 @@ interface InitialValues {
function FormFieldsDefault() {
const {
values: { tls },
values: { tls }
} = useFormikContext<InitialValues>();
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port" />
<TextFieldWide name="host" label="Host" help="Eg. client.domain.ltd, domain.ltd/client, domain.ltd:port"/>
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent and daemon port for Deluge" />
<NumberFieldWide name="port" label="Port" help="WebUI port for qBittorrent and daemon port for Deluge"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
<SwitchGroupWide name="tls" label="TLS" />
<SwitchGroupWide name="tls" label="TLS"/>
{tls && (
<Fragment>
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)" />
<SwitchGroupWide name="tls_skip_verify" label="Skip TLS verification (insecure)"/>
</Fragment>
)}
</div>
<TextFieldWide name="username" label="Username" />
<PasswordFieldWide name="password" label="Password" />
<TextFieldWide name="username" label="Username"/>
<PasswordFieldWide name="password" label="Password"/>
</Fragment>
);
}
function FormFieldsArr() {
const {
values: { settings },
values: { settings }
} = useFormikContext<InitialValues>();
return (
<Fragment>
<TextFieldWide name="host" label="Host" help="Full url http(s)://domain.ltd and/or subdomain/subfolder" />
<TextFieldWide name="host" label="Host" help="Full url http(s)://domain.ltd and/or subdomain/subfolder"/>
<PasswordFieldWide name="settings.apikey" label="API key" />
<PasswordFieldWide name="settings.apikey" label="API key"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="settings.basic.auth" label="Basic auth" />
<SwitchGroupWide name="settings.basic.auth" label="Basic auth"/>
</div>
{settings.basic?.auth === true && (
<Fragment>
<TextFieldWide name="settings.basic.username" label="Username" />
<PasswordFieldWide name="settings.basic.password" label="Password" />
<TextFieldWide name="settings.basic.username" label="Username"/>
<PasswordFieldWide name="settings.basic.password" label="Password"/>
</Fragment>
)}
</Fragment>
);
}
export const componentMap: any = {
DELUGE_V1: <FormFieldsDefault />,
DELUGE_V2: <FormFieldsDefault />,
QBITTORRENT: <FormFieldsDefault />,
RADARR: <FormFieldsArr />,
SONARR: <FormFieldsArr />,
LIDARR: <FormFieldsArr />,
WHISPARR: <FormFieldsArr />,
};
export interface componentMapType {
[key: string]: React.ReactElement;
}
export const componentMap: componentMapType = {
DELUGE_V1: <FormFieldsDefault/>,
DELUGE_V2: <FormFieldsDefault/>,
QBITTORRENT: <FormFieldsDefault/>,
RADARR: <FormFieldsArr/>,
SONARR: <FormFieldsArr/>,
LIDARR: <FormFieldsArr/>,
WHISPARR: <FormFieldsArr/>
};
function FormFieldsRulesBasic() {
const {
values: { settings },
values: { settings }
} = useFormikContext<InitialValues>();
return (
@ -123,12 +127,12 @@ function FormFieldsRulesBasic() {
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
</div>
{settings && settings.rules?.enabled === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/>
</Fragment>
)}
</div>
@ -137,7 +141,7 @@ function FormFieldsRulesBasic() {
function FormFieldsRules() {
const {
values: { settings },
values: { settings }
} = useFormikContext<InitialValues>();
return (
@ -151,19 +155,21 @@ function FormFieldsRules() {
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="settings.rules.enabled" label="Enabled" />
<SwitchGroupWide name="settings.rules.enabled" label="Enabled"/>
</div>
{settings.rules?.enabled === true && (
<Fragment>
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads" />
<NumberFieldWide name="settings.rules.max_active_downloads" label="Max active downloads"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents" />
<SwitchGroupWide name="settings.rules.ignore_slow_torrents" label="Ignore slow torrents"/>
</div>
{settings.rules?.ignore_slow_torrents === true && (
<Fragment>
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold" placeholder="in KB/s" help="If download speed is below this when max active downloads is hit, download anyways. KB/s" />
<NumberFieldWide name="settings.rules.download_speed_threshold" label="Download speed threshold"
placeholder="in KB/s"
help="If download speed is below this when max active downloads is hit, download anyways. KB/s"/>
</Fragment>
)}
</Fragment>
@ -172,28 +178,37 @@ function FormFieldsRules() {
);
}
export const rulesComponentMap: any = {
DELUGE_V1: <FormFieldsRulesBasic />,
DELUGE_V2: <FormFieldsRulesBasic />,
QBITTORRENT: <FormFieldsRules />,
export const rulesComponentMap: componentMapType = {
DELUGE_V1: <FormFieldsRulesBasic/>,
DELUGE_V2: <FormFieldsRulesBasic/>,
QBITTORRENT: <FormFieldsRules/>
};
interface formButtonsProps {
isSuccessfulTest: boolean;
isErrorTest: boolean;
isTesting: boolean;
cancelFn: any;
testFn: any;
values: any;
cancelFn: () => void;
testFn: (data: unknown) => void;
values: unknown;
type: "CREATE" | "UPDATE";
toggleDeleteModal?: any;
toggleDeleteModal?: () => void;
}
function DownloadClientFormButtons({ type, isSuccessfulTest, isErrorTest, isTesting, cancelFn, testFn, values, toggleDeleteModal }: formButtonsProps) {
function DownloadClientFormButtons({
type,
isSuccessfulTest,
isErrorTest,
isTesting,
cancelFn,
testFn,
values,
toggleDeleteModal
}: formButtonsProps) {
const test = () => {
testFn(values)
}
testFn(values);
};
return (
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
@ -269,10 +284,15 @@ function DownloadClientFormButtons({ type, isSuccessfulTest, isErrorTest, isTest
</div>
</div>
</div>
)
);
}
export function DownloadClientAddForm({ isOpen, toggle }: any) {
interface formProps {
isOpen: boolean;
toggle: () => void;
}
export function DownloadClientAddForm({ isOpen, toggle }: formProps) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
@ -282,12 +302,12 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body="Client was added" t={t} />)
toast.custom((t) => <Toast type="success" body="Client was added" t={t}/>);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t} />)
toast.custom((t) => <Toast type="error" body="Client could not be added" t={t}/>);
}
}
);
@ -313,22 +333,22 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
});
},
onError: () => {
console.log('not added')
console.log("not added");
setIsTesting(false);
setIsErrorTest(true);
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
const onSubmit = (data: unknown) => {
mutation.mutate(data as DownloadClient);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
const testClient = (data: unknown) => {
testClientMutation.mutate(data as DownloadClient);
};
const initialValues: InitialValues = {
@ -342,7 +362,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
username: "",
password: "",
settings: {}
}
};
return (
<Transition.Root show={isOpen} as={Fragment}>
@ -354,7 +374,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
onClose={toggle}
>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<Dialog.Overlay className="absolute inset-0"/>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
@ -404,10 +424,11 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" />
<TextFieldWide name="name" label="Name"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
<SwitchGroupWide name="enabled" label="Enabled" />
<div
className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:divide-gray-700">
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<RadioFieldsetWide
@ -432,7 +453,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
values={values}
/>
<DEBUG values={values} />
<DEBUG values={values}/>
</Form>
)}
</Formik>
@ -445,7 +466,13 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
);
}
export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
interface updateFormProps {
isOpen: boolean;
toggle: () => void;
client: DownloadClient;
}
export function DownloadClientUpdateForm({ client, isOpen, toggle }: updateFormProps) {
const [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false);
@ -456,9 +483,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
{
onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]);
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t} />)
toast.custom((t) => <Toast type="success" body={`${client.name} was updated successfully`} t={t}/>);
toggle();
},
}
}
);
@ -467,9 +494,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
{
onSuccess: () => {
queryClient.invalidateQueries();
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t} />)
toast.custom((t) => <Toast type="success" body={`${client.name} was deleted.`} t={t}/>);
toggleDeleteModal();
},
}
}
);
@ -499,12 +526,12 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
sleep(2500).then(() => {
setIsErrorTest(false);
});
},
}
}
);
const onSubmit = (data: any) => {
mutation.mutate(data);
const onSubmit = (data: unknown) => {
mutation.mutate(data as DownloadClient);
};
const cancelButtonRef = useRef(null);
@ -514,8 +541,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
deleteMutation.mutate(client.id);
};
const testClient = (data: any) => {
testClientMutation.mutate(data);
const testClient = (data: unknown) => {
testClientMutation.mutate(data as DownloadClient);
};
const initialValues = {
@ -529,8 +556,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
tls_skip_verify: client.tls_skip_verify,
username: client.username,
password: client.password,
settings: client.settings,
}
settings: client.settings
};
return (
<Transition.Root show={isOpen} as={Fragment}>
@ -551,7 +578,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
text="Are you sure you want to remove this download client? This action cannot be undone."
/>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" />
<Dialog.Overlay className="absolute inset-0"/>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
@ -602,10 +629,10 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
</div>
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="name" label="Name" />
<TextFieldWide name="name" label="Name"/>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="enabled" label="Enabled" />
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<RadioFieldsetWide
@ -631,7 +658,7 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
values={values}
/>
<DEBUG values={values} />
<DEBUG values={values}/>
</Form>
);
}}

View file

@ -1,27 +1,28 @@
import {useMutation} from "react-query";
import {APIClient} from "../../api/APIClient";
import {queryClient} from "../../App";
import {toast} from "react-hot-toast";
import { useMutation } from "react-query";
import { APIClient } from "../../api/APIClient";
import { queryClient } from "../../App";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import {SlideOver} from "../../components/panels";
import {NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide} from "../../components/inputs";
import {ImplementationMap} from "../../screens/settings/Feed";
import { SlideOver } from "../../components/panels";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
import { ImplementationMap } from "../../screens/settings/Feed";
import { componentMapType } from "./DownloadClientForms";
interface UpdateProps {
isOpen: boolean;
toggle: any;
toggle: () => void;
feed: Feed;
}
export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
const mutation = useMutation(
(feed: Feed) => APIClient.feeds.update(feed),
{
onSuccess: () => {
queryClient.invalidateQueries(["feeds"]);
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t}/>)
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t}/>);
toggle();
},
}
}
);
@ -30,14 +31,14 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
{
onSuccess: () => {
queryClient.invalidateQueries(["feeds"]);
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t}/>)
},
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t}/>);
}
}
);
const onSubmit = (formData: any) => {
mutation.mutate(formData);
}
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Feed);
};
const deleteAction = () => {
deleteMutation.mutate(feed.id);
@ -51,8 +52,8 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
name: feed.name,
url: feed.url,
api_key: feed.api_key,
interval: feed.interval,
}
interval: feed.interval
};
return (
<SlideOver
@ -69,7 +70,8 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
<TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div
className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="type"
@ -91,7 +93,7 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
</div>
)}
</SlideOver>
)
);
}
function FormFieldsTorznab() {
@ -103,13 +105,14 @@ function FormFieldsTorznab() {
help="Torznab url"
/>
<PasswordFieldWide name="api_key" label="API key" />
<PasswordFieldWide name="api_key" label="API key"/>
<NumberFieldWide name="interval" label="Refresh interval" help="Minutes. Recommended 15-30. To low and risk ban." />
<NumberFieldWide name="interval" label="Refresh interval"
help="Minutes. Recommended 15-30. To low and risk ban."/>
</div>
);
}
const componentMap: any = {
TORZNAB: <FormFieldsTorznab/>,
const componentMap: componentMapType = {
TORZNAB: <FormFieldsTorznab/>
};

View file

@ -1,14 +1,20 @@
import {Fragment, useState} from "react";
import { Fragment, useState } from "react";
import { toast } from "react-hot-toast";
import { useMutation, useQuery } from "react-query";
import Select, { components } from "react-select";
import { Field, Form, Formik } from "formik";
import Select, {
components,
ControlProps,
InputProps,
MenuProps,
OptionProps
} from "react-select";
import { Field, Form, Formik, FormikValues } from "formik";
import type { FieldProps } from "formik";
import { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react";
import {sleep, slugify} from "../../utils";
import { sleep, slugify } from "../../utils";
import { queryClient } from "../../App";
import DEBUG from "../../components/debug";
import { APIClient } from "../../api/APIClient";
@ -18,44 +24,48 @@ import {
SwitchGroupWide
} from "../../components/inputs";
import { SlideOver } from "../../components/panels";
import Toast from '../../components/notifications/Toast';
import Toast from "../../components/notifications/Toast";
const Input = (props: any) => {
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: any) => {
const Control = (props: ControlProps) => {
return (
<components.Control
{...props}
className="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: any) => {
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: any) => {
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 IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
if (indexer !== "") {
@ -72,21 +82,21 @@ const IrcSettingFields = (ind: IndexerDefinition, indexer: string) => {
{ind.irc.settings.map((f: IndexerSetting, idx: number) => {
switch (f.type) {
case "text":
return <TextFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />
return <TextFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />;
case "secret":
if (f.name === "invite_command") {
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultVisible={true} defaultValue={f.default} />
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultVisible={true} defaultValue={f.default} />;
}
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
return <PasswordFieldWide name={`irc.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />;
}
return null
return null;
})}
</div>
)}
</Fragment>
)
);
}
}
};
const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
if (indexer !== "") {
@ -106,48 +116,48 @@ const FeedSettingFields = (ind: IndexerDefinition, indexer: string) => {
{ind.torznab.settings.map((f: IndexerSetting, idx: number) => {
switch (f.type) {
case "text":
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />
return <TextFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} />;
case "secret":
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />
return <PasswordFieldWide name={`feed.${f.name}`} label={f.label} required={f.required} key={idx} help={f.help} defaultValue={f.default} />;
}
return null
return null;
})}
</div>
)}
</Fragment>
)
);
}
}
};
const SettingFields = (ind: IndexerDefinition, indexer: string) => {
if (indexer !== "") {
return (
<div key="opt">
{ind && ind.settings && ind.settings.map((f: any, idx: number) => {
{ind && ind.settings && ind.settings.map((f, idx: number) => {
switch (f.type) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
)
);
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} defaultValue="" />
)
);
}
return null
return null;
})}
<div hidden={true}>
<TextFieldWide name="name" label="Name" defaultValue={ind?.name} />
</div>
</div>
)
);
}
}
};
function slugIdentifier(name: string) {
const l = name.toLowerCase()
const r = l.replaceAll("torznab", "")
return slugify(`torznab-${r}`)
const l = name.toLowerCase();
const r = l.replaceAll("torznab", "");
return slugify(`torznab-${r}`);
}
// interface initialValues {
@ -160,33 +170,38 @@ function slugIdentifier(name: string) {
// settings?: Record<string, unknown>;
// }
type SelectValue = {
label: string;
value: string;
};
interface AddProps {
isOpen: boolean;
toggle: any;
toggle: () => void;
}
export function IndexerAddForm({ isOpen, toggle }: AddProps) {
const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition)
const [indexer, setIndexer] = useState<IndexerDefinition>({} as IndexerDefinition);
const { data } = useQuery('indexerDefinition', APIClient.indexers.getSchema,
const { data } = useQuery("indexerDefinition", APIClient.indexers.getSchema,
{
enabled: isOpen,
refetchOnWindowFocus: false
}
)
);
const mutation = useMutation(
(indexer: Indexer) => APIClient.indexers.create(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />)
sleep(1500)
toggle()
queryClient.invalidateQueries(["indexer"]);
toast.custom((t) => <Toast type="success" body="Indexer was added" t={t} />);
sleep(1500);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Indexer could not be added" t={t} />)
toast.custom((t) => <Toast type="error" body="Indexer could not be added" t={t} />);
}
})
});
const ircMutation = useMutation(
(network: IrcNetworkCreate) => APIClient.irc.createNetwork(network)
@ -196,14 +211,14 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
(feed: FeedCreate) => APIClient.feeds.create(feed)
);
const onSubmit = (formData: any) => {
const onSubmit = (formData: FormikValues) => {
const ind = data && data.find(i => i.identifier === formData.identifier);
if (!ind)
return;
if (formData.implementation === "torznab") {
// create slug for indexer identifier as "torznab-indexer_name"
const name = slugIdentifier(formData.name)
const name = slugIdentifier(formData.name);
const createFeed: FeedCreate = {
name: formData.name,
@ -213,14 +228,15 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
api_key: formData.feed.api_key,
interval: 30,
indexer: name,
indexer_id: 0,
}
indexer_id: 0
};
mutation.mutate(formData, {
mutation.mutate(formData as Indexer, {
onSuccess: (indexer) => {
createFeed.indexer_id = indexer!.id
// @eslint-ignore
createFeed.indexer_id = indexer.id;
feedMutation.mutate(createFeed)
feedMutation.mutate(createFeed);
}
});
return;
@ -252,12 +268,12 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
tls: ind.irc.tls,
nickserv: formData.irc.nickserv,
invite_command: formData.irc.invite_command,
channels: channels,
}
channels: channels
};
mutation.mutate(formData, {
mutation.mutate(formData as Indexer, {
onSuccess: () => {
ircMutation.mutate(network)
ircMutation.mutate(network);
}
});
}
@ -287,9 +303,10 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
identifier: "",
implementation: "irc",
name: "",
irc: {},
feed: {},
settings: {},
irc: {
invite_command: ""
},
settings: {}
}}
onSubmit={onSubmit}
>
@ -348,25 +365,27 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2,
baseUnit: 2
}
})}
value={field?.value && field.value.value}
onChange={(option: any) => {
resetForm()
setFieldValue("name", option?.label ?? "")
setFieldValue(field.name, option?.value ?? "")
onChange={(option: unknown) => {
const opt = option as SelectValue;
resetForm();
setFieldValue("name", opt.label ?? "");
setFieldValue(field.name, opt.value ?? "");
const ind = data!.find(i => i.identifier === option.value);
setFieldValue("implementation", ind?.implementation ? ind.implementation : "irc")
setIndexer(ind!)
if (ind!.irc?.settings) {
ind!.irc.settings.forEach((s) => {
setFieldValue(`irc.${s.name}`, s.default ?? "")
})
const ind = data && data.find(i => i.identifier === opt.value);
if (ind) {
setIndexer(ind);
if (ind.irc.settings) {
ind.irc.settings.forEach((s) => {
setFieldValue(`irc.${s.name}`, s.default ?? "");
});
}
}
}}
options={data && data.sort((a, b): any => a.name.localeCompare(b.name)).map(v => ({
options={data && data.sort((a, b) => a.name.localeCompare(b.name)).map(v => ({
label: v.name,
value: v.identifier
}))}
@ -419,48 +438,45 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
</div>
</Dialog>
</Transition.Root>
)
);
}
interface UpdateProps {
isOpen: boolean;
toggle: any;
indexer: Indexer;
toggle: () => void;
indexer: IndexerDefinition;
}
export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
const mutation = useMutation((indexer: Indexer) => APIClient.indexers.update(indexer), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />)
sleep(1500)
toggle()
}
})
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(['indexer']);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />)
queryClient.invalidateQueries(["indexer"]);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was updated successfully`} t={t} />);
sleep(1500);
toggle();
}
})
});
const onSubmit = (data: any) => {
const deleteMutation = useMutation((id: number) => APIClient.indexers.delete(id), {
onSuccess: () => {
queryClient.invalidateQueries(["indexer"]);
toast.custom((t) => <Toast type="success" body={`${indexer.name} was deleted.`} t={t} />);
}
});
const onSubmit = (data: unknown) => {
// TODO clear data depending on type
mutation.mutate(data)
mutation.mutate(data as Indexer);
};
const deleteAction = () => {
deleteMutation.mutate(indexer.id)
}
deleteMutation.mutate(indexer.id ?? 0);
};
const renderSettingFields = (settings: IndexerSetting[]) => {
if (settings === undefined || settings === null) {
return null
if (settings === undefined) {
return null;
}
return (
@ -470,17 +486,17 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
case "text":
return (
<TextFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
);
case "secret":
return (
<PasswordFieldWide name={`settings.${f.name}`} label={f.label} key={idx} help={f.help} />
)
);
}
return null
return null;
})}
</div>
)
}
);
};
const initialValues = {
id: indexer.id,
@ -494,8 +510,8 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
[obj.name]: obj.value
} as Record<string, string>),
{} as Record<string, string>
),
}
)
};
return (
<SlideOver
@ -540,5 +556,5 @@ export function IndexerUpdateForm({ isOpen, toggle, indexer }: UpdateProps) {
</div>
)}
</SlideOver>
)
);
}

View file

@ -1,7 +1,7 @@
import { useMutation } from "react-query";
import { toast } from "react-hot-toast";
import { XIcon } from "@heroicons/react/solid";
import { Field, FieldArray } from "formik";
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
import type { FieldProps } from "formik";
import { queryClient } from "../../App";
@ -12,9 +12,9 @@ import {
PasswordFieldWide,
SwitchGroupWide,
NumberFieldWide
} from "../../components/inputs/input_wide";
} from "../../components/inputs";
import { SlideOver } from "../../components/panels";
import Toast from '../../components/notifications/Toast';
import Toast from "../../components/notifications/Toast";
interface ChannelsFieldArrayProps {
channels: IrcChannel[];
@ -95,35 +95,31 @@ interface IrcNetworkAddFormValues {
channels: IrcChannel[];
}
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
interface AddFormProps {
isOpen: boolean;
toggle: () => void;
}
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
const mutation = useMutation(
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
{
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />)
toggle()
queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
},
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
}
}
);
const onSubmit = (data: any) => {
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
data.connect_commands = (
data.connect_commands && data.connect_commands.length > 0 ?
data.connect_commands.replace(/\r\n/g, "\n").split("\n") :
[]
);
mutation.mutate(data);
const onSubmit = (data: unknown) => {
mutation.mutate(data as IrcNetwork);
};
const validate = (values: IrcNetworkAddFormValues) => {
const errors = {} as any;
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name)
errors.name = "Required";
@ -137,7 +133,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
errors.nickserv = { account: "Required" };
return errors;
}
};
const initialValues: IrcNetworkAddFormValues = {
name: "",
@ -149,7 +145,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
nickserv: {
account: ""
},
channels: [],
channels: []
};
return (
@ -193,7 +189,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
</>
)}
</SlideOver>
)
);
}
interface IrcNetworkUpdateFormValues {
@ -222,34 +218,27 @@ export function IrcNetworkUpdateForm({
}: IrcNetworkUpdateFormProps) {
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
toggle()
queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
toggle();
}
})
});
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
toggle()
toggle();
}
})
});
const onSubmit = (data: any) => {
// easy way to split textarea lines into array of strings for each newline.
// parse on the field didn't really work.
// TODO fix connect_commands on network update
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
// data.connect_commands = cmds
// console.log("formatted", data)
mutation.mutate(data)
const onSubmit = (data: unknown) => {
mutation.mutate(data as IrcNetwork);
};
const validate = (values: any) => {
const errors = {} as any;
const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
@ -264,15 +253,17 @@ export function IrcNetworkUpdateForm({
}
if (!values.nickserv?.account) {
errors.nickserv.account = "Required";
errors.nickserv = {
account: "Required"
};
}
return errors;
}
};
const deleteAction = () => {
deleteMutation.mutate(network.id)
}
deleteMutation.mutate(network.id);
};
const initialValues: IrcNetworkUpdateFormValues = {
id: network.id,
@ -285,7 +276,7 @@ export function IrcNetworkUpdateForm({
pass: network.pass,
channels: network.channels,
invite_command: network.invite_command
}
};
return (
<SlideOver
@ -329,5 +320,5 @@ export function IrcNetworkUpdateForm({
</>
)}
</SlideOver>
)
);
}

View file

@ -1,59 +1,63 @@
import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react";
import {Field, Form, Formik} from "formik";
import type {FieldProps} from "formik";
import {XIcon} from "@heroicons/react/solid";
import Select, {components} from "react-select";
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import type { FieldProps } from "formik";
import { XIcon } from "@heroicons/react/solid";
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import {
SwitchGroupWide,
TextFieldWide
} from "../../components/inputs";
import DEBUG from "../../components/debug";
import {EventOptions, NotificationTypeOptions} from "../../domain/constants";
import {useMutation} from "react-query";
import {APIClient} from "../../api/APIClient";
import {queryClient} from "../../App";
import {toast} from "react-hot-toast";
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
import { useMutation } from "react-query";
import { APIClient } from "../../api/APIClient";
import { queryClient } from "../../App";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import {SlideOver} from "../../components/panels";
import { SlideOver } from "../../components/panels";
import { componentMapType } from "./DownloadClientForms";
const Input = (props: any) => {
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: any) => {
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: any) => {
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: any) => {
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}
/>
);
}
};
function FormFieldsDiscord() {
@ -76,8 +80,8 @@ function FormFieldsDiscord() {
);
}
const componentMap: any = {
DISCORD: <FormFieldsDiscord/>,
const componentMap: componentMapType = {
DISCORD: <FormFieldsDiscord/>
};
interface NotificationAddFormValues {
@ -87,35 +91,35 @@ interface NotificationAddFormValues {
interface AddProps {
isOpen: boolean;
toggle: any;
toggle: () => void;
}
export function NotificationAddForm({isOpen, toggle}: AddProps) {
export function NotificationAddForm({ isOpen, toggle }: AddProps) {
const mutation = useMutation(
(notification: Notification) => APIClient.notifications.create(notification),
{
onSuccess: () => {
queryClient.invalidateQueries(['notifications']);
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />)
toggle()
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
toggle();
},
onError: () => {
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />)
},
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />);
}
}
);
const onSubmit = (formData: any) => {
mutation.mutate(formData)
}
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Notification);
};
const validate = (values: NotificationAddFormValues) => {
const errors = {} as any;
const errors = {} as FormikErrors<FormikValues>;
if (!values.name)
errors.name = "Required";
return errors;
}
};
return (
<Transition.Root show={isOpen} as={Fragment}>
@ -141,19 +145,20 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
type: "",
name: "",
webhook: "",
events: [],
events: []
}}
onSubmit={onSubmit}
validate={validate}
>
{({values}) => (
{({ values }) => (
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Add
Notifications</Dialog.Title>
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Add Notifications
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200">
Trigger notifications on different events.
</p>
@ -187,12 +192,12 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
<Field name="type" type="select">
{({
field,
form: {setFieldValue, resetForm}
form: { setFieldValue, resetForm }
}: FieldProps) => (
<Select {...field}
isClearable={true}
isSearchable={true}
components={{Input, Control, Menu, Option}}
components={{ Input, Control, Menu, Option }}
placeholder="Choose a type"
styles={{
singleValue: (base) => ({
@ -205,14 +210,16 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2,
baseUnit: 2
}
})}
value={field?.value && field.value.value}
onChange={(option: any) => {
resetForm()
onChange={(option: unknown) => {
resetForm();
const opt = option as SelectOption;
// setFieldValue("name", option?.label ?? "")
setFieldValue(field.name, option?.value ?? "")
setFieldValue(field.name, opt.value ?? "");
}}
options={NotificationTypeOptions}
/>
@ -273,7 +280,7 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
</div>
</Dialog>
</Transition.Root>
)
);
}
const EventCheckBoxes = () => (
@ -303,23 +310,23 @@ const EventCheckBoxes = () => (
</div>
))}
</fieldset>
)
);
interface UpdateProps {
isOpen: boolean;
toggle: any;
toggle: () => void;
notification: Notification;
}
export function NotificationUpdateForm({isOpen, toggle, notification}: UpdateProps) {
export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) {
const mutation = useMutation(
(notification: Notification) => APIClient.notifications.update(notification),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>)
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>);
toggle();
},
}
}
);
@ -328,14 +335,14 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>)
},
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>);
}
}
);
const onSubmit = (formData: any) => {
mutation.mutate(formData);
}
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Notification);
};
const deleteAction = () => {
deleteMutation.mutate(notification.id);
@ -347,8 +354,8 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
type: notification.type,
name: notification.name,
webhook: notification.webhook,
events: notification.events || [],
}
events: notification.events || []
};
return (
<SlideOver
@ -376,11 +383,11 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
</div>
<div className="sm:col-span-2">
<Field name="type" type="select">
{({field, form: {setFieldValue, resetForm}}: FieldProps) => (
{({ field, form: { setFieldValue, resetForm } }: FieldProps) => (
<Select {...field}
isClearable={true}
isSearchable={true}
components={{Input, Control, Menu, Option}}
components={{ Input, Control, Menu, Option }}
placeholder="Choose a type"
styles={{
@ -394,13 +401,14 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2,
baseUnit: 2
}
})}
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
onChange={(option: any) => {
resetForm()
setFieldValue(field.name, option?.value ?? "")
onChange={(option: unknown) => {
resetForm();
const opt = option as SelectOption;
setFieldValue(field.name, opt.value ?? "");
}}
options={NotificationTypeOptions}
/>
@ -431,5 +439,5 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
</div>
)}
</SlideOver>
)
);
}

View file

@ -2,7 +2,7 @@ import type { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);

View file

@ -1,6 +1,6 @@
import { Fragment } from "react";
import { NavLink, Link, Route, Switch } from "react-router-dom";
import type { match } from "react-router-dom";
import { Link, NavLink, Route, Switch } from "react-router-dom";
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { ExternalLinkIcon } from "@heroicons/react/solid";
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
@ -11,9 +11,9 @@ import { Logs } from "./Logs";
import { Releases } from "./releases";
import { Dashboard } from "./dashboard";
import { FilterDetails, Filters } from "./filters";
import { AuthContext } from '../utils/Context';
import { AuthContext } from "../utils/Context";
import logo from '../logo.png';
import logo from "../logo.png";
interface NavItem {
name: string;
@ -21,11 +21,11 @@ interface NavItem {
}
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
return classes.filter(Boolean).join(" ");
}
const isActiveMatcher = (
match: match<any> | null,
match: match | null,
location: { pathname: string },
item: NavItem
) => {
@ -33,20 +33,20 @@ const isActiveMatcher = (
return false;
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
return true
return true;
if (match.url === "/")
return false;
return true;
}
};
export default function Base() {
const authContext = AuthContext.useValue();
const nav: Array<NavItem> = [
{ name: 'Dashboard', path: "/" },
{ name: 'Filters', path: "/filters" },
{ name: 'Releases', path: "/releases" },
{ name: "Dashboard", path: "/" },
{ name: "Filters", path: "/filters" },
{ name: "Releases", path: "/releases" },
{ name: "Settings", path: "/settings" },
{ name: "Logs", path: "/logs" }
];
@ -102,7 +102,8 @@ export default function Base() {
)}
>
Docs
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" />
<ExternalLinkIcon className="inline ml-1 h-5 w-5"
aria-hidden="true"/>
</a>
</div>
</div>
@ -148,8 +149,8 @@ export default function Base() {
<Link
to="/settings"
className={classNames(
active ? 'bg-gray-100 dark:bg-gray-600' : '',
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
active ? "bg-gray-100 dark:bg-gray-600" : "",
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
)}
>
Settings
@ -161,8 +162,8 @@ export default function Base() {
<Link
to="/logout"
className={classNames(
active ? 'bg-gray-100 dark:bg-gray-600' : '',
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
active ? "bg-gray-100 dark:bg-gray-600" : "",
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
)}
>
Logout
@ -182,9 +183,9 @@ export default function Base() {
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block h-6 w-6" aria-hidden="true" />
<XIcon className="block h-6 w-6" aria-hidden="true"/>
) : (
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
)}
</Disclosure.Button>
</div>
@ -221,29 +222,29 @@ export default function Base() {
<Switch>
<Route path="/logs">
<Logs />
<Logs/>
</Route>
<Route path="/settings">
<Settings />
<Settings/>
</Route>
<Route path="/releases">
<Releases />
<Releases/>
</Route>
<Route exact={true} path="/filters">
<Filters />
<Filters/>
</Route>
<Route path="/filters/:filterId">
<FilterDetails />
<FilterDetails/>
</Route>
<Route exact path="/">
<Dashboard />
<Dashboard/>
</Route>
</Switch>
</div>
)
);
}

View file

@ -18,7 +18,7 @@ const LogColors: Record<LogLevel, string> = {
"TRACE": "text-purple-300",
"DEBUG": "text-yellow-500",
"INFO": "text-green-500",
"ERROR": "text-red-500",
"ERROR": "text-red-500"
};
export const Logs = () => {
@ -29,7 +29,7 @@ export const Logs = () => {
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
}
};
useEffect(() => {
const es = APIClient.events.logs();
@ -40,7 +40,7 @@ export const Logs = () => {
if (settings.scrollOnNewLog)
scrollToBottom();
}
};
return () => es.close();
}, [setLogs, settings]);
@ -96,7 +96,7 @@ export const Logs = () => {
key={idx}
className={classNames(
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : "",
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : ""
)}
>
<span
@ -112,7 +112,7 @@ export const Logs = () => {
)}
>
{a.level}
{' '}
{" "}
</span>
) : null}
<span className="ml-2 text-black dark:text-gray-300">
@ -125,5 +125,5 @@ export const Logs = () => {
</div>
</div>
</main>
)
}
);
};

View file

@ -1,36 +1,48 @@
import {BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon} from '@heroicons/react/outline'
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
import { BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon } from "@heroicons/react/outline";
import { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
import { classNames } from "../utils";
import IndexerSettings from "./settings/Indexer";
import { IrcSettings } from "./settings/Irc";
import ApplicationSettings from "./settings/Application";
import DownloadClientSettings from "./settings/DownloadClient";
import { RegexPlayground } from './settings/RegexPlayground';
import { RegexPlayground } from "./settings/RegexPlayground";
import ReleaseSettings from "./settings/Releases";
import NotificationSettings from "./settings/Notifications";
import FeedSettings from "./settings/Feed";
const subNavigation = [
{name: 'Application', href: '', icon: CogIcon, current: true},
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
{name: 'IRC', href: 'irc', icon: ChatAlt2Icon, current: false},
{name: 'Feeds', href: 'feeds', icon: RssIcon, current: false},
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false},
interface NavTabType {
name: string;
href: string;
icon: typeof CogIcon;
current: boolean;
}
const subNavigation: NavTabType[] = [
{ name: "Application", href: "", icon: CogIcon, current: true },
{ name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
{ name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
{ name: "Feeds", href: "feeds", icon: RssIcon, current: false },
{ name: "Clients", href: "clients", icon: DownloadIcon, current: false },
{ name: "Notifications", href: "notifications", icon: BellIcon, current: false },
{ name: "Releases", href: "releases", icon: CollectionIcon, current: false }
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
]
];
function SubNavLink({item, url}: any) {
interface NavLinkProps {
item: NavTabType;
url: string;
}
function SubNavLink({ item, url }: NavLinkProps) {
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
// we need to clean the / if it's a base root path
const too = item.href ? `${url}/${item.href}` : url
const too = item.href ? `${url}/${item.href}` : url;
return (
<NavLink
key={item.name}
@ -38,9 +50,9 @@ function SubNavLink({item, url}: any) {
exact={true}
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
className={classNames(
'border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium'
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
)}
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
aria-current={splitLocation[2] === item.href ? "page" : undefined}
>
<item.icon
className="text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
@ -48,19 +60,24 @@ function SubNavLink({item, url}: any) {
/>
<span className="truncate">{item.name}</span>
</NavLink>
)
);
}
function SidebarNav({subNavigation, url}: any) {
interface SidebarNavProps {
subNavigation: NavTabType[];
url: string;
}
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
return (
<aside className="py-2 lg:col-span-3">
<nav className="space-y-1">
{subNavigation.map((item: any) => (
{subNavigation.map((item) => (
<SubNavLink item={item} url={url} key={item.href}/>
))}
</nav>
</aside>
)
);
}
export default function Settings() {
@ -115,6 +132,6 @@ export default function Settings() {
</div>
</div>
</main>
)
);
}

View file

@ -34,11 +34,11 @@ export const Login = () => {
isLoggedIn: true
});
history.push("/");
},
}
}
);
const handleSubmit = (data: any) => mutation.mutate(data);
const handleSubmit = (data: LoginData) => mutation.mutate(data);
return (
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
@ -75,4 +75,4 @@ export const Login = () => {
</div>
</div>
);
}
};

View file

@ -29,4 +29,4 @@ export const Logout = () => {
<p>Logged out</p>
</div>
);
}
};

View file

@ -37,7 +37,7 @@ export const Onboarding = () => {
{
onSuccess: () => {
history.push("/login");
},
}
}
);
@ -81,5 +81,5 @@ export const Onboarding = () => {
</div>
</div>
);
}
};

View file

@ -5,7 +5,7 @@ import {
useFilters,
useGlobalFilter,
useSortBy,
usePagination
usePagination, FilterProps, Column
} from "react-table";
import { APIClient } from "../../api/APIClient";
@ -17,17 +17,17 @@ import * as DataTable from "../../components/data-table";
// This is a custom filter UI for selecting
// a unique option from a list
function SelectColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id, render },
}: any) {
column: { filterValue, setFilter, preFilteredRows, id, render }
}: FilterProps<object>) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options: any = new Set()
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => {
options.add(row.values[id])
})
return [...options.values()]
}, [id, preFilteredRows])
const options = new Set<string>();
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
options.add(row.values[id]);
});
return [...options.values()];
}, [id, preFilteredRows]);
// Render a multi-select box
return (
@ -39,7 +39,7 @@ function SelectColumnFilter({
id={id}
value={filterValue}
onChange={e => {
setFilter(e.target.value || undefined)
setFilter(e.target.value || undefined);
}}
>
<option value="">All</option>
@ -50,17 +50,22 @@ function SelectColumnFilter({
))}
</select>
</label>
)
);
}
function Table({ columns, data }: any) {
interface TableProps {
columns: Column[];
data: Release[];
}
function Table({ columns, data }: TableProps) {
// Use the state and functions returned from useTable to build your UI
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page, // Instead of using 'rows', we'll use page,
page // Instead of using 'rows', we'll use page,
} = useTable(
{ columns, data },
useFilters,
@ -94,7 +99,7 @@ function Table({ columns, data }: any) {
{...columnRest}
>
<div className="flex items-center justify-between">
{column.render('Header')}
{column.render("Header")}
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (
@ -119,12 +124,12 @@ function Table({ columns, data }: any) {
{...getTableBodyProps()}
className="divide-y divide-gray-200 dark:divide-gray-700"
>
{page.map((row: any) => {
{page.map((row) => {
prepareRow(row);
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
return (
<tr key={bodyRowKey} {...bodyRowRest}>
{row.cells.map((cell: any) => {
{row.cells.map((cell) => {
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
return (
<td
@ -133,12 +138,12 @@ function Table({ columns, data }: any) {
role="cell"
{...cellRowRest}
>
{cell.render('Cell')}
{cell.render("Cell")}
</td>
)
);
})}
</tr>
)
);
})}
</tbody>
</table>
@ -151,30 +156,30 @@ export const ActivityTable = () => {
const columns = React.useMemo(() => [
{
Header: "Age",
accessor: 'timestamp',
Cell: DataTable.AgeCell,
accessor: "timestamp",
Cell: DataTable.AgeCell
},
{
Header: "Release",
accessor: 'torrent_name',
Cell: DataTable.TitleCell,
accessor: "torrent_name",
Cell: DataTable.TitleCell
},
{
Header: "Actions",
accessor: 'action_status',
Cell: DataTable.ReleaseStatusCell,
accessor: "action_status",
Cell: DataTable.ReleaseStatusCell
},
{
Header: "Indexer",
accessor: 'indexer',
accessor: "indexer",
Cell: DataTable.TitleCell,
Filter: SelectColumnFilter,
filter: 'includes',
},
], [])
filter: "includes"
}
], []);
const { isLoading, data } = useQuery(
'dash_release',
"dash_release",
() => APIClient.release.find("?limit=10"),
{ refetchOnWindowFocus: false }
);
@ -188,7 +193,7 @@ export const ActivityTable = () => {
Recent activity
</h3>
<Table columns={columns} data={data?.data} />
<Table columns={columns} data={data?.data ?? []} />
</div>
);
}
};

View file

@ -19,7 +19,7 @@ const StatsItem = ({ name, value }: StatsItemProps) => (
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
</dd>
</div>
)
);
export const Stats = () => {
const { isLoading, data } = useQuery(
@ -44,5 +44,5 @@ export const Stats = () => {
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
</dl>
</div>
)
}
);
};

View file

@ -1,4 +1,4 @@
import { Fragment, useRef } from "react";
import React, { Fragment, useRef } from "react";
import { useMutation, useQuery } from "react-query";
import {
NavLink,
@ -10,10 +10,9 @@ import {
useRouteMatch
} from "react-router-dom";
import { toast } from "react-hot-toast";
import { Field, FieldArray, Form, Formik } from "formik";
import { Field, FieldArray, FieldProps, Form, Formik, FormikValues } from "formik";
import { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
import { ChevronDownIcon, ChevronRightIcon, } from "@heroicons/react/solid";
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/solid";
import {
CONTAINER_OPTIONS,
@ -50,16 +49,27 @@ import { DeleteModal } from "../../components/modals";
import { TitleSubtitle } from "../../components/headings";
import { EmptyListState } from "../../components/emptystates";
const tabs = [
{ name: 'General', href: '', current: true },
{ name: 'Movies and TV', href: 'movies-tv', current: false },
{ name: 'Music', href: 'music', current: false },
// { name: 'P2P', href: 'p2p', current: false },
{ name: 'Advanced', href: 'advanced', current: false },
{ name: 'Actions', href: 'actions', current: false },
]
interface tabType {
name: string;
href: string;
current: boolean;
}
function TabNavLink({ item, url }: any) {
const tabs: tabType[] = [
{ name: "General", href: "", current: true },
{ name: "Movies and TV", href: "movies-tv", current: false },
{ name: "Music", href: "music", current: false },
// { name: 'P2P', href: 'p2p', current: false },
{ name: "Advanced", href: "advanced", current: false },
{ name: "Actions", href: "actions", current: false }
];
export interface NavLinkProps {
item: tabType;
url: string;
}
function TabNavLink({ item, url }: NavLinkProps) {
const location = useLocation();
const splitLocation = location.pathname.split("/");
@ -71,16 +81,23 @@ function TabNavLink({ item, url }: any) {
exact
activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white"
className={classNames(
'border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
"border-transparent text-gray-500 hover:text-purple-600 dark:hover:text-white hover:border-purple-600 dark:hover:border-blue-500 whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm"
)}
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
aria-current={splitLocation[2] === item.href ? "page" : undefined}
>
{item.name}
</NavLink>
)
);
}
const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
interface FormButtonsGroupProps {
values: FormikValues;
deleteAction: () => void;
reset: () => void;
dirty?: boolean;
}
const FormButtonsGroup = ({ values, deleteAction, reset }: FormButtonsGroupProps) => {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const cancelModalButtonRef = useRef(null);
@ -123,8 +140,8 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
</div>
</div>
</div>
)
}
);
};
export default function FilterDetails() {
const history = useHistory();
@ -164,16 +181,16 @@ export default function FilterDetails() {
queryClient.invalidateQueries(["filters"]);
// redirect
history.push("/filters")
history.push("/filters");
}
})
});
if (isLoading) {
return null
return null;
}
if (!filter) {
return null
return null;
}
const handleSubmit = (data: Filter) => {
@ -181,17 +198,17 @@ export default function FilterDetails() {
// TODO add options for these
data.actions.forEach((a: Action) => {
if (a.type === "WEBHOOK") {
a.webhook_method = "POST"
a.webhook_type = "JSON"
a.webhook_method = "POST";
a.webhook_type = "JSON";
}
})
});
updateMutation.mutate(data)
}
updateMutation.mutate(data);
};
const deleteAction = () => {
deleteMutation.mutate(filter.id)
}
deleteMutation.mutate(filter.id);
};
return (
<main>
@ -267,7 +284,7 @@ export default function FilterDetails() {
albums: filter.albums,
origins: filter.origins || [],
indexers: filter.indexers || [],
actions: filter.actions || [],
actions: filter.actions || []
} as Filter}
onSubmit={handleSubmit}
>
@ -308,7 +325,7 @@ export default function FilterDetails() {
</div>
</div>
</main>
)
);
}
function General() {
@ -320,12 +337,13 @@ function General() {
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
label: v.name,
value: {
id: v.id,
name: v.name,
identifier: v.identifier,
enabled: v.enabled
}
value: v.id
// value: {
// id: v.id,
// name: v.name,
// identifier: v.identifier,
// enabled: v.enabled
// }
})) : [];
return (
@ -401,7 +419,7 @@ function MoviesTv() {
</div>
</div>
</div>
)
);
}
function Music() {
@ -453,7 +471,7 @@ function Music() {
</div>
</div>
</div>
)
);
}
function Advanced() {
@ -497,19 +515,19 @@ function Advanced() {
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
</CollapsableSection>
</div>
)
);
}
interface CollapsableSectionProps {
title: string;
subtitle: string;
children: any;
children: React.ReactNode;
}
function CollapsableSection({ title, subtitle, children }: CollapsableSectionProps) {
const [isOpen, toggleOpen] = useToggle(false)
const [isOpen, toggleOpen] = useToggle(false);
return(
return (
<div className="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center cursor-pointer" onClick={toggleOpen}>
<div className="-ml-2 -mt-2 flex flex-wrap items-baseline">
@ -531,12 +549,12 @@ function CollapsableSection({ title, subtitle, children }: CollapsableSectionPro
</div>
)}
</div>
)
);
}
interface FilterActionsProps {
filter: Filter;
values: any;
values: FormikValues;
}
function FilterActions({ filter, values }: FilterActionsProps) {
@ -572,9 +590,9 @@ function FilterActions({ filter, values }: FilterActionsProps) {
webhook_type: "",
webhook_method: "",
webhook_data: "",
webhook_headers: [],
webhook_headers: []
// client_id: 0,
}
};
return (
<div className="mt-10">
@ -602,8 +620,8 @@ function FilterActions({ filter, values }: FilterActionsProps) {
<div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
{values.actions.length > 0 ?
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
{values.actions.map((action: any, index: number) => (
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} key={index} />
{values.actions.map((action: Action, index: number) => (
<FilterActionsItem action={action} clients={data ?? []} idx={index} remove={remove} key={index} />
))}
</ul>
: <EmptyListState text="No actions yet!" />
@ -613,14 +631,14 @@ function FilterActions({ filter, values }: FilterActionsProps) {
)}
</FieldArray>
</div>
)
);
}
interface FilterActionsItemProps {
action: Action;
clients: DownloadClient[];
idx: number;
remove: any;
remove: <T>(index: number) => T | undefined;
}
function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) {
@ -681,7 +699,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
name={`actions.${idx}.webhook_data`}
label="Data (json)"
columns={6}
placeholder={`Request data: { "key": "value" }`}
placeholder={"Request data: { \"key\": \"value\" }"}
/>
</div>
);
@ -856,27 +874,27 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<Field name={`actions.${idx}.enabled`} type="checkbox">
{({
field,
form: { setFieldValue },
}: any) => (
form: { setFieldValue }
}: FieldProps) => (
<SwitchBasic
{...field}
type="button"
value={field.value}
checked={field.checked}
onChange={value => {
setFieldValue(field?.name ?? '', value)
checked={field.checked ?? false}
onChange={(value: boolean) => {
setFieldValue(field?.name ?? "", value);
}}
className={classNames(
field.value ? 'bg-teal-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'
field.value ? "bg-teal-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">toggle enabled</span>
<span
aria-hidden="true"
className={classNames(
field.value ? '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'
field.value ? "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"
)}
/>
</SwitchBasic>
@ -972,5 +990,5 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
</div>
)}
</li>
)
);
}

View file

@ -7,7 +7,7 @@ import {
TrashIcon,
PencilAltIcon,
SwitchHorizontalIcon,
DotsHorizontalIcon, DuplicateIcon,
DotsHorizontalIcon, DuplicateIcon
} from "@heroicons/react/outline";
import { queryClient } from "../../App";
@ -20,7 +20,7 @@ import { EmptyListState } from "../../components/emptystates";
import { DeleteModal } from "../../components/modals";
export default function Filters() {
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
const { isLoading, error, data } = useQuery(
["filters"],
@ -63,7 +63,7 @@ export default function Filters() {
)}
</div>
</main>
)
);
}
interface FilterListProps {
@ -97,7 +97,7 @@ function FilterList({ filters }: FilterListProps) {
</tbody>
</table>
</div>
)
);
}
interface FilterItemDropdownProps {
@ -256,7 +256,7 @@ const FilterItemDropdown = ({
</Transition>
</Menu>
);
}
};
interface FilterListItemProps {
filter: Filter;
@ -264,13 +264,13 @@ interface FilterListItemProps {
}
function FilterListItem({ filter, idx }: FilterListItemProps) {
const [enabled, setEnabled] = useState(filter.enabled)
const [enabled, setEnabled] = useState(filter.enabled);
const updateMutation = useMutation(
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
{
onSuccess: () => {
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />)
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />);
// We need to invalidate both keys here.
// The filters key is used on the /filters page,
@ -284,7 +284,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status);
}
};
return (
<tr
@ -303,16 +303,16 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
checked={enabled}
onChange={toggleActive}
className={classNames(
enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
'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'
enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700",
"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(
enabled ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200'
enabled ? "translate-x-5" : "translate-x-0",
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
</Switch>
@ -342,5 +342,5 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
/>
</td>
</tr>
)
);
}

View file

@ -3,19 +3,20 @@ import { useQuery } from "react-query";
import { Listbox, Transition } from "@headlessui/react";
import {
CheckIcon,
ChevronDownIcon,
ChevronDownIcon
} from "@heroicons/react/solid";
import { APIClient } from "../../api/APIClient";
import { classNames } from "../../utils";
import { PushStatusOptions } from "../../domain/constants";
import { FilterProps } from "react-table";
interface ListboxFilterProps {
id: string;
label: string;
currentValue: string;
onChange: (newValue: string) => void;
children: any;
children: React.ReactNode;
}
const ListboxFilter = ({
@ -62,13 +63,13 @@ const ListboxFilter = ({
// a unique option from a list
export const IndexerSelectColumnFilter = ({
column: { filterValue, setFilter, id }
}: any) => {
}: FilterProps<object>) => {
const { data, isSuccess } = useQuery(
"release_indexers",
() => APIClient.release.indexerOptions(),
{
keepPreviousData: true,
staleTime: Infinity,
staleTime: Infinity
}
);
@ -84,8 +85,8 @@ export const IndexerSelectColumnFilter = ({
<FilterOption key={idx} label={indexer} value={indexer} />
))}
</ListboxFilter>
)
}
);
};
interface FilterOptionProps {
label: string;
@ -96,7 +97,7 @@ const FilterOption = ({ label, value }: FilterOptionProps) => (
<Listbox.Option
className={({ active }) => classNames(
"cursor-pointer select-none relative py-2 pl-10 pr-4",
active ? 'text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900' : 'text-gray-700 dark:text-gray-400'
active ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400"
)}
value={value}
>
@ -122,15 +123,13 @@ const FilterOption = ({ label, value }: FilterOptionProps) => (
export const PushStatusSelectColumnFilter = ({
column: { filterValue, setFilter, id }
}: any) => (
}: FilterProps<object>) => {
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
return (
<div className="mr-3">
<ListboxFilter
id={id}
label={
filterValue
? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label
: "Push status"
}
label={label ?? "Push status"}
currentValue={filterValue}
onChange={setFilter}
>
@ -139,4 +138,4 @@ export const PushStatusSelectColumnFilter = ({
))}
</ListboxFilter>
</div>
);
);};

View file

@ -25,30 +25,45 @@ import {
PushStatusSelectColumnFilter
} from "./Filters";
const initialState = {
type TableState = {
queryPageIndex: number;
queryPageSize: number;
totalCount: number;
queryFilters: ReleaseFilter[];
};
const initialState: TableState = {
queryPageIndex: 0,
queryPageSize: 10,
totalCount: null,
totalCount: 0,
queryFilters: []
};
const PAGE_CHANGED = 'PAGE_CHANGED';
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
const FILTER_CHANGED = 'FILTER_CHANGED';
enum ActionType {
PAGE_CHANGED = "PAGE_CHANGED",
PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
FILTER_CHANGED = "FILTER_CHANGED"
}
const TableReducer = (state: any, { type, payload }: any) => {
switch (type) {
case PAGE_CHANGED:
return { ...state, queryPageIndex: payload };
case PAGE_SIZE_CHANGED:
return { ...state, queryPageSize: payload };
case TOTAL_COUNT_CHANGED:
return { ...state, totalCount: payload };
case FILTER_CHANGED:
return { ...state, queryFilters: payload };
type Actions =
| { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
| { type: ActionType.PAGE_CHANGED; payload: number; }
| { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
| { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
const TableReducer = (state: TableState, action: Actions): TableState => {
switch (action.type) {
case ActionType.PAGE_CHANGED:
return { ...state, queryPageIndex: action.payload };
case ActionType.PAGE_SIZE_CHANGED:
return { ...state, queryPageSize: action.payload };
case ActionType.FILTER_CHANGED:
return { ...state, queryFilters: action.payload };
case ActionType.TOTAL_COUNT_CHANGED:
return { ...state, totalCount: action.payload };
default:
throw new Error(`Unhandled action type: ${type}`);
throw new Error(`Unhandled action type: ${action}`);
}
};
@ -56,38 +71,38 @@ export const ReleaseTable = () => {
const columns = React.useMemo(() => [
{
Header: "Age",
accessor: 'timestamp',
Cell: DataTable.AgeCell,
accessor: "timestamp",
Cell: DataTable.AgeCell
},
{
Header: "Release",
accessor: 'torrent_name',
Cell: DataTable.TitleCell,
accessor: "torrent_name",
Cell: DataTable.TitleCell
},
{
Header: "Actions",
accessor: 'action_status',
accessor: "action_status",
Cell: DataTable.ReleaseStatusCell,
Filter: PushStatusSelectColumnFilter,
Filter: PushStatusSelectColumnFilter
},
{
Header: "Indexer",
accessor: 'indexer',
accessor: "indexer",
Cell: DataTable.TitleCell,
Filter: IndexerSelectColumnFilter,
filter: 'equal',
},
] as Column<Release>[], [])
filter: "equal"
}
] as Column<Release>[], []);
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
React.useReducer(TableReducer, initialState);
const { isLoading, error, data, isSuccess } = useQuery(
['releases', queryPageIndex, queryPageSize, queryFilters],
["releases", queryPageIndex, queryPageSize, queryFilters],
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
{
keepPreviousData: true,
staleTime: 5000,
staleTime: 5000
}
);
@ -129,29 +144,29 @@ export const ReleaseTable = () => {
},
useFilters,
useSortBy,
usePagination,
usePagination
);
React.useEffect(() => {
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
}, [pageIndex]);
React.useEffect(() => {
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
gotoPage(0);
}, [pageSize, gotoPage]);
React.useEffect(() => {
if (data?.count) {
dispatch({
type: TOTAL_COUNT_CHANGED,
payload: data.count,
type: ActionType.TOTAL_COUNT_CHANGED,
payload: data.count
});
}
}, [data?.count]);
React.useEffect(() => {
dispatch({ type: FILTER_CHANGED, payload: filters });
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
}, [filters]);
if (error)
@ -161,13 +176,13 @@ export const ReleaseTable = () => {
return <p>Loading...</p>;
if (!data)
return <EmptyListState text="No recent activity" />
return <EmptyListState text="No recent activity" />;
// Render the UI for your table
return (
<div className="flex flex-col">
<div className="flex mb-6">
{headerGroups.map((headerGroup: { headers: any[] }) =>
{headerGroups.map((headerGroup) =>
headerGroup.headers.map((column) => (
column.Filter ? (
<div className="mt-2 sm:mt-0" key={column.id}>
@ -196,7 +211,7 @@ export const ReleaseTable = () => {
{...columnRest}
>
<div className="flex items-center justify-between">
{column.render('Header')}
{column.render("Header")}
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (
@ -221,13 +236,13 @@ export const ReleaseTable = () => {
{...getTableBodyProps()}
className="divide-y divide-gray-200 dark:divide-gray-700"
>
{page.map((row: any) => {
{page.map((row) => {
prepareRow(row);
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
return (
<tr key={bodyRowKey} {...bodyRowRest}>
{row.cells.map((cell: any) => {
{row.cells.map((cell) => {
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
return (
<td
@ -236,7 +251,7 @@ export const ReleaseTable = () => {
role="cell"
{...cellRowRest}
>
{cell.render('Cell')}
{cell.render("Cell")}
</td>
);
})}
@ -263,7 +278,7 @@ export const ReleaseTable = () => {
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
setPageSize(Number(e.target.value));
}}
>
{[5, 10, 20, 50].map(pageSize => (
@ -312,4 +327,4 @@ export const ReleaseTable = () => {
</div>
</div>
);
}
};

View file

@ -4,12 +4,11 @@ import { APIClient } from "../../api/APIClient";
import { Checkbox } from "../../components/Checkbox";
import { SettingsContext } from "../../utils/Context";
function ApplicationSettings() {
const [settings, setSettings] = SettingsContext.use();
const { isLoading, data } = useQuery(
['config'],
["config"],
() => APIClient.config.get(),
{
retry: false,
@ -125,7 +124,7 @@ function ApplicationSettings() {
</div>
</form>
)
);
}
export default ApplicationSettings;

View file

@ -13,10 +13,10 @@ interface DLSettingsItemProps {
}
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
return (
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
<tr key={client.name} className={idx % 2 === 0 ? "light:bg-white" : "light:bg-gray-50"}>
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@ -24,16 +24,16 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
checked={client.enabled}
onChange={toggleUpdateClient}
className={classNames(
client.enabled ? 'bg-teal-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'
client.enabled ? "bg-teal-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(
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'
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>
@ -47,14 +47,14 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
</span>
</td>
</tr>
)
);
}
function DownloadClientSettings() {
const [addClientIsOpen, toggleAddClient] = useToggle(false)
const [addClientIsOpen, toggleAddClient] = useToggle(false);
const { error, data } = useQuery(
'downloadClients',
"downloadClients",
APIClient.download_clients.getAll,
{ refetchOnWindowFocus: false }
);
@ -138,7 +138,7 @@ function DownloadClientSettings() {
</div>
</div>
)
);
}
export default DownloadClientSettings;

View file

@ -3,27 +3,28 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient";
import { Menu, Switch, Transition } from "@headlessui/react";
import {classNames} from "../../utils";
import {Fragment, useRef, useState} from "react";
import {toast} from "react-hot-toast";
import { classNames } from "../../utils";
import { Fragment, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast";
import {queryClient} from "../../App";
import {DeleteModal} from "../../components/modals";
import { queryClient } from "../../App";
import { DeleteModal } from "../../components/modals";
import {
DotsHorizontalIcon,
PencilAltIcon,
SwitchHorizontalIcon,
TrashIcon
} from "@heroicons/react/outline";
import {FeedUpdateForm} from "../../forms/settings/FeedForms";
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
import { EmptySimple } from "../../components/emptystates";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
function FeedSettings() {
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find,
const { data } = useQuery<Feed[], Error>("feeds", APIClient.feeds.find,
{
refetchOnWindowFocus: false
}
)
);
return (
<div className="divide-y divide-gray-200 lg:col-span-9">
@ -61,7 +62,7 @@ function FeedSettings() {
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
</div>
</div>
)
);
}
const ImplementationTorznab = () => (
@ -70,20 +71,20 @@ const ImplementationTorznab = () => (
>
Torznab
</span>
)
);
export const ImplementationMap: any = {
"TORZNAB": <ImplementationTorznab/>,
export const ImplementationMap: componentMapType = {
"TORZNAB": <ImplementationTorznab/>
};
interface ListItemProps {
feed: Feed;
}
function ListItem({feed}: ListItemProps) {
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
function ListItem({ feed }: ListItemProps) {
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
const [enabled, setEnabled] = useState(feed.enabled)
const [enabled, setEnabled] = useState(feed.enabled);
const updateMutation = useMutation(
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
@ -91,7 +92,7 @@ function ListItem({feed}: ListItemProps) {
onSuccess: () => {
toast.custom((t) => <Toast type="success"
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
t={t}/>)
t={t}/>);
queryClient.invalidateQueries(["feeds"]);
queryClient.invalidateQueries(["feeds", feed?.id]);
@ -102,7 +103,7 @@ function ListItem({feed}: ListItemProps) {
const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status);
}
};
return (
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
@ -114,16 +115,16 @@ function ListItem({feed}: ListItemProps) {
checked={feed.enabled}
onChange={toggleActive}
className={classNames(
feed.enabled ? 'bg-teal-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'
feed.enabled ? "bg-teal-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'
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>
@ -143,7 +144,7 @@ function ListItem({feed}: ListItemProps) {
</div>
</div>
</li>
)
);
}
interface FeedItemDropdownProps {
@ -155,8 +156,8 @@ interface FeedItemDropdownProps {
const FeedItemDropdown = ({
feed,
onToggle,
toggleUpdate,
}: FeedItemDropdownProps) => {
toggleUpdate
}: FeedItemDropdownProps) => {
const cancelModalButtonRef = useRef(null);
const queryClient = useQueryClient();
@ -207,7 +208,7 @@ const FeedItemDropdown = ({
>
<div className="px-1 py-1">
<Menu.Item>
{({active}) => (
{({ active }) => (
<button
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
@ -227,7 +228,7 @@ const FeedItemDropdown = ({
)}
</Menu.Item>
<Menu.Item>
{({active}) => (
{({ active }) => (
<button
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
@ -249,7 +250,7 @@ const FeedItemDropdown = ({
</div>
<div className="px-1 py-1">
<Menu.Item>
{({active}) => (
{({ active }) => (
<button
className={classNames(
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
@ -273,6 +274,6 @@ const FeedItemDropdown = ({
</Transition>
</Menu>
);
}
};
export default FeedSettings;

View file

@ -5,6 +5,7 @@ import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "../../api/APIClient";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
const ImplementationIRC = () => (
<span
@ -12,7 +13,7 @@ const ImplementationIRC = () => (
>
IRC
</span>
)
);
const ImplementationTorznab = () => (
<span
@ -20,15 +21,19 @@ const ImplementationTorznab = () => (
>
Torznab
</span>
)
);
const implementationMap: any = {
const implementationMap: componentMapType = {
"irc": <ImplementationIRC/>,
"torznab": <ImplementationTorznab />,
"torznab": <ImplementationTorznab />
};
const ListItem = ({ indexer }: any) => {
const [updateIsOpen, toggleUpdate] = useToggle(false)
interface ListItemProps {
indexer: IndexerDefinition;
}
const ListItem = ({ indexer }: ListItemProps) => {
const [updateIsOpen, toggleUpdate] = useToggle(false);
return (
<tr key={indexer.name}>
@ -36,19 +41,19 @@ const ListItem = ({ indexer }: any) => {
<td className="px-6 py-4 whitespace-nowrap">
<Switch
checked={indexer.enabled}
checked={indexer.enabled ?? false}
onChange={toggleUpdate}
className={classNames(
indexer.enabled ? 'bg-teal-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'
indexer.enabled ? "bg-teal-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">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'
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>
@ -61,14 +66,14 @@ const ListItem = ({ indexer }: any) => {
</span>
</td>
</tr>
)
}
);
};
function IndexerSettings() {
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
const { error, data } = useQuery(
'indexer',
"indexer",
APIClient.indexers.getAll,
{ refetchOnWindowFocus: false }
);
@ -146,7 +151,7 @@ function IndexerSettings() {
</div>
</div>
)
);
}
export default IndexerSettings;

View file

@ -13,7 +13,7 @@ import { APIClient } from "../../api/APIClient";
import { EmptySimple } from "../../components/emptystates";
export const IrcSettings = () => {
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
const { data } = useQuery(
"networks",
@ -66,8 +66,8 @@ export const IrcSettings = () => {
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
</div>
</div>
)
}
);
};
interface ListItemProps {
idx: number;
@ -75,7 +75,7 @@ interface ListItemProps {
}
const ListItem = ({ idx, network }: ListItemProps) => {
const [updateIsOpen, toggleUpdate] = useToggle(false)
const [updateIsOpen, toggleUpdate] = useToggle(false);
const [edit, toggleEdit] = useToggle(false);
return (
@ -153,5 +153,5 @@ const ListItem = ({ idx, network }: ListItemProps) => {
</div>
)}
</li>
)
}
);
};

View file

@ -7,13 +7,13 @@ import { Switch } from "@headlessui/react";
import { classNames } from "../../utils";
function NotificationSettings() {
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false)
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
const { data } = useQuery<Notification[], Error>('notifications', APIClient.notifications.getAll,
const { data } = useQuery<Notification[], Error>("notifications", APIClient.notifications.getAll,
{
refetchOnWindowFocus: false
}
)
);
return (
<div className="divide-y divide-gray-200 lg:col-span-9">
@ -56,7 +56,7 @@ function NotificationSettings() {
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
</div>
</div>
)
);
}
interface ListItemProps {
@ -64,7 +64,7 @@ interface ListItemProps {
}
function ListItem({ notification }: ListItemProps) {
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
return (
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
@ -76,16 +76,16 @@ function ListItem({ notification }: ListItemProps) {
checked={notification.enabled}
onChange={toggleUpdateForm}
className={classNames(
notification.enabled ? 'bg-teal-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'
notification.enabled ? "bg-teal-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(
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'
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>
@ -113,7 +113,7 @@ function ListItem({ notification }: ListItemProps) {
</div>
</div>
</li>
)
);
}
export default NotificationSettings;

View file

@ -15,7 +15,6 @@ export const RegexPlayground = () => {
const matches = line.matchAll(regexp);
let lastIndex = 0;
// @ts-ignore
for (const match of matches) {
if (match.index === undefined)
continue;
@ -52,7 +51,7 @@ export const RegexPlayground = () => {
});
setOutput(results);
}
};
return (
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
@ -103,4 +102,4 @@ export const RegexPlayground = () => {
</div>
</div>
);
}
};

View file

@ -13,19 +13,19 @@ function ReleaseSettings() {
const deleteMutation = useMutation(() => APIClient.release.delete(), {
onSuccess: () => {
toast.custom((t) => (
<Toast type="success" body={`All releases was deleted`} t={t}/>
<Toast type="success" body={"All releases was deleted"} t={t}/>
));
// Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries("releases");
toggleDeleteModal()
toggleDeleteModal();
}
})
});
const deleteAction = () => {
deleteMutation.mutate()
}
deleteMutation.mutate();
};
const cancelModalButtonRef = useRef(null);
@ -36,7 +36,7 @@ function ReleaseSettings() {
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title={`Delete all releases`}
title={"Delete all releases"}
text="Are you sure you want to delete all releases? This action cannot be undone."
/>
@ -75,7 +75,7 @@ function ReleaseSettings() {
</div>
</div>
</form>
)
);
}
export default ReleaseSettings;

View file

@ -1,10 +1,10 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function(app) {
app.use(
'/api',
"/api",
createProxyMiddleware({
target: 'http://127.0.0.1:7474',
target: "http://127.0.0.1:7474",
changeOrigin: true,
})
);

View file

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import "@testing-library/jest-dom";

View file

@ -1,11 +1,21 @@
type DownloadClientType =
'QBITTORRENT' |
'DELUGE_V1' |
'DELUGE_V2' |
'RADARR' |
'SONARR' |
'LIDARR' |
'WHISPARR';
"QBITTORRENT" |
"DELUGE_V1" |
"DELUGE_V2" |
"RADARR" |
"SONARR" |
"LIDARR" |
"WHISPARR";
// export enum DownloadClientTypeEnum {
// QBITTORRENT = "QBITTORRENT",
// DELUGE_V1 = "DELUGE_V1",
// DELUGE_V2 = "DELUGE_V2",
// RADARR = "RADARR",
// SONARR = "SONARR",
// LIDARR = "LIDARR",
// WHISPARR = "WHISPARR"
// }
interface DownloadClientRules {
enabled: boolean;
@ -27,7 +37,6 @@ interface DownloadClientSettings {
}
interface DownloadClient {
id?: number;
id: number;
name: string;
type: DownloadClientType;

View file

@ -83,4 +83,4 @@ interface Action {
client_id?: number;
}
type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'WEBHOOK' | DownloadClientType;
type ActionType = "TEST" | "EXEC" | "WATCH_FOLDER" | "WEBHOOK" | DownloadClientType;

View file

@ -8,7 +8,7 @@ interface Indexer {
}
interface IndexerDefinition {
id?: number;
id: number;
name: string;
identifier: string;
implementation: string;

View file

@ -10,7 +10,7 @@ interface IrcNetwork {
nickserv?: NickServ; // optional
channels: IrcChannel[];
connected: boolean;
connected_since: Time;
connected_since: string;
}
interface IrcNetworkCreate {

View file

@ -1,4 +1,4 @@
type NotificationType = 'DISCORD';
type NotificationType = "DISCORD";
interface Notification {
id: number;

View file

@ -19,7 +19,7 @@ export const InitializeGlobalContext = () => {
)
}));
}
}
};
interface AuthInfo {
username: string;

View file

@ -7,32 +7,32 @@ export function sleep(ms: number) {
// get baseUrl sent from server rendered index template
export function baseUrl() {
let baseUrl = ""
let baseUrl = "";
if (window.APP.baseUrl) {
if (window.APP.baseUrl === "/") {
baseUrl = "/"
baseUrl = "/";
} else if (window.APP.baseUrl === "{{.BaseUrl}}") {
baseUrl = "/"
baseUrl = "/";
} else if (window.APP.baseUrl === "/autobrr/") {
baseUrl = "/autobrr/"
baseUrl = "/autobrr/";
} else {
baseUrl = window.APP.baseUrl
baseUrl = window.APP.baseUrl;
}
}
return baseUrl
return baseUrl;
}
// get sseBaseUrl for SSE
export function sseBaseUrl() {
if (process.env.NODE_ENV === "development")
return `http://localhost:7474/`;
return "http://localhost:7474/";
return `${window.location.origin}${baseUrl()}`;
}
export function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ')
return classes.filter(Boolean).join(" ");
}
// column widths for inputs etc
@ -41,9 +41,9 @@ export type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
// simplify date
export function simplifyDate(date: string) {
if (date !== "0001-01-01T00:00:00Z") {
return formatISO9075(new Date(date))
return formatISO9075(new Date(date));
}
return "n/a"
return "n/a";
}
// if empty date show as n/a
@ -52,16 +52,16 @@ export function IsEmptyDate(date: string) {
return formatDistanceToNowStrict(
new Date(date),
{ addSuffix: true }
)
);
}
return "n/a"
return "n/a";
}
export function slugify(str: string) {
return str
.normalize('NFKD')
.normalize("NFKD")
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[^\w\s-]/g, "")
.trim()
.replace(/[-\s]+/g, '-');
.replace(/[-\s]+/g, "-");
}

View file

@ -8,7 +8,7 @@
],
"types": [],
"allowJs": false,
"skipLibCheck": true,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,

View file

@ -2088,7 +2088,22 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.10.2", "@typescript-eslint/eslint-plugin@^5.5.0":
"@typescript-eslint/eslint-plugin@^5.18.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8"
integrity sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA==
dependencies:
"@typescript-eslint/scope-manager" "5.23.0"
"@typescript-eslint/type-utils" "5.23.0"
"@typescript-eslint/utils" "5.23.0"
debug "^4.3.2"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
regexpp "^3.2.0"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/eslint-plugin@^5.5.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d"
integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==
@ -2110,7 +2125,17 @@
dependencies:
"@typescript-eslint/utils" "5.18.0"
"@typescript-eslint/parser@^5.10.2", "@typescript-eslint/parser@^5.5.0":
"@typescript-eslint/parser@^5.18.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.23.0.tgz#443778e1afc9a8ff180f91b5e260ac3bec5e2de1"
integrity sha512-V06cYUkqcGqpFjb8ttVgzNF53tgbB/KoQT/iB++DOIExKmzI9vBJKjZKt/6FuV9c+zrDsvJKbJ2DOCYwX91cbw==
dependencies:
"@typescript-eslint/scope-manager" "5.23.0"
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/typescript-estree" "5.23.0"
debug "^4.3.2"
"@typescript-eslint/parser@^5.5.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6"
integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ==
@ -2128,6 +2153,14 @@
"@typescript-eslint/types" "5.18.0"
"@typescript-eslint/visitor-keys" "5.18.0"
"@typescript-eslint/scope-manager@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz#4305e61c2c8e3cfa3787d30f54e79430cc17ce1b"
integrity sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==
dependencies:
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/visitor-keys" "5.23.0"
"@typescript-eslint/type-utils@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74"
@ -2137,11 +2170,25 @@
debug "^4.3.2"
tsutils "^3.21.0"
"@typescript-eslint/type-utils@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz#f852252f2fc27620d5bb279d8fed2a13d2e3685e"
integrity sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw==
dependencies:
"@typescript-eslint/utils" "5.23.0"
debug "^4.3.2"
tsutils "^3.21.0"
"@typescript-eslint/types@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e"
integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==
"@typescript-eslint/types@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09"
integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==
"@typescript-eslint/typescript-estree@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474"
@ -2155,6 +2202,19 @@
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065"
integrity sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==
dependencies:
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/visitor-keys" "5.23.0"
debug "^4.3.2"
globby "^11.0.4"
is-glob "^4.0.3"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.18.0", "@typescript-eslint/utils@^5.13.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855"
@ -2167,6 +2227,18 @@
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/utils@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.23.0.tgz#4691c3d1b414da2c53d8943310df36ab1c50648a"
integrity sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.23.0"
"@typescript-eslint/types" "5.23.0"
"@typescript-eslint/typescript-estree" "5.23.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/visitor-keys@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60"
@ -2175,6 +2247,14 @@
"@typescript-eslint/types" "5.18.0"
eslint-visitor-keys "^3.0.0"
"@typescript-eslint/visitor-keys@5.23.0":
version "5.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz#057c60a7ca64667a39f991473059377a8067c87b"
integrity sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==
dependencies:
"@typescript-eslint/types" "5.23.0"
eslint-visitor-keys "^3.0.0"
"@webassemblyjs/ast@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"