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

@ -1,157 +1,166 @@
import {baseUrl, sseBaseUrl} from "../utils"; import { baseUrl, sseBaseUrl } from "../utils";
import {AuthContext} from "../utils/Context"; 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,
{ body, ...customConfig }: ConfigType = {} { body, ...customConfig }: ConfigType = {}
): Promise<T> { ): Promise<T> {
const config = { const config = {
method: method, method: method,
body: body ? JSON.stringify(body) : null, body: body ? JSON.stringify(body) : null,
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
}, },
// NOTE: customConfig can override the above defined settings // NOTE: customConfig can override the above defined settings
...customConfig ...customConfig
} as RequestInit; } as RequestInit;
return window.fetch(`${baseUrl()}${endpoint}`, config) return window.fetch(`${baseUrl()}${endpoint}`, config)
.then(async response => { .then(async response => {
if (response.status === 401) { if (response.status === 401) {
// 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));
} }
if ([403, 404].includes(response.status)) if ([403, 404].includes(response.status))
return Promise.reject(new Error(response.statusText)); return Promise.reject(new Error(response.statusText));
// 201 comes from a POST and can contain data // 201 comes from a POST and can contain data
if ([201].includes(response.status)) if ([201].includes(response.status))
return await response.json(); return await response.json();
// 204 ok no data // 204 ok no data
if ([204].includes(response.status)) if ([204].includes(response.status))
return Promise.resolve(response); return Promise.resolve(response);
if (response.ok) { if (response.ok) {
return await response.json(); return await response.json();
} else { } else {
const errorMessage = await response.text(); const errorMessage = await response.text();
return Promise.reject(new Error(errorMessage)); return Promise.reject(new Error(errorMessage));
} }
}); });
} }
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 }),
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE") Patch: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PATCH", { body: data }),
} 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", {
logout: () => appClient.Post("api/auth/logout", null), username: username,
validate: () => appClient.Get<void>("api/auth/validate"), password: password
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { username: username, password: password }), }),
canOnboard: () => appClient.Get("api/auth/onboard"), logout: () => appClient.Post("api/auth/logout", null),
}, validate: () => appClient.Get<void>("api/auth/validate"),
actions: { onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", {
create: (action: Action) => appClient.Post("api/actions", action), username: username,
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action), password: password
delete: (id: number) => appClient.Delete(`api/actions/${id}`), }),
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null), canOnboard: () => appClient.Get("api/auth/onboard")
}, },
config: { actions: {
get: () => appClient.Get<Config>("api/config") create: (action: Action) => appClient.Post("api/actions", action),
}, update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
download_clients: { delete: (id: number) => appClient.Delete(`api/actions/${id}`),
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"), toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null)
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc), },
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc), config: {
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`), get: () => appClient.Get<Config>("api/config")
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc), },
}, download_clients: {
filters: { getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
getAll: () => appClient.Get<Filter[]>("api/filters"), create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`), update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
create: (filter: Filter) => appClient.Post("api/filters", filter), delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter), test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc)
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`), },
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }), filters: {
delete: (id: number) => appClient.Delete(`api/filters/${id}`), getAll: () => appClient.Get<Filter[]>("api/filters"),
}, getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
feeds: { create: (filter: Filter) => appClient.Post("api/filters", filter),
find: () => appClient.Get<Feed[]>("api/feeds"), update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed), duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }), toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed), delete: (id: number) => appClient.Delete(`api/filters/${id}`)
delete: (id: number) => appClient.Delete(`api/feeds/${id}`), },
}, feeds: {
indexers: { find: () => appClient.Get<Feed[]>("api/feeds"),
// returns indexer options for all currently present/enabled indexers create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"), toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
// returns indexer definitions for all currently present/enabled indexers update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"), delete: (id: number) => appClient.Delete(`api/feeds/${id}`)
// returns all possible indexer definitions },
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"), indexers: {
create: (indexer: Indexer) => appClient.Post<Indexer>("api/indexer", indexer), // returns indexer options for all currently present/enabled indexers
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer), getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
delete: (id: number) => appClient.Delete(`api/indexer/${id}`), // returns indexer definitions for all currently present/enabled indexers
}, getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
irc: { // returns all possible indexer definitions
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"), getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network), create: (indexer: Indexer) => appClient.PostBody<Indexer>("api/indexer", indexer),
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network), update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`), delete: (id: number) => appClient.Delete(`api/indexer/${id}`)
}, },
events: { irc: {
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true }) getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
}, createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
notifications: { updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
getAll: () => appClient.Get<Notification[]>("api/notification"), deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`)
create: (notification: Notification) => appClient.Post("api/notification", notification), },
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification), events: {
delete: (id: number) => appClient.Delete(`api/notification/${id}`), logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
}, },
release: { notifications: {
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`), getAll: () => appClient.Get<Notification[]>("api/notification"),
findQuery: (offset?: number, limit?: number, filters?: Array<ReleaseFilter>) => { create: (notification: Notification) => appClient.Post("api/notification", notification),
const params = new URLSearchParams(); update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
if (offset !== undefined) delete: (id: number) => appClient.Delete(`api/notification/${id}`)
params.append("offset", offset.toString()); },
release: {
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
findQuery: (offset?: number, limit?: number, filters?: Array<ReleaseFilter>) => {
const params = new URLSearchParams();
if (offset !== undefined)
params.append("offset", offset.toString());
if (limit !== undefined) if (limit !== undefined)
params.append("limit", limit.toString()); params.append("limit", limit.toString());
filters?.forEach((filter) => { filters?.forEach((filter) => {
if (!filter.value) if (!filter.value)
return; return;
if (filter.id == "indexer") if (filter.id == "indexer")
params.append("indexer", filter.value); params.append("indexer", filter.value);
else if (filter.id === "action_status") else if (filter.id === "action_status")
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

@ -8,27 +8,27 @@ interface CheckboxProps {
} }
export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => ( export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => (
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive> <Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
{label} {label}
</Switch.Label> </Switch.Label>
{description === undefined ? null : ( {description === undefined ? null : (
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400"> <Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
{description} {description}
</Switch.Description> </Switch.Description>
)} )}
</div> </div>
<Switch <Switch
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

@ -5,13 +5,13 @@ interface IconProps {
} }
export const SortIcon = ({ className }: IconProps) => ( export const SortIcon = ({ className }: IconProps) => (
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z"></path></svg> <svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z"></path></svg>
); );
export const SortUpIcon = ({ className }: IconProps) => ( export const SortUpIcon = ({ className }: IconProps) => (
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z"></path></svg> <svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z"></path></svg>
); );
export const SortDownIcon = ({ className }: IconProps) => ( export const SortDownIcon = ({ className }: IconProps) => (
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z"></path></svg> <svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z"></path></svg>
); );

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"
@ -77,11 +78,11 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
resetErrorBoundary(); resetErrorBoundary();
}} }}
> >
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5" /> <RefreshIcon className="-ml-0.5 mr-2 h-5 w-5"/>
Reset page state Reset page state
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); );
} };

View file

@ -1,34 +1,37 @@
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} >
</button> {children}
</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" )}
)} disabled={disabled}
{...rest} onClick={onClick}
> >
{children} {children}
</button> </button>
); );

View file

@ -10,24 +10,22 @@ interface CellProps {
} }
export const AgeCell = ({ value }: CellProps) => ( export const AgeCell = ({ value }: CellProps) => (
<div className="text-sm text-gray-500" title={value}> <div className="text-sm text-gray-500" title={value}>
{formatDistanceToNowStrict(new Date(value), { addSuffix: true })} {formatDistanceToNowStrict(new Date(value), { addSuffix: true })}
</div> </div>
); );
export const TitleCell = ({ value }: CellProps) => ( export const TitleCell = ({ value }: CellProps) => (
<div <div
className="text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[128px] sm:max-w-none overflow-auto py-4" className="text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[128px] sm:max-w-none overflow-auto py-4"
title={value} title={value}
> >
{value} {value}
</div> </div>
); );
interface ReleaseStatusCellProps { interface ReleaseStatusCellProps {
value: ReleaseActionStatus[]; value: ReleaseActionStatus[];
column: any;
row: any;
} }
interface StatusCellMapEntry { interface StatusCellMapEntry {
@ -36,22 +34,22 @@ interface StatusCellMapEntry {
} }
const StatusCellMap: Record<string, StatusCellMapEntry> = { const StatusCellMap: Record<string, StatusCellMapEntry> = {
"PUSH_ERROR": { "PUSH_ERROR": {
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300", colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" /> icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />
}, },
"PUSH_REJECTED": { "PUSH_REJECTED": {
colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400", colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
icon: <BanIcon className="h-5 w-5" aria-hidden="true" /> icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
}, },
"PUSH_APPROVED": { "PUSH_APPROVED": {
colors: "bg-green-100 text-green-800 hover:bg-green-300", colors: "bg-green-100 text-green-800 hover:bg-green-300",
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" /> icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />
}, },
"PENDING": { "PENDING": {
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200", colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" /> icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />
} }
}; };
const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => { const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
@ -67,18 +65,18 @@ const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
}; };
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => ( export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300"> <div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
{value.map((v, idx) => ( {value.map((v, idx) => (
<div <div
key={idx} key={idx}
title={GetReleaseStatusString(v)} title={GetReleaseStatusString(v)}
className={classNames( className={classNames(
StatusCellMap[v.status].colors, StatusCellMap[v.status].colors,
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer" "mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"
)} )}
> >
{StatusCellMap[v.status].icon} {StatusCellMap[v.status].icon}
</div> </div>
))} ))}
</div> </div>
); );

View file

@ -1,21 +1,19 @@
import { FC } from "react" import { FC } from "react";
interface DebugProps { interface DebugProps {
values: unknown; values: unknown;
} }
const DEBUG: FC<DebugProps> = ({ values }) => { const DEBUG: FC<DebugProps> = ({ values }) => {
if (process.env.NODE_ENV !== "development") { if (process.env.NODE_ENV !== "development") {
return null; return null;
} }
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)} </div>
</pre> );
</div>
);
}; };
export default DEBUG; export default DEBUG;

View file

@ -13,43 +13,43 @@ export const EmptySimple = ({
buttonText, buttonText,
buttonAction buttonAction
}: EmptySimpleProps) => ( }: EmptySimpleProps) => (
<div className="text-center py-8"> <div className="text-center py-8">
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3> <h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p> <p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
{buttonText && buttonAction ? ( {buttonText && buttonAction ? (
<div className="mt-6"> <div className="mt-6">
<button <button
type="button" type="button"
onClick={buttonAction} onClick={buttonAction}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" /> <PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{buttonText} {buttonText}
</button> </button>
</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) {
return ( return (
<div className="px-4 py-12 flex flex-col items-center"> <div className="px-4 py-12 flex flex-col items-center">
<p className="text-center text-gray-800 dark:text-white">{text}</p> <p className="text-center text-gray-800 dark:text-white">{text}</p>
{buttonText && buttonOnClick && ( {buttonText && buttonOnClick && (
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={buttonOnClick} onClick={buttonOnClick}
> >
{buttonText} {buttonText}
</button> </button>
)} )}
</div> </div>
) );
} }

View file

@ -6,8 +6,8 @@ interface Props {
} }
export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => ( export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
<div> <div>
<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,17 +1,16 @@
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>
); );
interface CheckboxFieldProps { interface CheckboxFieldProps {
@ -21,26 +20,26 @@ interface CheckboxFieldProps {
} }
const CheckboxField = ({ const CheckboxField = ({
name, name,
label, label,
sublabel sublabel
}: CheckboxFieldProps) => ( }: CheckboxFieldProps) => (
<div className="relative flex items-start"> <div className="relative flex items-start">
<div className="flex items-center h-5"> <div className="flex items-center h-5">
<Field <Field
id={name} id={name}
name={name} name={name}
type="checkbox" type="checkbox"
className="focus:ring-bkue-500 h-4 w-4 text-blue-600 border-gray-300 rounded" className="focus:ring-bkue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/> />
</div>
<div className="ml-3 text-sm">
<label htmlFor={name} className="font-medium text-gray-900 dark:text-gray-100">
{label}
</label>
<p className="text-gray-500">{sublabel}</p>
</div>
</div> </div>
) <div className="ml-3 text-sm">
<label htmlFor={name} className="font-medium text-gray-900 dark:text-gray-100">
{label}
</label>
<p className="text-gray-500">{sublabel}</p>
</div>
</div>
);
export { ErrorField, CheckboxField } export { ErrorField, CheckboxField };

View file

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

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";
@ -16,49 +16,49 @@ interface TextFieldProps {
} }
export const TextField = ({ export const TextField = ({
name, name,
defaultValue, defaultValue,
label, label,
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 && (
<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">
{label} {label}
</label> </label>
)} )}
<Field name={name}> <Field name={name}>
{({ {({
field, field,
meta, meta
}: any) => ( }: FieldProps) => (
<div> <div>
<input <input
{...field} {...field}
id={name} id={name}
type="text" type="text"
defaultValue={defaultValue} defaultValue={defaultValue}
autoComplete={autoComplete} autoComplete={autoComplete}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")} className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
placeholder={placeholder} placeholder={placeholder}
/> />
{meta.touched && meta.error && ( {meta.touched && meta.error && (
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p> <p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
)} )}
</div> </div>
)} )}
</Field> </Field>
</div> </div>
) );
interface PasswordFieldProps { interface PasswordFieldProps {
name: string; name: string;
@ -72,60 +72,60 @@ interface PasswordFieldProps {
} }
export const PasswordField = ({ export const PasswordField = ({
name, name,
label, label,
placeholder, placeholder,
defaultValue, defaultValue,
columns, columns,
autoComplete, autoComplete,
help, help,
required required
}: PasswordFieldProps) => { }: PasswordFieldProps) => {
const [isVisible, toggleVisibility] = useToggle(false) const [isVisible, toggleVisibility] = useToggle(false);
return ( return (
<div <div
className={classNames( className={classNames(
columns ? `col-span-${columns}` : "col-span-12" columns ? `col-span-${columns}` : "col-span-12"
)}
>
{label && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
{label} {required && <span className="text-gray-500">*</span>}
</label>
)}
<Field name={name} defaultValue={defaultValue}>
{({
field,
meta
}: FieldProps) => (
<div className="sm:col-span-2 relative">
<input
{...field}
id={name}
type={isVisible ? "text" : "password"}
autoComplete={autoComplete}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
placeholder={placeholder}
/>
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
</div>
{help && (
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
)} )}
>
{label && ( {meta.touched && meta.error && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
{label} {required && <span className="text-gray-500">*</span>}
</label>
)} )}
<Field name={name} defaultValue={defaultValue}> </div>
{({ )}
field, </Field>
meta, </div>
}: any) => ( );
<div className="sm:col-span-2 relative"> };
<input
{...field}
id={name}
type={isVisible ? "text" : "password"}
autoComplete={autoComplete}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
placeholder={placeholder}
/>
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
</div>
{help && (
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
)}
{meta.touched && meta.error && (
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
)}
</div>
)}
</Field>
</div>
)
}
interface NumberFieldProps { interface NumberFieldProps {
name: string; name: string;
@ -135,40 +135,40 @@ interface NumberFieldProps {
} }
export const NumberField = ({ 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">
{label} {label}
</label> </label>
<Field name={name} type="number">
{({
field,
meta,
}: any) => (
<div className="sm:col-span-2">
<input
type="number"
step={step}
{...field}
className={classNames(
meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md"
)}
placeholder={placeholder}
/>
{meta.touched && meta.error && (
<div className="error">{meta.error}</div>
)}
</div>
<Field name={name} type="number">
{({
field,
meta
}: FieldProps) => (
<div className="sm:col-span-2">
<input
type="number"
step={step}
{...field}
className={classNames(
meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md"
)} )}
</Field> placeholder={placeholder}
</div> />
{meta.touched && meta.error && (
<div className="error">{meta.error}</div>
)}
</div>
)}
</Field>
</div>
); );

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;
@ -17,46 +17,46 @@ interface TextFieldWideProps {
} }
export const TextFieldWide = ({ export const TextFieldWide = ({
name, name,
label, label,
help, help,
placeholder, placeholder,
defaultValue, defaultValue,
required, required,
hidden hidden
}: TextFieldWideProps) => ( }: TextFieldWideProps) => (
<div hidden={hidden} 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 hidden={hidden} 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> <div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"> <label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
</label> </label>
</div>
<div className="sm:col-span-2">
<Field
name={name}
value={defaultValue}
required={required}
>
{({ field, meta }: FieldProps) => (
<input
{...field}
id={name}
type="text"
value={field.value ? field.value : defaultValue ?? ""}
onChange={field.onChange}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
placeholder={placeholder}
hidden={hidden}
/>
)}
</Field>
{help && (
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
)}
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div> </div>
) <div className="sm:col-span-2">
<Field
name={name}
value={defaultValue}
required={required}
>
{({ field, meta }: FieldProps) => (
<input
{...field}
id={name}
type="text"
value={field.value ? field.value : defaultValue ?? ""}
onChange={field.onChange}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
placeholder={placeholder}
hidden={hidden}
/>
)}
</Field>
{help && (
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
)}
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div>
);
interface PasswordFieldWideProps { interface PasswordFieldWideProps {
name: string; name: string;
@ -69,53 +69,53 @@ interface PasswordFieldWideProps {
} }
export const PasswordFieldWide = ({ export const PasswordFieldWide = ({
name, name,
label, label,
placeholder, placeholder,
defaultValue, defaultValue,
help, help,
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">
<div> <div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"> <label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
</label> </label>
</div>
<div className="sm:col-span-2">
<Field
name={name}
defaultValue={defaultValue}
>
{({ field, meta }: FieldProps) => (
<div className="relative">
<input
{...field}
id={name}
value={field.value ? field.value : defaultValue ?? ""}
onChange={field.onChange}
type={isVisible ? "text" : "password"}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full pr-10 dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
placeholder={placeholder}
/>
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
</div>
</div> </div>
<div className="sm:col-span-2"> )}
<Field </Field>
name={name} {help && (
defaultValue={defaultValue} <p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
> )}
{({ field, meta }: FieldProps) => ( <ErrorField name={name} classNames="block text-red-500 mt-2" />
<div className="relative"> </div>
<input </div>
{...field} );
id={name} };
value={field.value ? field.value : defaultValue ?? ""}
onChange={field.onChange}
type={isVisible ? "text" : "password"}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full pr-10 dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
placeholder={placeholder}
/>
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
</div>
</div>
)}
</Field>
{help && (
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
)}
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div>
)
}
interface NumberFieldWideProps { interface NumberFieldWideProps {
name: string; name: string;
@ -127,50 +127,50 @@ interface NumberFieldWideProps {
} }
export const NumberFieldWide = ({ export const NumberFieldWide = ({
name, name,
label, label,
placeholder, placeholder,
help, help,
defaultValue, defaultValue,
required required
}: NumberFieldWideProps) => ( }: NumberFieldWideProps) => (
<div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5"> <div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div> <div>
<label <label
htmlFor={name} htmlFor={name}
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2" className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
> >
{label} {required && <span className="text-gray-500">*</span>} {label} {required && <span className="text-gray-500">*</span>}
</label> </label>
</div>
<div className="sm:col-span-2">
<Field
name={name}
defaultValue={defaultValue ?? 0}
>
{({ field, meta, form }: FieldProps) => (
<input
{...field}
id={name}
type="number"
value={field.value ? field.value : defaultValue ?? 0}
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)) }}
className={classNames(
meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
)}
placeholder={placeholder}
/>
)}
</Field>
{help && (
<p className="mt-2 text-sm text-gray-500 dark:text-gray-500" id={`${name}-description`}>{help}</p>
)}
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div> </div>
<div className="sm:col-span-2">
<Field
name={name}
defaultValue={defaultValue ?? 0}
>
{({ field, meta, form }: FieldProps) => (
<input
{...field}
id={name}
type="number"
value={field.value ? field.value : defaultValue ?? 0}
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)); }}
className={classNames(
meta.touched && meta.error
? "focus:ring-red-500 focus:border-red-500 border-red-500"
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
)}
placeholder={placeholder}
/>
)}
</Field>
{help && (
<p className="mt-2 text-sm text-gray-500 dark:text-gray-500" id={`${name}-description`}>{help}</p>
)}
<ErrorField name={name} classNames="block text-red-500 mt-2" />
</div>
</div>
); );
interface SwitchGroupWideProps { interface SwitchGroupWideProps {
@ -182,56 +182,56 @@ interface SwitchGroupWideProps {
} }
export const SwitchGroupWide = ({ export const SwitchGroupWide = ({
name, name,
label, label,
description, description,
defaultValue defaultValue
}: SwitchGroupWideProps) => ( }: SwitchGroupWideProps) => (
<ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700"> <ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700">
<Switch.Group as="li" className="py-4 flex items-center justify-between"> <Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" <Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
passive> passive>
{label} {label}
</Switch.Label> </Switch.Label>
{description && ( {description && (
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700"> <Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
{description} {description}
</Switch.Description> </Switch.Description>
)} )}
</div> </div>
<Field <Field
name={name} name={name}
defaultValue={defaultValue as boolean} defaultValue={defaultValue as boolean}
type="checkbox" type="checkbox"
> >
{({ field, form }: FieldProps) => ( {({ field, form }: FieldProps) => (
<Switch <Switch
{...field} {...field}
type="button" type="button"
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>
)} )}
</Field> </Field>
</Switch.Group> </Switch.Group>
</ul> </ul>
) );

View file

@ -15,101 +15,106 @@ 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) => {
setFieldValue(name, value)
}
return ( const onChange = (value: string) => {
<fieldset> setFieldValue(name, value);
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5"> };
<div>
<legend className="text-sm font-medium text-gray-900 dark:text-white"> return (
{legend} <fieldset>
</legend> <div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
</div> <div>
<div className="space-y-5 sm:col-span-2"> <legend className="text-sm font-medium text-gray-900 dark:text-white">
<div className="space-y-5 sm:mt-0"> {legend}
<Field name={name} type="radio"> </legend>
{() => ( </div>
<RadioGroup value={values[name]} onChange={onChange}> <div className="space-y-5 sm:col-span-2">
<RadioGroup.Label className="sr-only"> <div className="space-y-5 sm:mt-0">
{legend} <Field name={name} type="radio">
</RadioGroup.Label> {() => (
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px"> <RadioGroup value={values[name]} onChange={onChange}>
{options.map((setting, settingIdx) => ( <RadioGroup.Label className="sr-only">
<RadioGroup.Option {legend}
key={setting.value} </RadioGroup.Label>
value={setting.value} <div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
className={({ checked }) => {options.map((setting, settingIdx) => (
classNames( <RadioGroup.Option
settingIdx === 0 key={setting.value}
? "rounded-tl-md rounded-tr-md" value={setting.value}
: "", className={({ checked }) =>
settingIdx === options.length - 1 classNames(
? "rounded-bl-md rounded-br-md" settingIdx === 0
: "", ? "rounded-tl-md rounded-tr-md"
checked : "",
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10" settingIdx === options.length - 1
: "border-gray-200 dark:border-gray-700", ? "rounded-bl-md rounded-br-md"
"relative border p-4 flex cursor-pointer focus:outline-none" : "",
) checked
} ? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
> : "border-gray-200 dark:border-gray-700",
{({ active, checked }) => ( "relative border p-4 flex cursor-pointer focus:outline-none"
<Fragment> )
<span }
className={classNames( >
checked {({ active, checked }) => (
? "bg-indigo-600 dark:bg-blue-600 border-transparent" <Fragment>
: "bg-white border-gray-300 dark:border-gray-300", <span
active className={classNames(
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500" checked
: "", ? "bg-indigo-600 dark:bg-blue-600 border-transparent"
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center" : "bg-white border-gray-300 dark:border-gray-300",
)} active
aria-hidden="true" ? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
> : "",
<span className="rounded-full bg-white w-1.5 h-1.5" /> "h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
</span> )}
<div className="ml-3 flex flex-col"> aria-hidden="true"
<RadioGroup.Label >
as="span" <span className="rounded-full bg-white w-1.5 h-1.5" />
className={classNames( </span>
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300", <div className="ml-3 flex flex-col">
"block text-sm font-medium" <RadioGroup.Label
)} as="span"
> className={classNames(
{setting.label} checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
</RadioGroup.Label> "block text-sm font-medium"
<RadioGroup.Description )}
as="span" >
className={classNames( {setting.label}
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500", </RadioGroup.Label>
"block text-sm" <RadioGroup.Description
)} as="span"
> className={classNames(
{setting.description} checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
</RadioGroup.Description> "block text-sm"
</div> )}
</Fragment> >
)} {setting.description}
</RadioGroup.Option> </RadioGroup.Description>
))} </div>
</div> </Fragment>
</RadioGroup> )}
)} </RadioGroup.Option>
</Field> ))}
</div> </div>
</div> </RadioGroup>
</div> )}
</fieldset> </Field>
); </div>
</div>
</div>
</fieldset>
);
} }
export { RadioFieldsetWide }; export { RadioFieldsetWide };

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,114 +7,125 @@ 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;
} }
export const MultiSelect = ({ export const MultiSelect = ({
name, name,
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 (
<div <div
className={classNames( className={classNames(
columns ? `col-span-${columns}` : "col-span-12" columns ? `col-span-${columns}` : "col-span-12"
)} )}
> >
<label <label
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200" className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
htmlFor={label} htmlFor={label}
> >
{label} {label}
</label> </label>
<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: MultiSelectOption) => ({
value={field.value && field.value.map((item: any) => ({ 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: Array<MultiSelectOption>) => {
onChange={(values: any) => { const am = values && values.map((i) => i.value);
const am = values && values.map((i: any) => i.value);
setFieldValue(field.name, am); setFieldValue(field.name, am);
}} }}
className={settingsContext.darkTheme ? "dark" : ""} className={settingsContext.darkTheme ? "dark" : ""}
/> />
)} )}
</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 (
<div <div
className={classNames( className={classNames(
columns ? `col-span-${columns}` : "col-span-12" columns ? `col-span-${columns}` : "col-span-12"
)} )}
>
<label
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
htmlFor={label}
> >
<label {label}
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200" </label>
htmlFor={label}
>
{label}
</label>
<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: IndexerMultiSelectOption) => ({
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))} value: item.id, label: item.name
onChange={(values: any) => { }))}
const am = values && values.map((i: any) => i.value); onChange={(values: MultiSelectOption[]) => {
setFieldValue(field.name, am); const item = values && values.map((i) => ({ id: i.value, name: i.label }));
}} setFieldValue(field.name, item);
className={settingsContext.darkTheme ? "dark" : ""} }}
itemHeight={50} className={settingsContext.darkTheme ? "dark" : ""}
/> />
)} )}
</Field> </Field>
</div> </div>
); );
} };
interface DownloadClientSelectProps { interface DownloadClientSelectProps {
name: string; name: string;
@ -123,102 +134,102 @@ interface DownloadClientSelectProps {
} }
export function DownloadClientSelect({ export function DownloadClientSelect({
name, name,
action, action,
clients clients
}: DownloadClientSelectProps) { }: DownloadClientSelectProps) {
return ( return (
<div className="col-span-6 sm:col-span-6"> <div className="col-span-6 sm:col-span-6">
<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 }) => (
<> <>
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Client Client
</Listbox.Label> </Listbox.Label>
<div className="mt-2 relative"> <div className="mt-2 relative">
<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>*/}
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon <SelectorIcon
className="h-5 w-5 text-gray-400 dark:text-gray-300" className="h-5 w-5 text-gray-400 dark:text-gray-300"
aria-hidden="true" /> aria-hidden="true" />
</span> </span>
</Listbox.Button> </Listbox.Button>
<Transition <Transition
show={open} show={open}
as={Fragment} as={Fragment}
leave="transition ease-in duration-100" leave="transition ease-in duration-100"
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options <Listbox.Options
static static
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
> >
{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(
active active
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800" ? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900 dark:text-gray-300", : "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9" "cursor-default select-none relative py-2 pl-3 pr-9"
)} )}
value={client.id} value={client.id}
> >
{({ selected, active }) => ( {({ selected, active }) => (
<> <>
<span <span
className={classNames( className={classNames(
selected ? "font-semibold" : "font-normal", selected ? "font-semibold" : "font-normal",
"block truncate" "block truncate"
)} )}
> >
{client.name} {client.name}
</span> </span>
{selected ? ( {selected ? (
<span <span
className={classNames( className={classNames(
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700", active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
"absolute inset-y-0 right-0 flex items-center pr-4" "absolute inset-y-0 right-0 flex items-center pr-4"
)} )}
> >
<CheckIcon <CheckIcon
className="h-5 w-5" className="h-5 w-5"
aria-hidden="true" /> aria-hidden="true" />
</span> </span>
) : null} ) : null}
</> </>
)} )}
</Listbox.Option> </Listbox.Option>
))} ))}
</Listbox.Options> </Listbox.Options>
</Transition> </Transition>
</div> </div>
</> </>
)} )}
</Listbox> </Listbox>
)} )}
</Field> </Field>
</div> </div>
); );
} }
interface SelectFieldOption { interface SelectFieldOption {
@ -234,209 +245,209 @@ interface SelectFieldProps {
} }
export const Select = ({ export const Select = ({
name, name,
label, label,
optionDefaultText, optionDefaultText,
options options
}: SelectFieldProps) => { }: SelectFieldProps) => {
return ( return (
<div className="col-span-6"> <div className="col-span-6">
<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 }) => (
<>
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
{label}
</Listbox.Label>
<div className="mt-2 relative">
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate">
{field.value
? options.find((c) => c.value === field.value)?.label
: optionDefaultText
}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon
className="h-5 w-5 text-gray-400 dark:text-gray-300"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
> >
{({ open }) => ( {options.map((opt) => (
<Listbox.Option
key={opt.value}
className={({ active }) =>
classNames(
active
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9"
)
}
value={opt.value}
>
{({ selected, active }) => (
<> <>
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <span
{label} className={classNames(
</Listbox.Label> selected ? "font-semibold" : "font-normal",
<div className="mt-2 relative"> "block truncate"
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm"> )}
<span className="block truncate"> >
{field.value {opt.label}
? options.find((c) => c.value === field.value)!.label </span>
: optionDefaultText
}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon
className="h-5 w-5 text-gray-400 dark:text-gray-300"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition {selected ? (
show={open} <span
as={Fragment} className={classNames(
leave="transition ease-in duration-100" active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
leaveFrom="opacity-100" "absolute inset-y-0 right-0 flex items-center pr-4"
leaveTo="opacity-0" )}
> >
<Listbox.Options <CheckIcon
static className="h-5 w-5"
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" aria-hidden="true"
> />
{options.map((opt) => ( </span>
<Listbox.Option ) : null}
key={opt.value}
className={({ active }) =>
classNames(
active
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9"
)
}
value={opt.value}
>
{({ selected, active }) => (
<>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{opt.label}
</span>
{selected ? (
<span
className={classNames(
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon
className="h-5 w-5"
aria-hidden="true"
/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</> </>
)} )}
</Listbox> </Listbox.Option>
)} ))}
</Field> </Listbox.Options>
</div> </Transition>
); </div>
} </>
)}
</Listbox>
)}
</Field>
</div>
);
};
export const SelectWide = ({ export const SelectWide = ({
name, name,
label, label,
optionDefaultText, optionDefaultText,
options options
}: SelectFieldProps) => { }: SelectFieldProps) => {
return ( return (
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<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">
<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 }) => (
<div className="py-4 flex items-center justify-between">
<Listbox.Label className="block text-sm font-medium text-gray-900 dark:text-white">
{label}
</Listbox.Label>
<div className="w-full">
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
<span className="block truncate">
{field.value
? options.find((c) => c.value === field.value)?.label
: optionDefaultText
}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon
className="h-5 w-5 text-gray-400 dark:text-gray-300"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
> >
{({ open }) => ( <Listbox.Options
<div className="py-4 flex items-center justify-between"> static
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{options.map((opt) => (
<Listbox.Option
key={opt.value}
className={({ active }) =>
classNames(
active
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9"
)
}
value={opt.value}
>
{({ selected, active }) => (
<>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{opt.label}
</span>
<Listbox.Label className="block text-sm font-medium text-gray-900 dark:text-white"> {selected ? (
{label} <span
</Listbox.Label> className={classNames(
<div className="w-full"> active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
<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"> "absolute inset-y-0 right-0 flex items-center pr-4"
<span className="block truncate"> )}
{field.value >
? options.find((c) => c.value === field.value)!.label <CheckIcon
: optionDefaultText className="h-5 w-5"
} aria-hidden="true"
</span> />
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> </span>
<SelectorIcon ) : null}
className="h-5 w-5 text-gray-400 dark:text-gray-300" </>
aria-hidden="true" )}
/> </Listbox.Option>
</span> ))}
</Listbox.Button> </Listbox.Options>
</Transition>
<Transition </div>
show={open} </div>
as={Fragment} )}
leave="transition ease-in duration-100" </Listbox>
leaveFrom="opacity-100" )}
leaveTo="opacity-0" </Field>
> </div>
<Listbox.Options </div>
static );
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" };
>
{options.map((opt) => (
<Listbox.Option
key={opt.value}
className={({ active }) =>
classNames(
active
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
: "text-gray-900 dark:text-gray-300",
"cursor-default select-none relative py-2 pl-3 pr-9"
)
}
value={opt.value}
>
{({ selected, active }) => (
<>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{opt.label}
</span>
{selected ? (
<span
className={classNames(
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon
className="h-5 w-5"
aria-hidden="true"
/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</div>
)}
</Listbox>
)}
</Field>
</div>
</div>
);
}

View file

@ -1,69 +1,73 @@
import React from "react";
import { Field } from "formik"; import { Field } from "formik";
import type { import type {
FieldInputProps, FieldInputProps,
FieldMetaProps, FieldMetaProps,
FieldProps, FieldProps,
FormikProps, FormikProps,
FormikValues FormikValues
} from "formik"; } from "formik";
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,
checked: $checked, checked: $checked,
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">
<HeadlessSwitch.Label>{label}</HeadlessSwitch.Label> <HeadlessSwitch.Label>{label}</HeadlessSwitch.Label>
<HeadlessSwitch <HeadlessSwitch
as="button" as="button"
name={field?.name} name={field?.name}
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;
@ -73,54 +77,54 @@ interface SwitchGroupProps {
} }
const SwitchGroup = ({ const SwitchGroup = ({
name, name,
label, label,
description description
}: SwitchGroupProps) => ( }: SwitchGroupProps) => (
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between"> <HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
{label && <div className="flex flex-col"> {label && <div className="flex flex-col">
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100" <HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
passive> passive>
{label} {label}
</HeadlessSwitch.Label> </HeadlessSwitch.Label>
{description && ( {description && (
<HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400"> <HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400">
{description} {description}
</HeadlessSwitch.Description> </HeadlessSwitch.Description>
)} )}
</div> </div>
} }
<Field name={name} type="checkbox">
{({
field,
form: { setFieldValue },
}: any) => (
<Switch
{...field}
type="button"
value={field.value}
checked={field.checked}
onChange={value => {
setFieldValue(field?.name ?? '', value)
}}
className={classNames(
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)}
>
<span
aria-hidden="true"
className={classNames(
field.value ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
<Field name={name} type="checkbox">
{({
field,
form: { setFieldValue }
}: FieldProps) => (
<Switch
{...field}
// type="button"
value={field.value}
checked={field.checked ?? false}
onChange={value => {
setFieldValue(field?.name ?? "", value);
}}
className={classNames(
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200",
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
)}
>
<span
aria-hidden="true"
className={classNames(
field.value ? "translate-x-5" : "translate-x-0",
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)} )}
</Field> />
</HeadlessSwitch.Group> </Switch>
)}
</Field>
</HeadlessSwitch.Group>
); );
export { SwitchGroup } export { SwitchGroup };

View file

@ -1,92 +1,92 @@
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;
} }
export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => ( export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog <Dialog
as="div" as="div"
static static
className="fixed z-10 inset-0 overflow-y-auto" className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={buttonRef} initialFocus={buttonRef}
open={isOpen} open={isOpen}
onClose={toggle} onClose={toggle}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
> >
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> <Dialog.Overlay className="fixed inset-0 bg-gray-700/60 dark:bg-black/60 transition-opacity" />
<Transition.Child </Transition.Child>
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-700/60 dark:bg-black/60 transition-opacity" />
</Transition.Child>
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203; &#8203;
</span> </span>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100" enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<div className="inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"> <div className="inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> <div className="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<ExclamationIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" /> <ExclamationIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" />
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white"> <Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
{title} {title}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500 dark:text-gray-300"> <p className="text-sm text-gray-500 dark:text-gray-300">
{text} {text}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
if (isOpen) {
deleteAction();
toggle();
}
}}
>
Remove
</button>
<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"
onClick={toggle}
ref={buttonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div> </div>
</Dialog> <div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
</Transition.Root> <button
) type="button"
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
if (isOpen) {
deleteAction();
toggle();
}
}}
>
Remove
</button>
<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"
onClick={toggle}
// ref={buttonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</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,117 +30,117 @@ 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 (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}> <Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
{deleteAction && ( {deleteAction && (
<DeleteModal <DeleteModal
isOpen={deleteModalIsOpen} isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal} toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef} buttonRef={cancelModalButtonRef}
deleteAction={deleteAction} deleteAction={deleteAction}
title={`Remove ${title}`} title={`Remove ${title}`}
text={`Are you sure you want to remove this ${title}? This action cannot be undone.`} text={`Are you sure you want to remove this ${title}? This action cannot be undone.`}
/> />
)} )}
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0" /> <Dialog.Overlay className="absolute inset-0" />
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700" enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full" enterFrom="translate-x-full"
enterTo="translate-x-0" enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700" leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0" leaveFrom="translate-x-0"
leaveTo="translate-x-full" leaveTo="translate-x-full"
> >
<div className="w-screen max-w-2xl dark:border-gray-700 border-l"> <div className="w-screen max-w-2xl dark:border-gray-700 border-l">
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
onSubmit={onSubmit} onSubmit={onSubmit}
validate={validate} validate={validate}
> >
{({ handleSubmit, values }) => ( {({ handleSubmit, values }) => (
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll" <Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
onSubmit={handleSubmit}> onSubmit={handleSubmit}>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">{type === "CREATE" ? "Create" : "Update"} {title}</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
{type === "CREATE" ? "Create" : "Update"} {title}.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-900 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
{!!values && children !== undefined ? (
children(values)
) : null}
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
{type === "UPDATE" && (
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-white bg-red-100 dark:bg-red-700 hover:bg-red-200 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
)}
<div>
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm 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"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{type === "CREATE" ? "Create" : "Save"}
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</Form>
)}
</Formik>
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">{type === "CREATE" ? "Create" : "Update"} {title}</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
{type === "CREATE" ? "Create" : "Update"} {title}.
</p>
</div> </div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-900 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
</div>
</div>
</Transition.Child> {!!values && children !== undefined ? (
</div> children(values)
</div> ) : null}
</Dialog> </div>
</Transition.Root>
) <div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
{type === "UPDATE" && (
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-white bg-red-100 dark:bg-red-700 hover:bg-red-200 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
onClick={toggleDeleteModal}
>
Remove
</button>
)}
<div>
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm 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"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
{type === "CREATE" ? "Create" : "Save"}
</button>
</div>
</div>
</div>
<DEBUG values={values} />
</Form>
)}
</Formik>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
} }
export { SlideOver }; export { SlideOver };

View file

@ -1,156 +1,158 @@
import { MultiSelectOption } from "../components/inputs/select";
export const resolutions = [ export const resolutions = [
"2160p", "2160p",
"1080p", "1080p",
"1080i", "1080i",
"810p", "810p",
"720p", "720p",
"576p", "576p",
"480p", "480p",
"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",
"H.264", "H.264",
"H.265", "H.265",
"x264", "x264",
"x265", "x265",
"AVC", "AVC",
"VC-1", "VC-1",
"AV1", "AV1",
"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",
"UHD.BluRay", "UHD.BluRay",
"WEB-DL", "WEB-DL",
"WEB", "WEB",
"WEBRip", "WEBRip",
"BD5", "BD5",
"BD9", "BD9",
"BDr", "BDr",
"BDRip", "BDRip",
"BRRip", "BRRip",
"CAM", "CAM",
"DVDR", "DVDR",
"DVDRip", "DVDRip",
"DVDScr", "DVDScr",
"HDCAM", "HDCAM",
"HDDVD", "HDDVD",
"HDDVDRip", "HDDVDRip",
"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",
"HDR10", "HDR10",
"HDR10+", "HDR10+",
"HLG", "HLG",
"DV", "DV",
"DV HDR", "DV 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 }));
export const formatMusic = [ export const formatMusic = [
"MP3", "MP3",
"FLAC", "FLAC",
"Ogg Vorbis", "Ogg Vorbis",
"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",
"WEB", "WEB",
"DVD", "DVD",
"Vinyl", "Vinyl",
"Soundboard", "Soundboard",
"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",
"256", "256",
"320", "320",
"APS (VBR)", "APS (VBR)",
"APX (VBR)", "APX (VBR)",
"V2 (VBR)", "V2 (VBR)",
"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",
"Single", "Single",
"EP", "EP",
"Soundtrack", "Soundtrack",
"Anthology", "Anthology",
"Compilation", "Compilation",
"Live album", "Live album",
"Remix", "Remix",
"Bootleg", "Bootleg",
"Interview", "Interview",
"Mixtape", "Mixtape",
"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 }));
export interface RadioFieldsetOption { export interface RadioFieldsetOption {
label: string; label: string;
@ -159,123 +161,128 @@ export interface RadioFieldsetOption {
} }
export const DownloadClientTypeOptions: RadioFieldsetOption[] = [ export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
{ {
label: "qBittorrent", label: "qBittorrent",
description: "Add torrents directly to qBittorrent", description: "Add torrents directly to qBittorrent",
value: "QBITTORRENT" value: "QBITTORRENT"
}, },
{ {
label: "Deluge", label: "Deluge",
description: "Add torrents directly to Deluge", description: "Add torrents directly to Deluge",
value: "DELUGE_V1" value: "DELUGE_V1"
}, },
{ {
label: "Deluge 2", label: "Deluge 2",
description: "Add torrents directly to Deluge 2", description: "Add torrents directly to Deluge 2",
value: "DELUGE_V2" value: "DELUGE_V2"
}, },
{ {
label: "Radarr", label: "Radarr",
description: "Send to Radarr and let it decide", description: "Send to Radarr and let it decide",
value: "RADARR" value: "RADARR"
}, },
{ {
label: "Sonarr", label: "Sonarr",
description: "Send to Sonarr and let it decide", description: "Send to Sonarr and let it decide",
value: "SONARR" value: "SONARR"
}, },
{ {
label: "Lidarr", label: "Lidarr",
description: "Send to Lidarr and let it decide", description: "Send to Lidarr and let it decide",
value: "LIDARR" value: "LIDARR"
}, },
{ {
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> = {
"DELUGE_V1": "Deluge v1", "DELUGE_V1": "Deluge v1",
"DELUGE_V2": "Deluge v2", "DELUGE_V2": "Deluge v2",
"QBITTORRENT": "qBittorrent", "QBITTORRENT": "qBittorrent",
"RADARR": "Radarr", "RADARR": "Radarr",
"SONARR": "Sonarr", "SONARR": "Sonarr",
"LIDARR": "Lidarr", "LIDARR": "Lidarr",
"WHISPARR": "Whisparr", "WHISPARR": "Whisparr"
}; };
export const ActionTypeOptions: RadioFieldsetOption[] = [ export const ActionTypeOptions: RadioFieldsetOption[] = [
{label: "Test", description: "A simple action to test a filter.", value: "TEST"}, { label: "Test", description: "A simple action to test a filter.", value: "TEST" },
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"}, { label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER" },
{label: "Webhook", description: "Run webhook", value: "WEBHOOK"}, { label: "Webhook", description: "Run webhook", value: "WEBHOOK" },
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"}, { label: "Exec", description: "Run a custom command after a filter match", value: "EXEC" },
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"}, { label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT" },
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"}, { label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1" },
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"}, { label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
{label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR"}, { label: "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 = {
"TEST": "Test", "TEST": "Test",
"WATCH_FOLDER": "Watch folder", "WATCH_FOLDER": "Watch folder",
"WEBHOOK": "Webhook", "WEBHOOK": "Webhook",
"EXEC": "Exec", "EXEC": "Exec",
"DELUGE_V1": "Deluge v1", "DELUGE_V1": "Deluge v1",
"DELUGE_V2": "Deluge v2", "DELUGE_V2": "Deluge v2",
"QBITTORRENT": "qBittorrent", "QBITTORRENT": "qBittorrent",
"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;
label: "Rejected", value: string;
value: "PUSH_REJECTED", }
},
{ export const PushStatusOptions: OptionBasic[] = [
label: "Approved", {
value: "PUSH_APPROVED" label: "Rejected",
}, value: "PUSH_REJECTED"
{ },
label: "Error", {
value: "PUSH_ERROR" label: "Approved",
}, value: "PUSH_APPROVED"
},
{
label: "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,149 +3,160 @@ 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 {
const mutation = useMutation( isOpen: boolean;
(filter: Filter) => APIClient.filters.create(filter), toggle: () => void;
{ }
onSuccess: (_, filter) => {
queryClient.invalidateQueries("filters");
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
toggle(); function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
} const mutation = useMutation(
} (filter: Filter) => APIClient.filters.create(filter),
) {
onSuccess: (_, filter) => {
queryClient.invalidateQueries("filters");
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
const handleSubmit = (data: any) => mutation.mutate(data); toggle();
const validate = (values: any) => values.name ? {} : { name: "Required" }; }
}
);
return ( const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
<Transition.Root show={isOpen} as={Fragment}> const validate = (values: FormikValues) => {
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}> const errors = {} as FormikErrors<FormikValues>;
<div className="absolute inset-0 overflow-hidden"> if (!values.name) {
<Dialog.Overlay className="absolute inset-0" /> errors.name = "Required";
}
return errors;
};
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> return (
<Transition.Child <Transition.Root show={isOpen} as={Fragment}>
as={Fragment} <Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
enter="transform transition ease-in-out duration-500 sm:duration-700" <div className="absolute inset-0 overflow-hidden">
enterFrom="translate-x-full" <Dialog.Overlay className="absolute inset-0" />
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
<Formik <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
initialValues={{ <Transition.Child
name: "", as={Fragment}
enabled: false, enter="transform transition ease-in-out duration-500 sm:duration-700"
resolutions: [], enterFrom="translate-x-full"
codecs: [], enterTo="translate-x-0"
sources: [], leave="transform transition ease-in-out duration-500 sm:duration-700"
containers: [], leaveFrom="translate-x-0"
origins: [], leaveTo="translate-x-full"
}} >
onSubmit={handleSubmit} <div className="w-screen max-w-2xl border-l dark:border-gray-700">
validate={validate}
> <Formik
{({ values }) => ( initialValues={{
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"> name: "",
<div className="flex-1"> enabled: false,
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6"> resolutions: [],
<div className="flex items-start justify-between space-x-3"> codecs: [],
<div className="space-y-1"> sources: [],
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title> containers: [],
<p className="text-sm text-gray-500 dark:text-gray-400"> origins: []
}}
onSubmit={handleSubmit}
validate={validate}
>
{({ values }) => (
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add new filter. Add new filter.
</p> </p>
</div> </div>
<div className="h-7 flex items-center"> <div className="h-7 flex items-center">
<button <button
type="button" type="button"
className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle} onClick={toggle}
> >
<span className="sr-only">Close panel</span> <span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true" /> <XIcon className="h-6 w-6" aria-hidden="true" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div <div
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<div <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"> 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> <div>
<label <label
htmlFor="name" htmlFor="name"
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2" className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
> >
Name Name
</label> </label>
</div> </div>
<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
{...field} {...field}
id="name" id="name"
type="text" type="text"
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md" className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
/> />
{meta.touched && meta.error && {meta.touched && meta.error &&
<span className="block mt-2 text-red-500">{meta.error}</span>} <span className="block mt-2 text-red-500">{meta.error}</span>}
</div> </div>
)} )}
</Field> </Field>
</div> </div>
</div> </div>
</div> </div>
<div <div
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6"> className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end"> <div className="space-x-3 flex justify-end">
<button <button
type="button" type="button"
className="bg-white dark:bg-gray-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 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-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggle} onClick={toggle}
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
Create Create
</button> </button>
</div> </div>
</div> </div>
<DEBUG values={values} /> <DEBUG values={values} />
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </div>
</Transition.Child> </Transition.Child>
</div> </div>
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
) );
} }
export default FilterAddForm; export default FilterAddForm;

File diff suppressed because it is too large Load diff

View file

@ -1,115 +1,118 @@
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 {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;
} }
export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) { export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
const mutation = useMutation( const mutation = useMutation(
(feed: Feed) => APIClient.feeds.update(feed), (feed: Feed) => APIClient.feeds.update(feed),
{ {
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();
}, }
}
);
const deleteMutation = useMutation(
(feedID: number) => APIClient.feeds.delete(feedID),
{
onSuccess: () => {
queryClient.invalidateQueries(["feeds"]);
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t}/>)
},
}
);
const onSubmit = (formData: any) => {
mutation.mutate(formData);
} }
);
const deleteAction = () => { const deleteMutation = useMutation(
deleteMutation.mutate(feed.id); (feedID: number) => APIClient.feeds.delete(feedID),
}; {
onSuccess: () => {
const initialValues = { queryClient.invalidateQueries(["feeds"]);
id: feed.id, toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t}/>);
indexer: feed.indexer, }
enabled: feed.enabled,
type: feed.type,
name: feed.name,
url: feed.url,
api_key: feed.api_key,
interval: feed.interval,
} }
);
return ( const onSubmit = (formData: unknown) => {
<SlideOver mutation.mutate(formData as Feed);
type="UPDATE" };
title="Feed"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
>
{(values) => (
<div>
<TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> const deleteAction = () => {
<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"> deleteMutation.mutate(feed.id);
<div> };
<label
htmlFor="type"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Type
</label>
</div>
<div className="flex justify-end sm:col-span-2">
{ImplementationMap[feed.type]}
</div>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> const initialValues = {
<SwitchGroupWide name="enabled" label="Enabled"/> id: feed.id,
</div> indexer: feed.indexer,
</div> enabled: feed.enabled,
{componentMap[values.type]} type: feed.type,
</div> name: feed.name,
)} url: feed.url,
</SlideOver> api_key: feed.api_key,
) interval: feed.interval
};
return (
<SlideOver
type="UPDATE"
title="Feed"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
>
{(values) => (
<div>
<TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<div
className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="type"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Type
</label>
</div>
<div className="flex justify-end sm:col-span-2">
{ImplementationMap[feed.type]}
</div>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
</div>
{componentMap[values.type]}
</div>
)}
</SlideOver>
);
} }
function FormFieldsTorznab() { function FormFieldsTorznab() {
return ( return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5">
<TextFieldWide <TextFieldWide
name="url" name="url"
label="URL" label="URL"
help="Torznab url" help="Torznab url"
/> />
<PasswordFieldWide name="api_key" label="API key" /> <PasswordFieldWide name="api_key" label="API key"/>
<NumberFieldWide name="interval" label="Refresh interval" help="Minutes. Recommended 15-30. To low and risk ban." /> <NumberFieldWide name="interval" label="Refresh interval"
</div> help="Minutes. Recommended 15-30. To low and risk ban."/>
); </div>
);
} }
const componentMap: any = { const componentMap: componentMapType = {
TORZNAB: <FormFieldsTorznab/>, TORZNAB: <FormFieldsTorznab/>
}; };

File diff suppressed because it is too large Load diff

View file

@ -1,87 +1,87 @@
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";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { import {
TextFieldWide, TextFieldWide,
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[];
} }
const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => ( const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
<div className="p-6"> <div className="p-6">
<FieldArray name="channels"> <FieldArray name="channels">
{({ remove, push }) => ( {({ remove, push }) => (
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4"> <div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
{channels && channels.length > 0 ? ( {channels && channels.length > 0 ? (
channels.map((_channel: IrcChannel, index: number) => ( channels.map((_channel: IrcChannel, index: number) => (
<div key={index} className="flex justify-between"> <div key={index} className="flex justify-between">
<div className="flex"> <div className="flex">
<Field name={`channels.${index}.name`}> <Field name={`channels.${index}.name`}>
{({ field }: FieldProps) => ( {({ field }: FieldProps) => (
<input <input
{...field} {...field}
type="text" type="text"
value={field.value ?? ""} value={field.value ?? ""}
onChange={field.onChange} onChange={field.onChange}
placeholder="#Channel" placeholder="#Channel"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md" className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/> />
)}
</Field>
<Field name={`channels.${index}.password`}>
{({ field }: FieldProps) => (
<input
{...field}
type="text"
value={field.value ?? ""}
onChange={field.onChange}
placeholder="Password"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
)}
</Field>
</div>
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker dark:text-white">
No channels!
</span>
)} )}
<button </Field>
type="button"
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center" <Field name={`channels.${index}.password`}>
onClick={() => push({ name: "", password: "" })} {({ field }: FieldProps) => (
> <input
Add Channel {...field}
</button> type="text"
value={field.value ?? ""}
onChange={field.onChange}
placeholder="Password"
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
/>
)}
</Field>
</div> </div>
)}
</FieldArray> <button
</div> type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={() => remove(index)}
>
<span className="sr-only">Remove</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
</button>
</div>
))
) : (
<span className="text-center text-sm text-grey-darker dark:text-white">
No channels!
</span>
)}
<button
type="button"
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
onClick={() => push({ name: "", password: "" })}
>
Add Channel
</button>
</div>
)}
</FieldArray>
</div>
); );
interface IrcNetworkAddFormValues { interface IrcNetworkAddFormValues {
@ -95,105 +95,101 @@ interface IrcNetworkAddFormValues {
channels: IrcChannel[]; channels: IrcChannel[];
} }
export function IrcNetworkAddForm({ isOpen, toggle }: any) { interface AddFormProps {
const mutation = useMutation( isOpen: boolean;
(network: IrcNetwork) => APIClient.irc.createNetwork(network), toggle: () => void;
{ }
onSuccess: () => {
queryClient.invalidateQueries(['networks']);
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />)
toggle()
},
onError: () => {
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
},
}
);
const onSubmit = (data: any) => { export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
// easy way to split textarea lines into array of strings for each newline. const mutation = useMutation(
// parse on the field didn't really work. (network: IrcNetwork) => APIClient.irc.createNetwork(network),
data.connect_commands = ( {
data.connect_commands && data.connect_commands.length > 0 ? onSuccess: () => {
data.connect_commands.replace(/\r\n/g, "\n").split("\n") : queryClient.invalidateQueries(["networks"]);
[] toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
); toggle();
},
mutation.mutate(data); onError: () => {
}; toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
}
const validate = (values: IrcNetworkAddFormValues) => {
const errors = {} as any;
if (!values.name)
errors.name = "Required";
if (!values.port)
errors.port = "Required";
if (!values.server)
errors.server = "Required";
if (!values.nickserv || !values.nickserv.account)
errors.nickserv = { account: "Required" };
return errors;
} }
);
const initialValues: IrcNetworkAddFormValues = { const onSubmit = (data: unknown) => {
name: "", mutation.mutate(data as IrcNetwork);
enabled: true, };
server: "", const validate = (values: FormikValues) => {
port: 6667, const errors = {} as FormikErrors<FormikValues>;
tls: false, if (!values.name)
pass: "", errors.name = "Required";
nickserv: {
account: ""
},
channels: [],
};
return ( if (!values.port)
<SlideOver errors.port = "Required";
type="CREATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
initialValues={initialValues}
validate={validate}
>
{(values) => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> if (!values.server)
errors.server = "Required";
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700"> if (!values.nickserv || !values.nickserv.account)
<SwitchGroupWide name="enabled" label="Enabled" /> errors.nickserv = { account: "Required" };
</div>
<div> return errors;
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} /> };
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> const initialValues: IrcNetworkAddFormValues = {
<SwitchGroupWide name="tls" label="TLS" /> name: "",
</div> enabled: true,
server: "",
port: 6667,
tls: false,
pass: "",
nickserv: {
account: ""
},
channels: []
};
<PasswordFieldWide name="pass" label="Password" help="Network password" /> return (
<SlideOver
type="CREATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
initialValues={initialValues}
validate={validate}
>
{(values) => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} /> <div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" /> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
</div> <SwitchGroupWide name="enabled" label="Enabled" />
</div> </div>
<ChannelsFieldArray channels={values.channels} /> <div>
</> <TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
)} <NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
</SlideOver>
) <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="tls" label="TLS" />
</div>
<PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<ChannelsFieldArray channels={values.channels} />
</>
)}
</SlideOver>
);
} }
interface IrcNetworkUpdateFormValues { interface IrcNetworkUpdateFormValues {
@ -216,118 +212,113 @@ interface IrcNetworkUpdateFormProps {
} }
export function IrcNetworkUpdateForm({ export function IrcNetworkUpdateForm({
isOpen, isOpen,
toggle, toggle,
network network
}: 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: FormikValues) => {
}; const errors = {} as FormikErrors<FormikValues>;
const validate = (values: any) => { if (!values.name) {
const errors = {} as any; errors.name = "Required";
if (!values.name) {
errors.name = "Required";
}
if (!values.server) {
errors.server = "Required";
}
if (!values.port) {
errors.port = "Required";
}
if (!values.nickserv?.account) {
errors.nickserv.account = "Required";
}
return errors;
} }
const deleteAction = () => { if (!values.server) {
deleteMutation.mutate(network.id) errors.server = "Required";
} }
const initialValues: IrcNetworkUpdateFormValues = { if (!values.port) {
id: network.id, errors.port = "Required";
name: network.name,
enabled: network.enabled,
server: network.server,
port: network.port,
tls: network.tls,
nickserv: network.nickserv,
pass: network.pass,
channels: network.channels,
invite_command: network.invite_command
} }
return ( if (!values.nickserv?.account) {
<SlideOver errors.nickserv = {
type="UPDATE" account: "Required"
title="Network" };
isOpen={isOpen} }
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
validate={validate}
>
{(values) => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700"> return errors;
};
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0"> const deleteAction = () => {
<SwitchGroupWide name="enabled" label="Enabled" /> deleteMutation.mutate(network.id);
</div> };
<div> const initialValues: IrcNetworkUpdateFormValues = {
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} /> id: network.id,
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} /> name: network.name,
enabled: network.enabled,
server: network.server,
port: network.port,
tls: network.tls,
nickserv: network.nickserv,
pass: network.pass,
channels: network.channels,
invite_command: network.invite_command
};
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> return (
<SwitchGroupWide name="tls" label="TLS" /> <SlideOver
</div> type="UPDATE"
title="Network"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
validate={validate}
>
{(values) => (
<>
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
<PasswordFieldWide name="pass" label="Password" help="Network password" /> <div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} /> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
<PasswordFieldWide name="nickserv.password" label="NickServ Password" /> <SwitchGroupWide name="enabled" label="Enabled" />
</div>
<PasswordFieldWide name="invite_command" label="Invite command" /> <div>
</div> <TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
</div> <NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
<ChannelsFieldArray channels={values.channels} /> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
</> <SwitchGroupWide name="tls" label="TLS" />
)} </div>
</SlideOver>
) <PasswordFieldWide name="pass" label="Password" help="Network password" />
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
<PasswordFieldWide name="invite_command" label="Invite command" />
</div>
</div>
<ChannelsFieldArray channels={values.channels} />
</>
)}
</SlideOver>
);
} }

View file

@ -1,83 +1,87 @@
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) => {
return (
<components.Input
{...props}
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
className="text-gray-400 dark:text-gray-100"
children={props.children}
/>
);
};
const Input = (props: any) => { const Control = (props: ControlProps) => {
return ( return (
<components.Input <components.Control
{...props} {...props}
inputClassName="outline-none border-none shadow-none focus:ring-transparent" 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="text-gray-400 dark:text-gray-100" children={props.children}
/> />
); );
} };
const Control = (props: any) => { const Menu = (props: MenuProps) => {
return ( return (
<components.Control <components.Menu
{...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="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
/> children={props.children}
); />
} );
};
const Menu = (props: any) => { const Option = (props: OptionProps) => {
return ( return (
<components.Menu <components.Option
{...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:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
/> children={props.children}
); />
} );
};
const Option = (props: any) => {
return (
<components.Option
{...props}
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
/>
);
}
function FormFieldsDiscord() { function FormFieldsDiscord() {
return ( return (
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5">
{/*<div className="px-6 space-y-1">*/} {/*<div className="px-6 space-y-1">*/}
{/* <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Credentials</Dialog.Title>*/} {/* <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Credentials</Dialog.Title>*/}
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/} {/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
{/* Api keys etc*/} {/* Api keys etc*/}
{/* </p>*/} {/* </p>*/}
{/*</div>*/} {/*</div>*/}
<TextFieldWide <TextFieldWide
name="webhook" name="webhook"
label="Webhook URL" label="Webhook URL"
help="Discord channel webhook url" help="Discord channel webhook url"
placeholder="https://discordapp.com/api/webhooks/xx/xx" placeholder="https://discordapp.com/api/webhooks/xx/xx"
/> />
</div> </div>
); );
} }
const componentMap: any = { const componentMap: componentMapType = {
DISCORD: <FormFieldsDiscord/>, DISCORD: <FormFieldsDiscord/>
}; };
interface NotificationAddFormValues { interface NotificationAddFormValues {
@ -87,349 +91,353 @@ 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) {
const mutation = useMutation( const mutation = useMutation(
(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) => {
mutation.mutate(formData)
} }
);
const validate = (values: NotificationAddFormValues) => { const onSubmit = (formData: unknown) => {
const errors = {} as any; mutation.mutate(formData as Notification);
if (!values.name) };
errors.name = "Required";
return errors; const validate = (values: NotificationAddFormValues) => {
} const errors = {} as FormikErrors<FormikValues>;
if (!values.name)
errors.name = "Required";
return ( return errors;
<Transition.Root show={isOpen} as={Fragment}> };
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
<div className="absolute inset-0 overflow-hidden">
<Dialog.Overlay className="absolute inset-0"/>
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16"> return (
<Transition.Child <Transition.Root show={isOpen} as={Fragment}>
as={Fragment} <Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
enter="transform transition ease-in-out duration-500 sm:duration-700" <div className="absolute inset-0 overflow-hidden">
enterFrom="translate-x-full" <Dialog.Overlay className="absolute inset-0"/>
enterTo="translate-x-0"
leave="transform transition ease-in-out duration-500 sm:duration-700"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
<Formik
enableReinitialize={true}
initialValues={{
enabled: true,
type: "",
name: "",
webhook: "",
events: [],
}}
onSubmit={onSubmit}
validate={validate}
>
{({values}) => (
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
<div className="flex-1">
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
<div className="flex items-start justify-between space-x-3">
<div className="space-y-1">
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Add
Notifications</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-200">
Trigger notifications on different events.
</p>
</div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
</div>
</div>
<TextFieldWide name="name" label="Name" required={true}/> <div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<Transition.Child
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700"> as={Fragment}
<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"> enter="transform transition ease-in-out duration-500 sm:duration-700"
<div> enterFrom="translate-x-full"
<label enterTo="translate-x-0"
htmlFor="type" leave="transform transition ease-in-out duration-500 sm:duration-700"
className="block text-sm font-medium text-gray-900 dark:text-white" leaveFrom="translate-x-0"
> leaveTo="translate-x-full"
Type >
</label> <div className="w-screen max-w-2xl dark:border-gray-700 border-l">
</div> <Formik
<div className="sm:col-span-2"> enableReinitialize={true}
<Field name="type" type="select"> initialValues={{
{({ enabled: true,
field, type: "",
form: {setFieldValue, resetForm} name: "",
}: FieldProps) => ( webhook: "",
<Select {...field} events: []
isClearable={true} }}
isSearchable={true} onSubmit={onSubmit}
components={{Input, Control, Menu, Option}} validate={validate}
placeholder="Choose a type" >
styles={{ {({ values }) => (
singleValue: (base) => ({ <Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
...base, <div className="flex-1">
color: "unset" <div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
}) <div className="flex items-start justify-between space-x-3">
}} <div className="space-y-1">
theme={(theme) => ({ <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
...theme, Add Notifications
spacing: { </Dialog.Title>
...theme.spacing, <p className="text-sm text-gray-500 dark:text-gray-200">
controlHeight: 30, Trigger notifications on different events.
baseUnit: 2, </p>
}
})}
value={field?.value && field.value.value}
onChange={(option: any) => {
resetForm()
// setFieldValue("name", option?.label ?? "")
setFieldValue(field.name, option?.value ?? "")
}}
options={NotificationTypeOptions}
/>
)}
</Field>
</div>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Select what events to trigger on
</p>
</div>
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
<EventCheckBoxes />
</div>
</div>
</div>
{componentMap[values.type]}
</div>
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
<div className="space-x-3 flex justify-end">
<button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm 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"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Save
</button>
</div>
</div>
<DEBUG values={values}/>
</Form>
)}
</Formik>
</div> </div>
<div className="h-7 flex items-center">
<button
type="button"
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
onClick={toggle}
>
<span className="sr-only">Close panel</span>
<XIcon className="h-6 w-6" aria-hidden="true"/>
</button>
</div>
</div>
</div>
</Transition.Child> <TextFieldWide name="name" label="Name" required={true}/>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
const EventCheckBoxes = () => ( <div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<fieldset className="space-y-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">
<legend className="sr-only">Notifications</legend>
{EventOptions.map((e, idx) => (
<div key={idx} className="relative flex items-start">
<div className="flex items-center h-5">
<Field
id={`events-${e.value}`}
aria-describedby={`events-${e.value}-description`}
name="events"
type="checkbox"
value={e.value}
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor={`events-${e.value}`}
className="font-medium text-gray-900 dark:text-gray-100">
{e.label}
</label>
{e.description && (
<p className="text-gray-500">{e.description}</p>
)}
</div>
</div>
))}
</fieldset>
)
interface UpdateProps {
isOpen: boolean;
toggle: any;
notification: Notification;
}
export function NotificationUpdateForm({isOpen, toggle, notification}: UpdateProps) {
const mutation = useMutation(
(notification: Notification) => APIClient.notifications.update(notification),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>)
toggle();
},
}
);
const deleteMutation = useMutation(
(notificationID: number) => APIClient.notifications.delete(notificationID),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>)
},
}
);
const onSubmit = (formData: any) => {
mutation.mutate(formData);
}
const deleteAction = () => {
deleteMutation.mutate(notification.id);
};
const initialValues = {
id: notification.id,
enabled: notification.enabled,
type: notification.type,
name: notification.name,
webhook: notification.webhook,
events: notification.events || [],
}
return (
<SlideOver
type="UPDATE"
title="Notification"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
>
{(values) => (
<div>
<TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div> <div>
<label <label
htmlFor="type" htmlFor="type"
className="block text-sm font-medium text-gray-900 dark:text-white" className="block text-sm font-medium text-gray-900 dark:text-white"
> >
Type Type
</label> </label>
</div> </div>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<Field name="type" type="select"> <Field name="type" type="select">
{({field, form: {setFieldValue, resetForm}}: FieldProps) => ( {({
<Select {...field} field,
isClearable={true} form: { setFieldValue, resetForm }
isSearchable={true} }: FieldProps) => (
components={{Input, Control, Menu, Option}} <Select {...field}
isClearable={true}
isSearchable={true}
components={{ Input, Control, Menu, Option }}
placeholder="Choose a type"
styles={{
singleValue: (base) => ({
...base,
color: "unset"
})
}}
theme={(theme) => ({
...theme,
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2
}
})}
value={field?.value && field.value.value}
onChange={(option: unknown) => {
resetForm();
const opt = option as SelectOption;
// setFieldValue("name", option?.label ?? "")
setFieldValue(field.name, opt.value ?? "");
}}
options={NotificationTypeOptions}
/>
)}
</Field>
placeholder="Choose a type"
styles={{
singleValue: (base) => ({
...base,
color: "unset"
})
}}
theme={(theme) => ({
...theme,
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2,
}
})}
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
onChange={(option: any) => {
resetForm()
setFieldValue(field.name, option?.value ?? "")
}}
options={NotificationTypeOptions}
/>
)}
</Field>
</div> </div>
</div> </div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200"> <div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="enabled" label="Enabled"/> <SwitchGroupWide name="enabled" label="Enabled"/>
</div> </div>
<div className="border-t border-gray-200 dark:border-gray-700 py-5"> <div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1"> <div className="px-6 space-y-1">
<Dialog.Title <Dialog.Title
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title> className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Select what events to trigger on Select what events to trigger on
</p> </p>
</div> </div>
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5"> <div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
<EventCheckBoxes /> <EventCheckBoxes />
</div> </div>
</div>
</div> </div>
</div> {componentMap[values.type]}
{componentMap[values.type]} </div>
</div>
)} <div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
</SlideOver> <div className="space-x-3 flex justify-end">
) <button
type="button"
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm 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"
onClick={toggle}
>
Cancel
</button>
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
>
Save
</button>
</div>
</div>
<DEBUG values={values}/>
</Form>
)}
</Formik>
</div>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
}
const EventCheckBoxes = () => (
<fieldset className="space-y-5">
<legend className="sr-only">Notifications</legend>
{EventOptions.map((e, idx) => (
<div key={idx} className="relative flex items-start">
<div className="flex items-center h-5">
<Field
id={`events-${e.value}`}
aria-describedby={`events-${e.value}-description`}
name="events"
type="checkbox"
value={e.value}
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor={`events-${e.value}`}
className="font-medium text-gray-900 dark:text-gray-100">
{e.label}
</label>
{e.description && (
<p className="text-gray-500">{e.description}</p>
)}
</div>
</div>
))}
</fieldset>
);
interface UpdateProps {
isOpen: boolean;
toggle: () => void;
notification: Notification;
}
export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) {
const mutation = useMutation(
(notification: Notification) => APIClient.notifications.update(notification),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>);
toggle();
}
}
);
const deleteMutation = useMutation(
(notificationID: number) => APIClient.notifications.delete(notificationID),
{
onSuccess: () => {
queryClient.invalidateQueries(["notifications"]);
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>);
}
}
);
const onSubmit = (formData: unknown) => {
mutation.mutate(formData as Notification);
};
const deleteAction = () => {
deleteMutation.mutate(notification.id);
};
const initialValues = {
id: notification.id,
enabled: notification.enabled,
type: notification.type,
name: notification.name,
webhook: notification.webhook,
events: notification.events || []
};
return (
<SlideOver
type="UPDATE"
title="Notification"
isOpen={isOpen}
toggle={toggle}
onSubmit={onSubmit}
deleteAction={deleteAction}
initialValues={initialValues}
>
{(values) => (
<div>
<TextFieldWide name="name" label="Name" required={true}/>
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label
htmlFor="type"
className="block text-sm font-medium text-gray-900 dark:text-white"
>
Type
</label>
</div>
<div className="sm:col-span-2">
<Field name="type" type="select">
{({ field, form: { setFieldValue, resetForm } }: FieldProps) => (
<Select {...field}
isClearable={true}
isSearchable={true}
components={{ Input, Control, Menu, Option }}
placeholder="Choose a type"
styles={{
singleValue: (base) => ({
...base,
color: "unset"
})
}}
theme={(theme) => ({
...theme,
spacing: {
...theme.spacing,
controlHeight: 30,
baseUnit: 2
}
})}
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
onChange={(option: unknown) => {
resetForm();
const opt = option as SelectOption;
setFieldValue(field.name, opt.value ?? "");
}}
options={NotificationTypeOptions}
/>
)}
</Field>
</div>
</div>
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
<SwitchGroupWide name="enabled" label="Enabled"/>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
<div className="px-6 space-y-1">
<Dialog.Title
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
<p className="text-sm text-gray-500 dark:text-gray-400">
Select what events to trigger on
</p>
</div>
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
<EventCheckBoxes />
</div>
</div>
</div>
{componentMap[values.type]}
</div>
)}
</SlideOver>
);
} }

View file

@ -1,8 +1,8 @@
import { useState } from "react"; import { useState } from "react";
export function useToggle(initialValue = false): [boolean, () => void] { export function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const toggle = () => setValue(v => !v); const toggle = () => setValue(v => !v);
return [value, toggle]; return [value, toggle];
} }

View file

@ -17,8 +17,8 @@ window.APP = window.APP || {};
InitializeGlobalContext(); InitializeGlobalContext();
ReactDOM.render( ReactDOM.render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode>, </StrictMode>,
document.getElementById("root") document.getElementById("root")
); );

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,229 +21,230 @@ 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
) => { ) => {
if (!match) if (!match)
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" }
]; ];
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<Disclosure <Disclosure
as="nav" as="nav"
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]" className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
> >
{({ open }) => ( {({ open }) => (
<> <>
<div className="max-w-screen-xl mx-auto sm:px-6 lg:px-8"> <div className="max-w-screen-xl mx-auto sm:px-6 lg:px-8">
<div className="border-b border-gray-300 dark:border-gray-700"> <div className="border-b border-gray-300 dark:border-gray-700">
<div className="flex items-center justify-between h-16 px-4 sm:px-0"> <div className="flex items-center justify-between h-16 px-4 sm:px-0">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-shrink-0 flex items-center"> <div className="flex-shrink-0 flex items-center">
<img <img
className="block lg:hidden h-10 w-auto" className="block lg:hidden h-10 w-auto"
src={logo} src={logo}
alt="Logo" alt="Logo"
/> />
<img <img
className="hidden lg:block h-10 w-auto" className="hidden lg:block h-10 w-auto"
src={logo} src={logo}
alt="Logo" alt="Logo"
/> />
</div> </div>
<div className="sm:ml-3 hidden sm:block"> <div className="sm:ml-3 hidden sm:block">
<div className="flex items-baseline space-x-4"> <div className="flex items-baseline space-x-4">
{nav.map((item, itemIdx) => {nav.map((item, itemIdx) =>
<NavLink <NavLink
key={item.name + itemIdx} key={item.name + itemIdx}
to={item.path} to={item.path}
strict strict
className={classNames( className={classNames(
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium", "text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
"transition-colors duration-200" "transition-colors duration-200"
)} )}
activeClassName="text-black dark:text-gray-50 !font-bold" activeClassName="text-black dark:text-gray-50 !font-bold"
isActive={(match, location) => isActiveMatcher(match, location, item)} isActive={(match, location) => isActiveMatcher(match, location, item)}
> >
{item.name} {item.name}
</NavLink> </NavLink>
)} )}
<a <a
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
href="https://autobrr.com/docs/configuration/indexers" href="https://autobrr.com/docs/configuration/indexers"
className={classNames( className={classNames(
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium", "text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
"transition-colors duration-200 flex items-center justify-center" "transition-colors duration-200 flex items-center justify-center"
)} )}
> >
Docs Docs
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" /> <ExternalLinkIcon className="inline ml-1 h-5 w-5"
</a> aria-hidden="true"/>
</div> </a>
</div> </div>
</div> </div>
<div className="hidden sm:block"> </div>
<div className="ml-4 flex items-center sm:ml-6"> <div className="hidden sm:block">
<Menu as="div" className="ml-3 relative"> <div className="ml-4 flex items-center sm:ml-6">
{({ open }) => ( <Menu as="div" className="ml-3 relative">
<> {({ open }) => (
<Menu.Button <>
className={classNames( <Menu.Button
open ? "bg-gray-200 dark:bg-gray-800" : "", className={classNames(
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800", open ? "bg-gray-200 dark:bg-gray-800" : "",
"max-w-xs rounded-full flex items-center text-sm px-3 py-2", "text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
"transition-colors duration-200" "max-w-xs rounded-full flex items-center text-sm px-3 py-2",
)} "transition-colors duration-200"
> )}
<span className="hidden text-sm font-medium sm:block"> >
<span className="sr-only">Open user menu for </span> <span className="hidden text-sm font-medium sm:block">
{authContext.username} <span className="sr-only">Open user menu for </span>
</span> {authContext.username}
<ChevronDownIcon </span>
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block" <ChevronDownIcon
aria-hidden="true" className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block"
/> aria-hidden="true"
</Menu.Button> />
<Transition </Menu.Button>
show={open} <Transition
as={Fragment} show={open}
enter="transition ease-out duration-100" as={Fragment}
enterFrom="transform opacity-0 scale-95" enter="transition ease-out duration-100"
enterTo="transform opacity-100 scale-100" enterFrom="transform opacity-0 scale-95"
leave="transition ease-in duration-75" enterTo="transform opacity-100 scale-100"
leaveFrom="transform opacity-100 scale-100" leave="transition ease-in duration-75"
leaveTo="transform opacity-0 scale-95" leaveFrom="transform opacity-100 scale-100"
> leaveTo="transform opacity-0 scale-95"
<Menu.Items >
static <Menu.Items
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none" static
> className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
<Menu.Item> >
{({ active }) => ( <Menu.Item>
<Link {({ active }) => (
to="/settings" <Link
className={classNames( to="/settings"
active ? 'bg-gray-100 dark:bg-gray-600' : '', className={classNames(
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200' active ? "bg-gray-100 dark:bg-gray-600" : "",
)} "block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
> )}
Settings
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
to="/logout"
className={classNames(
active ? 'bg-gray-100 dark:bg-gray-600' : '',
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
)}
>
Logout
</Link>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
<div className="-mr-2 flex sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
</div>
</div>
</div>
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
<div className="px-2 py-3 space-y-1 sm:px-3">
{nav.map((item) =>
<NavLink
key={item.path}
to={item.path}
strict
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
activeClassName="font-bold bg-gray-300 text-black"
isActive={(match, location) => isActiveMatcher(match, location, item)}
> >
{item.name} Settings
</NavLink> </Link>
)} )}
<Link </Menu.Item>
to="/logout" <Menu.Item>
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium" {({ active }) => (
> <Link
Logout to="/logout"
</Link> className={classNames(
</div> active ? "bg-gray-100 dark:bg-gray-600" : "",
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
)}
>
Logout
</Link>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</>
)}
</Menu>
</div>
</div>
<div className="-mr-2 flex sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block h-6 w-6" aria-hidden="true"/>
) : (
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
)}
</Disclosure.Button>
</div>
</div>
</div>
</div>
</Disclosure.Panel> <Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
</> <div className="px-2 py-3 space-y-1 sm:px-3">
{nav.map((item) =>
<NavLink
key={item.path}
to={item.path}
strict
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
activeClassName="font-bold bg-gray-300 text-black"
isActive={(match, location) => isActiveMatcher(match, location, item)}
>
{item.name}
</NavLink>
)} )}
</Disclosure> <Link
to="/logout"
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
>
Logout
</Link>
</div>
<Switch> </Disclosure.Panel>
<Route path="/logs"> </>
<Logs /> )}
</Route> </Disclosure>
<Route path="/settings"> <Switch>
<Settings /> <Route path="/logs">
</Route> <Logs/>
</Route>
<Route path="/releases"> <Route path="/settings">
<Releases /> <Settings/>
</Route> </Route>
<Route exact={true} path="/filters"> <Route path="/releases">
<Filters /> <Releases/>
</Route> </Route>
<Route path="/filters/:filterId"> <Route exact={true} path="/filters">
<FilterDetails /> <Filters/>
</Route> </Route>
<Route exact path="/"> <Route path="/filters/:filterId">
<Dashboard /> <FilterDetails/>
</Route> </Route>
</Switch>
</div> <Route exact path="/">
) <Dashboard/>
</Route>
</Switch>
</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,120 +1,137 @@
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";
import IndexerSettings from "./settings/Indexer"; 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},
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
]
function SubNavLink({item, url}: any) {
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
// we need to clean the / if it's a base root path
const too = item.href ? `${url}/${item.href}` : url
return (
<NavLink
key={item.name}
to={too}
exact={true}
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
className={classNames(
'border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium'
)}
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
>
<item.icon
className="text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</NavLink>
)
} }
function SidebarNav({subNavigation, url}: any) { const subNavigation: NavTabType[] = [
return ( { name: "Application", href: "", icon: CogIcon, current: true },
<aside className="py-2 lg:col-span-3"> { name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
<nav className="space-y-1"> { name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
{subNavigation.map((item: any) => ( { name: "Feeds", href: "feeds", icon: RssIcon, current: false },
<SubNavLink item={item} url={url} key={item.href}/> { name: "Clients", href: "clients", icon: DownloadIcon, current: false },
))} { name: "Notifications", href: "notifications", icon: BellIcon, current: false },
</nav> { name: "Releases", href: "releases", icon: CollectionIcon, current: false }
</aside> // {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
) // {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
];
interface NavLinkProps {
item: NavTabType;
url: string;
}
function SubNavLink({ item, url }: NavLinkProps) {
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
// we need to clean the / if it's a base root path
const too = item.href ? `${url}/${item.href}` : url;
return (
<NavLink
key={item.name}
to={too}
exact={true}
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
className={classNames(
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
)}
aria-current={splitLocation[2] === item.href ? "page" : undefined}
>
<item.icon
className="text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
aria-hidden="true"
/>
<span className="truncate">{item.name}</span>
</NavLink>
);
}
interface SidebarNavProps {
subNavigation: NavTabType[];
url: string;
}
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
return (
<aside className="py-2 lg:col-span-3">
<nav className="space-y-1">
{subNavigation.map((item) => (
<SubNavLink item={item} url={url} key={item.href}/>
))}
</nav>
</aside>
);
} }
export default function Settings() { export default function Settings() {
const { url } = useRouteMatch(); const { url } = useRouteMatch();
return ( return (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-black dark:text-white">Settings</h1> <h1 className="text-3xl font-bold text-black dark:text-white">Settings</h1>
</div> </div>
</header> </header>
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg">
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x"> <div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
<SidebarNav url={url} subNavigation={subNavigation}/> <SidebarNav url={url} subNavigation={subNavigation}/>
<RouteSwitch> <RouteSwitch>
<Route exact path={url}> <Route exact path={url}>
<ApplicationSettings/> <ApplicationSettings/>
</Route> </Route>
<Route path={`${url}/indexers`}> <Route path={`${url}/indexers`}>
<IndexerSettings/> <IndexerSettings/>
</Route> </Route>
<Route path={`${url}/feeds`}> <Route path={`${url}/feeds`}>
<FeedSettings/> <FeedSettings/>
</Route> </Route>
<Route path={`${url}/irc`}> <Route path={`${url}/irc`}>
<IrcSettings/> <IrcSettings/>
</Route> </Route>
<Route path={`${url}/clients`}> <Route path={`${url}/clients`}>
<DownloadClientSettings/> <DownloadClientSettings/>
</Route> </Route>
<Route path={`${url}/notifications`}> <Route path={`${url}/notifications`}>
<NotificationSettings /> <NotificationSettings />
</Route> </Route>
<Route path={`${url}/releases`}> <Route path={`${url}/releases`}>
<ReleaseSettings/> <ReleaseSettings/>
</Route> </Route>
<Route path={`${url}/regex-playground`}> <Route path={`${url}/regex-playground`}>
<RegexPlayground /> <RegexPlayground />
</Route> </Route>
</RouteSwitch> </RouteSwitch>
</div> </div>
</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

@ -27,7 +27,7 @@ export const Onboarding = () => {
if (values.password1 !== values.password2) if (values.password1 !== values.password2)
obj.password2 = "Passwords don't match!"; obj.password2 = "Passwords don't match!";
return obj; return obj;
}; };
const history = useHistory(); const history = useHistory();
@ -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

@ -8,41 +8,41 @@ interface StatsItemProps {
const StatsItem = ({ name, value }: StatsItemProps) => ( const StatsItem = ({ name, value }: StatsItemProps) => (
<div <div
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800" className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
title="All time" title="All time"
> >
<dt> <dt>
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p> <p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
</dt> </dt>
<dd className="flex items-baseline"> <dd className="flex items-baseline">
<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(
"dash_release_stats", "dash_release_stats",
() => APIClient.release.stats(), () => APIClient.release.stats(),
{ refetchOnWindowFocus: false } { refetchOnWindowFocus: false }
); );
if (isLoading) if (isLoading)
return null; return null;
return ( return (
<div> <div>
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200"> <h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
Stats Stats
</h3> </h3>
<dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3"> <dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3">
<StatsItem name="Filtered Releases" value={data?.filtered_count} /> <StatsItem name="Filtered Releases" value={data?.filtered_count} />
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */} {/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} /> <StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
<StatsItem name="Approved Pushes" value={data?.push_approved_count} /> <StatsItem name="Approved Pushes" value={data?.push_approved_count} />
</dl> </dl>
</div> </div>
) );
} };

View file

@ -2,10 +2,10 @@ import { Stats } from "./Stats";
import { ActivityTable } from "./ActivityTable"; import { ActivityTable } from "./ActivityTable";
export const Dashboard = () => ( export const Dashboard = () => (
<main className="py-10"> <main className="py-10">
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<Stats /> <Stats />
<ActivityTable /> <ActivityTable />
</div> </div>
</main> </main>
); );

File diff suppressed because it is too large Load diff

View file

@ -4,10 +4,10 @@ import { toast } from "react-hot-toast";
import { Menu, Switch, Transition } from "@headlessui/react"; import { Menu, Switch, Transition } from "@headlessui/react";
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "react-query";
import { 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,50 +20,50 @@ 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"],
APIClient.filters.getAll, APIClient.filters.getAll,
{ refetchOnWindowFocus: false } { refetchOnWindowFocus: false }
); );
if (isLoading) if (isLoading)
return null; return null;
if (error) if (error)
return (<p>An error has occurred: </p>); return (<p>An error has occurred: </p>);
return ( return (
<main> <main>
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} /> <FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
<header className="py-10"> <header className="py-10">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-black dark:text-white"> <h1 className="text-3xl font-bold text-black dark:text-white">
Filters Filters
</h1> </h1>
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
onClick={toggleCreateFilter} onClick={toggleCreateFilter}
> >
Add new Add new
</button> </button>
</div> </div>
</div> </div>
</header> </header>
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative"> <div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
{data && data.length > 0 ? ( {data && data.length > 0 ? (
<FilterList filters={data} /> <FilterList filters={data} />
) : ( ) : (
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} /> <EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
)} )}
</div> </div>
</main> </main>
) );
} }
interface FilterListProps { interface FilterListProps {
@ -71,33 +71,33 @@ interface FilterListProps {
} }
function FilterList({ filters }: FilterListProps) { function FilterList({ filters }: FilterListProps) {
return ( return (
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg"> <div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700"> <thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
<tr> <tr>
{["Enabled", "Name", "Indexers"].map((label) => ( {["Enabled", "Name", "Indexers"].map((label) => (
<th <th
key={`th-${label}`} key={`th-${label}`}
scope="col" scope="col"
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider" className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
> >
{label} {label}
</th> </th>
))} ))}
<th scope="col" className="relative px-6 py-3"> <th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span> <span className="sr-only">Edit</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-800"> <tbody className="divide-y divide-gray-200 dark:divide-gray-800">
{filters.map((filter: Filter, idx) => ( {filters.map((filter: Filter, idx) => (
<FilterListItem filter={filter} key={filter.id} idx={idx} /> <FilterListItem filter={filter} key={filter.id} idx={idx} />
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
) );
} }
interface FilterItemDropdownProps { interface FilterItemDropdownProps {
@ -106,157 +106,157 @@ interface FilterItemDropdownProps {
} }
const FilterItemDropdown = ({ const FilterItemDropdown = ({
filter, filter,
onToggle onToggle
}: FilterItemDropdownProps) => { }: FilterItemDropdownProps) => {
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation( const deleteMutation = useMutation(
(id: number) => APIClient.filters.delete(id), (id: number) => APIClient.filters.delete(id),
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries(["filters"]);
queryClient.invalidateQueries(["filters", filter.id]); queryClient.invalidateQueries(["filters", filter.id]);
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />); toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
} }
} }
); );
const duplicateMutation = useMutation( const duplicateMutation = useMutation(
(id: number) => APIClient.filters.duplicate(id), (id: number) => APIClient.filters.duplicate(id),
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries(["filters"]);
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />); toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
} }
} }
); );
return ( return (
<Menu as="div"> <Menu as="div">
<DeleteModal <DeleteModal
isOpen={deleteModalIsOpen} isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal} toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef} buttonRef={cancelModalButtonRef}
deleteAction={() => { deleteAction={() => {
deleteMutation.mutate(filter.id); deleteMutation.mutate(filter.id);
toggleDeleteModal(); toggleDeleteModal();
}} }}
title={`Remove filter: ${filter.name}`} title={`Remove filter: ${filter.name}`}
text="Are you sure you want to remove this filter? This action cannot be undone." text="Are you sure you want to remove this filter? This action cannot be undone."
/> />
<Menu.Button className="px-4 py-2"> <Menu.Button className="px-4 py-2">
<DotsHorizontalIcon <DotsHorizontalIcon
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400" className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
aria-hidden="true" aria-hidden="true"
/> />
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95" enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100" enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75" leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items <Menu.Items
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none" className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
>
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<Link
to={`filters/${filter.id.toString()}`}
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
> >
<div className="px-1 py-1"> <PencilAltIcon
<Menu.Item> className={classNames(
{({ active }) => ( active ? "text-white" : "text-blue-500",
<Link "w-5 h-5 mr-2"
to={`filters/${filter.id.toString()}`} )}
className={classNames( aria-hidden="true"
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", />
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
>
<PencilAltIcon
className={classNames(
active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2"
)}
aria-hidden="true"
/>
Edit Edit
</Link> </Link>
)} )}
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button
className={classNames( className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={() => onToggle(!filter.enabled)} onClick={() => onToggle(!filter.enabled)}
> >
<SwitchHorizontalIcon <SwitchHorizontalIcon
className={classNames( className={classNames(
active ? "text-white" : "text-blue-500", active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2" "w-5 h-5 mr-2"
)} )}
aria-hidden="true" aria-hidden="true"
/> />
Toggle Toggle
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button
className={classNames( className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={() => duplicateMutation.mutate(filter.id)} onClick={() => duplicateMutation.mutate(filter.id)}
> >
<DuplicateIcon <DuplicateIcon
className={classNames( className={classNames(
active ? "text-white" : "text-blue-500", active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2" "w-5 h-5 mr-2"
)} )}
aria-hidden="true" aria-hidden="true"
/> />
Duplicate Duplicate
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
</div> </div>
<div className="px-1 py-1"> <div className="px-1 py-1">
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button
className={classNames( className={classNames(
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={() => toggleDeleteModal()} onClick={() => toggleDeleteModal()}
> >
<TrashIcon <TrashIcon
className={classNames( className={classNames(
active ? "text-white" : "text-red-500", active ? "text-white" : "text-red-500",
"w-5 h-5 mr-2" "w-5 h-5 mr-2"
)} )}
aria-hidden="true" aria-hidden="true"
/> />
Delete Delete
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
</div> </div>
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>
); );
} };
interface FilterListItemProps { interface FilterListItemProps {
filter: Filter; filter: Filter;
@ -264,83 +264,83 @@ 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,
// while the ["filter", filter.id] key is used on the details page. // while the ["filter", filter.id] key is used on the details page.
queryClient.invalidateQueries(["filters"]); queryClient.invalidateQueries(["filters"]);
queryClient.invalidateQueries(["filters", filter?.id]); queryClient.invalidateQueries(["filters", filter?.id]);
} }
}
);
const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status);
} }
);
return ( const toggleActive = (status: boolean) => {
<tr setEnabled(status);
key={filter.id} updateMutation.mutate(status);
className={classNames( };
idx % 2 === 0 ?
"bg-white dark:bg-[#2e2e31]" : return (
"bg-gray-50 dark:bg-gray-800", <tr
"hover:bg-gray-100 dark:hover:bg-[#222225]" key={filter.id}
)} className={classNames(
idx % 2 === 0 ?
"bg-white dark:bg-[#2e2e31]" :
"bg-gray-50 dark:bg-gray-800",
"hover:bg-gray-100 dark:hover:bg-[#222225]"
)}
>
<td
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
>
<Switch
checked={enabled}
onChange={toggleActive}
className={classNames(
enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700",
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
)}
> >
<td <span className="sr-only">Use setting</span>
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100" <span
> aria-hidden="true"
<Switch className={classNames(
checked={enabled} enabled ? "translate-x-5" : "translate-x-0",
onChange={toggleActive} "inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
className={classNames( )}
enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700', />
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500' </Switch>
)} </td>
> <td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
<span className="sr-only">Use setting</span> <Link
<span to={`filters/${filter.id.toString()}`}
aria-hidden="true" className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
className={classNames( >
enabled ? 'translate-x-5' : 'translate-x-0', {filter.name}
'inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200' </Link>
)} </td>
/> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
</Switch> {filter.indexers && filter.indexers.map((t) => (
</td> <span
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100"> key={t.id}
<Link className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
to={`filters/${filter.id.toString()}`} >
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex" {t.name}
> </span>
{filter.name} ))}
</Link> </td>
</td> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> <FilterItemDropdown
{filter.indexers && filter.indexers.map((t) => ( filter={filter}
<span onToggle={toggleActive}
key={t.id} />
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400" </td>
> </tr>
{t.name} );
</span>
))}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<FilterItemDropdown
filter={filter}
onToggle={toggleActive}
/>
</td>
</tr>
)
} }

View file

@ -2,90 +2,91 @@ import * as React from "react";
import { useQuery } from "react-query"; 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 = ({
id, id,
label, label,
currentValue, currentValue,
onChange, onChange,
children children
}: ListboxFilterProps) => ( }: ListboxFilterProps) => (
<div className="w-48"> <div className="w-48">
<Listbox <Listbox
refName={id} refName={id}
value={currentValue} value={currentValue}
onChange={onChange} onChange={onChange}
>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm">
<span className="block truncate">{label}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<ChevronDownIcon
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
> >
<div className="relative mt-1"> <Listbox.Options
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm"> className="absolute w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
<span className="block truncate">{label}</span> >
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <FilterOption label="All" />
<ChevronDownIcon {children}
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600" </Listbox.Options>
aria-hidden="true" </Transition>
/> </div>
</span> </Listbox>
</Listbox.Button> </div>
<Transition
as={React.Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
>
<FilterOption label="All" />
{children}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
); );
// 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
} }
); );
// Render a multi-select box // Render a multi-select box
return ( return (
<ListboxFilter <ListboxFilter
id={id} id={id}
label={filterValue ?? "Indexer"} label={filterValue ?? "Indexer"}
currentValue={filterValue} currentValue={filterValue}
onChange={setFilter} onChange={setFilter}
> >
{isSuccess && data?.map((indexer, idx) => ( {isSuccess && data?.map((indexer, idx) => (
<FilterOption key={idx} label={indexer} value={indexer} /> <FilterOption key={idx} label={indexer} value={indexer} />
))} ))}
</ListboxFilter> </ListboxFilter>
) );
} };
interface FilterOptionProps { interface FilterOptionProps {
label: string; label: string;
@ -93,50 +94,48 @@ interface FilterOptionProps {
} }
const FilterOption = ({ label, value }: FilterOptionProps) => ( 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}
> >
{({ selected }) => ( {({ selected }) => (
<> <>
<span <span
className={classNames( className={classNames(
"block truncate", "block truncate",
selected ? "font-medium text-black dark:text-white" : "font-normal" selected ? "font-medium text-black dark:text-white" : "font-normal"
)} )}
> >
{label} {label}
</span> </span>
{selected ? ( {selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400"> <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
<CheckIcon className="w-5 h-5" aria-hidden="true" /> <CheckIcon className="w-5 h-5" aria-hidden="true" />
</span> </span>
) : null} ) : null}
</> </>
)} )}
</Listbox.Option> </Listbox.Option>
); );
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 currentValue={filterValue}
? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label onChange={setFilter}
: "Push status" >
} {PushStatusOptions.map((status, idx) => (
currentValue={filterValue} <FilterOption key={idx} value={status.value} label={status.label} />
onChange={setFilter} ))}
> </ListboxFilter>
{PushStatusOptions.map((status, idx) => (
<FilterOption key={idx} value={status.value} label={status.label} />
))}
</ListboxFilter>
</div> </div>
); );};

View file

@ -1,17 +1,17 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { import {
useTable, useTable,
useSortBy, useSortBy,
usePagination, usePagination,
useFilters, useFilters,
Column Column
} from "react-table"; } from "react-table";
import { import {
ChevronDoubleLeftIcon, ChevronDoubleLeftIcon,
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
ChevronDoubleRightIcon ChevronDoubleRightIcon
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
@ -21,295 +21,310 @@ import * as Icons from "../../components/Icons";
import * as DataTable from "../../components/data-table"; import * as DataTable from "../../components/data-table";
import { import {
IndexerSelectColumnFilter, IndexerSelectColumnFilter,
PushStatusSelectColumnFilter PushStatusSelectColumnFilter
} from "./Filters"; } from "./Filters";
const initialState = { type TableState = {
queryPageIndex: 0, queryPageIndex: number;
queryPageSize: 10, queryPageSize: number;
totalCount: null, totalCount: number;
queryFilters: [] queryFilters: ReleaseFilter[];
}; };
const PAGE_CHANGED = 'PAGE_CHANGED'; const initialState: TableState = {
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED'; queryPageIndex: 0,
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED'; queryPageSize: 10,
const FILTER_CHANGED = 'FILTER_CHANGED'; totalCount: 0,
queryFilters: []
};
const TableReducer = (state: any, { type, payload }: any) => { enum ActionType {
switch (type) { PAGE_CHANGED = "PAGE_CHANGED",
case PAGE_CHANGED: PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
return { ...state, queryPageIndex: payload }; TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
case PAGE_SIZE_CHANGED: FILTER_CHANGED = "FILTER_CHANGED"
return { ...state, queryPageSize: payload }; }
case TOTAL_COUNT_CHANGED:
return { ...state, totalCount: payload }; type Actions =
case FILTER_CHANGED: | { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
return { ...state, queryFilters: payload }; | { type: ActionType.PAGE_CHANGED; payload: number; }
default: | { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
throw new Error(`Unhandled action type: ${type}`); | { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
}
const TableReducer = (state: TableState, action: Actions): TableState => {
switch (action.type) {
case ActionType.PAGE_CHANGED:
return { ...state, queryPageIndex: action.payload };
case ActionType.PAGE_SIZE_CHANGED:
return { ...state, queryPageSize: action.payload };
case ActionType.FILTER_CHANGED:
return { ...state, queryFilters: action.payload };
case ActionType.TOTAL_COUNT_CHANGED:
return { ...state, totalCount: action.payload };
default:
throw new Error(`Unhandled action type: ${action}`);
}
}; };
export const ReleaseTable = () => { 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
} }
); );
// 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,
// which has only the rows for the active page // which has only the rows for the active page
// The rest of these things are super handy, too ;) // The rest of these things are super handy, too ;)
canPreviousPage, canPreviousPage,
canNextPage, canNextPage,
pageOptions, pageOptions,
pageCount, pageCount,
gotoPage, gotoPage,
nextPage, nextPage,
previousPage, previousPage,
setPageSize, setPageSize,
state: { pageIndex, pageSize, filters } state: { pageIndex, pageSize, filters }
} = useTable( } = useTable(
{ {
columns, columns,
data: data && isSuccess ? data.data : [], data: data && isSuccess ? data.data : [],
initialState: { initialState: {
pageIndex: queryPageIndex, pageIndex: queryPageIndex,
pageSize: queryPageSize, pageSize: queryPageSize,
filters: [] filters: []
}, },
manualPagination: true, manualPagination: true,
manualFilters: true, manualFilters: true,
manualSortBy: true, manualSortBy: true,
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0, pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
autoResetSortBy: false, autoResetSortBy: false,
autoResetExpanded: false, autoResetExpanded: false,
autoResetPage: false autoResetPage: false
}, },
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)
return <p>Error</p>; return <p>Error</p>;
if (isLoading) if (isLoading)
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}>
{column.render("Filter")} {column.render("Filter")}
</div> </div>
) : null ) : null
)) ))
)} )}
</div> </div>
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg"> <div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg">
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> <table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-800"> <thead className="bg-gray-50 dark:bg-gray-800">
{headerGroups.map((headerGroup) => { {headerGroups.map((headerGroup) => {
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps(); const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
return ( return (
<tr key={rowKey} {...rowRest}> <tr key={rowKey} {...rowRest}>
{headerGroup.headers.map((column) => { {headerGroup.headers.map((column) => {
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps()); const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
return ( return (
// Add the sorting props to control sorting. For this example // Add the sorting props to control sorting. For this example
// we can add them into the header props // we can add them into the header props
<th <th
key={`${rowKey}-${columnKey}`} key={`${rowKey}-${columnKey}`}
scope="col" scope="col"
className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group" className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
{...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 ? (
column.isSortedDesc ? ( column.isSortedDesc ? (
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" /> <Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
) : ( ) : (
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" /> <Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
) )
) : ( ) : (
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" /> <Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
)} )}
</span> </span>
</div> </div>
</th> </th>
); );
})} })}
</tr> </tr>
); );
})} })}
</thead> </thead>
<tbody <tbody
{...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
key={cellRowKey} key={cellRowKey}
className="first:pl-5 pl-3 pr-3 whitespace-nowrap" className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
role="cell" role="cell"
{...cellRowRest} {...cellRowRest}
> >
{cell.render('Cell')} {cell.render("Cell")}
</td> </td>
); );
})} })}
</tr> </tr>
); );
})} })}
</tbody> </tbody>
</table> </table>
{/* Pagination */} {/* Pagination */}
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between flex-1 sm:hidden"> <div className="flex justify-between flex-1 sm:hidden">
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button> <DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button> <DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
</div> </div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div className="flex items-baseline gap-x-2"> <div className="flex items-baseline gap-x-2">
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700">
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span> Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
</span> </span>
<label> <label>
<span className="sr-only bg-gray-700">Items Per Page</span> <span className="sr-only bg-gray-700">Items Per Page</span>
<select <select
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 => (
<option key={pageSize} value={pageSize}> <option key={pageSize} value={pageSize}>
Show {pageSize} Show {pageSize}
</option> </option>
))} ))}
</select> </select>
</label> </label>
</div>
<div>
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<DataTable.PageButton
className="rounded-l-md"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
<DataTable.PageButton
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
<DataTable.PageButton
onClick={() => nextPage()}
disabled={!canNextPage}>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
<DataTable.PageButton
className="rounded-r-md"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
</nav>
</div>
</div>
</div>
</div> </div>
<div>
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<DataTable.PageButton
className="rounded-l-md"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
<DataTable.PageButton
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
<DataTable.PageButton
onClick={() => nextPage()}
disabled={!canNextPage}>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
<DataTable.PageButton
className="rounded-r-md"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
</DataTable.PageButton>
</nav>
</div>
</div>
</div> </div>
); </div>
} </div>
);
};

View file

@ -1,14 +1,14 @@
import { ReleaseTable } from "./ReleaseTable"; import { ReleaseTable } from "./ReleaseTable";
export const Releases = () => ( export const Releases = () => (
<main> <main>
<header className="py-10"> <header className="py-10">
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between"> <div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
<h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1> <h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1>
</div> </div>
</header> </header>
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8"> <div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
<ReleaseTable /> <ReleaseTable />
</div> </div>
</main> </main>
); );

View file

@ -1,79 +1,79 @@
function ActionSettings() { function ActionSettings() {
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
{/*{addClientIsOpen &&*/} {/*{addClientIsOpen &&*/}
{/*<AddNewClientForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>*/} {/*<AddNewClientForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>*/}
{/*}*/} {/*}*/}
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3> <h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
Manage actions. Manage actions.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
// onClick={toggleAddClient} // onClick={toggleAddClient}
> >
Add new Add new
</button> </button>
</div> </div>
</div>
<div className="flex flex-col mt-6">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Type
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Port
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Enabled
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>empty</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div> </div>
);
<div className="flex flex-col mt-6">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Type
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Port
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Enabled
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>empty</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
);
} }
export default ActionSettings; export default ActionSettings;

View file

@ -4,128 +4,127 @@ 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,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
onError: err => console.log(err) onError: err => console.log(err)
} }
); );
return ( return (
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST"> <form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div> <div>
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2> <h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Application settings. Change in config.toml and restart to take effect. Application settings. Change in config.toml and restart to take effect.
</p> </p>
</div> </div>
{!isLoading && data && ( {!isLoading && data && (
<div className="mt-6 grid grid-cols-12 gap-6"> <div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Host Host
</label> </label>
<input <input
type="text" type="text"
name="host" name="host"
id="host" id="host"
value={data.host} value={data.host}
disabled={true} disabled={true}
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Port Port
</label> </label>
<input <input
type="text" type="text"
name="port" name="port"
id="port" id="port"
value={data.port} value={data.port}
disabled={true} disabled={true}
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div> </div>
<div className="col-span-6 sm:col-span-4"> <div className="col-span-6 sm:col-span-4">
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide"> <label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
Base url Base url
</label> </label>
<input <input
type="text" type="text"
name="base_url" name="base_url"
id="base_url" id="base_url"
value={data.base_url} value={data.base_url}
disabled={true} disabled={true}
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm" className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/> />
</div>
</div>
)}
</div> </div>
</div>
)}
</div>
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700"> <div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 py-5 sm:p-0"> <div className="px-4 py-5 sm:p-0">
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700"> <dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
{data?.version ? ( {data?.version ? (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt> <dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd> <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
</div> </div>
) : null} ) : null}
{data?.commit ? ( {data?.commit ? (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt> <dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd> <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
</div> </div>
) : null} ) : null}
{data?.date ? ( {data?.date ? (
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6"> <div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt> <dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd> <dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
</div> </div>
) : null} ) : null}
</dl> </dl>
</div> </div>
<ul className="divide-y divide-gray-200 dark:divide-gray-700"> <ul className="divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 sm:px-6 py-1"> <div className="px-4 sm:px-6 py-1">
<Checkbox <Checkbox
label="Debug" label="Debug"
description="Enable debug mode to get more logs." description="Enable debug mode to get more logs."
value={settings.debug} value={settings.debug}
setValue={(newValue: boolean) => setSettings({ setValue={(newValue: boolean) => setSettings({
...settings, ...settings,
debug: newValue debug: newValue
})} })}
/> />
</div> </div>
<div className="px-4 sm:px-6 py-1"> <div className="px-4 sm:px-6 py-1">
<Checkbox <Checkbox
label="Dark theme" label="Dark theme"
description="Switch between dark and light theme." description="Switch between dark and light theme."
value={settings.darkTheme} value={settings.darkTheme}
setValue={(newValue: boolean) => setSettings({ setValue={(newValue: boolean) => setSettings({
...settings, ...settings,
darkTheme: newValue darkTheme: newValue
})} })}
/> />
</div> </div>
</ul> </ul>
</div> </div>
</form> </form>
) );
} }
export default ApplicationSettings; export default ApplicationSettings;

View file

@ -13,132 +13,132 @@ 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">
<Switch <Switch
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>
</td> </td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{client.name}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{client.name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{client.host}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{client.host}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}> <span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
Edit Edit
</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 }
); );
if (error) if (error)
return (<p>An error has occurred: </p>); return (<p>An error has occurred: </p>);
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} /> <DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage download clients. Manage download clients.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={toggleAddClient} onClick={toggleAddClient}
> >
Add new Add new
</button> </button>
</div> </div>
</div>
<div className="flex flex-col mt-6">
{data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="light:bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Enabled
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Host
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Type
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((client, idx) => (
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
))}
</tbody>
</table>
</div>
</div>
</div>
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
}
</div>
</div>
</div> </div>
) <div className="flex flex-col mt-6">
{data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="light:bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Enabled
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Host
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Type
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((client, idx) => (
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
))}
</tbody>
</table>
</div>
</div>
</div>
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
}
</div>
</div>
</div>
);
} }
export default DownloadClientSettings; export default DownloadClientSettings;

View file

@ -3,147 +3,148 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { APIClient } from "../../api/APIClient"; import { APIClient } from "../../api/APIClient";
import { Menu, Switch, Transition } from "@headlessui/react"; import { Menu, Switch, Transition } from "@headlessui/react";
import {classNames} from "../../utils"; import { classNames } from "../../utils";
import {Fragment, useRef, useState} from "react"; import { Fragment, useRef, useState } from "react";
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 {queryClient} from "../../App"; import { queryClient } from "../../App";
import {DeleteModal} from "../../components/modals"; import { DeleteModal } from "../../components/modals";
import { import {
DotsHorizontalIcon, DotsHorizontalIcon,
PencilAltIcon, PencilAltIcon,
SwitchHorizontalIcon, SwitchHorizontalIcon,
TrashIcon TrashIcon
} 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">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage Torznab feeds. Manage Torznab feeds.
</p> </p>
</div> </div>
</div>
{data && data.length > 0 ?
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full relative">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
<div
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled
</div>
<div
className="col-span-6 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name
</div>
<div
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type
</div>
{/*<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>*/}
</li>
{data && data.map((f) => (
<ListItem key={f.id} feed={f}/>
))}
</ol>
</section>
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
</div>
</div> </div>
)
{data && data.length > 0 ?
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full relative">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
<div
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled
</div>
<div
className="col-span-6 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name
</div>
<div
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type
</div>
{/*<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>*/}
</li>
{data && data.map((f) => (
<ListItem key={f.id} feed={f}/>
))}
</ol>
</section>
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
</div>
</div>
);
} }
const ImplementationTorznab = () => ( const ImplementationTorznab = () => (
<span <span
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800" className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
> >
Torznab Torznab
</span> </span>
) );
export const ImplementationMap: any = { export const ImplementationMap: componentMapType = {
"TORZNAB": <ImplementationTorznab/>, "TORZNAB": <ImplementationTorznab/>
}; };
interface ListItemProps { interface ListItemProps {
feed: Feed; feed: Feed;
} }
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),
{ {
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]);
} }
}
);
const toggleActive = (status: boolean) => {
setEnabled(status);
updateMutation.mutate(status);
} }
);
return ( const toggleActive = (status: boolean) => {
<li key={feed.id} className="text-gray-500 dark:text-gray-400"> setEnabled(status);
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/> updateMutation.mutate(status);
};
<div className="grid grid-cols-12 gap-4 items-center py-4"> return (
<div className="col-span-2 flex items-center sm:px-6 "> <li key={feed.id} className="text-gray-500 dark:text-gray-400">
<Switch <FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
checked={feed.enabled}
onChange={toggleActive} <div className="grid grid-cols-12 gap-4 items-center py-4">
className={classNames( <div className="col-span-2 flex items-center sm:px-6 ">
feed.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600', <Switch
'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={feed.enabled}
)} onChange={toggleActive}
> className={classNames(
<span className="sr-only">Use setting</span> feed.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
<span "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"
aria-hidden="true" )}
className={classNames( >
feed.enabled ? 'translate-x-5' : 'translate-x-0', <span className="sr-only">Use setting</span>
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200' <span
)} aria-hidden="true"
/> className={classNames(
</Switch> feed.enabled ? "translate-x-5" : "translate-x-0",
</div> "inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white"> )}
{feed.name} />
</div> </Switch>
<div className="col-span-2 flex items-center sm:px-6"> </div>
{ImplementationMap[feed.type]} <div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
</div> {feed.name}
<div className="col-span-1 flex items-center sm:px-6"> </div>
<FeedItemDropdown <div className="col-span-2 flex items-center sm:px-6">
feed={feed} {ImplementationMap[feed.type]}
onToggle={toggleActive} </div>
toggleUpdate={toggleUpdateForm} <div className="col-span-1 flex items-center sm:px-6">
/> <FeedItemDropdown
</div> feed={feed}
</div> onToggle={toggleActive}
</li> toggleUpdate={toggleUpdateForm}
) />
</div>
</div>
</li>
);
} }
interface FeedItemDropdownProps { interface FeedItemDropdownProps {
@ -153,126 +154,126 @@ interface FeedItemDropdownProps {
} }
const FeedItemDropdown = ({ const FeedItemDropdown = ({
feed, feed,
onToggle, onToggle,
toggleUpdate, toggleUpdate
}: FeedItemDropdownProps) => { }: FeedItemDropdownProps) => {
const cancelModalButtonRef = useRef(null); const cancelModalButtonRef = useRef(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
const deleteMutation = useMutation( const deleteMutation = useMutation(
(id: number) => APIClient.feeds.delete(id), (id: number) => APIClient.feeds.delete(id),
{ {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["feeds"]); queryClient.invalidateQueries(["feeds"]);
queryClient.invalidateQueries(["feeds", feed.id]); queryClient.invalidateQueries(["feeds", feed.id]);
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>); toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
} }
} }
); );
return ( return (
<Menu as="div"> <Menu as="div">
<DeleteModal <DeleteModal
isOpen={deleteModalIsOpen} isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal} toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef} buttonRef={cancelModalButtonRef}
deleteAction={() => { deleteAction={() => {
deleteMutation.mutate(feed.id); deleteMutation.mutate(feed.id);
toggleDeleteModal(); toggleDeleteModal();
}} }}
title={`Remove feed: ${feed.name}`} title={`Remove feed: ${feed.name}`}
text="Are you sure you want to remove this feed? This action cannot be undone." text="Are you sure you want to remove this feed? This action cannot be undone."
/> />
<Menu.Button className="px-4 py-2"> <Menu.Button className="px-4 py-2">
<DotsHorizontalIcon <DotsHorizontalIcon
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400" className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
aria-hidden="true" aria-hidden="true"
/> />
</Menu.Button> </Menu.Button>
<Transition <Transition
as={Fragment} as={Fragment}
enter="transition ease-out duration-100" enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95" enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100" enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75" leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items <Menu.Items
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none" className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
>
<div className="px-1 py-1">
<Menu.Item>
{({ active }) => (
<button
className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)}
onClick={() => toggleUpdate()}
> >
<div className="px-1 py-1"> <PencilAltIcon
<Menu.Item> className={classNames(
{({active}) => ( active ? "text-white" : "text-blue-500",
<button "w-5 h-5 mr-2"
className={classNames( )}
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", aria-hidden="true"
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" />
)}
onClick={() => toggleUpdate()}
>
<PencilAltIcon
className={classNames(
active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2"
)}
aria-hidden="true"
/>
Edit Edit
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
{({active}) => ( {({ active }) => (
<button <button
className={classNames( className={classNames(
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={() => onToggle(!feed.enabled)} onClick={() => onToggle(!feed.enabled)}
> >
<SwitchHorizontalIcon <SwitchHorizontalIcon
className={classNames( className={classNames(
active ? "text-white" : "text-blue-500", active ? "text-white" : "text-blue-500",
"w-5 h-5 mr-2" "w-5 h-5 mr-2"
)} )}
aria-hidden="true" aria-hidden="true"
/> />
Toggle Toggle
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
</div> </div>
<div className="px-1 py-1"> <div className="px-1 py-1">
<Menu.Item> <Menu.Item>
{({active}) => ( {({ active }) => (
<button <button
className={classNames( className={classNames(
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300", active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm" "font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
)} )}
onClick={() => toggleDeleteModal()} onClick={() => toggleDeleteModal()}
> >
<TrashIcon <TrashIcon
className={classNames( className={classNames(
active ? "text-white" : "text-red-500", active ? "text-white" : "text-red-500",
"w-5 h-5 mr-2" "w-5 h-5 mr-2"
)} )}
aria-hidden="true" aria-hidden="true"
/> />
Delete Delete
</button> </button>
)} )}
</Menu.Item> </Menu.Item>
</div> </div>
</Menu.Items> </Menu.Items>
</Transition> </Transition>
</Menu> </Menu>
); );
} };
export default FeedSettings; export default FeedSettings;

View file

@ -5,148 +5,153 @@ 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
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-green-200 dark:bg-green-400 text-green-800 dark:text-green-800" className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-green-200 dark:bg-green-400 text-green-800 dark:text-green-800"
> >
IRC IRC
</span> </span>
) );
const ImplementationTorznab = () => ( const ImplementationTorznab = () => (
<span <span
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800" className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
> >
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;
return (
<tr key={indexer.name}>
<IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
<td className="px-6 py-4 whitespace-nowrap">
<Switch
checked={indexer.enabled}
onChange={toggleUpdate}
className={classNames(
indexer.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
)}
>
<span className="sr-only">Enable</span>
<span
aria-hidden="true"
className={classNames(
indexer.enabled ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
</td>
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{indexer.name}</td>
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{implementationMap[indexer.implementation]}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 dark:hover:text-blue-500 cursor-pointer" onClick={toggleUpdate}>
Edit
</span>
</td>
</tr>
)
} }
const ListItem = ({ indexer }: ListItemProps) => {
const [updateIsOpen, toggleUpdate] = useToggle(false);
return (
<tr key={indexer.name}>
<IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
<td className="px-6 py-4 whitespace-nowrap">
<Switch
checked={indexer.enabled ?? false}
onChange={toggleUpdate}
className={classNames(
indexer.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
)}
>
<span className="sr-only">Enable</span>
<span
aria-hidden="true"
className={classNames(
indexer.enabled ? "translate-x-5" : "translate-x-0",
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
)}
/>
</Switch>
</td>
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{indexer.name}</td>
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{implementationMap[indexer.implementation]}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 dark:hover:text-blue-500 cursor-pointer" onClick={toggleUpdate}>
Edit
</span>
</td>
</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 }
); );
if (error) if (error)
return (<p>An error has occurred</p>); return (<p>An error has occurred</p>);
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} /> <IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Indexer settings. Indexer settings.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
onClick={toggleAddIndexer} onClick={toggleAddIndexer}
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
> >
Add new Add new
</button> </button>
</div> </div>
</div>
<div className="flex flex-col mt-6">
{data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 dark:border-gray-700 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="light:bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Enabled
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Implementation
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((indexer: IndexerDefinition, idx: number) => (
<ListItem indexer={indexer} key={idx} />
))}
</tbody>
</table>
</div>
</div>
</div>
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer} />
}
</div>
</div>
</div> </div>
)
<div className="flex flex-col mt-6">
{data && data.length > 0 ?
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 dark:border-gray-700 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="light:bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Enabled
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Implementation
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
{data && data.map((indexer: IndexerDefinition, idx: number) => (
<ListItem indexer={indexer} key={idx} />
))}
</tbody>
</table>
</div>
</div>
</div>
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer} />
}
</div>
</div>
</div>
);
} }
export default IndexerSettings; export default IndexerSettings;

View file

@ -1,73 +1,73 @@
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { import {
simplifyDate, simplifyDate,
IsEmptyDate IsEmptyDate
} from "../../utils"; } from "../../utils";
import { import {
IrcNetworkAddForm, IrcNetworkAddForm,
IrcNetworkUpdateForm IrcNetworkUpdateForm
} from "../../forms"; } from "../../forms";
import { useToggle } from "../../hooks/hooks"; import { useToggle } from "../../hooks/hooks";
import { APIClient } from "../../api/APIClient"; 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",
APIClient.irc.getNetworks, APIClient.irc.getNetworks,
{ {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
// Refetch every 3 seconds // Refetch every 3 seconds
refetchInterval: 3000 refetchInterval: 3000
} }
); );
return ( return (
<div className="divide-y divide-gray-200 lg:col-span-9"> <div className="divide-y divide-gray-200 lg:col-span-9">
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} /> <IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
IRC networks and channels. Click on a network to view channel status. IRC networks and channels. Click on a network to view channel status.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
onClick={toggleAddNetwork} onClick={toggleAddNetwork}
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
Add new Add new
</button> </button>
</div> </div>
</div>
{data && data.length > 0 ? (
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */}
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div>
</li>
{data && data.map((network, idx) => (
<ListItem key={idx} idx={idx} network={network} />
))}
</ol>
</section>
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
</div>
</div> </div>
)
} {data && data.length > 0 ? (
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */}
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div>
</li>
{data && data.map((network, idx) => (
<ListItem key={idx} idx={idx} network={network} />
))}
</ol>
</section>
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
</div>
</div>
);
};
interface ListItemProps { interface ListItemProps {
idx: number; idx: number;
@ -75,83 +75,83 @@ 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 (
<li key={idx} > <li key={idx} >
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4"> <div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} /> <IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}> <div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}>
<span className="relative inline-flex items-center"> <span className="relative inline-flex items-center">
{ {
network.enabled ? ( network.enabled ? (
network.connected ? ( network.connected ? (
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}> <span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}>
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/> <span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/> <span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
</span> </span>
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" /> ) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" /> ) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
} }
{network.name} {network.name}
</span> </span>
</div> </div>
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div> <div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
{network.nickserv && network.nickserv.account ? ( {network.nickserv && network.nickserv.account ? (
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div> <div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div>
) : null} ) : null}
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400"> <div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}> <span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
Edit Edit
</span> </span>
</div> </div>
</div> </div>
{edit && ( {edit && (
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700"> <div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
<div className="min-w-full"> <div className="min-w-full">
{network.channels.length > 0 ? ( {network.channels.length > 0 ? (
<ol> <ol>
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700"> <li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div> <div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div> <div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div> <div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div>
</li> </li>
{network.channels.map(c => ( {network.channels.map(c => (
<li key={c.id} className="text-gray-500 dark:text-gray-400"> <li key={c.id} className="text-gray-500 dark:text-gray-400">
<div className="grid grid-cols-12 gap-4 items-center py-4"> <div className="grid grid-cols-12 gap-4 items-center py-4">
<div className="col-span-4 flex items-center sm:px-6 "> <div className="col-span-4 flex items-center sm:px-6 ">
<span className="relative inline-flex items-center"> <span className="relative inline-flex items-center">
{ {
network.enabled ? ( network.enabled ? (
c.monitoring ? ( c.monitoring ? (
<span className="mr-3 flex h-3 w-3 relative" title="monitoring"> <span className="mr-3 flex h-3 w-3 relative" title="monitoring">
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/> <span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/> <span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
</span> </span>
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" /> ) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" /> ) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
} }
{c.name} {c.name}
</span> </span>
</div> </div>
<div className="col-span-4 flex items-center sm:px-6 "> <div className="col-span-4 flex items-center sm:px-6 ">
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span> <span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
</div> </div>
<div className="col-span-4 flex items-center sm:px-6 "> <div className="col-span-4 flex items-center sm:px-6 ">
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span> <span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
</div> </div>
</div>
</li>
))}
</ol>
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
</div> </div>
</div> </li>
)} ))}
</li> </ol>
) ) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
} </div>
</div>
)}
</li>
);
};

View file

@ -7,56 +7,56 @@ 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">
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} /> <NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap"> <div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4"> <div className="ml-4 mt-4">
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3> <h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Send notifications on events. Send notifications on events.
</p> </p>
</div> </div>
<div className="ml-4 mt-4 flex-shrink-0"> <div className="ml-4 mt-4 flex-shrink-0">
<button <button
type="button" type="button"
onClick={toggleAddNotifications} onClick={toggleAddNotifications}
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
> >
Add new Add new
</button> </button>
</div> </div>
</div>
{data && data.length > 0 ?
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
</li>
{data && data.map((n: Notification) => (
<ListItem key={n.id} notification={n} />
))}
</ol>
</section>
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
</div>
</div> </div>
)
{data && data.length > 0 ?
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
<ol className="min-w-full">
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
</li>
{data && data.map((n: Notification) => (
<ListItem key={n.id} notification={n} />
))}
</ol>
</section>
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
</div>
</div>
);
} }
interface ListItemProps { interface ListItemProps {
@ -64,56 +64,56 @@ 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">
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} /> <NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
<div className="grid grid-cols-12 gap-4 items-center py-4"> <div className="grid grid-cols-12 gap-4 items-center py-4">
<div className="col-span-1 flex items-center sm:px-6 "> <div className="col-span-1 flex items-center sm:px-6 ">
<Switch <Switch
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>
</div> </div>
<div className="col-span-2 flex items-center sm:px-6 "> <div className="col-span-2 flex items-center sm:px-6 ">
{notification.name} {notification.name}
</div> </div>
<div className="col-span-2 flex items-center sm:px-6 "> <div className="col-span-2 flex items-center sm:px-6 ">
{notification.type} {notification.type}
</div> </div>
<div className="col-span-5 flex items-center sm:px-6 "> <div className="col-span-5 flex items-center sm:px-6 ">
{notification.events.map((n, idx) => ( {notification.events.map((n, idx) => (
<span <span
key={idx} key={idx}
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400" className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
> >
{n} {n}
</span> </span>
))} ))}
</div> </div>
<div className="col-span-1 flex items-center sm:px-6 "> <div className="col-span-1 flex items-center sm:px-6 ">
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}> <span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
Edit Edit
</span> </span>
</div> </div>
</div> </div>
</li> </li>
) );
} }
export default NotificationSettings; export default NotificationSettings;

View file

@ -1,106 +1,105 @@
import { useRef, useState } from "react"; import { useRef, useState } from "react";
export const RegexPlayground = () => { export const RegexPlayground = () => {
const regexRef = useRef<HTMLInputElement>(null); const regexRef = useRef<HTMLInputElement>(null);
const [output, setOutput] = useState<Array<React.ReactElement>>(); const [output, setOutput] = useState<Array<React.ReactElement>>();
const onInput = (text: string) => { const onInput = (text: string) => {
if (!regexRef || !regexRef.current) if (!regexRef || !regexRef.current)
return; return;
const regexp = new RegExp(regexRef.current.value, "g"); const regexp = new RegExp(regexRef.current.value, "g");
const results: Array<React.ReactElement> = []; const results: Array<React.ReactElement> = [];
text.split("\n").forEach((line, index) => { text.split("\n").forEach((line, index) => {
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;
if (!match.length) if (!match.length)
continue; continue;
const start = match.index; const start = match.index;
let length = 0; let length = 0;
match.forEach((group) => length += group.length); match.forEach((group) => length += group.length);
results.push( results.push(
<span key={`match-${start}`}> <span key={`match-${start}`}>
{line.substring(lastIndex, start)} {line.substring(lastIndex, start)}
<span className="bg-blue-300 text-black font-bold"> <span className="bg-blue-300 text-black font-bold">
{line.substring(start, start + length)} {line.substring(start, start + length)}
</span> </span>
</span> </span>
); );
lastIndex = start + length; lastIndex = start + length;
} }
if (lastIndex < line.length) { if (lastIndex < line.length) {
results.push( results.push(
<span key={`last-${lastIndex + 1}`}> <span key={`last-${lastIndex + 1}`}>
{line.substring(lastIndex)} {line.substring(lastIndex)}
</span> </span>
); );
} }
if (lastIndex > 0) if (lastIndex > 0)
results.push(<br key={`line-delim-${index}`}/>); results.push(<br key={`line-delim-${index}`}/>);
}); });
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">
<div className="py-6 px-4 sm:p-6 lg:pb-8"> <div className="py-6 px-4 sm:p-6 lg:pb-8">
<div> <div>
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2> <h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Regex playground. Experiment with your filters here. WIP. Regex playground. Experiment with your filters here. WIP.
</p> </p>
</div>
</div>
<div className="px-6 py-4">
<label
htmlFor="input-regex"
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
>
RegExp filter
</label>
<input
ref={regexRef}
id="input-regex"
type="text"
autoComplete="true"
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/>
<label
htmlFor="input-lines"
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
>
Lines to match
</label>
<div
id="input-lines"
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
contentEditable
></div>
</div>
<div className="py-4 px-4 sm:p-6 lg:pb-8">
<div>
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
Matches
</h3>
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
{output}
</p>
</div>
</div>
</div> </div>
); </div>
} <div className="px-6 py-4">
<label
htmlFor="input-regex"
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
>
RegExp filter
</label>
<input
ref={regexRef}
id="input-regex"
type="text"
autoComplete="true"
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
/>
<label
htmlFor="input-lines"
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
>
Lines to match
</label>
<div
id="input-lines"
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
contentEditable
></div>
</div>
<div className="py-4 px-4 sm:p-6 lg:pb-8">
<div>
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
Matches
</h3>
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
{output}
</p>
</div>
</div>
</div>
);
};

View file

@ -9,73 +9,73 @@ import { useToggle } from "../../hooks/hooks";
import { DeleteModal } from "../../components/modals"; import { DeleteModal } from "../../components/modals";
function ReleaseSettings() { function ReleaseSettings() {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false); const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
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 = () => {
deleteMutation.mutate()
} }
});
const cancelModalButtonRef = useRef(null); const deleteAction = () => {
deleteMutation.mutate();
};
return ( const cancelModalButtonRef = useRef(null);
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
<DeleteModal
isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title={`Delete all releases`}
text="Are you sure you want to delete all releases? This action cannot be undone."
/>
<div className="py-6 px-4 sm:p-6 lg:pb-8"> return (
<div> <form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2> <DeleteModal
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400"> isOpen={deleteModalIsOpen}
toggle={toggleDeleteModal}
buttonRef={cancelModalButtonRef}
deleteAction={deleteAction}
title={"Delete all releases"}
text="Are you sure you want to delete all releases? This action cannot be undone."
/>
<div className="py-6 px-4 sm:p-6 lg:pb-8">
<div>
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Release settings. Reset state. Release settings. Reset state.
</p> </p>
</div> </div>
</div>
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
<div className="px-4 py-5 sm:p-0">
<div className="px-4 py-5 sm:p-6">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3>
</div> </div>
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700"> <ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg">
<div className="px-4 py-5 sm:p-0"> <div className="flex justify-between items-center py-2">
<div className="px-4 py-5 sm:p-6"> <p className="text-sm text-gray-500 dark:text-gray-400">
<div>
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3>
</div>
<ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg">
<div className="flex justify-between items-center py-2">
<p className="text-sm text-gray-500 dark:text-gray-400">
Delete all releases Delete all releases
</p> </p>
<button <button
type="button" type="button"
onClick={toggleDeleteModal} onClick={toggleDeleteModal}
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm" className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
> >
Delete all releases Delete all releases
</button> </button>
</div> </div>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
) );
} }
export default ReleaseSettings; export default ReleaseSettings;

View file

@ -1,11 +1,11 @@
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

@ -1,23 +1,23 @@
interface Feed { interface Feed {
id: number; id: number;
indexer: string; indexer: string;
name: string; name: string;
type: string; type: string;
enabled: boolean; enabled: boolean;
url: string; url: string;
interval: number; interval: number;
api_key: string; api_key: string;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
} }
interface FeedCreate { interface FeedCreate {
indexer: string; indexer: string;
name: string; name: string;
type: string; type: string;
enabled: boolean; enabled: boolean;
url: string; url: string;
interval: number; interval: number;
api_key: string; api_key: string;
indexer_id: number; indexer_id: number;
} }

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

@ -1,3 +1,3 @@
interface APP { interface APP {
baseUrl: string; baseUrl: string;
} }

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

@ -1,29 +1,29 @@
interface IrcNetwork { interface IrcNetwork {
id: number; id: number;
name: string; name: string;
enabled: boolean; enabled: boolean;
server: string; server: string;
port: number; port: number;
tls: boolean; tls: boolean;
pass: string; pass: string;
invite_command: string; invite_command: string;
nickserv?: NickServ; // optional nickserv?: NickServ; // optional
channels: IrcChannel[]; channels: IrcChannel[];
connected: boolean; connected: boolean;
connected_since: Time; connected_since: string;
} }
interface IrcNetworkCreate { interface IrcNetworkCreate {
name: string; name: string;
enabled: boolean; enabled: boolean;
server: string; server: string;
port: number; port: number;
tls: boolean; tls: boolean;
pass: string; pass: string;
invite_command: string; invite_command: string;
nickserv?: NickServ; // optional nickserv?: NickServ; // optional
channels: IrcChannel[]; channels: IrcChannel[];
connected: boolean; connected: boolean;
} }
interface IrcChannel { interface IrcChannel {
@ -61,12 +61,12 @@ interface NickServ {
} }
interface Config { interface Config {
host: string; host: string;
port: number; port: number;
log_level: string; log_level: string;
log_path: string; log_path: string;
base_url: string; base_url: string;
version: string; version: string;
commit: string; commit: string;
date: string; date: string;
} }

View file

@ -1,10 +1,10 @@
type NotificationType = 'DISCORD'; type NotificationType = "DISCORD";
interface Notification { interface Notification {
id: number; id: number;
name: string; name: string;
enabled: boolean; enabled: boolean;
type: NotificationType; type: NotificationType;
events: string[]; events: string[];
webhook: string; webhook: string;
} }

View file

@ -1,38 +1,38 @@
interface Release { interface Release {
id: number; id: number;
filter_status: string; filter_status: string;
rejections: string[]; rejections: string[];
indexer: string; indexer: string;
filter: string; filter: string;
protocol: string; protocol: string;
title: string; title: string;
size: number; size: number;
raw: string; raw: string;
timestamp: Date timestamp: Date
action_status: ReleaseActionStatus[] action_status: ReleaseActionStatus[]
} }
interface ReleaseActionStatus { interface ReleaseActionStatus {
id: number; id: number;
status: string; status: string;
action: string; action: string;
type: string; type: string;
rejections: string[]; rejections: string[];
timestamp: string timestamp: string
} }
interface ReleaseFindResponse { interface ReleaseFindResponse {
data: Release[]; data: Release[];
next_cursor: number; next_cursor: number;
count: number; count: number;
} }
interface ReleaseStats { interface ReleaseStats {
total_count: number; total_count: number;
filtered_count: number; filtered_count: number;
filter_rejected_count: number; filter_rejected_count: number;
push_approved_count: number; push_approved_count: number;
push_rejected_count: number; push_rejected_count: number;
} }
interface ReleaseFilter { interface ReleaseFilter {

View file

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

View file

@ -2,37 +2,37 @@ import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
// sleep for x ms // sleep for x ms
export function sleep(ms: number) { export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
// 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
@ -40,28 +40,28 @@ 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
export function IsEmptyDate(date: string) { export function IsEmptyDate(date: string) {
if (date !== "0001-01-01T00:00:00Z") { if (date !== "0001-01-01T00:00:00Z") {
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"