diff --git a/web/.eslintrc.js b/web/.eslintrc.js new file mode 100644 index 0000000..f06d41b --- /dev/null +++ b/web/.eslintrc.js @@ -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"], + }, + }, + ], +}; diff --git a/web/package.json b/web/package.json index f9778d9..8dee884 100644 --- a/web/package.json +++ b/web/package.json @@ -29,7 +29,7 @@ "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "lint": "esw src/ --ext \".ts,.tsx,.js,.jsx\" --color", + "lint": "eslint src/ --ext .js,.jsx,.ts,.tsx --color", "lint:watch": "npm run lint -- --watch" }, "browserslist": { @@ -59,8 +59,8 @@ "@types/react-dom": "17.0.0", "@types/react-router-dom": "^5.1.7", "@types/react-table": "^7.7.7", - "@typescript-eslint/eslint-plugin": "^5.10.2", - "@typescript-eslint/parser": "^5.10.2", + "@typescript-eslint/eslint-plugin": "^5.18.0", + "@typescript-eslint/parser": "^5.18.0", "autoprefixer": "^10.4.2", "eslint": "^8.8.0", "eslint-plugin-import": "^2.25.4", @@ -71,59 +71,5 @@ "postcss": "^8.4.6", "tailwindcss": "^3.0.18", "typescript": "^4.1.2" - }, - "eslintConfig": { - "root": true, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended" - ], - "plugins": [ - "react", - "@typescript-eslint" - ], - "rules": { - "react/jsx-uses-react": "off", - "react/react-in-jsx-scope": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-explicit-any": "off" - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 11, - "sourceType": "module", - "ecmaFeatures": { - "jsx": true, - "experimentalObjectRestSpread": true - } - }, - "settings": { - "react": { - "version": "detect" - }, - "import/resolver": { - "node": { - "extensions": [ - ".ts", - ".tsx", - ".js", - ".jsx" - ] - } - } - }, - "env": { - "browser": true, - "node": true, - "jquery": false - }, - "globals": {} } } diff --git a/web/src/App.tsx b/web/src/App.tsx index f4875e8..34b4f30 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -16,7 +16,7 @@ import Toast from "./components/notifications/Toast"; export const queryClient = new QueryClient({ defaultOptions: { - queries: { useErrorBoundary: true, }, + queries: { useErrorBoundary: true }, mutations: { onError: (error) => { // Use a format string to convert the error object to a proper string without much hassle. @@ -27,8 +27,8 @@ export const queryClient = new QueryClient({ ); toast.custom((t) => ); } - }, - }, + } + } }); export function App() { diff --git a/web/src/api/APIClient.ts b/web/src/api/APIClient.ts index eb13000..783e6e3 100644 --- a/web/src/api/APIClient.ts +++ b/web/src/api/APIClient.ts @@ -1,157 +1,166 @@ -import {baseUrl, sseBaseUrl} from "../utils"; -import {AuthContext} from "../utils/Context"; -import {Cookies} from "react-cookie"; +import { baseUrl, sseBaseUrl } from "../utils"; +import { AuthContext } from "../utils/Context"; +import { Cookies } from "react-cookie"; interface ConfigType { - body?: BodyInit | Record | null; - headers?: Record; + body?: BodyInit | Record | unknown | null; + headers?: Record; } +type PostBody = BodyInit | Record | unknown | null; + export async function HttpClient( - endpoint: string, - method: string, - { body, ...customConfig }: ConfigType = {} + endpoint: string, + method: string, + { body, ...customConfig }: ConfigType = {} ): Promise { - const config = { - method: method, - body: body ? JSON.stringify(body) : null, - headers: { - "Content-Type": "application/json" - }, - // NOTE: customConfig can override the above defined settings - ...customConfig - } as RequestInit; + const config = { + method: method, + body: body ? JSON.stringify(body) : null, + headers: { + "Content-Type": "application/json" + }, + // NOTE: customConfig can override the above defined settings + ...customConfig + } as RequestInit; - return window.fetch(`${baseUrl()}${endpoint}`, config) - .then(async response => { - if (response.status === 401) { - // if 401 consider the session expired and force logout - const cookies = new Cookies(); - cookies.remove("user_session"); - AuthContext.reset() + return window.fetch(`${baseUrl()}${endpoint}`, config) + .then(async response => { + if (response.status === 401) { + // if 401 consider the session expired and force logout + const cookies = new Cookies(); + cookies.remove("user_session"); + AuthContext.reset(); - return Promise.reject(new Error(response.statusText)); - } + return Promise.reject(new Error(response.statusText)); + } - if ([403, 404].includes(response.status)) - return Promise.reject(new Error(response.statusText)); + if ([403, 404].includes(response.status)) + return Promise.reject(new Error(response.statusText)); - // 201 comes from a POST and can contain data - if ([201].includes(response.status)) - return await response.json(); + // 201 comes from a POST and can contain data + if ([201].includes(response.status)) + return await response.json(); - // 204 ok no data - if ([204].includes(response.status)) - return Promise.resolve(response); + // 204 ok no data + if ([204].includes(response.status)) + return Promise.resolve(response); - if (response.ok) { - return await response.json(); - } else { - const errorMessage = await response.text(); - return Promise.reject(new Error(errorMessage)); - } - }); + if (response.ok) { + return await response.json(); + } else { + const errorMessage = await response.text(); + return Promise.reject(new Error(errorMessage)); + } + }); } const appClient = { - Get: (endpoint: string) => HttpClient(endpoint, "GET"), - Post: (endpoint: string, data: any) => HttpClient(endpoint, "POST", { body: data }), - Put: (endpoint: string, data: any) => HttpClient(endpoint, "PUT", { body: data }), - Patch: (endpoint: string, data: any) => HttpClient(endpoint, "PATCH", { body: data }), - Delete: (endpoint: string) => HttpClient(endpoint, "DELETE") -} + Get: (endpoint: string) => HttpClient(endpoint, "GET"), + Post: (endpoint: string, data: PostBody) => HttpClient(endpoint, "POST", { body: data }), + PostBody: (endpoint: string, data: PostBody) => HttpClient(endpoint, "POST", { body: data }), + Put: (endpoint: string, data: PostBody) => HttpClient(endpoint, "PUT", { body: data }), + Patch: (endpoint: string, data: PostBody) => HttpClient(endpoint, "PATCH", { body: data }), + Delete: (endpoint: string) => HttpClient(endpoint, "DELETE") +}; export const APIClient = { - auth: { - login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }), - logout: () => appClient.Post("api/auth/logout", null), - validate: () => appClient.Get("api/auth/validate"), - onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { username: username, password: password }), - canOnboard: () => appClient.Get("api/auth/onboard"), - }, - actions: { - create: (action: Action) => appClient.Post("api/actions", action), - update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action), - delete: (id: number) => appClient.Delete(`api/actions/${id}`), - toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null), - }, - config: { - get: () => appClient.Get("api/config") - }, - download_clients: { - getAll: () => appClient.Get("api/download_clients"), - create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc), - update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc), - delete: (id: number) => appClient.Delete(`api/download_clients/${id}`), - test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc), - }, - filters: { - getAll: () => appClient.Get("api/filters"), - getByID: (id: number) => appClient.Get(`api/filters/${id}`), - create: (filter: Filter) => appClient.Post("api/filters", filter), - update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), - duplicate: (id: number) => appClient.Get(`api/filters/${id}/duplicate`), - toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), - delete: (id: number) => appClient.Delete(`api/filters/${id}`), - }, - feeds: { - find: () => appClient.Get("api/feeds"), - create: (feed: FeedCreate) => appClient.Post("api/feeds", feed), - toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }), - update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed), - delete: (id: number) => appClient.Delete(`api/feeds/${id}`), - }, - indexers: { - // returns indexer options for all currently present/enabled indexers - getOptions: () => appClient.Get("api/indexer/options"), - // returns indexer definitions for all currently present/enabled indexers - getAll: () => appClient.Get("api/indexer"), - // returns all possible indexer definitions - getSchema: () => appClient.Get("api/indexer/schema"), - create: (indexer: Indexer) => appClient.Post("api/indexer", indexer), - update: (indexer: Indexer) => appClient.Put("api/indexer", indexer), - delete: (id: number) => appClient.Delete(`api/indexer/${id}`), - }, - irc: { - getNetworks: () => appClient.Get("api/irc"), - createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network), - updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network), - deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`), - }, - events: { - logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true }) - }, - notifications: { - getAll: () => appClient.Get("api/notification"), - create: (notification: Notification) => appClient.Post("api/notification", notification), - update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification), - delete: (id: number) => appClient.Delete(`api/notification/${id}`), - }, - release: { - find: (query?: string) => appClient.Get(`api/release${query}`), - findQuery: (offset?: number, limit?: number, filters?: Array) => { - const params = new URLSearchParams(); - if (offset !== undefined) - params.append("offset", offset.toString()); + auth: { + login: (username: string, password: string) => appClient.Post("api/auth/login", { + username: username, + password: password + }), + logout: () => appClient.Post("api/auth/logout", null), + validate: () => appClient.Get("api/auth/validate"), + onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { + username: username, + password: password + }), + canOnboard: () => appClient.Get("api/auth/onboard") + }, + actions: { + create: (action: Action) => appClient.Post("api/actions", action), + update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action), + delete: (id: number) => appClient.Delete(`api/actions/${id}`), + toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null) + }, + config: { + get: () => appClient.Get("api/config") + }, + download_clients: { + getAll: () => appClient.Get("api/download_clients"), + create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc), + update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc), + delete: (id: number) => appClient.Delete(`api/download_clients/${id}`), + test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc) + }, + filters: { + getAll: () => appClient.Get("api/filters"), + getByID: (id: number) => appClient.Get(`api/filters/${id}`), + create: (filter: Filter) => appClient.Post("api/filters", filter), + update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), + duplicate: (id: number) => appClient.Get(`api/filters/${id}/duplicate`), + toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), + delete: (id: number) => appClient.Delete(`api/filters/${id}`) + }, + feeds: { + find: () => appClient.Get("api/feeds"), + create: (feed: FeedCreate) => appClient.Post("api/feeds", feed), + toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }), + update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed), + delete: (id: number) => appClient.Delete(`api/feeds/${id}`) + }, + indexers: { + // returns indexer options for all currently present/enabled indexers + getOptions: () => appClient.Get("api/indexer/options"), + // returns indexer definitions for all currently present/enabled indexers + getAll: () => appClient.Get("api/indexer"), + // returns all possible indexer definitions + getSchema: () => appClient.Get("api/indexer/schema"), + create: (indexer: Indexer) => appClient.PostBody("api/indexer", indexer), + update: (indexer: Indexer) => appClient.Put("api/indexer", indexer), + delete: (id: number) => appClient.Delete(`api/indexer/${id}`) + }, + irc: { + getNetworks: () => appClient.Get("api/irc"), + createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network), + updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network), + deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`) + }, + events: { + logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true }) + }, + notifications: { + getAll: () => appClient.Get("api/notification"), + create: (notification: Notification) => appClient.Post("api/notification", notification), + update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification), + delete: (id: number) => appClient.Delete(`api/notification/${id}`) + }, + release: { + find: (query?: string) => appClient.Get(`api/release${query}`), + findQuery: (offset?: number, limit?: number, filters?: Array) => { + const params = new URLSearchParams(); + if (offset !== undefined) + params.append("offset", offset.toString()); - if (limit !== undefined) - params.append("limit", limit.toString()); + if (limit !== undefined) + params.append("limit", limit.toString()); - filters?.forEach((filter) => { - if (!filter.value) - return; + filters?.forEach((filter) => { + if (!filter.value) + return; - if (filter.id == "indexer") - params.append("indexer", filter.value); - else if (filter.id === "action_status") - params.append("push_status", filter.value); - }); + if (filter.id == "indexer") + params.append("indexer", filter.value); + else if (filter.id === "action_status") + params.append("push_status", filter.value); + }); - return appClient.Get(`api/release?${params.toString()}`) - }, - indexerOptions: () => appClient.Get(`api/release/indexers`), - stats: () => appClient.Get("api/release/stats"), - delete: () => appClient.Delete(`api/release/all`), - } -}; \ No newline at end of file + return appClient.Get(`api/release?${params.toString()}`); + }, + indexerOptions: () => appClient.Get("api/release/indexers"), + stats: () => appClient.Get("api/release/stats"), + delete: () => appClient.Delete("api/release/all") + } +}; diff --git a/web/src/components/Checkbox.tsx b/web/src/components/Checkbox.tsx index 4898653..06eff43 100644 --- a/web/src/components/Checkbox.tsx +++ b/web/src/components/Checkbox.tsx @@ -8,27 +8,27 @@ interface CheckboxProps { } export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => ( - -
- - {label} - - {description === undefined ? null : ( - - {description} - - )} -
- - - -
+ +
+ + {label} + + {description === undefined ? null : ( + + {description} + + )} +
+ + + +
); \ No newline at end of file diff --git a/web/src/components/Icons.tsx b/web/src/components/Icons.tsx index 26bdac9..a5c4da3 100644 --- a/web/src/components/Icons.tsx +++ b/web/src/components/Icons.tsx @@ -5,13 +5,13 @@ interface IconProps { } export const SortIcon = ({ className }: IconProps) => ( - + ); export const SortUpIcon = ({ className }: IconProps) => ( - + ); export const SortDownIcon = ({ className }: IconProps) => ( - + ); diff --git a/web/src/components/alerts/ErrorPage.tsx b/web/src/components/alerts/ErrorPage.tsx index 59c3029..b9fd295 100644 --- a/web/src/components/alerts/ErrorPage.tsx +++ b/web/src/components/alerts/ErrorPage.tsx @@ -51,7 +51,8 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => { role="alert" >
- + { resetErrorBoundary(); }} > - + Reset page state
); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/web/src/components/data-table/Buttons.tsx b/web/src/components/data-table/Buttons.tsx index 2b3e961..eb4cfba 100644 --- a/web/src/components/data-table/Buttons.tsx +++ b/web/src/components/data-table/Buttons.tsx @@ -1,34 +1,37 @@ -import { classNames } from "../../utils" +import React from "react"; +import { classNames } from "../../utils"; interface ButtonProps { className?: string; - children: any; - [rest: string]: any; + children: React.ReactNode; + disabled?: boolean; + onClick?: () => void; } -export const Button = ({ children, className, ...rest }: ButtonProps) => ( - +export const Button = ({ children, className, disabled, onClick }: ButtonProps) => ( + ); - -export const PageButton = ({ children, className, ...rest }: ButtonProps) => ( - +export const PageButton = ({ children, className, disabled, onClick }: ButtonProps) => ( + ); \ No newline at end of file diff --git a/web/src/components/data-table/Cells.tsx b/web/src/components/data-table/Cells.tsx index d09e9cc..bff2d09 100644 --- a/web/src/components/data-table/Cells.tsx +++ b/web/src/components/data-table/Cells.tsx @@ -10,24 +10,22 @@ interface CellProps { } export const AgeCell = ({ value }: CellProps) => ( -
- {formatDistanceToNowStrict(new Date(value), { addSuffix: true })} -
+
+ {formatDistanceToNowStrict(new Date(value), { addSuffix: true })} +
); export const TitleCell = ({ value }: CellProps) => ( -
- {value} -
+
+ {value} +
); interface ReleaseStatusCellProps { value: ReleaseActionStatus[]; - column: any; - row: any; } interface StatusCellMapEntry { @@ -36,22 +34,22 @@ interface StatusCellMapEntry { } const StatusCellMap: Record = { - "PUSH_ERROR": { - colors: "bg-pink-100 text-pink-800 hover:bg-pink-300", - icon: