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", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject", "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" "lint:watch": "npm run lint -- --watch"
}, },
"browserslist": { "browserslist": {
@ -59,8 +59,8 @@
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.0",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-table": "^7.7.7", "@types/react-table": "^7.7.7",
"@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.10.2", "@typescript-eslint/parser": "^5.18.0",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"eslint": "^8.8.0", "eslint": "^8.8.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
@ -71,59 +71,5 @@
"postcss": "^8.4.6", "postcss": "^8.4.6",
"tailwindcss": "^3.0.18", "tailwindcss": "^3.0.18",
"typescript": "^4.1.2" "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({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { useErrorBoundary: true, }, queries: { useErrorBoundary: true },
mutations: { mutations: {
onError: (error) => { onError: (error) => {
// Use a format string to convert the error object to a proper string without much hassle. // 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} />); toast.custom((t) => <Toast type="error" body={message} t={t} />);
} }
}, }
}, }
}); });
export function App() { export function App() {

View file

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

View file

@ -51,7 +51,8 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
role="alert" role="alert"
> >
<div className="flex items-center"> <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 <path
fillRule="evenodd" 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" 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"
@ -84,4 +85,4 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
</div> </div>
</div> </div>
); );
} };

View file

@ -1,33 +1,36 @@
import { classNames } from "../../utils" import React from "react";
import { classNames } from "../../utils";
interface ButtonProps { interface ButtonProps {
className?: string; className?: string;
children: any; children: React.ReactNode;
[rest: string]: any; disabled?: boolean;
onClick?: () => void;
} }
export const Button = ({ children, className, ...rest }: ButtonProps) => ( export const Button = ({ children, className, disabled, onClick }: ButtonProps) => (
<button <button
type="button" type="button"
className={classNames( className={classNames(
className ?? "", 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" "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} {children}
</button> </button>
); );
export const PageButton = ({ children, className, disabled, onClick }: ButtonProps) => (
export const PageButton = ({ children, className, ...rest }: ButtonProps) => (
<button <button
type="button" type="button"
className={classNames( className={classNames(
className ?? "", 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" "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} {children}
</button> </button>

View file

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

View file

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

View file

@ -29,12 +29,12 @@ export const EmptySimple = ({
</div> </div>
) : null} ) : null}
</div> </div>
) );
interface EmptyListStateProps { interface EmptyListStateProps {
text: string; text: string;
buttonText?: string; buttonText?: string;
buttonOnClick?: any; buttonOnClick?: () => void;
} }
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) { export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
@ -51,5 +51,5 @@ export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListSta
</button> </button>
)} )}
</div> </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> <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> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
</div> </div>
) );

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { classNames } from "../../utils";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid"; import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { ErrorField } from "./common" import { ErrorField } from "./common";
interface TextFieldWideProps { interface TextFieldWideProps {
name: string; name: string;
@ -56,7 +56,7 @@ export const TextFieldWide = ({
<ErrorField name={name} classNames="block text-red-500 mt-2" /> <ErrorField name={name} classNames="block text-red-500 mt-2" />
</div> </div>
</div> </div>
) );
interface PasswordFieldWideProps { interface PasswordFieldWideProps {
name: string; name: string;
@ -77,7 +77,7 @@ export const PasswordFieldWide = ({
required, required,
defaultVisible defaultVisible
}: PasswordFieldWideProps) => { }: PasswordFieldWideProps) => {
const [isVisible, toggleVisibility] = useToggle(defaultVisible) const [isVisible, toggleVisibility] = useToggle(defaultVisible);
return ( 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"> <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" /> <ErrorField name={name} classNames="block text-red-500 mt-2" />
</div> </div>
</div> </div>
) );
} };
interface NumberFieldWideProps { interface NumberFieldWideProps {
name: string; name: string;
@ -154,7 +154,7 @@ export const NumberFieldWide = ({
id={name} id={name}
type="number" type="number"
value={field.value ? field.value : defaultValue ?? 0} 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( className={classNames(
meta.touched && meta.error meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500" ? "focus:ring-red-500 focus:border-red-500 border-red-500"
@ -213,19 +213,19 @@ export const SwitchGroupWide = ({
value={field.value} value={field.value}
checked={field.checked ?? false} checked={field.checked ?? false}
onChange={value => { onChange={value => {
form.setFieldValue(field?.name ?? '', value) form.setFieldValue(field?.name ?? "", value);
}} }}
className={classNames( className={classNames(
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-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' "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 className="sr-only">Use setting</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
field.value ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</Switch> </Switch>
@ -233,5 +233,5 @@ export const SwitchGroupWide = ({
</Field> </Field>
</Switch.Group> </Switch.Group>
</ul> </ul>
) );

View file

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

View file

@ -1,5 +1,5 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { Field } from "formik"; import { Field, FieldProps } from "formik";
import { Transition, Listbox } from "@headlessui/react"; import { Transition, Listbox } from "@headlessui/react";
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid"; import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
import { MultiSelect as RMSC } from "react-multi-select-component"; 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 { classNames, COL_WIDTHS } from "../../utils";
import { SettingsContext } from "../../utils/Context"; import { SettingsContext } from "../../utils/Context";
export interface MultiSelectOption {
value: string | number;
label: string;
key?: string;
disabled?: boolean;
}
interface MultiSelectProps { interface MultiSelectProps {
name: string; name: string;
label?: string; label?: string;
options?: [] | any; options: MultiSelectOption[];
columns?: COL_WIDTHS; columns?: COL_WIDTHS;
creatable?: boolean; creatable?: boolean;
} }
@ -20,14 +27,14 @@ export const MultiSelect = ({
label, label,
options, options,
columns, columns,
creatable, creatable
}: MultiSelectProps) => { }: MultiSelectProps) => {
const settingsContext = SettingsContext.useValue(); const settingsContext = SettingsContext.useValue();
const handleNewField = (value: string) => ({ const handleNewField = (value: string) => ({
value: value.toUpperCase(), value: value.toUpperCase(),
label: value.toUpperCase(), label: value.toUpperCase(),
key: value, key: value
}); });
return ( return (
@ -46,21 +53,20 @@ export const MultiSelect = ({
<Field name={name} type="select" multiple={true}> <Field name={name} type="select" multiple={true}>
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<RMSC <RMSC
{...field} {...field}
type="select" 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()]}
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()]}
labelledBy={name} labelledBy={name}
isCreatable={creatable} isCreatable={creatable}
onCreateOption={handleNewField} onCreateOption={handleNewField}
value={field.value && field.value.map((item: any) => ({ value={field.value && field.value.map((item: MultiSelectOption) => ({
value: item.value ? item.value : item, value: item.value ? item.value : item,
label: item.label ? item.label : item, label: item.label ? item.label : item
}))} }))}
onChange={(values: any) => { onChange={(values: Array<MultiSelectOption>) => {
const am = values && values.map((i: any) => i.value); const am = values && values.map((i) => i.value);
setFieldValue(field.name, am); setFieldValue(field.name, am);
}} }}
@ -70,13 +76,18 @@ export const MultiSelect = ({
</Field> </Field>
</div> </div>
); );
};
interface IndexerMultiSelectOption {
id: number;
name: string;
} }
export const IndexerMultiSelect = ({ export const IndexerMultiSelect = ({
name, name,
label, label,
options, options,
columns, columns
}: MultiSelectProps) => { }: MultiSelectProps) => {
const settingsContext = SettingsContext.useValue(); const settingsContext = SettingsContext.useValue();
return ( return (
@ -95,26 +106,26 @@ export const IndexerMultiSelect = ({
<Field name={name} type="select" multiple={true}> <Field name={name} type="select" multiple={true}>
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<RMSC <RMSC
{...field} {...field}
type="select"
options={options} options={options}
labelledBy={name} labelledBy={name}
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))} value={field.value && field.value.map((item: IndexerMultiSelectOption) => ({
onChange={(values: any) => { value: item.id, label: item.name
const am = values && values.map((i: any) => i.value); }))}
setFieldValue(field.name, am); onChange={(values: MultiSelectOption[]) => {
const item = values && values.map((i) => ({ id: i.value, name: i.label }));
setFieldValue(field.name, item);
}} }}
className={settingsContext.darkTheme ? "dark" : ""} className={settingsContext.darkTheme ? "dark" : ""}
itemHeight={50}
/> />
)} )}
</Field> </Field>
</div> </div>
); );
} };
interface DownloadClientSelectProps { interface DownloadClientSelectProps {
name: string; name: string;
@ -132,11 +143,11 @@ export function DownloadClientSelect({
<Field name={name} type="select"> <Field name={name} type="select">
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<Listbox <Listbox
value={field.value} value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)} onChange={(value) => setFieldValue(field?.name, value)}
> >
{({ open }) => ( {({ 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"> <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"> <span className="block truncate">
{field.value {field.value
? clients.find((c) => c.id === field.value)!.name ? clients.find((c) => c.id === field.value)?.name
: "Choose a client"} : "Choose a client"}
</span> </span>
{/*<span className="block truncate">Choose a client</span>*/} {/*<span className="block truncate">Choose a client</span>*/}
@ -171,7 +182,7 @@ export function DownloadClientSelect({
> >
{clients {clients
.filter((c) => c.type === action.type) .filter((c) => c.type === action.type)
.map((client: any) => ( .map((client) => (
<Listbox.Option <Listbox.Option
key={client.id} key={client.id}
className={({ active }) => classNames( className={({ active }) => classNames(
@ -244,11 +255,11 @@ export const Select = ({
<Field name={name} type="select"> <Field name={name} type="select">
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<Listbox <Listbox
value={field.value} value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)} onChange={(value) => setFieldValue(field?.name, value)}
> >
{({ open }) => ( {({ 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"> <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"> <span className="block truncate">
{field.value {field.value
? options.find((c) => c.value === field.value)!.label ? options.find((c) => c.value === field.value)?.label
: optionDefaultText : optionDefaultText
} }
</span> </span>
@ -333,7 +344,7 @@ export const Select = ({
</Field> </Field>
</div> </div>
); );
} };
export const SelectWide = ({ export const SelectWide = ({
name, name,
@ -348,11 +359,11 @@ export const SelectWide = ({
<Field name={name} type="select"> <Field name={name} type="select">
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<Listbox <Listbox
value={field.value} value={field.value}
onChange={(value: any) => setFieldValue(field?.name, value)} onChange={(value) => setFieldValue(field?.name, value)}
> >
{({ open }) => ( {({ open }) => (
<div className="py-4 flex items-center justify-between"> <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"> <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"> <span className="block truncate">
{field.value {field.value
? options.find((c) => c.value === field.value)!.label ? options.find((c) => c.value === field.value)?.label
: optionDefaultText : optionDefaultText
} }
</span> </span>
@ -439,4 +450,4 @@ export const SelectWide = ({
</div> </div>
</div> </div>
); );
} };

View file

@ -1,3 +1,4 @@
import React from "react";
import { Field } from "formik"; import { Field } from "formik";
import type { import type {
FieldInputProps, FieldInputProps,
@ -9,15 +10,18 @@ import type {
import { Switch as HeadlessSwitch } from "@headlessui/react"; import { Switch as HeadlessSwitch } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
type SwitchProps<V = any> = { type SwitchProps<V = unknown> = {
label: string label?: string
checked: boolean checked: boolean
value: boolean
disabled?: boolean disabled?: boolean
onChange: (value: boolean) => void onChange: (value: boolean) => void
field?: FieldInputProps<V> field?: FieldInputProps<V>
form?: FormikProps<FormikValues> form?: FormikProps<FormikValues>
meta?: FieldMetaProps<V> meta?: FieldMetaProps<V>
} children: React.ReactNode
className: string
};
export const Switch = ({ export const Switch = ({
label, label,
@ -25,9 +29,9 @@ export const Switch = ({
disabled = false, disabled = false,
onChange: $onChange, onChange: $onChange,
field, field,
form, form
}: SwitchProps) => { }: SwitchProps) => {
const checked = field?.checked ?? $checked const checked = field?.checked ?? $checked;
return ( return (
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4"> <HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
@ -38,32 +42,32 @@ export const Switch = ({
disabled={disabled} disabled={disabled}
checked={checked} checked={checked}
onChange={value => { onChange={value => {
form?.setFieldValue(field?.name ?? '', value) form?.setFieldValue(field?.name ?? "", value);
$onChange && $onChange(value) $onChange && $onChange(value);
}} }}
className={classNames( className={classNames(
checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', 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' "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 }) => ( {({ checked }) => (
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
checked ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
)} )}
</HeadlessSwitch> </HeadlessSwitch>
</HeadlessSwitch.Group> </HeadlessSwitch.Group>
) );
} };
export type SwitchFormikProps = SwitchProps & FieldProps & React.InputHTMLAttributes<HTMLInputElement>; 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 { interface SwitchGroupProps {
name: string; name: string;
@ -94,26 +98,26 @@ const SwitchGroup = ({
<Field name={name} type="checkbox"> <Field name={name} type="checkbox">
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<Switch <Switch
{...field} {...field}
type="button" // type="button"
value={field.value} value={field.value}
checked={field.checked} checked={field.checked ?? false}
onChange={value => { onChange={value => {
setFieldValue(field?.name ?? '', value) setFieldValue(field?.name ?? "", value);
}} }}
className={classNames( className={classNames(
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200', 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' "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 <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
field.value ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</Switch> </Switch>
@ -123,4 +127,4 @@ const SwitchGroup = ({
</HeadlessSwitch.Group> </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 { Dialog, Transition } from "@headlessui/react";
import { ExclamationIcon } from "@heroicons/react/solid"; import { ExclamationIcon } from "@heroicons/react/solid";
interface DeleteModalProps { interface DeleteModalProps {
isOpen: boolean; isOpen: boolean;
buttonRef: any; buttonRef: React.MutableRefObject<HTMLElement | null> | undefined;
toggle: any; toggle: () => void;
deleteAction: any; deleteAction: () => void;
title: string; title: string;
text: string; text: string;
} }
@ -79,7 +79,7 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
type="button" 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" 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} onClick={toggle}
ref={buttonRef} // ref={buttonRef}
> >
Cancel Cancel
</button> </button>
@ -89,4 +89,4 @@ export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, d
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
) );

View file

@ -1,30 +1,30 @@
import { FC } from 'react' import { FC } from "react";
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid' import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from "@heroicons/react/solid";
import { toast } from 'react-hot-toast' import { toast, Toast as Tooast } from "react-hot-toast";
import { classNames } from '../../utils' import { classNames } from "../../utils";
type Props = { type Props = {
type: 'error' | 'success' | 'warning' type: "error" | "success" | "warning"
body?: string body?: string
t?: any; t?: Tooast;
} };
const Toast: FC<Props> = ({ type, body, t }) => ( const Toast: FC<Props> = ({ type, body, t }) => (
<div className={classNames( <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")}> "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="p-4">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-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 === "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 === "warning" && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
</div> </div>
<div className="ml-3 w-0 flex-1 pt-0.5"> <div className="ml-3 w-0 flex-1 pt-0.5">
<p className="text-sm font-medium text-gray-900 dark:text-gray-200"> <p className="text-sm font-medium text-gray-900 dark:text-gray-200">
{type === 'success' && "Success"} {type === "success" && "Success"}
{type === 'error' && "Error"} {type === "error" && "Error"}
{type === 'warning' && "Warning"} {type === "warning" && "Warning"}
</p> </p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
</div> </div>
@ -32,7 +32,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
<button <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" 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={() => { onClick={() => {
toast.dismiss(t.id) toast.dismiss(t?.id);
}} }}
> >
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
@ -42,6 +42,6 @@ const Toast: FC<Props> = ({ type, body, t }) => (
</div> </div>
</div> </div>
</div> </div>
) );
export default Toast; 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 { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
@ -10,7 +10,7 @@ import { classNames } from "../../utils";
interface SlideOverProps<DataType> { interface SlideOverProps<DataType> {
title: string; title: string;
initialValues: DataType; initialValues: DataType;
validate?: (values?: any) => void; validate?: (values: DataType) => void;
onSubmit: (values?: DataType) => void; onSubmit: (values?: DataType) => void;
isOpen: boolean; isOpen: boolean;
toggle: () => void; toggle: () => void;
@ -30,7 +30,7 @@ function SlideOver<DataType>({
type, type,
children children
}: SlideOverProps<DataType>): React.ReactElement { }: SlideOverProps<DataType>): React.ReactElement {
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
return ( return (
@ -140,7 +140,7 @@ function SlideOver<DataType>({
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
) );
} }
export { SlideOver }; export { SlideOver };

View file

@ -1,3 +1,5 @@
import { MultiSelectOption } from "../components/inputs/select";
export const resolutions = [ export const resolutions = [
"2160p", "2160p",
"1080p", "1080p",
@ -9,7 +11,7 @@ export const resolutions = [
"480i" "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 = [ export const codecs = [
"HEVC", "HEVC",
@ -23,7 +25,7 @@ export const codecs = [
"XviD" "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 = [ export const sources = [
"BluRay", "BluRay",
@ -46,18 +48,18 @@ export const sources = [
"HDTS", "HDTS",
"HDTV", "HDTV",
"Mixed", "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 = [ export const containers = [
"avi", "avi",
"mp4", "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 = [ export const hdr = [
"HDR", "HDR",
@ -69,15 +71,15 @@ export const hdr = [
"DV HDR10", "DV HDR10",
"DV HDR10+", "DV HDR10+",
"DoVi", "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 = [ export const quality_other = [
"REMUX", "REMUX",
"HYBRID", "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 }));
@ -89,10 +91,10 @@ export const formatMusic = [
"Ogg", "Ogg",
"AAC", "AAC",
"AC3", "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 = [ export const sourcesMusic = [
"CD", "CD",
@ -103,10 +105,10 @@ export const sourcesMusic = [
"DAT", "DAT",
"Cassette", "Cassette",
"Blu-Ray", "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 = [ export const qualityMusic = [
"192", "192",
@ -118,10 +120,10 @@ export const qualityMusic = [
"V1 (VBR)", "V1 (VBR)",
"V0 (VBR)", "V0 (VBR)",
"Lossless", "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 = [ export const releaseTypeMusic = [
"Album", "Album",
@ -138,16 +140,16 @@ export const releaseTypeMusic = [
"Demo", "Demo",
"Concert Recording", "Concert Recording",
"DJ Mix", "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 = [ export const originOptions = [
"P2P", "P2P",
"Internal", "Internal",
"SCENE", "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 }));
@ -193,7 +195,7 @@ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
label: "Whisparr", label: "Whisparr",
description: "Send to Whisparr and let it decide", description: "Send to Whisparr and let it decide",
value: "WHISPARR" value: "WHISPARR"
}, }
]; ];
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = { export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
@ -203,7 +205,7 @@ export const DownloadClientTypeNameMap: Record<DownloadClientType | string, stri
"RADARR": "Radarr", "RADARR": "Radarr",
"SONARR": "Sonarr", "SONARR": "Sonarr",
"LIDARR": "Lidarr", "LIDARR": "Lidarr",
"WHISPARR": "Whisparr", "WHISPARR": "Whisparr"
}; };
export const ActionTypeOptions: RadioFieldsetOption[] = [ export const ActionTypeOptions: RadioFieldsetOption[] = [
@ -217,7 +219,7 @@ export const ActionTypeOptions: RadioFieldsetOption[] = [
{ label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" }, { 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: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" }, { 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: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR" }
]; ];
export const ActionTypeNameMap = { export const ActionTypeNameMap = {
@ -231,13 +233,18 @@ export const ActionTypeNameMap = {
"RADARR": "Radarr", "RADARR": "Radarr",
"SONARR": "Sonarr", "SONARR": "Sonarr",
"LIDARR": "Lidarr", "LIDARR": "Lidarr",
"WHISPARR": "Whisparr", "WHISPARR": "Whisparr"
}; };
export const PushStatusOptions: any[] = [ export interface OptionBasic {
label: string;
value: string;
}
export const PushStatusOptions: OptionBasic[] = [
{ {
label: "Rejected", label: "Rejected",
value: "PUSH_REJECTED", value: "PUSH_REJECTED"
}, },
{ {
label: "Approved", label: "Approved",
@ -246,36 +253,36 @@ export const PushStatusOptions: any[] = [
{ {
label: "Error", label: "Error",
value: "PUSH_ERROR" value: "PUSH_ERROR"
}, }
]; ];
export const NotificationTypeOptions: any[] = [ export const NotificationTypeOptions: OptionBasic[] = [
{ {
label: "Discord", label: "Discord",
value: "DISCORD", value: "DISCORD"
}, }
]; ];
export interface SelectOption { export interface SelectOption {
label: string; label: string;
description: string; description: string;
value: any; value: string;
} }
export const EventOptions: SelectOption[] = [ export const EventOptions: SelectOption[] = [
{ {
label: "Push Rejected", label: "Push Rejected",
value: "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", label: "Push Approved",
value: "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", label: "Push Error",
value: "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, UseSortByInstanceProps,
UseSortByOptions, UseSortByOptions,
UseSortByState 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 // 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>> 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 { toast } from "react-hot-toast";
import { XIcon } from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import { Dialog, Transition } from "@headlessui/react"; 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 type { FieldProps } from "formik";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import DEBUG from "../../components/debug"; 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( const mutation = useMutation(
(filter: Filter) => APIClient.filters.create(filter), (filter: Filter) => APIClient.filters.create(filter),
{ {
@ -22,10 +27,16 @@ function FilterAddForm({ isOpen, toggle }: any) {
toggle(); toggle();
} }
} }
) );
const handleSubmit = (data: any) => mutation.mutate(data); const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
const validate = (values: any) => values.name ? {} : { name: "Required" }; const validate = (values: FormikValues) => {
const errors = {} as FormikErrors<FormikValues>;
if (!values.name) {
errors.name = "Required";
}
return errors;
};
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
@ -53,7 +64,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
codecs: [], codecs: [],
sources: [], sources: [],
containers: [], containers: [],
origins: [], origins: []
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
validate={validate} validate={validate}
@ -97,7 +108,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
<Field name="name"> <Field name="name">
{({ {({
field, field,
meta, meta
}: FieldProps ) => ( }: FieldProps ) => (
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<input <input
@ -145,7 +156,7 @@ function FilterAddForm({ isOpen, toggle }: any) {
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
) );
} }
export default FilterAddForm; 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 { useMutation } from "react-query";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { XIcon } from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import { sleep, classNames } from "../../utils"; import { classNames, sleep } from "../../utils";
import { Form, Formik, useFormikContext } from "formik"; import { Form, Formik, useFormikContext } from "formik";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { DownloadClientTypeOptions } from "../../domain/constants"; import { DownloadClientTypeOptions } from "../../domain/constants";
import { toast } from 'react-hot-toast' import { toast } from "react-hot-toast";
import Toast from '../../components/notifications/Toast'; import Toast from "../../components/notifications/Toast";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals"; import { DeleteModal } from "../../components/modals";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs/input_wide"; import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs/input_wide";
import { RadioFieldsetWide } from "../../components/inputs/radio"; import { RadioFieldsetWide } from "../../components/inputs/radio";
import DownloadClient from "../../screens/settings/DownloadClient";
interface InitialValuesSettings { interface InitialValuesSettings {
basic?: { basic?: {
@ -46,7 +47,7 @@ interface InitialValues {
function FormFieldsDefault() { function FormFieldsDefault() {
const { const {
values: { tls }, values: { tls }
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
@ -73,7 +74,7 @@ function FormFieldsDefault() {
function FormFieldsArr() { function FormFieldsArr() {
const { const {
values: { settings }, values: { settings }
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
@ -96,20 +97,23 @@ function FormFieldsArr() {
); );
} }
export const componentMap: any = { export interface componentMapType {
[key: string]: React.ReactElement;
}
export const componentMap: componentMapType = {
DELUGE_V1: <FormFieldsDefault/>, DELUGE_V1: <FormFieldsDefault/>,
DELUGE_V2: <FormFieldsDefault/>, DELUGE_V2: <FormFieldsDefault/>,
QBITTORRENT: <FormFieldsDefault/>, QBITTORRENT: <FormFieldsDefault/>,
RADARR: <FormFieldsArr/>, RADARR: <FormFieldsArr/>,
SONARR: <FormFieldsArr/>, SONARR: <FormFieldsArr/>,
LIDARR: <FormFieldsArr/>, LIDARR: <FormFieldsArr/>,
WHISPARR: <FormFieldsArr />, WHISPARR: <FormFieldsArr/>
}; };
function FormFieldsRulesBasic() { function FormFieldsRulesBasic() {
const { const {
values: { settings }, values: { settings }
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
@ -137,7 +141,7 @@ function FormFieldsRulesBasic() {
function FormFieldsRules() { function FormFieldsRules() {
const { const {
values: { settings }, values: { settings }
} = useFormikContext<InitialValues>(); } = useFormikContext<InitialValues>();
return ( return (
@ -163,7 +167,9 @@ function FormFieldsRules() {
{settings.rules?.ignore_slow_torrents === true && ( {settings.rules?.ignore_slow_torrents === true && (
<Fragment> <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>
)} )}
</Fragment> </Fragment>
@ -172,28 +178,37 @@ function FormFieldsRules() {
); );
} }
export const rulesComponentMap: any = { export const rulesComponentMap: componentMapType = {
DELUGE_V1: <FormFieldsRulesBasic/>, DELUGE_V1: <FormFieldsRulesBasic/>,
DELUGE_V2: <FormFieldsRulesBasic/>, DELUGE_V2: <FormFieldsRulesBasic/>,
QBITTORRENT: <FormFieldsRules />, QBITTORRENT: <FormFieldsRules/>
}; };
interface formButtonsProps { interface formButtonsProps {
isSuccessfulTest: boolean; isSuccessfulTest: boolean;
isErrorTest: boolean; isErrorTest: boolean;
isTesting: boolean; isTesting: boolean;
cancelFn: any; cancelFn: () => void;
testFn: any; testFn: (data: unknown) => void;
values: any; values: unknown;
type: "CREATE" | "UPDATE"; 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 = () => { const test = () => {
testFn(values) testFn(values);
} };
return ( return (
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6"> <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> </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 [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false); const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false); const [isErrorTest, setIsErrorTest] = useState(false);
@ -282,12 +302,12 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]); 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(); toggle();
}, },
onError: () => { 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: () => { onError: () => {
console.log('not added') console.log("not added");
setIsTesting(false); setIsTesting(false);
setIsErrorTest(true); setIsErrorTest(true);
sleep(2500).then(() => { sleep(2500).then(() => {
setIsErrorTest(false); setIsErrorTest(false);
}); });
}, }
} }
); );
const onSubmit = (data: any) => { const onSubmit = (data: unknown) => {
mutation.mutate(data); mutation.mutate(data as DownloadClient);
}; };
const testClient = (data: any) => { const testClient = (data: unknown) => {
testClientMutation.mutate(data); testClientMutation.mutate(data as DownloadClient);
}; };
const initialValues: InitialValues = { const initialValues: InitialValues = {
@ -342,7 +362,7 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
username: "", username: "",
password: "", password: "",
settings: {} settings: {}
} };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
@ -406,7 +426,8 @@ export function DownloadClientAddForm({ isOpen, toggle }: any) {
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> <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"> <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"/> <SwitchGroupWide name="enabled" label="Enabled"/>
</div> </div>
@ -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 [isTesting, setIsTesting] = useState(false);
const [isSuccessfulTest, setIsSuccessfulTest] = useState(false); const [isSuccessfulTest, setIsSuccessfulTest] = useState(false);
const [isErrorTest, setIsErrorTest] = useState(false); const [isErrorTest, setIsErrorTest] = useState(false);
@ -456,9 +483,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["downloadClients"]); 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(); toggle();
}, }
} }
); );
@ -467,9 +494,9 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(); 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(); toggleDeleteModal();
}, }
} }
); );
@ -499,12 +526,12 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
sleep(2500).then(() => { sleep(2500).then(() => {
setIsErrorTest(false); setIsErrorTest(false);
}); });
}, }
} }
); );
const onSubmit = (data: any) => { const onSubmit = (data: unknown) => {
mutation.mutate(data); mutation.mutate(data as DownloadClient);
}; };
const cancelButtonRef = useRef(null); const cancelButtonRef = useRef(null);
@ -514,8 +541,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
deleteMutation.mutate(client.id); deleteMutation.mutate(client.id);
}; };
const testClient = (data: any) => { const testClient = (data: unknown) => {
testClientMutation.mutate(data); testClientMutation.mutate(data as DownloadClient);
}; };
const initialValues = { const initialValues = {
@ -529,8 +556,8 @@ export function DownloadClientUpdateForm({ client, isOpen, toggle }: any) {
tls_skip_verify: client.tls_skip_verify, tls_skip_verify: client.tls_skip_verify,
username: client.username, username: client.username,
password: client.password, password: client.password,
settings: client.settings, settings: client.settings
} };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>

View file

@ -6,10 +6,11 @@ import Toast from "../../components/notifications/Toast";
import { SlideOver } from "../../components/panels"; import { SlideOver } from "../../components/panels";
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs"; import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
import { ImplementationMap } from "../../screens/settings/Feed"; import { ImplementationMap } from "../../screens/settings/Feed";
import { componentMapType } from "./DownloadClientForms";
interface UpdateProps { interface UpdateProps {
isOpen: boolean; isOpen: boolean;
toggle: any; toggle: () => void;
feed: Feed; feed: Feed;
} }
@ -19,9 +20,9 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["feeds"]); 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(); toggle();
}, }
} }
); );
@ -30,14 +31,14 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["feeds"]); 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) => { const onSubmit = (formData: unknown) => {
mutation.mutate(formData); mutation.mutate(formData as Feed);
} };
const deleteAction = () => { const deleteAction = () => {
deleteMutation.mutate(feed.id); deleteMutation.mutate(feed.id);
@ -51,8 +52,8 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
name: feed.name, name: feed.name,
url: feed.url, url: feed.url,
api_key: feed.api_key, api_key: feed.api_key,
interval: feed.interval, interval: feed.interval
} };
return ( return (
<SlideOver <SlideOver
@ -69,7 +70,8 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
<TextFieldWide name="name" label="Name" required={true}/> <TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> <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> <div>
<label <label
htmlFor="type" htmlFor="type"
@ -91,7 +93,7 @@ export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
</div> </div>
)} )}
</SlideOver> </SlideOver>
) );
} }
function FormFieldsTorznab() { function FormFieldsTorznab() {
@ -105,11 +107,12 @@ function FormFieldsTorznab() {
<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> </div>
); );
} }
const componentMap: any = { const componentMap: componentMapType = {
TORZNAB: <FormFieldsTorznab/>, TORZNAB: <FormFieldsTorznab/>
}; };

View file

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

View file

@ -1,7 +1,7 @@
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { XIcon } from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import { Field, FieldArray } from "formik"; import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
@ -12,9 +12,9 @@ import {
PasswordFieldWide, PasswordFieldWide,
SwitchGroupWide, SwitchGroupWide,
NumberFieldWide NumberFieldWide
} from "../../components/inputs/input_wide"; } from "../../components/inputs";
import { SlideOver } from "../../components/panels"; import { SlideOver } from "../../components/panels";
import Toast from '../../components/notifications/Toast'; import Toast from "../../components/notifications/Toast";
interface ChannelsFieldArrayProps { interface ChannelsFieldArrayProps {
channels: IrcChannel[]; channels: IrcChannel[];
@ -95,35 +95,31 @@ interface IrcNetworkAddFormValues {
channels: IrcChannel[]; channels: IrcChannel[];
} }
export function IrcNetworkAddForm({ isOpen, toggle }: any) { interface AddFormProps {
isOpen: boolean;
toggle: () => void;
}
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
const mutation = useMutation( const mutation = useMutation(
(network: IrcNetwork) => APIClient.irc.createNetwork(network), (network: IrcNetwork) => APIClient.irc.createNetwork(network),
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(['networks']); 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} />) 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() toggle();
}, },
onError: () => { 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) => { const onSubmit = (data: unknown) => {
// easy way to split textarea lines into array of strings for each newline. mutation.mutate(data as IrcNetwork);
// 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 validate = (values: FormikValues) => {
const validate = (values: IrcNetworkAddFormValues) => { const errors = {} as FormikErrors<FormikValues>;
const errors = {} as any;
if (!values.name) if (!values.name)
errors.name = "Required"; errors.name = "Required";
@ -137,7 +133,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
errors.nickserv = { account: "Required" }; errors.nickserv = { account: "Required" };
return errors; return errors;
} };
const initialValues: IrcNetworkAddFormValues = { const initialValues: IrcNetworkAddFormValues = {
name: "", name: "",
@ -149,7 +145,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
nickserv: { nickserv: {
account: "" account: ""
}, },
channels: [], channels: []
}; };
return ( return (
@ -193,7 +189,7 @@ export function IrcNetworkAddForm({ isOpen, toggle }: any) {
</> </>
)} )}
</SlideOver> </SlideOver>
) );
} }
interface IrcNetworkUpdateFormValues { interface IrcNetworkUpdateFormValues {
@ -222,34 +218,27 @@ export function IrcNetworkUpdateForm({
}: IrcNetworkUpdateFormProps) { }: IrcNetworkUpdateFormProps) {
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), { const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(['networks']); queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />) toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
toggle() toggle();
} }
}) });
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), { const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(['networks']); queryClient.invalidateQueries(["networks"]);
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />) toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
toggle() toggle();
} }
}) });
const onSubmit = (data: any) => { const onSubmit = (data: unknown) => {
// easy way to split textarea lines into array of strings for each newline. mutation.mutate(data as IrcNetwork);
// 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 validate = (values: any) => { const validate = (values: FormikValues) => {
const errors = {} as any; const errors = {} as FormikErrors<FormikValues>;
if (!values.name) { if (!values.name) {
errors.name = "Required"; errors.name = "Required";
@ -264,15 +253,17 @@ export function IrcNetworkUpdateForm({
} }
if (!values.nickserv?.account) { if (!values.nickserv?.account) {
errors.nickserv.account = "Required"; errors.nickserv = {
account: "Required"
};
} }
return errors; return errors;
} };
const deleteAction = () => { const deleteAction = () => {
deleteMutation.mutate(network.id) deleteMutation.mutate(network.id);
} };
const initialValues: IrcNetworkUpdateFormValues = { const initialValues: IrcNetworkUpdateFormValues = {
id: network.id, id: network.id,
@ -285,7 +276,7 @@ export function IrcNetworkUpdateForm({
pass: network.pass, pass: network.pass,
channels: network.channels, channels: network.channels,
invite_command: network.invite_command invite_command: network.invite_command
} };
return ( return (
<SlideOver <SlideOver
@ -329,5 +320,5 @@ export function IrcNetworkUpdateForm({
</> </>
)} )}
</SlideOver> </SlideOver>
) );
} }

View file

@ -1,59 +1,63 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from "react"; import { Fragment } from "react";
import {Field, Form, Formik} from "formik"; import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
import type { FieldProps } from "formik"; import type { FieldProps } from "formik";
import { XIcon } from "@heroicons/react/solid"; import { XIcon } from "@heroicons/react/solid";
import Select, {components} from "react-select"; import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
import { import {
SwitchGroupWide, SwitchGroupWide,
TextFieldWide TextFieldWide
} from "../../components/inputs"; } from "../../components/inputs";
import DEBUG from "../../components/debug"; import DEBUG from "../../components/debug";
import {EventOptions, NotificationTypeOptions} from "../../domain/constants"; import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import Toast from "../../components/notifications/Toast"; import Toast from "../../components/notifications/Toast";
import { SlideOver } from "../../components/panels"; import { SlideOver } from "../../components/panels";
import { componentMapType } from "./DownloadClientForms";
const Input = (props: InputProps) => {
const Input = (props: any) => {
return ( return (
<components.Input <components.Input
{...props} {...props}
inputClassName="outline-none border-none shadow-none focus:ring-transparent" inputClassName="outline-none border-none shadow-none focus:ring-transparent"
className="text-gray-400 dark:text-gray-100" className="text-gray-400 dark:text-gray-100"
children={props.children}
/> />
); );
} };
const Control = (props: any) => { const Control = (props: ControlProps) => {
return ( return (
<components.Control <components.Control
{...props} {...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" 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 ( return (
<components.Menu <components.Menu
{...props} {...props}
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm" 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 ( return (
<components.Option <components.Option
{...props} {...props}
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900" className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
children={props.children}
/> />
); );
} };
function FormFieldsDiscord() { function FormFieldsDiscord() {
@ -76,8 +80,8 @@ function FormFieldsDiscord() {
); );
} }
const componentMap: any = { const componentMap: componentMapType = {
DISCORD: <FormFieldsDiscord/>, DISCORD: <FormFieldsDiscord/>
}; };
interface NotificationAddFormValues { interface NotificationAddFormValues {
@ -87,7 +91,7 @@ interface NotificationAddFormValues {
interface AddProps { interface AddProps {
isOpen: boolean; isOpen: boolean;
toggle: any; toggle: () => void;
} }
export function NotificationAddForm({ isOpen, toggle }: AddProps) { export function NotificationAddForm({ isOpen, toggle }: AddProps) {
@ -95,27 +99,27 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
(notification: Notification) => APIClient.notifications.create(notification), (notification: Notification) => APIClient.notifications.create(notification),
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(['notifications']); queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />) toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
toggle() toggle();
}, },
onError: () => { 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) => { const onSubmit = (formData: unknown) => {
mutation.mutate(formData) mutation.mutate(formData as Notification);
} };
const validate = (values: NotificationAddFormValues) => { const validate = (values: NotificationAddFormValues) => {
const errors = {} as any; const errors = {} as FormikErrors<FormikValues>;
if (!values.name) if (!values.name)
errors.name = "Required"; errors.name = "Required";
return errors; return errors;
} };
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
@ -141,7 +145,7 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
type: "", type: "",
name: "", name: "",
webhook: "", webhook: "",
events: [], events: []
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
validate={validate} validate={validate}
@ -152,8 +156,9 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6"> <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="flex items-start justify-between space-x-3">
<div className="space-y-1"> <div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Add <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
Notifications</Dialog.Title> Add Notifications
</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200"> <p className="text-sm text-gray-500 dark:text-gray-200">
Trigger notifications on different events. Trigger notifications on different events.
</p> </p>
@ -205,14 +210,16 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
spacing: { spacing: {
...theme.spacing, ...theme.spacing,
controlHeight: 30, controlHeight: 30,
baseUnit: 2, baseUnit: 2
} }
})} })}
value={field?.value && field.value.value} value={field?.value && field.value.value}
onChange={(option: any) => { onChange={(option: unknown) => {
resetForm() resetForm();
const opt = option as SelectOption;
// setFieldValue("name", option?.label ?? "") // setFieldValue("name", option?.label ?? "")
setFieldValue(field.name, option?.value ?? "") setFieldValue(field.name, opt.value ?? "");
}} }}
options={NotificationTypeOptions} options={NotificationTypeOptions}
/> />
@ -273,7 +280,7 @@ export function NotificationAddForm({isOpen, toggle}: AddProps) {
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
) );
} }
const EventCheckBoxes = () => ( const EventCheckBoxes = () => (
@ -303,11 +310,11 @@ const EventCheckBoxes = () => (
</div> </div>
))} ))}
</fieldset> </fieldset>
) );
interface UpdateProps { interface UpdateProps {
isOpen: boolean; isOpen: boolean;
toggle: any; toggle: () => void;
notification: Notification; notification: Notification;
} }
@ -317,9 +324,9 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["notifications"]); 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(); toggle();
}, }
} }
); );
@ -328,14 +335,14 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["notifications"]); 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) => { const onSubmit = (formData: unknown) => {
mutation.mutate(formData); mutation.mutate(formData as Notification);
} };
const deleteAction = () => { const deleteAction = () => {
deleteMutation.mutate(notification.id); deleteMutation.mutate(notification.id);
@ -347,8 +354,8 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
type: notification.type, type: notification.type,
name: notification.name, name: notification.name,
webhook: notification.webhook, webhook: notification.webhook,
events: notification.events || [], events: notification.events || []
} };
return ( return (
<SlideOver <SlideOver
@ -394,13 +401,14 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
spacing: { spacing: {
...theme.spacing, ...theme.spacing,
controlHeight: 30, controlHeight: 30,
baseUnit: 2, baseUnit: 2
} }
})} })}
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)} value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
onChange={(option: any) => { onChange={(option: unknown) => {
resetForm() resetForm();
setFieldValue(field.name, option?.value ?? "") const opt = option as SelectOption;
setFieldValue(field.name, opt.value ?? "");
}} }}
options={NotificationTypeOptions} options={NotificationTypeOptions}
/> />
@ -431,5 +439,5 @@ export function NotificationUpdateForm({isOpen, toggle, notification}: UpdatePro
</div> </div>
)} )}
</SlideOver> </SlideOver>
) );
} }

View file

@ -2,7 +2,7 @@ import type { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { 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); getCLS(onPerfEntry);
getFID(onPerfEntry); getFID(onPerfEntry);
getFCP(onPerfEntry); getFCP(onPerfEntry);

View file

@ -1,6 +1,6 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { NavLink, Link, Route, Switch } from "react-router-dom";
import type { match } 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 { Disclosure, Menu, Transition } from "@headlessui/react";
import { ExternalLinkIcon } from "@heroicons/react/solid"; import { ExternalLinkIcon } from "@heroicons/react/solid";
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline"; import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
@ -11,9 +11,9 @@ import { Logs } from "./Logs";
import { Releases } from "./releases"; import { Releases } from "./releases";
import { Dashboard } from "./dashboard"; import { Dashboard } from "./dashboard";
import { FilterDetails, Filters } from "./filters"; 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 { interface NavItem {
name: string; name: string;
@ -21,11 +21,11 @@ interface NavItem {
} }
function classNames(...classes: string[]) { function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(" ");
} }
const isActiveMatcher = ( const isActiveMatcher = (
match: match<any> | null, match: match | null,
location: { pathname: string }, location: { pathname: string },
item: NavItem item: NavItem
) => { ) => {
@ -33,20 +33,20 @@ const isActiveMatcher = (
return false; return false;
if (match?.url === "/" && item.path === "/" && location.pathname === "/") if (match?.url === "/" && item.path === "/" && location.pathname === "/")
return true return true;
if (match.url === "/") if (match.url === "/")
return false; return false;
return true; return true;
} };
export default function Base() { export default function Base() {
const authContext = AuthContext.useValue(); const authContext = AuthContext.useValue();
const nav: Array<NavItem> = [ const nav: Array<NavItem> = [
{ name: 'Dashboard', path: "/" }, { name: "Dashboard", path: "/" },
{ name: 'Filters', path: "/filters" }, { name: "Filters", path: "/filters" },
{ name: 'Releases', path: "/releases" }, { name: "Releases", path: "/releases" },
{ name: "Settings", path: "/settings" }, { name: "Settings", path: "/settings" },
{ name: "Logs", path: "/logs" } { name: "Logs", path: "/logs" }
]; ];
@ -102,7 +102,8 @@ export default function Base() {
)} )}
> >
Docs 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> </a>
</div> </div>
</div> </div>
@ -148,8 +149,8 @@ export default function Base() {
<Link <Link
to="/settings" to="/settings"
className={classNames( className={classNames(
active ? 'bg-gray-100 dark:bg-gray-600' : '', active ? "bg-gray-100 dark:bg-gray-600" : "",
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200' "block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
)} )}
> >
Settings Settings
@ -161,8 +162,8 @@ export default function Base() {
<Link <Link
to="/logout" to="/logout"
className={classNames( className={classNames(
active ? 'bg-gray-100 dark:bg-gray-600' : '', active ? "bg-gray-100 dark:bg-gray-600" : "",
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200' "block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
)} )}
> >
Logout Logout
@ -245,5 +246,5 @@ export default function Base() {
</Route> </Route>
</Switch> </Switch>
</div> </div>
) );
} }

View file

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

View file

@ -1,4 +1,4 @@
import {BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon} from '@heroicons/react/outline' 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 { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
import { classNames } from "../utils"; import { classNames } from "../utils";
@ -6,31 +6,43 @@ import IndexerSettings from "./settings/Indexer";
import { IrcSettings } from "./settings/Irc"; import { IrcSettings } from "./settings/Irc";
import ApplicationSettings from "./settings/Application"; import ApplicationSettings from "./settings/Application";
import DownloadClientSettings from "./settings/DownloadClient"; import DownloadClientSettings from "./settings/DownloadClient";
import { RegexPlayground } from './settings/RegexPlayground'; import { RegexPlayground } from "./settings/RegexPlayground";
import ReleaseSettings from "./settings/Releases"; import ReleaseSettings from "./settings/Releases";
import NotificationSettings from "./settings/Notifications"; import NotificationSettings from "./settings/Notifications";
import FeedSettings from "./settings/Feed"; import FeedSettings from "./settings/Feed";
const subNavigation = [ interface NavTabType {
{name: 'Application', href: '', icon: CogIcon, current: true}, name: string;
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false}, href: string;
{name: 'IRC', href: 'irc', icon: ChatAlt2Icon, current: false}, icon: typeof CogIcon;
{name: 'Feeds', href: 'feeds', icon: RssIcon, current: false}, current: boolean;
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false}, }
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false}, 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: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, 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 location = useLocation();
const { pathname } = location; const { pathname } = location;
const splitLocation = pathname.split("/"); const splitLocation = pathname.split("/");
// we need to clean the / if it's a base root path // 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 ( return (
<NavLink <NavLink
key={item.name} key={item.name}
@ -38,9 +50,9 @@ function SubNavLink({item, url}: any) {
exact={true} 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" 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( 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 <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" 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> <span className="truncate">{item.name}</span>
</NavLink> </NavLink>
) );
} }
function SidebarNav({subNavigation, url}: any) { interface SidebarNavProps {
subNavigation: NavTabType[];
url: string;
}
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
return ( return (
<aside className="py-2 lg:col-span-3"> <aside className="py-2 lg:col-span-3">
<nav className="space-y-1"> <nav className="space-y-1">
{subNavigation.map((item: any) => ( {subNavigation.map((item) => (
<SubNavLink item={item} url={url} key={item.href}/> <SubNavLink item={item} url={url} key={item.href}/>
))} ))}
</nav> </nav>
</aside> </aside>
) );
} }
export default function Settings() { export default function Settings() {
@ -115,6 +132,6 @@ export default function Settings() {
</div> </div>
</div> </div>
</main> </main>
) );
} }

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import {
useFilters, useFilters,
useGlobalFilter, useGlobalFilter,
useSortBy, useSortBy,
usePagination usePagination, FilterProps, Column
} from "react-table"; } from "react-table";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
@ -17,17 +17,17 @@ import * as DataTable from "../../components/data-table";
// This is a custom filter UI for selecting // This is a custom filter UI for selecting
// a unique option from a list // a unique option from a list
function SelectColumnFilter({ function SelectColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id, render }, column: { filterValue, setFilter, preFilteredRows, id, render }
}: any) { }: FilterProps<object>) {
// Calculate the options for filtering // Calculate the options for filtering
// using the preFilteredRows // using the preFilteredRows
const options = React.useMemo(() => { const options = React.useMemo(() => {
const options: any = new Set() const options = new Set<string>();
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => { preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
options.add(row.values[id]) options.add(row.values[id]);
}) });
return [...options.values()] return [...options.values()];
}, [id, preFilteredRows]) }, [id, preFilteredRows]);
// Render a multi-select box // Render a multi-select box
return ( return (
@ -39,7 +39,7 @@ function SelectColumnFilter({
id={id} id={id}
value={filterValue} value={filterValue}
onChange={e => { onChange={e => {
setFilter(e.target.value || undefined) setFilter(e.target.value || undefined);
}} }}
> >
<option value="">All</option> <option value="">All</option>
@ -50,17 +50,22 @@ function SelectColumnFilter({
))} ))}
</select> </select>
</label> </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 // Use the state and functions returned from useTable to build your UI
const { const {
getTableProps, getTableProps,
getTableBodyProps, getTableBodyProps,
headerGroups, headerGroups,
prepareRow, prepareRow,
page, // Instead of using 'rows', we'll use page, page // Instead of using 'rows', we'll use page,
} = useTable( } = useTable(
{ columns, data }, { columns, data },
useFilters, useFilters,
@ -94,7 +99,7 @@ function Table({ columns, data }: any) {
{...columnRest} {...columnRest}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{column.render('Header')} {column.render("Header")}
{/* Add a sort direction indicator */} {/* Add a sort direction indicator */}
<span> <span>
{column.isSorted ? ( {column.isSorted ? (
@ -119,12 +124,12 @@ function Table({ columns, data }: any) {
{...getTableBodyProps()} {...getTableBodyProps()}
className="divide-y divide-gray-200 dark:divide-gray-700" className="divide-y divide-gray-200 dark:divide-gray-700"
> >
{page.map((row: any) => { {page.map((row) => {
prepareRow(row); prepareRow(row);
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps(); const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
return ( return (
<tr key={bodyRowKey} {...bodyRowRest}> <tr key={bodyRowKey} {...bodyRowRest}>
{row.cells.map((cell: any) => { {row.cells.map((cell) => {
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps(); const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
return ( return (
<td <td
@ -133,12 +138,12 @@ function Table({ columns, data }: any) {
role="cell" role="cell"
{...cellRowRest} {...cellRowRest}
> >
{cell.render('Cell')} {cell.render("Cell")}
</td> </td>
) );
})} })}
</tr> </tr>
) );
})} })}
</tbody> </tbody>
</table> </table>
@ -151,30 +156,30 @@ export const ActivityTable = () => {
const columns = React.useMemo(() => [ const columns = React.useMemo(() => [
{ {
Header: "Age", Header: "Age",
accessor: 'timestamp', accessor: "timestamp",
Cell: DataTable.AgeCell, Cell: DataTable.AgeCell
}, },
{ {
Header: "Release", Header: "Release",
accessor: 'torrent_name', accessor: "torrent_name",
Cell: DataTable.TitleCell, Cell: DataTable.TitleCell
}, },
{ {
Header: "Actions", Header: "Actions",
accessor: 'action_status', accessor: "action_status",
Cell: DataTable.ReleaseStatusCell, Cell: DataTable.ReleaseStatusCell
}, },
{ {
Header: "Indexer", Header: "Indexer",
accessor: 'indexer', accessor: "indexer",
Cell: DataTable.TitleCell, Cell: DataTable.TitleCell,
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
filter: 'includes', filter: "includes"
}, }
], []) ], []);
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery(
'dash_release', "dash_release",
() => APIClient.release.find("?limit=10"), () => APIClient.release.find("?limit=10"),
{ refetchOnWindowFocus: false } { refetchOnWindowFocus: false }
); );
@ -188,7 +193,7 @@ export const ActivityTable = () => {
Recent activity Recent activity
</h3> </h3>
<Table columns={columns} data={data?.data} /> <Table columns={columns} data={data?.data ?? []} />
</div> </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> <p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
</dd> </dd>
</div> </div>
) );
export const Stats = () => { export const Stats = () => {
const { isLoading, data } = useQuery( const { isLoading, data } = useQuery(
@ -44,5 +44,5 @@ export const Stats = () => {
<StatsItem name="Approved Pushes" value={data?.push_approved_count} /> <StatsItem name="Approved Pushes" value={data?.push_approved_count} />
</dl> </dl>
</div> </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 { useMutation, useQuery } from "react-query";
import { import {
NavLink, NavLink,
@ -10,10 +10,9 @@ import {
useRouteMatch useRouteMatch
} from "react-router-dom"; } from "react-router-dom";
import { toast } from "react-hot-toast"; 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 { Dialog, Transition, Switch as SwitchBasic } from "@headlessui/react";
import { ChevronDownIcon, ChevronRightIcon, } from "@heroicons/react/solid"; import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/solid";
import { import {
CONTAINER_OPTIONS, CONTAINER_OPTIONS,
@ -50,16 +49,27 @@ import { DeleteModal } from "../../components/modals";
import { TitleSubtitle } from "../../components/headings"; import { TitleSubtitle } from "../../components/headings";
import { EmptyListState } from "../../components/emptystates"; import { EmptyListState } from "../../components/emptystates";
const tabs = [ interface tabType {
{ name: 'General', href: '', current: true }, name: string;
{ name: 'Movies and TV', href: 'movies-tv', current: false }, href: string;
{ name: 'Music', href: 'music', current: false }, current: boolean;
// { name: 'P2P', href: 'p2p', current: false }, }
{ name: 'Advanced', href: 'advanced', current: false },
{ name: 'Actions', href: 'actions', current: false },
]
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 location = useLocation();
const splitLocation = location.pathname.split("/"); const splitLocation = location.pathname.split("/");
@ -71,16 +81,23 @@ function TabNavLink({ item, url }: any) {
exact exact
activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white" activeClassName="border-purple-600 dark:border-blue-500 text-purple-600 dark:text-white"
className={classNames( 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} {item.name}
</NavLink> </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 [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
@ -123,8 +140,8 @@ const FormButtonsGroup = ({ values, deleteAction, reset }: any) => {
</div> </div>
</div> </div>
</div> </div>
) );
} };
export default function FilterDetails() { export default function FilterDetails() {
const history = useHistory(); const history = useHistory();
@ -164,16 +181,16 @@ export default function FilterDetails() {
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries(["filters"]);
// redirect // redirect
history.push("/filters") history.push("/filters");
} }
}) });
if (isLoading) { if (isLoading) {
return null return null;
} }
if (!filter) { if (!filter) {
return null return null;
} }
const handleSubmit = (data: Filter) => { const handleSubmit = (data: Filter) => {
@ -181,17 +198,17 @@ export default function FilterDetails() {
// TODO add options for these // TODO add options for these
data.actions.forEach((a: Action) => { data.actions.forEach((a: Action) => {
if (a.type === "WEBHOOK") { if (a.type === "WEBHOOK") {
a.webhook_method = "POST" a.webhook_method = "POST";
a.webhook_type = "JSON" a.webhook_type = "JSON";
} }
}) });
updateMutation.mutate(data) updateMutation.mutate(data);
} };
const deleteAction = () => { const deleteAction = () => {
deleteMutation.mutate(filter.id) deleteMutation.mutate(filter.id);
} };
return ( return (
<main> <main>
@ -267,7 +284,7 @@ export default function FilterDetails() {
albums: filter.albums, albums: filter.albums,
origins: filter.origins || [], origins: filter.origins || [],
indexers: filter.indexers || [], indexers: filter.indexers || [],
actions: filter.actions || [], actions: filter.actions || []
} as Filter} } as Filter}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
@ -308,7 +325,7 @@ export default function FilterDetails() {
</div> </div>
</div> </div>
</main> </main>
) );
} }
function General() { function General() {
@ -320,12 +337,13 @@ function General() {
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({ const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
label: v.name, label: v.name,
value: { value: v.id
id: v.id, // value: {
name: v.name, // id: v.id,
identifier: v.identifier, // name: v.name,
enabled: v.enabled // identifier: v.identifier,
} // enabled: v.enabled
// }
})) : []; })) : [];
return ( return (
@ -401,7 +419,7 @@ function MoviesTv() {
</div> </div>
</div> </div>
</div> </div>
) );
} }
function Music() { function Music() {
@ -453,7 +471,7 @@ function Music() {
</div> </div>
</div> </div>
</div> </div>
) );
} }
function Advanced() { function Advanced() {
@ -497,17 +515,17 @@ function Advanced() {
<TextField name="freeleech_percent" label="Freeleech percent" columns={6} /> <TextField name="freeleech_percent" label="Freeleech percent" columns={6} />
</CollapsableSection> </CollapsableSection>
</div> </div>
) );
} }
interface CollapsableSectionProps { interface CollapsableSectionProps {
title: string; title: string;
subtitle: string; subtitle: string;
children: any; children: React.ReactNode;
} }
function CollapsableSection({ title, subtitle, children }: CollapsableSectionProps) { 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="mt-6 lg:pb-6 border-b border-gray-200 dark:border-gray-700">
@ -531,12 +549,12 @@ function CollapsableSection({ title, subtitle, children }: CollapsableSectionPro
</div> </div>
)} )}
</div> </div>
) );
} }
interface FilterActionsProps { interface FilterActionsProps {
filter: Filter; filter: Filter;
values: any; values: FormikValues;
} }
function FilterActions({ filter, values }: FilterActionsProps) { function FilterActions({ filter, values }: FilterActionsProps) {
@ -572,9 +590,9 @@ function FilterActions({ filter, values }: FilterActionsProps) {
webhook_type: "", webhook_type: "",
webhook_method: "", webhook_method: "",
webhook_data: "", webhook_data: "",
webhook_headers: [], webhook_headers: []
// client_id: 0, // client_id: 0,
} };
return ( return (
<div className="mt-10"> <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"> <div className="light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
{values.actions.length > 0 ? {values.actions.length > 0 ?
<ul className="divide-y divide-gray-200 dark:divide-gray-700"> <ul className="divide-y divide-gray-200 dark:divide-gray-700">
{values.actions.map((action: any, index: number) => ( {values.actions.map((action: Action, index: number) => (
<FilterActionsItem action={action} clients={data!} idx={index} remove={remove} key={index} /> <FilterActionsItem action={action} clients={data ?? []} idx={index} remove={remove} key={index} />
))} ))}
</ul> </ul>
: <EmptyListState text="No actions yet!" /> : <EmptyListState text="No actions yet!" />
@ -613,14 +631,14 @@ function FilterActions({ filter, values }: FilterActionsProps) {
)} )}
</FieldArray> </FieldArray>
</div> </div>
) );
} }
interface FilterActionsItemProps { interface FilterActionsItemProps {
action: Action; action: Action;
clients: DownloadClient[]; clients: DownloadClient[];
idx: number; idx: number;
remove: any; remove: <T>(index: number) => T | undefined;
} }
function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) { function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemProps) {
@ -681,7 +699,7 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
name={`actions.${idx}.webhook_data`} name={`actions.${idx}.webhook_data`}
label="Data (json)" label="Data (json)"
columns={6} columns={6}
placeholder={`Request data: { "key": "value" }`} placeholder={"Request data: { \"key\": \"value\" }"}
/> />
</div> </div>
); );
@ -856,27 +874,27 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
<Field name={`actions.${idx}.enabled`} type="checkbox"> <Field name={`actions.${idx}.enabled`} type="checkbox">
{({ {({
field, field,
form: { setFieldValue }, form: { setFieldValue }
}: any) => ( }: FieldProps) => (
<SwitchBasic <SwitchBasic
{...field} {...field}
type="button" type="button"
value={field.value} value={field.value}
checked={field.checked} checked={field.checked ?? false}
onChange={value => { onChange={(value: boolean) => {
setFieldValue(field?.name ?? '', value) setFieldValue(field?.name ?? "", value);
}} }}
className={classNames( className={classNames(
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', 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' "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 className="sr-only">toggle enabled</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
field.value ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</SwitchBasic> </SwitchBasic>
@ -972,5 +990,5 @@ function FilterActionsItem({ action, clients, idx, remove }: FilterActionsItemPr
</div> </div>
)} )}
</li> </li>
) );
} }

View file

@ -7,7 +7,7 @@ import {
TrashIcon, TrashIcon,
PencilAltIcon, PencilAltIcon,
SwitchHorizontalIcon, SwitchHorizontalIcon,
DotsHorizontalIcon, DuplicateIcon, DotsHorizontalIcon, DuplicateIcon
} from "@heroicons/react/outline"; } from "@heroicons/react/outline";
import { queryClient } from "../../App"; import { queryClient } from "../../App";
@ -20,7 +20,7 @@ import { EmptyListState } from "../../components/emptystates";
import { DeleteModal } from "../../components/modals"; import { DeleteModal } from "../../components/modals";
export default function Filters() { export default function Filters() {
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false) const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
const { isLoading, error, data } = useQuery( const { isLoading, error, data } = useQuery(
["filters"], ["filters"],
@ -63,7 +63,7 @@ export default function Filters() {
)} )}
</div> </div>
</main> </main>
) );
} }
interface FilterListProps { interface FilterListProps {
@ -97,7 +97,7 @@ function FilterList({ filters }: FilterListProps) {
</tbody> </tbody>
</table> </table>
</div> </div>
) );
} }
interface FilterItemDropdownProps { interface FilterItemDropdownProps {
@ -256,7 +256,7 @@ const FilterItemDropdown = ({
</Transition> </Transition>
</Menu> </Menu>
); );
} };
interface FilterListItemProps { interface FilterListItemProps {
filter: Filter; filter: Filter;
@ -264,13 +264,13 @@ interface FilterListItemProps {
} }
function FilterListItem({ filter, idx }: FilterListItemProps) { function FilterListItem({ filter, idx }: FilterListItemProps) {
const [enabled, setEnabled] = useState(filter.enabled) const [enabled, setEnabled] = useState(filter.enabled);
const updateMutation = useMutation( const updateMutation = useMutation(
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status), (status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
{ {
onSuccess: () => { 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. // We need to invalidate both keys here.
// The filters key is used on the /filters page, // The filters key is used on the /filters page,
@ -284,7 +284,7 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
const toggleActive = (status: boolean) => { const toggleActive = (status: boolean) => {
setEnabled(status); setEnabled(status);
updateMutation.mutate(status); updateMutation.mutate(status);
} };
return ( return (
<tr <tr
@ -303,16 +303,16 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
checked={enabled} checked={enabled}
onChange={toggleActive} onChange={toggleActive}
className={classNames( className={classNames(
enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700', 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' "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 className="sr-only">Use setting</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
enabled ? 'translate-x-5' : 'translate-x-0', 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' "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> </Switch>
@ -342,5 +342,5 @@ function FilterListItem({ filter, idx }: FilterListItemProps) {
/> />
</td> </td>
</tr> </tr>
) );
} }

View file

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

View file

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

View file

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

View file

@ -13,10 +13,10 @@ interface DLSettingsItemProps {
} }
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) { function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false) const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
return ( 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} /> <DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <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} checked={client.enabled}
onChange={toggleUpdateClient} onChange={toggleUpdateClient}
className={classNames( className={classNames(
client.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', 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' "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 className="sr-only">Use setting</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
client.enabled ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</Switch> </Switch>
@ -47,14 +47,14 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
</span> </span>
</td> </td>
</tr> </tr>
) );
} }
function DownloadClientSettings() { function DownloadClientSettings() {
const [addClientIsOpen, toggleAddClient] = useToggle(false) const [addClientIsOpen, toggleAddClient] = useToggle(false);
const { error, data } = useQuery( const { error, data } = useQuery(
'downloadClients', "downloadClients",
APIClient.download_clients.getAll, APIClient.download_clients.getAll,
{ refetchOnWindowFocus: false } { refetchOnWindowFocus: false }
); );
@ -138,7 +138,7 @@ function DownloadClientSettings() {
</div> </div>
</div> </div>
) );
} }
export default DownloadClientSettings; export default DownloadClientSettings;

View file

@ -17,13 +17,14 @@ import {
} from "@heroicons/react/outline"; } from "@heroicons/react/outline";
import { FeedUpdateForm } from "../../forms/settings/FeedForms"; import { FeedUpdateForm } from "../../forms/settings/FeedForms";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
function FeedSettings() { function FeedSettings() {
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find, const { data } = useQuery<Feed[], Error>("feeds", APIClient.feeds.find,
{ {
refetchOnWindowFocus: false refetchOnWindowFocus: false
} }
) );
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <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" />} : <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
</div> </div>
</div> </div>
) );
} }
const ImplementationTorznab = () => ( const ImplementationTorznab = () => (
@ -70,10 +71,10 @@ const ImplementationTorznab = () => (
> >
Torznab Torznab
</span> </span>
) );
export const ImplementationMap: any = { export const ImplementationMap: componentMapType = {
"TORZNAB": <ImplementationTorznab/>, "TORZNAB": <ImplementationTorznab/>
}; };
interface ListItemProps { interface ListItemProps {
@ -81,9 +82,9 @@ interface ListItemProps {
} }
function ListItem({ feed }: ListItemProps) { function ListItem({ feed }: ListItemProps) {
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false) const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
const [enabled, setEnabled] = useState(feed.enabled) const [enabled, setEnabled] = useState(feed.enabled);
const updateMutation = useMutation( const updateMutation = useMutation(
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status), (status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
@ -91,7 +92,7 @@ function ListItem({feed}: ListItemProps) {
onSuccess: () => { onSuccess: () => {
toast.custom((t) => <Toast type="success" toast.custom((t) => <Toast type="success"
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`} body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
t={t}/>) t={t}/>);
queryClient.invalidateQueries(["feeds"]); queryClient.invalidateQueries(["feeds"]);
queryClient.invalidateQueries(["feeds", feed?.id]); queryClient.invalidateQueries(["feeds", feed?.id]);
@ -102,7 +103,7 @@ function ListItem({feed}: ListItemProps) {
const toggleActive = (status: boolean) => { const toggleActive = (status: boolean) => {
setEnabled(status); setEnabled(status);
updateMutation.mutate(status); updateMutation.mutate(status);
} };
return ( return (
<li key={feed.id} className="text-gray-500 dark:text-gray-400"> <li key={feed.id} className="text-gray-500 dark:text-gray-400">
@ -114,16 +115,16 @@ function ListItem({feed}: ListItemProps) {
checked={feed.enabled} checked={feed.enabled}
onChange={toggleActive} onChange={toggleActive}
className={classNames( className={classNames(
feed.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', 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' "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 className="sr-only">Use setting</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
feed.enabled ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</Switch> </Switch>
@ -143,7 +144,7 @@ function ListItem({feed}: ListItemProps) {
</div> </div>
</div> </div>
</li> </li>
) );
} }
interface FeedItemDropdownProps { interface FeedItemDropdownProps {
@ -155,7 +156,7 @@ interface FeedItemDropdownProps {
const FeedItemDropdown = ({ const FeedItemDropdown = ({
feed, feed,
onToggle, onToggle,
toggleUpdate, toggleUpdate
}: FeedItemDropdownProps) => { }: FeedItemDropdownProps) => {
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
@ -273,6 +274,6 @@ const FeedItemDropdown = ({
</Transition> </Transition>
</Menu> </Menu>
); );
} };
export default FeedSettings; export default FeedSettings;

View file

@ -5,6 +5,7 @@ import { Switch } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
import { EmptySimple } from "../../components/emptystates"; import { EmptySimple } from "../../components/emptystates";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { componentMapType } from "../../forms/settings/DownloadClientForms";
const ImplementationIRC = () => ( const ImplementationIRC = () => (
<span <span
@ -12,7 +13,7 @@ const ImplementationIRC = () => (
> >
IRC IRC
</span> </span>
) );
const ImplementationTorznab = () => ( const ImplementationTorznab = () => (
<span <span
@ -20,15 +21,19 @@ const ImplementationTorznab = () => (
> >
Torznab Torznab
</span> </span>
) );
const implementationMap: any = { const implementationMap: componentMapType = {
"irc": <ImplementationIRC/>, "irc": <ImplementationIRC/>,
"torznab": <ImplementationTorznab />, "torznab": <ImplementationTorznab />
}; };
const ListItem = ({ indexer }: any) => { interface ListItemProps {
const [updateIsOpen, toggleUpdate] = useToggle(false) indexer: IndexerDefinition;
}
const ListItem = ({ indexer }: ListItemProps) => {
const [updateIsOpen, toggleUpdate] = useToggle(false);
return ( return (
<tr key={indexer.name}> <tr key={indexer.name}>
@ -36,19 +41,19 @@ const ListItem = ({ indexer }: any) => {
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<Switch <Switch
checked={indexer.enabled} checked={indexer.enabled ?? false}
onChange={toggleUpdate} onChange={toggleUpdate}
className={classNames( className={classNames(
indexer.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', 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' "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 className="sr-only">Enable</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
indexer.enabled ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</Switch> </Switch>
@ -61,14 +66,14 @@ const ListItem = ({ indexer }: any) => {
</span> </span>
</td> </td>
</tr> </tr>
) );
} };
function IndexerSettings() { function IndexerSettings() {
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false) const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
const { error, data } = useQuery( const { error, data } = useQuery(
'indexer', "indexer",
APIClient.indexers.getAll, APIClient.indexers.getAll,
{ refetchOnWindowFocus: false } { refetchOnWindowFocus: false }
); );
@ -146,7 +151,7 @@ function IndexerSettings() {
</div> </div>
</div> </div>
) );
} }
export default IndexerSettings; export default IndexerSettings;

View file

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

View file

@ -7,13 +7,13 @@ import { Switch } from "@headlessui/react";
import { classNames } from "../../utils"; import { classNames } from "../../utils";
function NotificationSettings() { 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 refetchOnWindowFocus: false
} }
) );
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <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} />} : <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
</div> </div>
</div> </div>
) );
} }
interface ListItemProps { interface ListItemProps {
@ -64,7 +64,7 @@ interface ListItemProps {
} }
function ListItem({ notification }: ListItemProps) { function ListItem({ notification }: ListItemProps) {
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false) const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
return ( return (
<li key={notification.id} className="text-gray-500 dark:text-gray-400"> <li key={notification.id} className="text-gray-500 dark:text-gray-400">
@ -76,16 +76,16 @@ function ListItem({ notification }: ListItemProps) {
checked={notification.enabled} checked={notification.enabled}
onChange={toggleUpdateForm} onChange={toggleUpdateForm}
className={classNames( className={classNames(
notification.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', 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' "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 className="sr-only">Use setting</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
notification.enabled ? 'translate-x-5' : 'translate-x-0', 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' "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
/> />
</Switch> </Switch>
@ -113,7 +113,7 @@ function ListItem({ notification }: ListItemProps) {
</div> </div>
</div> </div>
</li> </li>
) );
} }
export default NotificationSettings; export default NotificationSettings;

View file

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

View file

@ -13,19 +13,19 @@ function ReleaseSettings() {
const deleteMutation = useMutation(() => APIClient.release.delete(), { const deleteMutation = useMutation(() => APIClient.release.delete(), {
onSuccess: () => { onSuccess: () => {
toast.custom((t) => ( 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. // Invalidate filters just in case, most likely not necessary but can't hurt.
queryClient.invalidateQueries("releases"); queryClient.invalidateQueries("releases");
toggleDeleteModal() toggleDeleteModal();
} }
}) });
const deleteAction = () => { const deleteAction = () => {
deleteMutation.mutate() deleteMutation.mutate();
} };
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
@ -36,7 +36,7 @@ function ReleaseSettings() {
toggle={toggleDeleteModal} toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef} buttonRef={cancelModalButtonRef}
deleteAction={deleteAction} 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." text="Are you sure you want to delete all releases? This action cannot be undone."
/> />
@ -75,7 +75,7 @@ function ReleaseSettings() {
</div> </div>
</div> </div>
</form> </form>
) );
} }
export default ReleaseSettings; 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) { module.exports = function(app) {
app.use( app.use(
'/api', "/api",
createProxyMiddleware({ createProxyMiddleware({
target: 'http://127.0.0.1:7474', target: "http://127.0.0.1:7474",
changeOrigin: true, changeOrigin: true,
}) })
); );

View file

@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // 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 = type DownloadClientType =
'QBITTORRENT' | "QBITTORRENT" |
'DELUGE_V1' | "DELUGE_V1" |
'DELUGE_V2' | "DELUGE_V2" |
'RADARR' | "RADARR" |
'SONARR' | "SONARR" |
'LIDARR' | "LIDARR" |
'WHISPARR'; "WHISPARR";
// export enum DownloadClientTypeEnum {
// QBITTORRENT = "QBITTORRENT",
// DELUGE_V1 = "DELUGE_V1",
// DELUGE_V2 = "DELUGE_V2",
// RADARR = "RADARR",
// SONARR = "SONARR",
// LIDARR = "LIDARR",
// WHISPARR = "WHISPARR"
// }
interface DownloadClientRules { interface DownloadClientRules {
enabled: boolean; enabled: boolean;
@ -27,7 +37,6 @@ interface DownloadClientSettings {
} }
interface DownloadClient { interface DownloadClient {
id?: number;
id: number; id: number;
name: string; name: string;
type: DownloadClientType; type: DownloadClientType;

View file

@ -83,4 +83,4 @@ interface Action {
client_id?: number; 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 { interface IndexerDefinition {
id?: number; id: number;
name: string; name: string;
identifier: string; identifier: string;
implementation: string; implementation: string;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2088,7 +2088,22 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@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" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d" 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== integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==
@ -2110,7 +2125,17 @@
dependencies: dependencies:
"@typescript-eslint/utils" "5.18.0" "@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" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6"
integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ== integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ==
@ -2128,6 +2153,14 @@
"@typescript-eslint/types" "5.18.0" "@typescript-eslint/types" "5.18.0"
"@typescript-eslint/visitor-keys" "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": "@typescript-eslint/type-utils@5.18.0":
version "5.18.0" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74"
@ -2137,11 +2170,25 @@
debug "^4.3.2" debug "^4.3.2"
tsutils "^3.21.0" 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": "@typescript-eslint/types@5.18.0":
version "5.18.0" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e"
integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw== 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": "@typescript-eslint/typescript-estree@5.18.0":
version "5.18.0" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474"
@ -2155,6 +2202,19 @@
semver "^7.3.5" semver "^7.3.5"
tsutils "^3.21.0" 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": "@typescript-eslint/utils@5.18.0", "@typescript-eslint/utils@^5.13.0":
version "5.18.0" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855"
@ -2167,6 +2227,18 @@
eslint-scope "^5.1.1" eslint-scope "^5.1.1"
eslint-utils "^3.0.0" 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": "@typescript-eslint/visitor-keys@5.18.0":
version "5.18.0" version "5.18.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60" 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" "@typescript-eslint/types" "5.18.0"
eslint-visitor-keys "^3.0.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": "@webassemblyjs/ast@1.11.1":
version "1.11.1" version "1.11.1"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"