mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
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:
parent
7f06a4c707
commit
cb8f280e86
70 changed files with 6797 additions and 6541 deletions
71
web/.eslintrc.js
Normal file
71
web/.eslintrc.js
Normal 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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -29,7 +29,7 @@
|
|||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "esw src/ --ext \".ts,.tsx,.js,.jsx\" --color",
|
||||
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx --color",
|
||||
"lint:watch": "npm run lint -- --watch"
|
||||
},
|
||||
"browserslist": {
|
||||
|
@ -59,8 +59,8 @@
|
|||
"@types/react-dom": "17.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-table": "^7.7.7",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.18.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
|
@ -71,59 +71,5 @@
|
|||
"postcss": "^8.4.6",
|
||||
"tailwindcss": "^3.0.18",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 11,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true,
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
},
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"jquery": false
|
||||
},
|
||||
"globals": {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import Toast from "./components/notifications/Toast";
|
|||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { useErrorBoundary: true, },
|
||||
queries: { useErrorBoundary: true },
|
||||
mutations: {
|
||||
onError: (error) => {
|
||||
// Use a format string to convert the error object to a proper string without much hassle.
|
||||
|
@ -27,8 +27,8 @@ export const queryClient = new QueryClient({
|
|||
);
|
||||
toast.custom((t) => <Toast type="error" body={message} t={t} />);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function App() {
|
||||
|
|
|
@ -1,157 +1,166 @@
|
|||
import {baseUrl, sseBaseUrl} from "../utils";
|
||||
import {AuthContext} from "../utils/Context";
|
||||
import {Cookies} from "react-cookie";
|
||||
import { baseUrl, sseBaseUrl } from "../utils";
|
||||
import { AuthContext } from "../utils/Context";
|
||||
import { Cookies } from "react-cookie";
|
||||
|
||||
interface ConfigType {
|
||||
body?: BodyInit | Record<string, unknown> | null;
|
||||
headers?: Record<string, string>;
|
||||
body?: BodyInit | Record<string, unknown> | unknown | null;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
type PostBody = BodyInit | Record<string, unknown> | unknown | null;
|
||||
|
||||
export async function HttpClient<T>(
|
||||
endpoint: string,
|
||||
method: string,
|
||||
{ body, ...customConfig }: ConfigType = {}
|
||||
endpoint: string,
|
||||
method: string,
|
||||
{ body, ...customConfig }: ConfigType = {}
|
||||
): Promise<T> {
|
||||
const config = {
|
||||
method: method,
|
||||
body: body ? JSON.stringify(body) : null,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
// NOTE: customConfig can override the above defined settings
|
||||
...customConfig
|
||||
} as RequestInit;
|
||||
const config = {
|
||||
method: method,
|
||||
body: body ? JSON.stringify(body) : null,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
// NOTE: customConfig can override the above defined settings
|
||||
...customConfig
|
||||
} as RequestInit;
|
||||
|
||||
|
||||
return window.fetch(`${baseUrl()}${endpoint}`, config)
|
||||
.then(async response => {
|
||||
if (response.status === 401) {
|
||||
// if 401 consider the session expired and force logout
|
||||
const cookies = new Cookies();
|
||||
cookies.remove("user_session");
|
||||
AuthContext.reset()
|
||||
return window.fetch(`${baseUrl()}${endpoint}`, config)
|
||||
.then(async response => {
|
||||
if (response.status === 401) {
|
||||
// if 401 consider the session expired and force logout
|
||||
const cookies = new Cookies();
|
||||
cookies.remove("user_session");
|
||||
AuthContext.reset();
|
||||
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
|
||||
if ([403, 404].includes(response.status))
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
if ([403, 404].includes(response.status))
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
|
||||
// 201 comes from a POST and can contain data
|
||||
if ([201].includes(response.status))
|
||||
return await response.json();
|
||||
// 201 comes from a POST and can contain data
|
||||
if ([201].includes(response.status))
|
||||
return await response.json();
|
||||
|
||||
// 204 ok no data
|
||||
if ([204].includes(response.status))
|
||||
return Promise.resolve(response);
|
||||
// 204 ok no data
|
||||
if ([204].includes(response.status))
|
||||
return Promise.resolve(response);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
const errorMessage = await response.text();
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
const errorMessage = await response.text();
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const appClient = {
|
||||
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
||||
Post: <T>(endpoint: string, data: any) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
||||
}
|
||||
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
||||
Post: <T>(endpoint: string, data: PostBody) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
||||
PostBody: <T>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
||||
};
|
||||
|
||||
export const APIClient = {
|
||||
auth: {
|
||||
login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }),
|
||||
logout: () => appClient.Post("api/auth/logout", null),
|
||||
validate: () => appClient.Get<void>("api/auth/validate"),
|
||||
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { username: username, password: password }),
|
||||
canOnboard: () => appClient.Get("api/auth/onboard"),
|
||||
},
|
||||
actions: {
|
||||
create: (action: Action) => appClient.Post("api/actions", action),
|
||||
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
|
||||
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
|
||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null),
|
||||
},
|
||||
config: {
|
||||
get: () => appClient.Get<Config>("api/config")
|
||||
},
|
||||
download_clients: {
|
||||
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
||||
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
|
||||
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
|
||||
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
|
||||
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc),
|
||||
},
|
||||
filters: {
|
||||
getAll: () => appClient.Get<Filter[]>("api/filters"),
|
||||
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
||||
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
||||
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
||||
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
|
||||
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
||||
delete: (id: number) => appClient.Delete(`api/filters/${id}`),
|
||||
},
|
||||
feeds: {
|
||||
find: () => appClient.Get<Feed[]>("api/feeds"),
|
||||
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
|
||||
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
|
||||
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
|
||||
delete: (id: number) => appClient.Delete(`api/feeds/${id}`),
|
||||
},
|
||||
indexers: {
|
||||
// returns indexer options for all currently present/enabled indexers
|
||||
getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
|
||||
// returns indexer definitions for all currently present/enabled indexers
|
||||
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
||||
// returns all possible indexer definitions
|
||||
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
||||
create: (indexer: Indexer) => appClient.Post<Indexer>("api/indexer", indexer),
|
||||
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`),
|
||||
},
|
||||
irc: {
|
||||
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
||||
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
||||
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
|
||||
},
|
||||
events: {
|
||||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||
},
|
||||
notifications: {
|
||||
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
||||
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
||||
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`),
|
||||
},
|
||||
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());
|
||||
auth: {
|
||||
login: (username: string, password: string) => appClient.Post("api/auth/login", {
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
logout: () => appClient.Post("api/auth/logout", null),
|
||||
validate: () => appClient.Get<void>("api/auth/validate"),
|
||||
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", {
|
||||
username: username,
|
||||
password: password
|
||||
}),
|
||||
canOnboard: () => appClient.Get("api/auth/onboard")
|
||||
},
|
||||
actions: {
|
||||
create: (action: Action) => appClient.Post("api/actions", action),
|
||||
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
|
||||
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
|
||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null)
|
||||
},
|
||||
config: {
|
||||
get: () => appClient.Get<Config>("api/config")
|
||||
},
|
||||
download_clients: {
|
||||
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
||||
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
|
||||
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
|
||||
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
|
||||
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc)
|
||||
},
|
||||
filters: {
|
||||
getAll: () => appClient.Get<Filter[]>("api/filters"),
|
||||
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
||||
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
||||
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
||||
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
|
||||
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
||||
delete: (id: number) => appClient.Delete(`api/filters/${id}`)
|
||||
},
|
||||
feeds: {
|
||||
find: () => appClient.Get<Feed[]>("api/feeds"),
|
||||
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
|
||||
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
|
||||
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
|
||||
delete: (id: number) => appClient.Delete(`api/feeds/${id}`)
|
||||
},
|
||||
indexers: {
|
||||
// returns indexer options for all currently present/enabled indexers
|
||||
getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
|
||||
// returns indexer definitions for all currently present/enabled indexers
|
||||
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
||||
// returns all possible indexer definitions
|
||||
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
||||
create: (indexer: Indexer) => appClient.PostBody<Indexer>("api/indexer", indexer),
|
||||
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`)
|
||||
},
|
||||
irc: {
|
||||
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
||||
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
||||
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`)
|
||||
},
|
||||
events: {
|
||||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||
},
|
||||
notifications: {
|
||||
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
||||
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
||||
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`)
|
||||
},
|
||||
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)
|
||||
params.append("limit", limit.toString());
|
||||
if (limit !== undefined)
|
||||
params.append("limit", limit.toString());
|
||||
|
||||
filters?.forEach((filter) => {
|
||||
if (!filter.value)
|
||||
return;
|
||||
filters?.forEach((filter) => {
|
||||
if (!filter.value)
|
||||
return;
|
||||
|
||||
if (filter.id == "indexer")
|
||||
params.append("indexer", filter.value);
|
||||
else if (filter.id === "action_status")
|
||||
params.append("push_status", filter.value);
|
||||
});
|
||||
if (filter.id == "indexer")
|
||||
params.append("indexer", filter.value);
|
||||
else if (filter.id === "action_status")
|
||||
params.append("push_status", filter.value);
|
||||
});
|
||||
|
||||
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`)
|
||||
},
|
||||
indexerOptions: () => appClient.Get<string[]>(`api/release/indexers`),
|
||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
||||
delete: () => appClient.Delete(`api/release/all`),
|
||||
}
|
||||
};
|
||||
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`);
|
||||
},
|
||||
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
||||
delete: () => appClient.Delete("api/release/all")
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,27 +8,27 @@ interface CheckboxProps {
|
|||
}
|
||||
|
||||
export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => (
|
||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{description === undefined ? null : (
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</Switch.Description>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={setValue}
|
||||
className={
|
||||
`${value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'
|
||||
} ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
|
||||
>
|
||||
<span
|
||||
className={`${value ? 'translate-x-5' : 'translate-x-0'} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
||||
/>
|
||||
</Switch>
|
||||
</Switch.Group>
|
||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{description === undefined ? null : (
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</Switch.Description>
|
||||
)}
|
||||
</div>
|
||||
<Switch
|
||||
checked={value}
|
||||
onChange={setValue}
|
||||
className={
|
||||
`${value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700"
|
||||
} ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
|
||||
>
|
||||
<span
|
||||
className={`${value ? "translate-x-5" : "translate-x-0"} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
||||
/>
|
||||
</Switch>
|
||||
</Switch.Group>
|
||||
);
|
|
@ -5,13 +5,13 @@ interface 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) => (
|
||||
<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) => (
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -51,7 +51,8 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
|||
role="alert"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
|
@ -77,11 +78,11 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
|||
resetErrorBoundary();
|
||||
}}
|
||||
>
|
||||
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5" />
|
||||
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
||||
Reset page state
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,34 +1,37 @@
|
|||
import { classNames } from "../../utils"
|
||||
import React from "react";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
interface ButtonProps {
|
||||
className?: string;
|
||||
children: any;
|
||||
[rest: string]: any;
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Button = ({ children, className, ...rest }: ButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
className ?? "",
|
||||
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50"
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
export const Button = ({ children, className, disabled, onClick }: ButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
className ?? "",
|
||||
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50"
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
export const PageButton = ({ children, className, ...rest }: ButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
className ?? "",
|
||||
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
export const PageButton = ({ children, className, disabled, onClick }: ButtonProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
className ?? "",
|
||||
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
|
@ -10,24 +10,22 @@ interface CellProps {
|
|||
}
|
||||
|
||||
export const AgeCell = ({ value }: CellProps) => (
|
||||
<div className="text-sm text-gray-500" title={value}>
|
||||
{formatDistanceToNowStrict(new Date(value), { addSuffix: true })}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500" title={value}>
|
||||
{formatDistanceToNowStrict(new Date(value), { addSuffix: true })}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const TitleCell = ({ value }: CellProps) => (
|
||||
<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"
|
||||
title={value}
|
||||
>
|
||||
{value}
|
||||
</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"
|
||||
title={value}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ReleaseStatusCellProps {
|
||||
value: ReleaseActionStatus[];
|
||||
column: any;
|
||||
row: any;
|
||||
}
|
||||
|
||||
interface StatusCellMapEntry {
|
||||
|
@ -36,22 +34,22 @@ interface StatusCellMapEntry {
|
|||
}
|
||||
|
||||
const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
||||
"PUSH_ERROR": {
|
||||
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />
|
||||
},
|
||||
"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",
|
||||
icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
|
||||
},
|
||||
"PUSH_APPROVED": {
|
||||
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
||||
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
},
|
||||
"PENDING": {
|
||||
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
||||
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />
|
||||
}
|
||||
"PUSH_ERROR": {
|
||||
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
|
||||
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />
|
||||
},
|
||||
"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",
|
||||
icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
|
||||
},
|
||||
"PUSH_APPROVED": {
|
||||
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
||||
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
},
|
||||
"PENDING": {
|
||||
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
||||
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />
|
||||
}
|
||||
};
|
||||
|
||||
const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
|
||||
|
@ -67,18 +65,18 @@ const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
|
|||
};
|
||||
|
||||
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
|
||||
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
{value.map((v, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
title={GetReleaseStatusString(v)}
|
||||
className={classNames(
|
||||
StatusCellMap[v.status].colors,
|
||||
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"
|
||||
)}
|
||||
>
|
||||
{StatusCellMap[v.status].icon}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
{value.map((v, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
title={GetReleaseStatusString(v)}
|
||||
className={classNames(
|
||||
StatusCellMap[v.status].colors,
|
||||
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"
|
||||
)}
|
||||
>
|
||||
{StatusCellMap[v.status].icon}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
import { FC } from "react"
|
||||
import { FC } from "react";
|
||||
|
||||
interface DebugProps {
|
||||
values: unknown;
|
||||
}
|
||||
|
||||
const DEBUG: FC<DebugProps> = ({ values }) => {
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return null;
|
||||
}
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full p-2 flex flex-col mt-6 bg-gray-100 dark:bg-gray-900">
|
||||
<pre className="overflow-x-auto dark:text-gray-400">
|
||||
{JSON.stringify(values, undefined, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="w-full p-2 flex flex-col mt-12 mb-12 bg-gray-100 dark:bg-gray-900">
|
||||
<pre className="dark:text-gray-400">{JSON.stringify(values, null, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DEBUG;
|
||||
|
|
|
@ -13,43 +13,43 @@ export const EmptySimple = ({
|
|||
buttonText,
|
||||
buttonAction
|
||||
}: EmptySimpleProps) => (
|
||||
<div className="text-center py-8">
|
||||
<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>
|
||||
{buttonText && buttonAction ? (
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
<div className="text-center py-8">
|
||||
<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>
|
||||
{buttonText && buttonAction ? (
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||
{buttonText}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface EmptyListStateProps {
|
||||
text: string;
|
||||
buttonText?: string;
|
||||
buttonOnClick?: any;
|
||||
buttonOnClick?: () => void;
|
||||
}
|
||||
|
||||
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
|
||||
return (
|
||||
<div className="px-4 py-12 flex flex-col items-center">
|
||||
<p className="text-center text-gray-800 dark:text-white">{text}</p>
|
||||
{buttonText && buttonOnClick && (
|
||||
<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"
|
||||
onClick={buttonOnClick}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="px-4 py-12 flex flex-col items-center">
|
||||
<p className="text-center text-gray-800 dark:text-white">{text}</p>
|
||||
{buttonText && buttonOnClick && (
|
||||
<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"
|
||||
onClick={buttonOnClick}
|
||||
>
|
||||
{buttonText}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -6,8 +6,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
||||
</div>
|
||||
)
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
||||
</div>
|
||||
);
|
|
@ -1,17 +1,16 @@
|
|||
import { Field } from "formik";
|
||||
import { Field, FieldProps } from "formik";
|
||||
|
||||
interface ErrorFieldProps {
|
||||
name: string;
|
||||
classNames?: string;
|
||||
subscribe?: any;
|
||||
}
|
||||
|
||||
const ErrorField = ({ name, classNames }: ErrorFieldProps) => (
|
||||
<Field name={name} subscribe={{ touched: true, error: true }}>
|
||||
{({ meta: { touched, error } }: any) =>
|
||||
touched && error ? <span className={classNames}>{error}</span> : null
|
||||
}
|
||||
</Field>
|
||||
<Field name={name} subscribe={{ touched: true, error: true }}>
|
||||
{({ meta: { touched, error } }: FieldProps) =>
|
||||
touched && error ? <span className={classNames}>{error}</span> : null
|
||||
}
|
||||
</Field>
|
||||
);
|
||||
|
||||
interface CheckboxFieldProps {
|
||||
|
@ -21,26 +20,26 @@ interface CheckboxFieldProps {
|
|||
}
|
||||
|
||||
const CheckboxField = ({
|
||||
name,
|
||||
label,
|
||||
sublabel
|
||||
name,
|
||||
label,
|
||||
sublabel
|
||||
}: CheckboxFieldProps) => (
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<Field
|
||||
id={name}
|
||||
name={name}
|
||||
type="checkbox"
|
||||
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 className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<Field
|
||||
id={name}
|
||||
name={name}
|
||||
type="checkbox"
|
||||
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>
|
||||
);
|
||||
|
||||
export { ErrorField, CheckboxField }
|
||||
export { ErrorField, CheckboxField };
|
|
@ -2,6 +2,6 @@ export { ErrorField, CheckboxField } from "./common";
|
|||
export { TextField, NumberField, PasswordField } from "./input";
|
||||
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||
export { RadioFieldsetWide } from "./radio";
|
||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect} from "./select";
|
||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect } from "./select";
|
||||
export { SwitchGroup } from "./switch";
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Field } from "formik";
|
||||
import { Field, FieldProps } from "formik";
|
||||
import { classNames } from "../../utils";
|
||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
|
@ -16,49 +16,49 @@ interface TextFieldProps {
|
|||
}
|
||||
|
||||
export const TextField = ({
|
||||
name,
|
||||
defaultValue,
|
||||
label,
|
||||
placeholder,
|
||||
columns,
|
||||
autoComplete,
|
||||
hidden,
|
||||
name,
|
||||
defaultValue,
|
||||
label,
|
||||
placeholder,
|
||||
columns,
|
||||
autoComplete,
|
||||
hidden
|
||||
}: TextFieldProps) => (
|
||||
<div
|
||||
className={classNames(
|
||||
hidden ? "hidden" : "",
|
||||
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}
|
||||
</label>
|
||||
)}
|
||||
<Field name={name}>
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
}: any) => (
|
||||
<div>
|
||||
<input
|
||||
{...field}
|
||||
id={name}
|
||||
type="text"
|
||||
defaultValue={defaultValue}
|
||||
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={classNames(
|
||||
hidden ? "hidden" : "",
|
||||
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}
|
||||
</label>
|
||||
)}
|
||||
<Field name={name}>
|
||||
{({
|
||||
field,
|
||||
meta
|
||||
}: FieldProps) => (
|
||||
<div>
|
||||
<input
|
||||
{...field}
|
||||
id={name}
|
||||
type="text"
|
||||
defaultValue={defaultValue}
|
||||
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}
|
||||
/>
|
||||
|
||||
{meta.touched && meta.error && (
|
||||
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
)
|
||||
{meta.touched && meta.error && (
|
||||
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface PasswordFieldProps {
|
||||
name: string;
|
||||
|
@ -72,60 +72,60 @@ interface PasswordFieldProps {
|
|||
}
|
||||
|
||||
export const PasswordField = ({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
columns,
|
||||
autoComplete,
|
||||
help,
|
||||
required
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
columns,
|
||||
autoComplete,
|
||||
help,
|
||||
required
|
||||
}: PasswordFieldProps) => {
|
||||
const [isVisible, toggleVisibility] = useToggle(false)
|
||||
const [isVisible, toggleVisibility] = useToggle(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
columns ? `col-span-${columns}` : "col-span-12"
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
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 && (
|
||||
<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>
|
||||
|
||||
{meta.touched && meta.error && (
|
||||
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
||||
)}
|
||||
<Field name={name} defaultValue={defaultValue}>
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
}: 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>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface NumberFieldProps {
|
||||
name: string;
|
||||
|
@ -135,40 +135,40 @@ interface NumberFieldProps {
|
|||
}
|
||||
|
||||
export const NumberField = ({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
step,
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
step
|
||||
}: NumberFieldProps) => (
|
||||
<div className="col-span-12 sm:col-span-6">
|
||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
{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>
|
||||
<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}
|
||||
</label>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
{meta.touched && meta.error && (
|
||||
<div className="error">{meta.error}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { classNames } from "../../utils";
|
|||
import { useToggle } from "../../hooks/hooks";
|
||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { ErrorField } from "./common"
|
||||
import { ErrorField } from "./common";
|
||||
|
||||
interface TextFieldWideProps {
|
||||
name: string;
|
||||
|
@ -17,46 +17,46 @@ interface TextFieldWideProps {
|
|||
}
|
||||
|
||||
export const TextFieldWide = ({
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
required,
|
||||
hidden
|
||||
name,
|
||||
label,
|
||||
help,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
required,
|
||||
hidden
|
||||
}: 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>
|
||||
<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>
|
||||
</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 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>
|
||||
<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>
|
||||
</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 {
|
||||
name: string;
|
||||
|
@ -69,53 +69,53 @@ interface PasswordFieldWideProps {
|
|||
}
|
||||
|
||||
export const PasswordFieldWide = ({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
help,
|
||||
required,
|
||||
defaultVisible
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
defaultValue,
|
||||
help,
|
||||
required,
|
||||
defaultVisible
|
||||
}: PasswordFieldWideProps) => {
|
||||
const [isVisible, toggleVisibility] = useToggle(defaultVisible)
|
||||
const [isVisible, toggleVisibility] = useToggle(defaultVisible);
|
||||
|
||||
return (
|
||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<div>
|
||||
<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>
|
||||
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>
|
||||
<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>
|
||||
</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 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>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</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 {
|
||||
name: string;
|
||||
|
@ -127,50 +127,50 @@ interface NumberFieldWideProps {
|
|||
}
|
||||
|
||||
export const NumberFieldWide = ({
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
help,
|
||||
defaultValue,
|
||||
required
|
||||
name,
|
||||
label,
|
||||
placeholder,
|
||||
help,
|
||||
defaultValue,
|
||||
required
|
||||
}: 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>
|
||||
<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>
|
||||
</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 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>
|
||||
<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>
|
||||
</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 {
|
||||
|
@ -182,56 +182,56 @@ interface SwitchGroupWideProps {
|
|||
}
|
||||
|
||||
export const SwitchGroupWide = ({
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
defaultValue
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
defaultValue
|
||||
}: SwitchGroupWideProps) => (
|
||||
<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">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
||||
passive>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{description && (
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
||||
{description}
|
||||
</Switch.Description>
|
||||
)}
|
||||
</div>
|
||||
<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">
|
||||
<div className="flex flex-col">
|
||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
||||
passive>
|
||||
{label}
|
||||
</Switch.Label>
|
||||
{description && (
|
||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
||||
{description}
|
||||
</Switch.Description>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Field
|
||||
name={name}
|
||||
defaultValue={defaultValue as boolean}
|
||||
type="checkbox"
|
||||
>
|
||||
{({ field, form }: FieldProps) => (
|
||||
<Switch
|
||||
{...field}
|
||||
type="button"
|
||||
value={field.value}
|
||||
checked={field.checked ?? false}
|
||||
onChange={value => {
|
||||
form.setFieldValue(field?.name ?? '', value)
|
||||
}}
|
||||
className={classNames(
|
||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
|
||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
</Field>
|
||||
</Switch.Group>
|
||||
</ul>
|
||||
)
|
||||
<Field
|
||||
name={name}
|
||||
defaultValue={defaultValue as boolean}
|
||||
type="checkbox"
|
||||
>
|
||||
{({ field, form }: FieldProps) => (
|
||||
<Switch
|
||||
{...field}
|
||||
type="button"
|
||||
value={field.value}
|
||||
checked={field.checked ?? false}
|
||||
onChange={value => {
|
||||
form.setFieldValue(field?.name ?? "", value);
|
||||
}}
|
||||
className={classNames(
|
||||
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-500",
|
||||
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
field.value ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
)}
|
||||
</Field>
|
||||
</Switch.Group>
|
||||
</ul>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,101 +15,106 @@ interface props {
|
|||
options: radioFieldsetOption[];
|
||||
}
|
||||
|
||||
interface anyObj {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
function RadioFieldsetWide({ name, legend, options }: props) {
|
||||
const {
|
||||
values,
|
||||
setFieldValue,
|
||||
} = useFormikContext<any>();
|
||||
const {
|
||||
values,
|
||||
setFieldValue
|
||||
} = useFormikContext<anyObj>();
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setFieldValue(name, value)
|
||||
}
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<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">
|
||||
{legend}
|
||||
</legend>
|
||||
</div>
|
||||
<div className="space-y-5 sm:col-span-2">
|
||||
<div className="space-y-5 sm:mt-0">
|
||||
<Field name={name} type="radio">
|
||||
{() => (
|
||||
<RadioGroup value={values[name]} onChange={onChange}>
|
||||
<RadioGroup.Label className="sr-only">
|
||||
{legend}
|
||||
</RadioGroup.Label>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
||||
{options.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.value}
|
||||
value={setting.value}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
settingIdx === 0
|
||||
? "rounded-tl-md rounded-tr-md"
|
||||
: "",
|
||||
settingIdx === options.length - 1
|
||||
? "rounded-bl-md rounded-br-md"
|
||||
: "",
|
||||
checked
|
||||
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
||||
: "border-gray-200 dark:border-gray-700",
|
||||
"relative border p-4 flex cursor-pointer focus:outline-none"
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ active, checked }) => (
|
||||
<Fragment>
|
||||
<span
|
||||
className={classNames(
|
||||
checked
|
||||
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
||||
: "bg-white border-gray-300 dark:border-gray-300",
|
||||
active
|
||||
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
|
||||
: "",
|
||||
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||
</span>
|
||||
<div className="ml-3 flex flex-col">
|
||||
<RadioGroup.Label
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
||||
"block text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
{setting.label}
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
||||
"block text-sm"
|
||||
)}
|
||||
>
|
||||
{setting.description}
|
||||
</RadioGroup.Description>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
const onChange = (value: string) => {
|
||||
setFieldValue(name, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<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">
|
||||
{legend}
|
||||
</legend>
|
||||
</div>
|
||||
<div className="space-y-5 sm:col-span-2">
|
||||
<div className="space-y-5 sm:mt-0">
|
||||
<Field name={name} type="radio">
|
||||
{() => (
|
||||
<RadioGroup value={values[name]} onChange={onChange}>
|
||||
<RadioGroup.Label className="sr-only">
|
||||
{legend}
|
||||
</RadioGroup.Label>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
||||
{options.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.value}
|
||||
value={setting.value}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
settingIdx === 0
|
||||
? "rounded-tl-md rounded-tr-md"
|
||||
: "",
|
||||
settingIdx === options.length - 1
|
||||
? "rounded-bl-md rounded-br-md"
|
||||
: "",
|
||||
checked
|
||||
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
||||
: "border-gray-200 dark:border-gray-700",
|
||||
"relative border p-4 flex cursor-pointer focus:outline-none"
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ active, checked }) => (
|
||||
<Fragment>
|
||||
<span
|
||||
className={classNames(
|
||||
checked
|
||||
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
||||
: "bg-white border-gray-300 dark:border-gray-300",
|
||||
active
|
||||
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
|
||||
: "",
|
||||
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||
</span>
|
||||
<div className="ml-3 flex flex-col">
|
||||
<RadioGroup.Label
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
||||
"block text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
{setting.label}
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
||||
"block text-sm"
|
||||
)}
|
||||
>
|
||||
{setting.description}
|
||||
</RadioGroup.Description>
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioFieldsetWide };
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Fragment } from "react";
|
||||
import { Field } from "formik";
|
||||
import { Field, FieldProps } from "formik";
|
||||
import { Transition, Listbox } from "@headlessui/react";
|
||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
||||
import { MultiSelect as RMSC } from "react-multi-select-component";
|
||||
|
@ -7,114 +7,125 @@ import { MultiSelect as RMSC } from "react-multi-select-component";
|
|||
import { classNames, COL_WIDTHS } from "../../utils";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
export interface MultiSelectOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
key?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface MultiSelectProps {
|
||||
name: string;
|
||||
label?: string;
|
||||
options?: [] | any;
|
||||
options: MultiSelectOption[];
|
||||
columns?: COL_WIDTHS;
|
||||
creatable?: boolean;
|
||||
}
|
||||
|
||||
export const MultiSelect = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
columns,
|
||||
creatable,
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
columns,
|
||||
creatable
|
||||
}: MultiSelectProps) => {
|
||||
const settingsContext = SettingsContext.useValue();
|
||||
const settingsContext = SettingsContext.useValue();
|
||||
|
||||
const handleNewField = (value: string) => ({
|
||||
value: value.toUpperCase(),
|
||||
label: value.toUpperCase(),
|
||||
key: value,
|
||||
});
|
||||
const handleNewField = (value: string) => ({
|
||||
value: value.toUpperCase(),
|
||||
label: value.toUpperCase(),
|
||||
key: value
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
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>
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
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>
|
||||
|
||||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
type="select"
|
||||
options={[...[...options, ...field.value.map((i: any) => ({ value: i.value ?? i, label: i.label ?? i}))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
||||
labelledBy={name}
|
||||
isCreatable={creatable}
|
||||
onCreateOption={handleNewField}
|
||||
value={field.value && field.value.map((item: any) => ({
|
||||
value: item.value ? item.value : item,
|
||||
label: item.label ? item.label : item,
|
||||
}))}
|
||||
onChange={(values: any) => {
|
||||
const am = values && values.map((i: any) => i.value);
|
||||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
options={[...[...options, ...field.value.map((i: MultiSelectOption) => ({ value: i.value ?? i, label: i.label ?? i }))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
||||
labelledBy={name}
|
||||
isCreatable={creatable}
|
||||
onCreateOption={handleNewField}
|
||||
value={field.value && field.value.map((item: MultiSelectOption) => ({
|
||||
value: item.value ? item.value : item,
|
||||
label: item.label ? item.label : item
|
||||
}))}
|
||||
onChange={(values: Array<MultiSelectOption>) => {
|
||||
const am = values && values.map((i) => i.value);
|
||||
|
||||
setFieldValue(field.name, am);
|
||||
}}
|
||||
className={settingsContext.darkTheme ? "dark" : ""}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
setFieldValue(field.name, am);
|
||||
}}
|
||||
className={settingsContext.darkTheme ? "dark" : ""}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IndexerMultiSelectOption {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const IndexerMultiSelect = ({
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
columns,
|
||||
name,
|
||||
label,
|
||||
options,
|
||||
columns
|
||||
}: MultiSelectProps) => {
|
||||
const settingsContext = SettingsContext.useValue();
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
columns ? `col-span-${columns}` : "col-span-12"
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
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
|
||||
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
||||
htmlFor={label}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
type="select"
|
||||
options={options}
|
||||
labelledBy={name}
|
||||
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))}
|
||||
onChange={(values: any) => {
|
||||
const am = values && values.map((i: any) => i.value);
|
||||
setFieldValue(field.name, am);
|
||||
}}
|
||||
className={settingsContext.darkTheme ? "dark" : ""}
|
||||
itemHeight={50}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
<Field name={name} type="select" multiple={true}>
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<RMSC
|
||||
{...field}
|
||||
options={options}
|
||||
labelledBy={name}
|
||||
value={field.value && field.value.map((item: IndexerMultiSelectOption) => ({
|
||||
value: item.id, label: item.name
|
||||
}))}
|
||||
onChange={(values: MultiSelectOption[]) => {
|
||||
const item = values && values.map((i) => ({ id: i.value, name: i.label }));
|
||||
setFieldValue(field.name, item);
|
||||
}}
|
||||
className={settingsContext.darkTheme ? "dark" : ""}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface DownloadClientSelectProps {
|
||||
name: string;
|
||||
|
@ -123,102 +134,102 @@ interface DownloadClientSelectProps {
|
|||
}
|
||||
|
||||
export function DownloadClientSelect({
|
||||
name,
|
||||
action,
|
||||
clients
|
||||
name,
|
||||
action,
|
||||
clients
|
||||
}: DownloadClientSelectProps) {
|
||||
return (
|
||||
<div className="col-span-6 sm:col-span-6">
|
||||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
return (
|
||||
<div className="col-span-6 sm:col-span-6">
|
||||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Listbox
|
||||
value={field.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">
|
||||
Client
|
||||
</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
|
||||
? clients.find((c) => c.id === field.value)!.name
|
||||
: "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">
|
||||
<SelectorIcon
|
||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||
aria-hidden="true" />
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
</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
|
||||
? clients.find((c) => c.id === field.value)?.name
|
||||
: "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">
|
||||
<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"
|
||||
>
|
||||
{clients
|
||||
.filter((c) => c.type === action.type)
|
||||
.map((client: any) => (
|
||||
<Listbox.Option
|
||||
key={client.id}
|
||||
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={client.id}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
)}
|
||||
>
|
||||
{client.name}
|
||||
</span>
|
||||
<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"
|
||||
>
|
||||
{clients
|
||||
.filter((c) => c.type === action.type)
|
||||
.map((client) => (
|
||||
<Listbox.Option
|
||||
key={client.id}
|
||||
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={client.id}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
)}
|
||||
>
|
||||
{client.name}
|
||||
</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>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
{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>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectFieldOption {
|
||||
|
@ -234,209 +245,209 @@ interface SelectFieldProps {
|
|||
}
|
||||
|
||||
export const Select = ({
|
||||
name,
|
||||
label,
|
||||
optionDefaultText,
|
||||
options
|
||||
name,
|
||||
label,
|
||||
optionDefaultText,
|
||||
options
|
||||
}: SelectFieldProps) => {
|
||||
return (
|
||||
<div className="col-span-6">
|
||||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
return (
|
||||
<div className="col-span-6">
|
||||
<Field name={name} type="select">
|
||||
{({
|
||||
field,
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Listbox
|
||||
value={field.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">
|
||||
{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>
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
)}
|
||||
>
|
||||
{opt.label}
|
||||
</span>
|
||||
|
||||
<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"
|
||||
>
|
||||
{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>
|
||||
{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>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectWide = ({
|
||||
name,
|
||||
label,
|
||||
optionDefaultText,
|
||||
options
|
||||
name,
|
||||
label,
|
||||
optionDefaultText,
|
||||
options
|
||||
}: SelectFieldProps) => {
|
||||
return (
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
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="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,
|
||||
form: { setFieldValue },
|
||||
}: any) => (
|
||||
<Listbox
|
||||
value={field.value}
|
||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
||||
<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,
|
||||
form: { setFieldValue }
|
||||
}: FieldProps) => (
|
||||
<Listbox
|
||||
value={field.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 }) => (
|
||||
<div className="py-4 flex items-center justify-between">
|
||||
<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"
|
||||
>
|
||||
{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">
|
||||
{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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,69 +1,73 @@
|
|||
import React from "react";
|
||||
import { Field } from "formik";
|
||||
import type {
|
||||
FieldInputProps,
|
||||
FieldMetaProps,
|
||||
FieldProps,
|
||||
FormikProps,
|
||||
FormikValues
|
||||
FieldInputProps,
|
||||
FieldMetaProps,
|
||||
FieldProps,
|
||||
FormikProps,
|
||||
FormikValues
|
||||
} from "formik";
|
||||
import { Switch as HeadlessSwitch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
type SwitchProps<V = any> = {
|
||||
label: string
|
||||
type SwitchProps<V = unknown> = {
|
||||
label?: string
|
||||
checked: boolean
|
||||
value: boolean
|
||||
disabled?: boolean
|
||||
onChange: (value: boolean) => void
|
||||
field?: FieldInputProps<V>
|
||||
form?: FormikProps<FormikValues>
|
||||
meta?: FieldMetaProps<V>
|
||||
}
|
||||
children: React.ReactNode
|
||||
className: string
|
||||
};
|
||||
|
||||
export const Switch = ({
|
||||
label,
|
||||
checked: $checked,
|
||||
disabled = false,
|
||||
onChange: $onChange,
|
||||
field,
|
||||
form,
|
||||
label,
|
||||
checked: $checked,
|
||||
disabled = false,
|
||||
onChange: $onChange,
|
||||
field,
|
||||
form
|
||||
}: SwitchProps) => {
|
||||
const checked = field?.checked ?? $checked
|
||||
const checked = field?.checked ?? $checked;
|
||||
|
||||
return (
|
||||
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
|
||||
<HeadlessSwitch.Label>{label}</HeadlessSwitch.Label>
|
||||
<HeadlessSwitch
|
||||
as="button"
|
||||
name={field?.name}
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
onChange={value => {
|
||||
form?.setFieldValue(field?.name ?? '', value)
|
||||
$onChange && $onChange(value)
|
||||
}}
|
||||
return (
|
||||
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
|
||||
<HeadlessSwitch.Label>{label}</HeadlessSwitch.Label>
|
||||
<HeadlessSwitch
|
||||
as="button"
|
||||
name={field?.name}
|
||||
disabled={disabled}
|
||||
checked={checked}
|
||||
onChange={value => {
|
||||
form?.setFieldValue(field?.name ?? "", value);
|
||||
$onChange && $onChange(value);
|
||||
}}
|
||||
|
||||
className={classNames(
|
||||
checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
{({ checked }) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
checked ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</HeadlessSwitch>
|
||||
</HeadlessSwitch.Group>
|
||||
)
|
||||
}
|
||||
className={classNames(
|
||||
checked ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
{({ checked }) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
checked ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</HeadlessSwitch>
|
||||
</HeadlessSwitch.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export type SwitchFormikProps = SwitchProps & FieldProps & React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} />
|
||||
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} children={props.children}/>;
|
||||
|
||||
interface SwitchGroupProps {
|
||||
name: string;
|
||||
|
@ -73,54 +77,54 @@ interface SwitchGroupProps {
|
|||
}
|
||||
|
||||
const SwitchGroup = ({
|
||||
name,
|
||||
label,
|
||||
description
|
||||
name,
|
||||
label,
|
||||
description
|
||||
}: SwitchGroupProps) => (
|
||||
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
|
||||
{label && <div className="flex flex-col">
|
||||
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
passive>
|
||||
{label}
|
||||
</HeadlessSwitch.Label>
|
||||
{description && (
|
||||
<HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</HeadlessSwitch.Description>
|
||||
)}
|
||||
</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>
|
||||
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
|
||||
{label && <div className="flex flex-col">
|
||||
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||
passive>
|
||||
{label}
|
||||
</HeadlessSwitch.Label>
|
||||
{description && (
|
||||
<HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400">
|
||||
{description}
|
||||
</HeadlessSwitch.Description>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
<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 };
|
|
@ -1,92 +1,92 @@
|
|||
import { Fragment, FC } from "react";
|
||||
import React, { Fragment, FC } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||
|
||||
interface DeleteModalProps {
|
||||
isOpen: boolean;
|
||||
buttonRef: any;
|
||||
toggle: any;
|
||||
deleteAction: any;
|
||||
buttonRef: React.MutableRefObject<HTMLElement | null> | undefined;
|
||||
toggle: () => void;
|
||||
deleteAction: () => void;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
static
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
initialFocus={buttonRef}
|
||||
open={isOpen}
|
||||
onClose={toggle}
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
static
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
initialFocus={buttonRef}
|
||||
open={isOpen}
|
||||
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">
|
||||
<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>
|
||||
<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">
|
||||
​
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
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="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">
|
||||
<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">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||
{text}
|
||||
</p>
|
||||
</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>
|
||||
</span>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
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="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">
|
||||
<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">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
<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>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
|
@ -1,30 +1,30 @@
|
|||
import { FC } from 'react'
|
||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { classNames } from '../../utils'
|
||||
import { FC } from "react";
|
||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from "@heroicons/react/solid";
|
||||
import { toast, Toast as Tooast } from "react-hot-toast";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
type Props = {
|
||||
type: 'error' | 'success' | 'warning'
|
||||
type: "error" | "success" | "warning"
|
||||
body?: string
|
||||
t?: any;
|
||||
}
|
||||
t?: Tooast;
|
||||
};
|
||||
|
||||
const Toast: FC<Props> = ({ type, body, t }) => (
|
||||
<div className={classNames(
|
||||
t.visible ? 'animate-enter' : 'animate-leave',
|
||||
t?.visible ? "animate-enter" : "animate-leave",
|
||||
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||
{type === 'error' && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||
{type === "success" && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||
{type === "error" && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||
{type === "warning" && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||
</div>
|
||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||
{type === 'success' && "Success"}
|
||||
{type === 'error' && "Error"}
|
||||
{type === 'warning' && "Warning"}
|
||||
{type === "success" && "Success"}
|
||||
{type === "error" && "Error"}
|
||||
{type === "warning" && "Warning"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
|||
<button
|
||||
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={() => {
|
||||
toast.dismiss(t.id)
|
||||
toast.dismiss(t?.id);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
|
@ -42,6 +42,6 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export default Toast;
|
|
@ -1,4 +1,4 @@
|
|||
import { Fragment, useRef } from "react";
|
||||
import React, { Fragment, useRef } from "react";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Form, Formik } from "formik";
|
||||
|
@ -10,7 +10,7 @@ import { classNames } from "../../utils";
|
|||
interface SlideOverProps<DataType> {
|
||||
title: string;
|
||||
initialValues: DataType;
|
||||
validate?: (values?: any) => void;
|
||||
validate?: (values: DataType) => void;
|
||||
onSubmit: (values?: DataType) => void;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
|
@ -30,117 +30,117 @@ function SlideOver<DataType>({
|
|||
type,
|
||||
children
|
||||
}: SlideOverProps<DataType>): React.ReactElement {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||
{deleteAction && (
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={deleteAction}
|
||||
title={`Remove ${title}`}
|
||||
text={`Are you sure you want to remove this ${title}? This action cannot be undone.`}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||
{deleteAction && (
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={deleteAction}
|
||||
title={`Remove ${title}`}
|
||||
text={`Are you sure you want to remove this ${title}? This action cannot be undone.`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<Dialog.Overlay className="absolute inset-0" />
|
||||
<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">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
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">
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
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
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
validate={validate}
|
||||
>
|
||||
{({ handleSubmit, values }) => (
|
||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||
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>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
validate={validate}
|
||||
>
|
||||
{({ handleSubmit, values }) => (
|
||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||
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>
|
||||
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
{!!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>
|
||||
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { SlideOver };
|
|
@ -1,156 +1,158 @@
|
|||
import { MultiSelectOption } from "../components/inputs/select";
|
||||
|
||||
export const resolutions = [
|
||||
"2160p",
|
||||
"1080p",
|
||||
"1080i",
|
||||
"810p",
|
||||
"720p",
|
||||
"576p",
|
||||
"480p",
|
||||
"480i"
|
||||
"2160p",
|
||||
"1080p",
|
||||
"1080i",
|
||||
"810p",
|
||||
"720p",
|
||||
"576p",
|
||||
"480p",
|
||||
"480i"
|
||||
];
|
||||
|
||||
export const RESOLUTION_OPTIONS = resolutions.map(r => ({ value: r, label: r, key: r}));
|
||||
export const RESOLUTION_OPTIONS: MultiSelectOption[] = resolutions.map(r => ({ value: r, label: r, key: r }));
|
||||
|
||||
export const codecs = [
|
||||
"HEVC",
|
||||
"H.264",
|
||||
"H.265",
|
||||
"x264",
|
||||
"x265",
|
||||
"AVC",
|
||||
"VC-1",
|
||||
"AV1",
|
||||
"XviD"
|
||||
"HEVC",
|
||||
"H.264",
|
||||
"H.265",
|
||||
"x264",
|
||||
"x265",
|
||||
"AVC",
|
||||
"VC-1",
|
||||
"AV1",
|
||||
"XviD"
|
||||
];
|
||||
|
||||
export const CODECS_OPTIONS = codecs.map(v => ({ value: v, label: v, key: v}));
|
||||
export const CODECS_OPTIONS: MultiSelectOption[] = codecs.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const sources = [
|
||||
"BluRay",
|
||||
"UHD.BluRay",
|
||||
"WEB-DL",
|
||||
"WEB",
|
||||
"WEBRip",
|
||||
"BD5",
|
||||
"BD9",
|
||||
"BDr",
|
||||
"BDRip",
|
||||
"BRRip",
|
||||
"CAM",
|
||||
"DVDR",
|
||||
"DVDRip",
|
||||
"DVDScr",
|
||||
"HDCAM",
|
||||
"HDDVD",
|
||||
"HDDVDRip",
|
||||
"HDTS",
|
||||
"HDTV",
|
||||
"Mixed",
|
||||
"SiteRip",
|
||||
"BluRay",
|
||||
"UHD.BluRay",
|
||||
"WEB-DL",
|
||||
"WEB",
|
||||
"WEBRip",
|
||||
"BD5",
|
||||
"BD9",
|
||||
"BDr",
|
||||
"BDRip",
|
||||
"BRRip",
|
||||
"CAM",
|
||||
"DVDR",
|
||||
"DVDRip",
|
||||
"DVDScr",
|
||||
"HDCAM",
|
||||
"HDDVD",
|
||||
"HDDVDRip",
|
||||
"HDTS",
|
||||
"HDTV",
|
||||
"Mixed",
|
||||
"SiteRip"
|
||||
];
|
||||
|
||||
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
|
||||
export const SOURCES_OPTIONS: MultiSelectOption[] = sources.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const containers = [
|
||||
"avi",
|
||||
"mp4",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mp4",
|
||||
"mkv"
|
||||
];
|
||||
|
||||
export const CONTAINER_OPTIONS = containers.map(v => ({ value: v, label: v, key: v}));
|
||||
export const CONTAINER_OPTIONS: MultiSelectOption[] = containers.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const hdr = [
|
||||
"HDR",
|
||||
"HDR10",
|
||||
"HDR10+",
|
||||
"HLG",
|
||||
"DV",
|
||||
"DV HDR",
|
||||
"DV HDR10",
|
||||
"DV HDR10+",
|
||||
"DoVi",
|
||||
"Dolby Vision",
|
||||
"HDR",
|
||||
"HDR10",
|
||||
"HDR10+",
|
||||
"HLG",
|
||||
"DV",
|
||||
"DV HDR",
|
||||
"DV HDR10",
|
||||
"DV HDR10+",
|
||||
"DoVi",
|
||||
"Dolby Vision"
|
||||
];
|
||||
|
||||
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
|
||||
export const HDR_OPTIONS: MultiSelectOption[] = hdr.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const quality_other = [
|
||||
"REMUX",
|
||||
"HYBRID",
|
||||
"REPACK",
|
||||
"REMUX",
|
||||
"HYBRID",
|
||||
"REPACK"
|
||||
];
|
||||
|
||||
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v}));
|
||||
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const formatMusic = [
|
||||
"MP3",
|
||||
"FLAC",
|
||||
"Ogg Vorbis",
|
||||
"Ogg",
|
||||
"AAC",
|
||||
"AC3",
|
||||
"DTS",
|
||||
"MP3",
|
||||
"FLAC",
|
||||
"Ogg Vorbis",
|
||||
"Ogg",
|
||||
"AAC",
|
||||
"AC3",
|
||||
"DTS"
|
||||
];
|
||||
|
||||
export const FORMATS_OPTIONS = formatMusic.map(r => ({ value: r, label: r, key: r}));
|
||||
export const FORMATS_OPTIONS: MultiSelectOption[] = formatMusic.map(r => ({ value: r, label: r, key: r }));
|
||||
|
||||
export const sourcesMusic = [
|
||||
"CD",
|
||||
"WEB",
|
||||
"DVD",
|
||||
"Vinyl",
|
||||
"Soundboard",
|
||||
"DAT",
|
||||
"Cassette",
|
||||
"Blu-Ray",
|
||||
"SACD",
|
||||
"CD",
|
||||
"WEB",
|
||||
"DVD",
|
||||
"Vinyl",
|
||||
"Soundboard",
|
||||
"DAT",
|
||||
"Cassette",
|
||||
"Blu-Ray",
|
||||
"SACD"
|
||||
];
|
||||
|
||||
export const SOURCES_MUSIC_OPTIONS = sourcesMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
export const SOURCES_MUSIC_OPTIONS: MultiSelectOption[] = sourcesMusic.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const qualityMusic = [
|
||||
"192",
|
||||
"256",
|
||||
"320",
|
||||
"APS (VBR)",
|
||||
"APX (VBR)",
|
||||
"V2 (VBR)",
|
||||
"V1 (VBR)",
|
||||
"V0 (VBR)",
|
||||
"Lossless",
|
||||
"24bit Lossless",
|
||||
"192",
|
||||
"256",
|
||||
"320",
|
||||
"APS (VBR)",
|
||||
"APX (VBR)",
|
||||
"V2 (VBR)",
|
||||
"V1 (VBR)",
|
||||
"V0 (VBR)",
|
||||
"Lossless",
|
||||
"24bit Lossless"
|
||||
];
|
||||
|
||||
export const QUALITY_MUSIC_OPTIONS = qualityMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
export const QUALITY_MUSIC_OPTIONS: MultiSelectOption[] = qualityMusic.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const releaseTypeMusic = [
|
||||
"Album",
|
||||
"Single",
|
||||
"EP",
|
||||
"Soundtrack",
|
||||
"Anthology",
|
||||
"Compilation",
|
||||
"Live album",
|
||||
"Remix",
|
||||
"Bootleg",
|
||||
"Interview",
|
||||
"Mixtape",
|
||||
"Demo",
|
||||
"Concert Recording",
|
||||
"DJ Mix",
|
||||
"Unknown",
|
||||
"Album",
|
||||
"Single",
|
||||
"EP",
|
||||
"Soundtrack",
|
||||
"Anthology",
|
||||
"Compilation",
|
||||
"Live album",
|
||||
"Remix",
|
||||
"Bootleg",
|
||||
"Interview",
|
||||
"Mixtape",
|
||||
"Demo",
|
||||
"Concert Recording",
|
||||
"DJ Mix",
|
||||
"Unknown"
|
||||
];
|
||||
|
||||
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
|
||||
export const RELEASE_TYPE_MUSIC_OPTIONS: MultiSelectOption[] = releaseTypeMusic.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export const originOptions = [
|
||||
"P2P",
|
||||
"Internal",
|
||||
"SCENE",
|
||||
"O-SCENE",
|
||||
"P2P",
|
||||
"Internal",
|
||||
"SCENE",
|
||||
"O-SCENE"
|
||||
];
|
||||
|
||||
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v}));
|
||||
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v }));
|
||||
|
||||
export interface RadioFieldsetOption {
|
||||
label: string;
|
||||
|
@ -159,123 +161,128 @@ export interface RadioFieldsetOption {
|
|||
}
|
||||
|
||||
export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
|
||||
{
|
||||
label: "qBittorrent",
|
||||
description: "Add torrents directly to qBittorrent",
|
||||
value: "QBITTORRENT"
|
||||
},
|
||||
{
|
||||
label: "Deluge",
|
||||
description: "Add torrents directly to Deluge",
|
||||
value: "DELUGE_V1"
|
||||
},
|
||||
{
|
||||
label: "Deluge 2",
|
||||
description: "Add torrents directly to Deluge 2",
|
||||
value: "DELUGE_V2"
|
||||
},
|
||||
{
|
||||
label: "Radarr",
|
||||
description: "Send to Radarr and let it decide",
|
||||
value: "RADARR"
|
||||
},
|
||||
{
|
||||
label: "Sonarr",
|
||||
description: "Send to Sonarr and let it decide",
|
||||
value: "SONARR"
|
||||
},
|
||||
{
|
||||
label: "Lidarr",
|
||||
description: "Send to Lidarr and let it decide",
|
||||
value: "LIDARR"
|
||||
},
|
||||
{
|
||||
label: "Whisparr",
|
||||
description: "Send to Whisparr and let it decide",
|
||||
value: "WHISPARR"
|
||||
},
|
||||
{
|
||||
label: "qBittorrent",
|
||||
description: "Add torrents directly to qBittorrent",
|
||||
value: "QBITTORRENT"
|
||||
},
|
||||
{
|
||||
label: "Deluge",
|
||||
description: "Add torrents directly to Deluge",
|
||||
value: "DELUGE_V1"
|
||||
},
|
||||
{
|
||||
label: "Deluge 2",
|
||||
description: "Add torrents directly to Deluge 2",
|
||||
value: "DELUGE_V2"
|
||||
},
|
||||
{
|
||||
label: "Radarr",
|
||||
description: "Send to Radarr and let it decide",
|
||||
value: "RADARR"
|
||||
},
|
||||
{
|
||||
label: "Sonarr",
|
||||
description: "Send to Sonarr and let it decide",
|
||||
value: "SONARR"
|
||||
},
|
||||
{
|
||||
label: "Lidarr",
|
||||
description: "Send to Lidarr and let it decide",
|
||||
value: "LIDARR"
|
||||
},
|
||||
{
|
||||
label: "Whisparr",
|
||||
description: "Send to Whisparr and let it decide",
|
||||
value: "WHISPARR"
|
||||
}
|
||||
];
|
||||
|
||||
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent",
|
||||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
"WHISPARR": "Whisparr",
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent",
|
||||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
"WHISPARR": "Whisparr"
|
||||
};
|
||||
|
||||
export const ActionTypeOptions: RadioFieldsetOption[] = [
|
||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
||||
{label: "Webhook", description: "Run webhook", value: "WEBHOOK"},
|
||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
||||
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
|
||||
{label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR"},
|
||||
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR"},
|
||||
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR"},
|
||||
{label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR"},
|
||||
{ label: "Test", description: "A simple action to test a filter.", value: "TEST" },
|
||||
{ label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER" },
|
||||
{ label: "Webhook", description: "Run webhook", value: "WEBHOOK" },
|
||||
{ label: "Exec", description: "Run a custom command after a filter match", value: "EXEC" },
|
||||
{ label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT" },
|
||||
{ label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1" },
|
||||
{ label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
|
||||
{ label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" },
|
||||
{ label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
|
||||
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
|
||||
{ label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR" }
|
||||
];
|
||||
|
||||
export const ActionTypeNameMap = {
|
||||
"TEST": "Test",
|
||||
"WATCH_FOLDER": "Watch folder",
|
||||
"WEBHOOK": "Webhook",
|
||||
"EXEC": "Exec",
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent",
|
||||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
"WHISPARR": "Whisparr",
|
||||
"TEST": "Test",
|
||||
"WATCH_FOLDER": "Watch folder",
|
||||
"WEBHOOK": "Webhook",
|
||||
"EXEC": "Exec",
|
||||
"DELUGE_V1": "Deluge v1",
|
||||
"DELUGE_V2": "Deluge v2",
|
||||
"QBITTORRENT": "qBittorrent",
|
||||
"RADARR": "Radarr",
|
||||
"SONARR": "Sonarr",
|
||||
"LIDARR": "Lidarr",
|
||||
"WHISPARR": "Whisparr"
|
||||
};
|
||||
|
||||
export const PushStatusOptions: any[] = [
|
||||
{
|
||||
label: "Rejected",
|
||||
value: "PUSH_REJECTED",
|
||||
},
|
||||
{
|
||||
label: "Approved",
|
||||
value: "PUSH_APPROVED"
|
||||
},
|
||||
{
|
||||
label: "Error",
|
||||
value: "PUSH_ERROR"
|
||||
},
|
||||
export interface OptionBasic {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const PushStatusOptions: OptionBasic[] = [
|
||||
{
|
||||
label: "Rejected",
|
||||
value: "PUSH_REJECTED"
|
||||
},
|
||||
{
|
||||
label: "Approved",
|
||||
value: "PUSH_APPROVED"
|
||||
},
|
||||
{
|
||||
label: "Error",
|
||||
value: "PUSH_ERROR"
|
||||
}
|
||||
];
|
||||
|
||||
export const NotificationTypeOptions: any[] = [
|
||||
{
|
||||
label: "Discord",
|
||||
value: "DISCORD",
|
||||
},
|
||||
export const NotificationTypeOptions: OptionBasic[] = [
|
||||
{
|
||||
label: "Discord",
|
||||
value: "DISCORD"
|
||||
}
|
||||
];
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: any;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const EventOptions: SelectOption[] = [
|
||||
{
|
||||
label: "Push Rejected",
|
||||
value: "PUSH_REJECTED",
|
||||
description: "On push rejected for the arrs or download client",
|
||||
},
|
||||
{
|
||||
label: "Push Approved",
|
||||
value: "PUSH_APPROVED",
|
||||
description: "On push approved for the arrs or download client",
|
||||
},
|
||||
{
|
||||
label: "Push Error",
|
||||
value: "PUSH_ERROR",
|
||||
description: "On push error for the arrs or download client",
|
||||
},
|
||||
{
|
||||
label: "Push Rejected",
|
||||
value: "PUSH_REJECTED",
|
||||
description: "On push rejected for the arrs or download client"
|
||||
},
|
||||
{
|
||||
label: "Push Approved",
|
||||
value: "PUSH_APPROVED",
|
||||
description: "On push approved for the arrs or download client"
|
||||
},
|
||||
{
|
||||
label: "Push Error",
|
||||
value: "PUSH_ERROR",
|
||||
description: "On push error for the arrs or download client"
|
||||
}
|
||||
];
|
||||
|
|
4
web/src/domain/react-table-config.d.ts
vendored
4
web/src/domain/react-table-config.d.ts
vendored
|
@ -46,9 +46,9 @@ import {
|
|||
UseSortByInstanceProps,
|
||||
UseSortByOptions,
|
||||
UseSortByState
|
||||
} from 'react-table'
|
||||
} from "react-table";
|
||||
|
||||
declare module 'react-table' {
|
||||
declare module "react-table" {
|
||||
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
||||
|
||||
export interface TableOptions<D extends Record<string, unknown>>
|
||||
|
|
|
@ -3,149 +3,160 @@ import { useMutation } from "react-query";
|
|||
import { toast } from "react-hot-toast";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Field, Form, Formik } from "formik";
|
||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import DEBUG from "../../components/debug";
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
|
||||
function FilterAddForm({ isOpen, toggle }: any) {
|
||||
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} />);
|
||||
interface filterAddFormProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
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);
|
||||
const validate = (values: any) => values.name ? {} : { name: "Required" };
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<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" />
|
||||
const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
|
||||
const validate = (values: FormikValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name) {
|
||||
errors.name = "Required";
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
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">
|
||||
return (
|
||||
<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" />
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
name: "",
|
||||
enabled: false,
|
||||
resolutions: [],
|
||||
codecs: [],
|
||||
sources: [],
|
||||
containers: [],
|
||||
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">
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
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
|
||||
initialValues={{
|
||||
name: "",
|
||||
enabled: false,
|
||||
resolutions: [],
|
||||
codecs: [],
|
||||
sources: [],
|
||||
containers: [],
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-7 flex items-center">
|
||||
<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"
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="sr-only">Close panel</span>
|
||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-7 flex items-center">
|
||||
<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"
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="sr-only">Close panel</span>
|
||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="py-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>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||
>
|
||||
<div
|
||||
className="py-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>
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
</div>
|
||||
<Field name="name">
|
||||
{({
|
||||
field,
|
||||
meta,
|
||||
}: FieldProps ) => (
|
||||
<div className="sm:col-span-2">
|
||||
<input
|
||||
{...field}
|
||||
id="name"
|
||||
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"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Field name="name">
|
||||
{({
|
||||
field,
|
||||
meta
|
||||
}: FieldProps ) => (
|
||||
<div className="sm:col-span-2">
|
||||
<input
|
||||
{...field}
|
||||
id="name"
|
||||
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"
|
||||
/>
|
||||
|
||||
{meta.touched && meta.error &&
|
||||
{meta.touched && meta.error &&
|
||||
<span className="block mt-2 text-red-500">{meta.error}</span>}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</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-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}
|
||||
>
|
||||
<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-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}
|
||||
>
|
||||
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"
|
||||
>
|
||||
</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"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DEBUG values={values} />
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DEBUG values={values} />
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterAddForm;
|
File diff suppressed because it is too large
Load diff
|
@ -1,115 +1,118 @@
|
|||
import {useMutation} from "react-query";
|
||||
import {APIClient} from "../../api/APIClient";
|
||||
import {queryClient} from "../../App";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { useMutation } from "react-query";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { queryClient } from "../../App";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {SlideOver} from "../../components/panels";
|
||||
import {NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide} from "../../components/inputs";
|
||||
import {ImplementationMap} from "../../screens/settings/Feed";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
|
||||
import { ImplementationMap } from "../../screens/settings/Feed";
|
||||
import { componentMapType } from "./DownloadClientForms";
|
||||
|
||||
interface UpdateProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
feed: Feed;
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
feed: Feed;
|
||||
}
|
||||
|
||||
export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
||||
const mutation = useMutation(
|
||||
(feed: Feed) => APIClient.feeds.update(feed),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t}/>)
|
||||
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);
|
||||
export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
|
||||
const mutation = useMutation(
|
||||
(feed: Feed) => APIClient.feeds.update(feed),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t}/>);
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
};
|
||||
|
||||
const initialValues = {
|
||||
id: feed.id,
|
||||
indexer: feed.indexer,
|
||||
enabled: feed.enabled,
|
||||
type: feed.type,
|
||||
name: feed.name,
|
||||
url: feed.url,
|
||||
api_key: feed.api_key,
|
||||
interval: feed.interval,
|
||||
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}/>);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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}/>
|
||||
const onSubmit = (formData: unknown) => {
|
||||
mutation.mutate(formData as Feed);
|
||||
};
|
||||
|
||||
<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>
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
};
|
||||
|
||||
<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>
|
||||
)
|
||||
const initialValues = {
|
||||
id: feed.id,
|
||||
indexer: feed.indexer,
|
||||
enabled: feed.enabled,
|
||||
type: feed.type,
|
||||
name: feed.name,
|
||||
url: feed.url,
|
||||
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() {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<TextFieldWide
|
||||
name="url"
|
||||
label="URL"
|
||||
help="Torznab url"
|
||||
/>
|
||||
|
||||
<PasswordFieldWide name="api_key" label="API key" />
|
||||
|
||||
<NumberFieldWide name="interval" label="Refresh interval" help="Minutes. Recommended 15-30. To low and risk ban." />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<TextFieldWide
|
||||
name="url"
|
||||
label="URL"
|
||||
help="Torznab url"
|
||||
/>
|
||||
|
||||
<PasswordFieldWide name="api_key" label="API key"/>
|
||||
|
||||
<NumberFieldWide name="interval" label="Refresh interval"
|
||||
help="Minutes. Recommended 15-30. To low and risk ban."/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentMap: any = {
|
||||
TORZNAB: <FormFieldsTorznab/>,
|
||||
const componentMap: componentMapType = {
|
||||
TORZNAB: <FormFieldsTorznab/>
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -1,87 +1,87 @@
|
|||
import { useMutation } from "react-query";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import { Field, FieldArray } from "formik";
|
||||
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
||||
import {
|
||||
TextFieldWide,
|
||||
PasswordFieldWide,
|
||||
SwitchGroupWide,
|
||||
NumberFieldWide
|
||||
} from "../../components/inputs/input_wide";
|
||||
TextFieldWide,
|
||||
PasswordFieldWide,
|
||||
SwitchGroupWide,
|
||||
NumberFieldWide
|
||||
} from "../../components/inputs";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
|
||||
interface ChannelsFieldArrayProps {
|
||||
channels: IrcChannel[];
|
||||
}
|
||||
|
||||
const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
|
||||
<div className="p-6">
|
||||
<FieldArray name="channels">
|
||||
{({ remove, push }) => (
|
||||
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
||||
{channels && channels.length > 0 ? (
|
||||
channels.map((_channel: IrcChannel, index: number) => (
|
||||
<div key={index} className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Field name={`channels.${index}.name`}>
|
||||
{({ field }: FieldProps) => (
|
||||
<input
|
||||
{...field}
|
||||
type="text"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
<div className="p-6">
|
||||
<FieldArray name="channels">
|
||||
{({ remove, push }) => (
|
||||
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
||||
{channels && channels.length > 0 ? (
|
||||
channels.map((_channel: IrcChannel, index: number) => (
|
||||
<div key={index} className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Field name={`channels.${index}.name`}>
|
||||
{({ field }: FieldProps) => (
|
||||
<input
|
||||
{...field}
|
||||
type="text"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
</FieldArray>
|
||||
</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
|
||||
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 {
|
||||
|
@ -95,105 +95,101 @@ interface IrcNetworkAddFormValues {
|
|||
channels: IrcChannel[];
|
||||
}
|
||||
|
||||
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
||||
const mutation = useMutation(
|
||||
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['networks']);
|
||||
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />)
|
||||
toggle()
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
|
||||
},
|
||||
}
|
||||
);
|
||||
interface AddFormProps {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// easy way to split textarea lines into array of strings for each newline.
|
||||
// parse on the field didn't really work.
|
||||
data.connect_commands = (
|
||||
data.connect_commands && data.connect_commands.length > 0 ?
|
||||
data.connect_commands.replace(/\r\n/g, "\n").split("\n") :
|
||||
[]
|
||||
);
|
||||
|
||||
mutation.mutate(data);
|
||||
};
|
||||
|
||||
const 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;
|
||||
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
|
||||
const mutation = useMutation(
|
||||
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["networks"]);
|
||||
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
|
||||
toggle();
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const initialValues: IrcNetworkAddFormValues = {
|
||||
name: "",
|
||||
enabled: true,
|
||||
server: "",
|
||||
port: 6667,
|
||||
tls: false,
|
||||
pass: "",
|
||||
nickserv: {
|
||||
account: ""
|
||||
},
|
||||
channels: [],
|
||||
};
|
||||
const onSubmit = (data: unknown) => {
|
||||
mutation.mutate(data as IrcNetwork);
|
||||
};
|
||||
const validate = (values: FormikValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name)
|
||||
errors.name = "Required";
|
||||
|
||||
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} />
|
||||
if (!values.port)
|
||||
errors.port = "Required";
|
||||
|
||||
<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">
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
</div>
|
||||
if (!values.nickserv || !values.nickserv.account)
|
||||
errors.nickserv = { account: "Required" };
|
||||
|
||||
<div>
|
||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||
return errors;
|
||||
};
|
||||
|
||||
<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>
|
||||
const initialValues: IrcNetworkAddFormValues = {
|
||||
name: "",
|
||||
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} />
|
||||
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||
|
||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||
</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 dark:sm:divide-gray-700">
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
</div>
|
||||
|
||||
<ChannelsFieldArray channels={values.channels} />
|
||||
</>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
<div>
|
||||
<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">
|
||||
<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 {
|
||||
|
@ -216,118 +212,113 @@ interface IrcNetworkUpdateFormProps {
|
|||
}
|
||||
|
||||
export function IrcNetworkUpdateForm({
|
||||
isOpen,
|
||||
toggle,
|
||||
network
|
||||
isOpen,
|
||||
toggle,
|
||||
network
|
||||
}: IrcNetworkUpdateFormProps) {
|
||||
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['networks']);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
|
||||
toggle()
|
||||
}
|
||||
})
|
||||
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["networks"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['networks']);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
|
||||
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["networks"]);
|
||||
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
|
||||
|
||||
toggle()
|
||||
}
|
||||
})
|
||||
toggle();
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// easy way to split textarea lines into array of strings for each newline.
|
||||
// parse on the field didn't really work.
|
||||
// TODO fix connect_commands on network update
|
||||
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
|
||||
// data.connect_commands = cmds
|
||||
// console.log("formatted", data)
|
||||
const onSubmit = (data: unknown) => {
|
||||
mutation.mutate(data as IrcNetwork);
|
||||
};
|
||||
|
||||
mutation.mutate(data)
|
||||
};
|
||||
const validate = (values: FormikValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors = {} as any;
|
||||
|
||||
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;
|
||||
if (!values.name) {
|
||||
errors.name = "Required";
|
||||
}
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(network.id)
|
||||
if (!values.server) {
|
||||
errors.server = "Required";
|
||||
}
|
||||
|
||||
const initialValues: IrcNetworkUpdateFormValues = {
|
||||
id: network.id,
|
||||
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
|
||||
if (!values.port) {
|
||||
errors.port = "Required";
|
||||
}
|
||||
|
||||
return (
|
||||
<SlideOver
|
||||
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} />
|
||||
if (!values.nickserv?.account) {
|
||||
errors.nickserv = {
|
||||
account: "Required"
|
||||
};
|
||||
}
|
||||
|
||||
<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">
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
</div>
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate(network.id);
|
||||
};
|
||||
|
||||
<div>
|
||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||
const initialValues: IrcNetworkUpdateFormValues = {
|
||||
id: network.id,
|
||||
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">
|
||||
<SwitchGroupWide name="tls" label="TLS" />
|
||||
</div>
|
||||
return (
|
||||
<SlideOver
|
||||
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} />
|
||||
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
|
||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||
</div>
|
||||
|
||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||
|
||||
<ChannelsFieldArray channels={values.channels} />
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
|
@ -1,83 +1,87 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import type {FieldProps} from "formik";
|
||||
import {XIcon} from "@heroicons/react/solid";
|
||||
import Select, {components} from "react-select";
|
||||
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||
import type { FieldProps } from "formik";
|
||||
import { XIcon } from "@heroicons/react/solid";
|
||||
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||
import {
|
||||
SwitchGroupWide,
|
||||
TextFieldWide
|
||||
SwitchGroupWide,
|
||||
TextFieldWide
|
||||
} from "../../components/inputs";
|
||||
import DEBUG from "../../components/debug";
|
||||
import {EventOptions, NotificationTypeOptions} from "../../domain/constants";
|
||||
import {useMutation} from "react-query";
|
||||
import {APIClient} from "../../api/APIClient";
|
||||
import {queryClient} from "../../App";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
|
||||
import { useMutation } from "react-query";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { queryClient } from "../../App";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {SlideOver} from "../../components/panels";
|
||||
import { SlideOver } from "../../components/panels";
|
||||
import { componentMapType } from "./DownloadClientForms";
|
||||
|
||||
const Input = (props: 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) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Control = (props: ControlProps) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Control = (props: any) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu = (props: any) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
const Option = (props: OptionProps) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function FormFieldsDiscord() {
|
||||
return (
|
||||
<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">Credentials</Dialog.Title>*/}
|
||||
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
|
||||
{/* Api keys etc*/}
|
||||
{/* </p>*/}
|
||||
{/*</div>*/}
|
||||
return (
|
||||
<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">Credentials</Dialog.Title>*/}
|
||||
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
|
||||
{/* Api keys etc*/}
|
||||
{/* </p>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<TextFieldWide
|
||||
name="webhook"
|
||||
label="Webhook URL"
|
||||
help="Discord channel webhook url"
|
||||
placeholder="https://discordapp.com/api/webhooks/xx/xx"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
<TextFieldWide
|
||||
name="webhook"
|
||||
label="Webhook URL"
|
||||
help="Discord channel webhook url"
|
||||
placeholder="https://discordapp.com/api/webhooks/xx/xx"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentMap: any = {
|
||||
DISCORD: <FormFieldsDiscord/>,
|
||||
const componentMap: componentMapType = {
|
||||
DISCORD: <FormFieldsDiscord/>
|
||||
};
|
||||
|
||||
interface NotificationAddFormValues {
|
||||
|
@ -87,349 +91,353 @@ interface NotificationAddFormValues {
|
|||
|
||||
interface AddProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
||||
const mutation = useMutation(
|
||||
(notification: Notification) => APIClient.notifications.create(notification),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['notifications']);
|
||||
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />)
|
||||
toggle()
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />)
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = (formData: any) => {
|
||||
mutation.mutate(formData)
|
||||
export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
||||
const mutation = useMutation(
|
||||
(notification: Notification) => APIClient.notifications.create(notification),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["notifications"]);
|
||||
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
|
||||
toggle();
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const validate = (values: NotificationAddFormValues) => {
|
||||
const errors = {} as any;
|
||||
if (!values.name)
|
||||
errors.name = "Required";
|
||||
const onSubmit = (formData: unknown) => {
|
||||
mutation.mutate(formData as Notification);
|
||||
};
|
||||
|
||||
return errors;
|
||||
}
|
||||
const validate = (values: NotificationAddFormValues) => {
|
||||
const errors = {} as FormikErrors<FormikValues>;
|
||||
if (!values.name)
|
||||
errors.name = "Required";
|
||||
|
||||
return (
|
||||
<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"/>
|
||||
return errors;
|
||||
};
|
||||
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
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>
|
||||
return (
|
||||
<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"/>
|
||||
|
||||
<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 && 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 className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
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>
|
||||
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
<TextFieldWide name="name" label="Name" required={true}/>
|
||||
|
||||
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: 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 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>
|
||||
<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}}
|
||||
<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 && 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 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"/>
|
||||
</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">
|
||||
<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>
|
||||
<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 />
|
||||
<EventCheckBoxes />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{componentMap[values.type]}
|
||||
</div>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
{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>
|
||||
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from "react";
|
||||
|
||||
export function useToggle(initialValue = false): [boolean, () => void] {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const toggle = () => setValue(v => !v);
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const toggle = () => setValue(v => !v);
|
||||
|
||||
return [value, toggle];
|
||||
return [value, toggle];
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ window.APP = window.APP || {};
|
|||
InitializeGlobalContext();
|
||||
|
||||
ReactDOM.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
document.getElementById("root")
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { ReportHandler } from "web-vitals";
|
|||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Fragment } from "react";
|
||||
import { NavLink, Link, Route, Switch } from "react-router-dom";
|
||||
import type { match } from "react-router-dom";
|
||||
import { Link, NavLink, Route, Switch } from "react-router-dom";
|
||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||
|
@ -11,9 +11,9 @@ import { Logs } from "./Logs";
|
|||
import { Releases } from "./releases";
|
||||
import { Dashboard } from "./dashboard";
|
||||
import { FilterDetails, Filters } from "./filters";
|
||||
import { AuthContext } from '../utils/Context';
|
||||
import { AuthContext } from "../utils/Context";
|
||||
|
||||
import logo from '../logo.png';
|
||||
import logo from "../logo.png";
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
|
@ -21,229 +21,230 @@ interface NavItem {
|
|||
}
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const isActiveMatcher = (
|
||||
match: match<any> | null,
|
||||
location: { pathname: string },
|
||||
item: NavItem
|
||||
match: match | null,
|
||||
location: { pathname: string },
|
||||
item: NavItem
|
||||
) => {
|
||||
if (!match)
|
||||
return false;
|
||||
|
||||
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
|
||||
return true
|
||||
return true;
|
||||
|
||||
if (match.url === "/")
|
||||
return false;
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Base() {
|
||||
const authContext = AuthContext.useValue();
|
||||
const nav: Array<NavItem> = [
|
||||
{ name: 'Dashboard', path: "/" },
|
||||
{ name: 'Filters', path: "/filters" },
|
||||
{ name: 'Releases', path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
{ name: "Logs", path: "/logs" }
|
||||
];
|
||||
const authContext = AuthContext.useValue();
|
||||
const nav: Array<NavItem> = [
|
||||
{ name: "Dashboard", path: "/" },
|
||||
{ name: "Filters", path: "/filters" },
|
||||
{ name: "Releases", path: "/releases" },
|
||||
{ name: "Settings", path: "/settings" },
|
||||
{ name: "Logs", path: "/logs" }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Disclosure
|
||||
as="nav"
|
||||
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<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="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<img
|
||||
className="block lg:hidden h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
<img
|
||||
className="hidden lg:block h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:ml-3 hidden sm:block">
|
||||
<div className="flex items-baseline space-x-4">
|
||||
{nav.map((item, itemIdx) =>
|
||||
<NavLink
|
||||
key={item.name + itemIdx}
|
||||
to={item.path}
|
||||
strict
|
||||
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",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
activeClassName="text-black dark:text-gray-50 !font-bold"
|
||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://autobrr.com/docs/configuration/indexers"
|
||||
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",
|
||||
"transition-colors duration-200 flex items-center justify-center"
|
||||
)}
|
||||
>
|
||||
Docs
|
||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="ml-4 flex items-center sm:ml-6">
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-200 dark:bg-gray-800" : "",
|
||||
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
|
||||
"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>
|
||||
{authContext.username}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
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
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
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 }) => (
|
||||
<Link
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
||||
)}
|
||||
>
|
||||
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)}
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Disclosure
|
||||
as="nav"
|
||||
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<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="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<img
|
||||
className="block lg:hidden h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
<img
|
||||
className="hidden lg:block h-10 w-auto"
|
||||
src={logo}
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:ml-3 hidden sm:block">
|
||||
<div className="flex items-baseline space-x-4">
|
||||
{nav.map((item, itemIdx) =>
|
||||
<NavLink
|
||||
key={item.name + itemIdx}
|
||||
to={item.path}
|
||||
strict
|
||||
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",
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
activeClassName="text-black dark:text-gray-50 !font-bold"
|
||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://autobrr.com/docs/configuration/indexers"
|
||||
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",
|
||||
"transition-colors duration-200 flex items-center justify-center"
|
||||
)}
|
||||
>
|
||||
Docs
|
||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5"
|
||||
aria-hidden="true"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden sm:block">
|
||||
<div className="ml-4 flex items-center sm:ml-6">
|
||||
<Menu as="div" className="ml-3 relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={classNames(
|
||||
open ? "bg-gray-200 dark:bg-gray-800" : "",
|
||||
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
|
||||
"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>
|
||||
{authContext.username}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
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
|
||||
show={open}
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
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 }) => (
|
||||
<Link
|
||||
to="/settings"
|
||||
className={classNames(
|
||||
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</NavLink>
|
||||
)}
|
||||
<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>
|
||||
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>
|
||||
</>
|
||||
<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>
|
||||
<Route path="/logs">
|
||||
<Logs />
|
||||
</Route>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
<Route path="/settings">
|
||||
<Settings />
|
||||
</Route>
|
||||
<Switch>
|
||||
<Route path="/logs">
|
||||
<Logs/>
|
||||
</Route>
|
||||
|
||||
<Route path="/releases">
|
||||
<Releases />
|
||||
</Route>
|
||||
<Route path="/settings">
|
||||
<Settings/>
|
||||
</Route>
|
||||
|
||||
<Route exact={true} path="/filters">
|
||||
<Filters />
|
||||
</Route>
|
||||
<Route path="/releases">
|
||||
<Releases/>
|
||||
</Route>
|
||||
|
||||
<Route path="/filters/:filterId">
|
||||
<FilterDetails />
|
||||
</Route>
|
||||
<Route exact={true} path="/filters">
|
||||
<Filters/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/">
|
||||
<Dashboard />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
<Route path="/filters/:filterId">
|
||||
<FilterDetails/>
|
||||
</Route>
|
||||
|
||||
<Route exact path="/">
|
||||
<Dashboard/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const LogColors: Record<LogLevel, string> = {
|
|||
"TRACE": "text-purple-300",
|
||||
"DEBUG": "text-yellow-500",
|
||||
"INFO": "text-green-500",
|
||||
"ERROR": "text-red-500",
|
||||
"ERROR": "text-red-500"
|
||||
};
|
||||
|
||||
export const Logs = () => {
|
||||
|
@ -29,7 +29,7 @@ export const Logs = () => {
|
|||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const es = APIClient.events.logs();
|
||||
|
@ -40,7 +40,7 @@ export const Logs = () => {
|
|||
|
||||
if (settings.scrollOnNewLog)
|
||||
scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
return () => es.close();
|
||||
}, [setLogs, settings]);
|
||||
|
@ -96,7 +96,7 @@ export const Logs = () => {
|
|||
key={idx}
|
||||
className={classNames(
|
||||
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : "",
|
||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : ""
|
||||
)}
|
||||
>
|
||||
<span
|
||||
|
@ -112,7 +112,7 @@ export const Logs = () => {
|
|||
)}
|
||||
>
|
||||
{a.level}
|
||||
{' '}
|
||||
{" "}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="ml-2 text-black dark:text-gray-300">
|
||||
|
@ -125,5 +125,5 @@ export const Logs = () => {
|
|||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,120 +1,137 @@
|
|||
import {BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon} from '@heroicons/react/outline'
|
||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
||||
import { BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon } from "@heroicons/react/outline";
|
||||
import { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
|
||||
|
||||
import { classNames } from "../utils";
|
||||
import IndexerSettings from "./settings/Indexer";
|
||||
import { IrcSettings } from "./settings/Irc";
|
||||
import ApplicationSettings from "./settings/Application";
|
||||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import { RegexPlayground } from './settings/RegexPlayground';
|
||||
import { RegexPlayground } from "./settings/RegexPlayground";
|
||||
import ReleaseSettings from "./settings/Releases";
|
||||
import NotificationSettings from "./settings/Notifications";
|
||||
import FeedSettings from "./settings/Feed";
|
||||
|
||||
const subNavigation = [
|
||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
||||
{name: 'IRC', href: 'irc', icon: ChatAlt2Icon, current: false},
|
||||
{name: 'Feeds', href: 'feeds', icon: RssIcon, current: false},
|
||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
||||
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false},
|
||||
// {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>
|
||||
)
|
||||
interface NavTabType {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: typeof CogIcon;
|
||||
current: boolean;
|
||||
}
|
||||
|
||||
function SidebarNav({subNavigation, url}: any) {
|
||||
return (
|
||||
<aside className="py-2 lg:col-span-3">
|
||||
<nav className="space-y-1">
|
||||
{subNavigation.map((item: any) => (
|
||||
<SubNavLink item={item} url={url} key={item.href}/>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
)
|
||||
const subNavigation: NavTabType[] = [
|
||||
{ name: "Application", href: "", icon: CogIcon, current: true },
|
||||
{ name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
|
||||
{ name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
|
||||
{ name: "Feeds", href: "feeds", icon: RssIcon, current: false },
|
||||
{ name: "Clients", href: "clients", icon: DownloadIcon, current: false },
|
||||
{ name: "Notifications", href: "notifications", icon: BellIcon, current: false },
|
||||
{ name: "Releases", href: "releases", icon: CollectionIcon, current: false }
|
||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||
];
|
||||
|
||||
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() {
|
||||
const { url } = useRouteMatch();
|
||||
return (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
const { url } = useRouteMatch();
|
||||
return (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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="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}/>
|
||||
<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="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}/>
|
||||
|
||||
<RouteSwitch>
|
||||
<Route exact path={url}>
|
||||
<ApplicationSettings/>
|
||||
</Route>
|
||||
<RouteSwitch>
|
||||
<Route exact path={url}>
|
||||
<ApplicationSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/indexers`}>
|
||||
<IndexerSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/indexers`}>
|
||||
<IndexerSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/feeds`}>
|
||||
<FeedSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/feeds`}>
|
||||
<FeedSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/irc`}>
|
||||
<IrcSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/irc`}>
|
||||
<IrcSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/clients`}>
|
||||
<DownloadClientSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/clients`}>
|
||||
<DownloadClientSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/notifications`}>
|
||||
<NotificationSettings />
|
||||
</Route>
|
||||
<Route path={`${url}/notifications`}>
|
||||
<NotificationSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/releases`}>
|
||||
<ReleaseSettings/>
|
||||
</Route>
|
||||
<Route path={`${url}/releases`}>
|
||||
<ReleaseSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/regex-playground`}>
|
||||
<RegexPlayground />
|
||||
</Route>
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
<Route path={`${url}/regex-playground`}>
|
||||
<RegexPlayground />
|
||||
</Route>
|
||||
</RouteSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -34,11 +34,11 @@ export const Login = () => {
|
|||
isLoggedIn: true
|
||||
});
|
||||
history.push("/");
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
||||
const handleSubmit = (data: LoginData) => mutation.mutate(data);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
|
@ -75,4 +75,4 @@ export const Login = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -29,4 +29,4 @@ export const Logout = () => {
|
|||
<p>Logged out</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ export const Onboarding = () => {
|
|||
if (values.password1 !== values.password2)
|
||||
obj.password2 = "Passwords don't match!";
|
||||
|
||||
return obj;
|
||||
return obj;
|
||||
};
|
||||
|
||||
const history = useHistory();
|
||||
|
@ -37,7 +37,7 @@ export const Onboarding = () => {
|
|||
{
|
||||
onSuccess: () => {
|
||||
history.push("/login");
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -81,5 +81,5 @@ export const Onboarding = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
usePagination, FilterProps, Column
|
||||
} from "react-table";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -17,17 +17,17 @@ import * as DataTable from "../../components/data-table";
|
|||
// This is a custom filter UI for selecting
|
||||
// a unique option from a list
|
||||
function SelectColumnFilter({
|
||||
column: { filterValue, setFilter, preFilteredRows, id, render },
|
||||
}: any) {
|
||||
column: { filterValue, setFilter, preFilteredRows, id, render }
|
||||
}: FilterProps<object>) {
|
||||
// Calculate the options for filtering
|
||||
// using the preFilteredRows
|
||||
const options = React.useMemo(() => {
|
||||
const options: any = new Set()
|
||||
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => {
|
||||
options.add(row.values[id])
|
||||
})
|
||||
return [...options.values()]
|
||||
}, [id, preFilteredRows])
|
||||
const options = new Set<string>();
|
||||
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
|
||||
options.add(row.values[id]);
|
||||
});
|
||||
return [...options.values()];
|
||||
}, [id, preFilteredRows]);
|
||||
|
||||
// Render a multi-select box
|
||||
return (
|
||||
|
@ -39,7 +39,7 @@ function SelectColumnFilter({
|
|||
id={id}
|
||||
value={filterValue}
|
||||
onChange={e => {
|
||||
setFilter(e.target.value || undefined)
|
||||
setFilter(e.target.value || undefined);
|
||||
}}
|
||||
>
|
||||
<option value="">All</option>
|
||||
|
@ -50,17 +50,22 @@ function SelectColumnFilter({
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Table({ columns, data }: any) {
|
||||
interface TableProps {
|
||||
columns: Column[];
|
||||
data: Release[];
|
||||
}
|
||||
|
||||
function Table({ columns, data }: TableProps) {
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
page // Instead of using 'rows', we'll use page,
|
||||
} = useTable(
|
||||
{ columns, data },
|
||||
useFilters,
|
||||
|
@ -94,7 +99,7 @@ function Table({ columns, data }: any) {
|
|||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render('Header')}
|
||||
{column.render("Header")}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
|
@ -119,12 +124,12 @@ function Table({ columns, data }: any) {
|
|||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row: any) => {
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell: any) => {
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
|
@ -133,12 +138,12 @@ function Table({ columns, data }: any) {
|
|||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -151,30 +156,30 @@ export const ActivityTable = () => {
|
|||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: 'timestamp',
|
||||
Cell: DataTable.AgeCell,
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: 'torrent_name',
|
||||
Cell: DataTable.TitleCell,
|
||||
accessor: "torrent_name",
|
||||
Cell: DataTable.TitleCell
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: 'indexer',
|
||||
accessor: "indexer",
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: SelectColumnFilter,
|
||||
filter: 'includes',
|
||||
},
|
||||
], [])
|
||||
filter: "includes"
|
||||
}
|
||||
], []);
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
'dash_release',
|
||||
"dash_release",
|
||||
() => APIClient.release.find("?limit=10"),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
@ -188,7 +193,7 @@ export const ActivityTable = () => {
|
|||
Recent activity
|
||||
</h3>
|
||||
|
||||
<Table columns={columns} data={data?.data} />
|
||||
<Table columns={columns} data={data?.data ?? []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,41 +8,41 @@ interface StatsItemProps {
|
|||
|
||||
const StatsItem = ({ name, value }: StatsItemProps) => (
|
||||
<div
|
||||
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
|
||||
title="All time"
|
||||
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
|
||||
title="All time"
|
||||
>
|
||||
<dt>
|
||||
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
|
||||
</dt>
|
||||
<dt>
|
||||
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
|
||||
</dt>
|
||||
|
||||
<dd className="flex items-baseline">
|
||||
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
||||
</dd>
|
||||
<dd className="flex items-baseline">
|
||||
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
export const Stats = () => {
|
||||
const { isLoading, data } = useQuery(
|
||||
"dash_release_stats",
|
||||
() => APIClient.release.stats(),
|
||||
{ refetchOnWindowFocus: false }
|
||||
"dash_release_stats",
|
||||
() => APIClient.release.stats(),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading)
|
||||
return null;
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||
<div>
|
||||
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||
Stats
|
||||
</h3>
|
||||
</h3>
|
||||
|
||||
<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="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
|
||||
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
|
||||
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
||||
</dl>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<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="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
|
||||
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
|
||||
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -2,10 +2,10 @@ import { Stats } from "./Stats";
|
|||
import { ActivityTable } from "./ActivityTable";
|
||||
|
||||
export const Dashboard = () => (
|
||||
<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">
|
||||
<Stats />
|
||||
<ActivityTable />
|
||||
</div>
|
||||
</main>
|
||||
<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">
|
||||
<Stats />
|
||||
<ActivityTable />
|
||||
</div>
|
||||
</main>
|
||||
);
|
File diff suppressed because it is too large
Load diff
|
@ -4,10 +4,10 @@ import { toast } from "react-hot-toast";
|
|||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
DotsHorizontalIcon, DuplicateIcon,
|
||||
TrashIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
DotsHorizontalIcon, DuplicateIcon
|
||||
} from "@heroicons/react/outline";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
|
@ -20,50 +20,50 @@ import { EmptyListState } from "../../components/emptystates";
|
|||
import { DeleteModal } from "../../components/modals";
|
||||
|
||||
export default function Filters() {
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
|
||||
|
||||
const { isLoading, error, data } = useQuery(
|
||||
["filters"],
|
||||
APIClient.filters.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const { isLoading, error, data } = useQuery(
|
||||
["filters"],
|
||||
APIClient.filters.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading)
|
||||
return null;
|
||||
if (isLoading)
|
||||
return null;
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
||||
return (
|
||||
<main>
|
||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
||||
|
||||
<header className="py-10">
|
||||
<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">
|
||||
<header className="py-10">
|
||||
<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">
|
||||
Filters
|
||||
</h1>
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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}
|
||||
>
|
||||
</h1>
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
||||
{data && data.length > 0 ? (
|
||||
<FilterList filters={data} />
|
||||
) : (
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
||||
{data && data.length > 0 ? (
|
||||
<FilterList filters={data} />
|
||||
) : (
|
||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterListProps {
|
||||
|
@ -71,33 +71,33 @@ interface FilterListProps {
|
|||
}
|
||||
|
||||
function FilterList({ filters }: FilterListProps) {
|
||||
return (
|
||||
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
||||
<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">
|
||||
<tr>
|
||||
{["Enabled", "Name", "Indexers"].map((label) => (
|
||||
<th
|
||||
key={`th-${label}`}
|
||||
scope="col"
|
||||
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
||||
<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">
|
||||
<tr>
|
||||
{["Enabled", "Name", "Indexers"].map((label) => (
|
||||
<th
|
||||
key={`th-${label}`}
|
||||
scope="col"
|
||||
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||
>
|
||||
{label}
|
||||
</th>
|
||||
))}
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filters.map((filter: Filter, idx) => (
|
||||
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface FilterItemDropdownProps {
|
||||
|
@ -106,157 +106,157 @@ interface FilterItemDropdownProps {
|
|||
}
|
||||
|
||||
const FilterItemDropdown = ({
|
||||
filter,
|
||||
onToggle
|
||||
filter,
|
||||
onToggle
|
||||
}: FilterItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.filters.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter.id]);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.filters.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
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(
|
||||
(id: number) => APIClient.filters.duplicate(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
const duplicateMutation = useMutation(
|
||||
(id: number) => APIClient.filters.duplicate(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
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 (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(filter.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove filter: ${filter.name}`}
|
||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(filter.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove filter: ${filter.name}`}
|
||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
>
|
||||
<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">
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<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={() => onToggle(!filter.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<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={() => onToggle(!filter.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Toggle
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<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={() => duplicateMutation.mutate(filter.id)}
|
||||
>
|
||||
<DuplicateIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<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={() => duplicateMutation.mutate(filter.id)}
|
||||
>
|
||||
<DuplicateIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Duplicate
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
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"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
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"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterListItemProps {
|
||||
filter: Filter;
|
||||
|
@ -264,83 +264,83 @@ interface FilterListItemProps {
|
|||
}
|
||||
|
||||
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||
const [enabled, setEnabled] = useState(filter.enabled)
|
||||
const [enabled, setEnabled] = useState(filter.enabled);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />)
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />);
|
||||
|
||||
// We need to invalidate both keys here.
|
||||
// The filters key is used on the /filters page,
|
||||
// while the ["filter", filter.id] key is used on the details page.
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
// We need to invalidate both keys here.
|
||||
// The filters key is used on the /filters page,
|
||||
// while the ["filter", filter.id] key is used on the details page.
|
||||
queryClient.invalidateQueries(["filters"]);
|
||||
queryClient.invalidateQueries(["filters", filter?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<tr
|
||||
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]"
|
||||
)}
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
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
|
||||
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'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
||||
>
|
||||
{filter.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{filter.indexers && filter.indexers.map((t) => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
)
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</td>
|
||||
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<Link
|
||||
to={`filters/${filter.id.toString()}`}
|
||||
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
||||
>
|
||||
{filter.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{filter.indexers && filter.indexers.map((t) => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
|
@ -2,90 +2,91 @@ import * as React from "react";
|
|||
import { useQuery } from "react-query";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon
|
||||
} from "@heroicons/react/solid";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { classNames } from "../../utils";
|
||||
import { PushStatusOptions } from "../../domain/constants";
|
||||
import { FilterProps } from "react-table";
|
||||
|
||||
interface ListboxFilterProps {
|
||||
id: string;
|
||||
label: string;
|
||||
currentValue: string;
|
||||
onChange: (newValue: string) => void;
|
||||
children: any;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListboxFilter = ({
|
||||
id,
|
||||
label,
|
||||
currentValue,
|
||||
onChange,
|
||||
children
|
||||
id,
|
||||
label,
|
||||
currentValue,
|
||||
onChange,
|
||||
children
|
||||
}: ListboxFilterProps) => (
|
||||
<div className="w-48">
|
||||
<Listbox
|
||||
refName={id}
|
||||
value={currentValue}
|
||||
onChange={onChange}
|
||||
<div className="w-48">
|
||||
<Listbox
|
||||
refName={id}
|
||||
value={currentValue}
|
||||
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.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"
|
||||
>
|
||||
<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>
|
||||
<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
|
||||
export const IndexerSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: any) => {
|
||||
const { data, isSuccess } = useQuery(
|
||||
"release_indexers",
|
||||
() => APIClient.release.indexerOptions(),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity,
|
||||
}
|
||||
);
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
const { data, isSuccess } = useQuery(
|
||||
"release_indexers",
|
||||
() => APIClient.release.indexerOptions(),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
// Render a multi-select box
|
||||
return (
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={filterValue ?? "Indexer"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{isSuccess && data?.map((indexer, idx) => (
|
||||
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
)
|
||||
}
|
||||
// Render a multi-select box
|
||||
return (
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={filterValue ?? "Indexer"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{isSuccess && data?.map((indexer, idx) => (
|
||||
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
);
|
||||
};
|
||||
|
||||
interface FilterOptionProps {
|
||||
label: string;
|
||||
|
@ -93,50 +94,48 @@ interface FilterOptionProps {
|
|||
}
|
||||
|
||||
const FilterOption = ({ label, value }: FilterOptionProps) => (
|
||||
<Listbox.Option
|
||||
className={({ active }) => classNames(
|
||||
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
||||
active ? 'text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900' : 'text-gray-700 dark:text-gray-400'
|
||||
)}
|
||||
value={value}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
"block truncate",
|
||||
selected ? "font-medium text-black dark:text-white" : "font-normal"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<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" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
<Listbox.Option
|
||||
className={({ active }) => classNames(
|
||||
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
||||
active ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400"
|
||||
)}
|
||||
value={value}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
"block truncate",
|
||||
selected ? "font-medium text-black dark:text-white" : "font-normal"
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{selected ? (
|
||||
<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" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
|
||||
export const PushStatusSelectColumnFilter = ({
|
||||
column: { filterValue, setFilter, id }
|
||||
}: any) => (
|
||||
column: { filterValue, setFilter, id }
|
||||
}: FilterProps<object>) => {
|
||||
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
|
||||
return (
|
||||
<div className="mr-3">
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={
|
||||
filterValue
|
||||
? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label
|
||||
: "Push status"
|
||||
}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{PushStatusOptions.map((status, idx) => (
|
||||
<FilterOption key={idx} value={status.value} label={status.label} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
<ListboxFilter
|
||||
id={id}
|
||||
label={label ?? "Push status"}
|
||||
currentValue={filterValue}
|
||||
onChange={setFilter}
|
||||
>
|
||||
{PushStatusOptions.map((status, idx) => (
|
||||
<FilterOption key={idx} value={status.value} label={status.label} />
|
||||
))}
|
||||
</ListboxFilter>
|
||||
</div>
|
||||
);
|
||||
);};
|
|
@ -1,17 +1,17 @@
|
|||
import * as React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import {
|
||||
useTable,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useFilters,
|
||||
Column
|
||||
useTable,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useFilters,
|
||||
Column
|
||||
} from "react-table";
|
||||
import {
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronDoubleRightIcon
|
||||
ChevronDoubleLeftIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronDoubleRightIcon
|
||||
} from "@heroicons/react/solid";
|
||||
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
@ -21,295 +21,310 @@ import * as Icons from "../../components/Icons";
|
|||
import * as DataTable from "../../components/data-table";
|
||||
|
||||
import {
|
||||
IndexerSelectColumnFilter,
|
||||
PushStatusSelectColumnFilter
|
||||
IndexerSelectColumnFilter,
|
||||
PushStatusSelectColumnFilter
|
||||
} from "./Filters";
|
||||
|
||||
const initialState = {
|
||||
queryPageIndex: 0,
|
||||
queryPageSize: 10,
|
||||
totalCount: null,
|
||||
queryFilters: []
|
||||
type TableState = {
|
||||
queryPageIndex: number;
|
||||
queryPageSize: number;
|
||||
totalCount: number;
|
||||
queryFilters: ReleaseFilter[];
|
||||
};
|
||||
|
||||
const PAGE_CHANGED = 'PAGE_CHANGED';
|
||||
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
|
||||
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
|
||||
const FILTER_CHANGED = 'FILTER_CHANGED';
|
||||
const initialState: TableState = {
|
||||
queryPageIndex: 0,
|
||||
queryPageSize: 10,
|
||||
totalCount: 0,
|
||||
queryFilters: []
|
||||
};
|
||||
|
||||
const TableReducer = (state: any, { type, payload }: any) => {
|
||||
switch (type) {
|
||||
case PAGE_CHANGED:
|
||||
return { ...state, queryPageIndex: payload };
|
||||
case PAGE_SIZE_CHANGED:
|
||||
return { ...state, queryPageSize: payload };
|
||||
case TOTAL_COUNT_CHANGED:
|
||||
return { ...state, totalCount: payload };
|
||||
case FILTER_CHANGED:
|
||||
return { ...state, queryFilters: payload };
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${type}`);
|
||||
}
|
||||
enum ActionType {
|
||||
PAGE_CHANGED = "PAGE_CHANGED",
|
||||
PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
|
||||
TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
|
||||
FILTER_CHANGED = "FILTER_CHANGED"
|
||||
}
|
||||
|
||||
type Actions =
|
||||
| { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
|
||||
| { type: ActionType.PAGE_CHANGED; payload: number; }
|
||||
| { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
|
||||
| { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
|
||||
|
||||
const TableReducer = (state: TableState, action: Actions): TableState => {
|
||||
switch (action.type) {
|
||||
case ActionType.PAGE_CHANGED:
|
||||
return { ...state, queryPageIndex: action.payload };
|
||||
case ActionType.PAGE_SIZE_CHANGED:
|
||||
return { ...state, queryPageSize: action.payload };
|
||||
case ActionType.FILTER_CHANGED:
|
||||
return { ...state, queryFilters: action.payload };
|
||||
case ActionType.TOTAL_COUNT_CHANGED:
|
||||
return { ...state, totalCount: action.payload };
|
||||
default:
|
||||
throw new Error(`Unhandled action type: ${action}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const ReleaseTable = () => {
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: 'timestamp',
|
||||
Cell: DataTable.AgeCell,
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: 'torrent_name',
|
||||
Cell: DataTable.TitleCell,
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
Filter: PushStatusSelectColumnFilter,
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: 'indexer',
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: IndexerSelectColumnFilter,
|
||||
filter: 'equal',
|
||||
},
|
||||
] as Column<Release>[], [])
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
accessor: "timestamp",
|
||||
Cell: DataTable.AgeCell
|
||||
},
|
||||
{
|
||||
Header: "Release",
|
||||
accessor: "torrent_name",
|
||||
Cell: DataTable.TitleCell
|
||||
},
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: "action_status",
|
||||
Cell: DataTable.ReleaseStatusCell,
|
||||
Filter: PushStatusSelectColumnFilter
|
||||
},
|
||||
{
|
||||
Header: "Indexer",
|
||||
accessor: "indexer",
|
||||
Cell: DataTable.TitleCell,
|
||||
Filter: IndexerSelectColumnFilter,
|
||||
filter: "equal"
|
||||
}
|
||||
] as Column<Release>[], []);
|
||||
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||
React.useReducer(TableReducer, initialState);
|
||||
|
||||
const { isLoading, error, data, isSuccess } = useQuery(
|
||||
['releases', queryPageIndex, queryPageSize, queryFilters],
|
||||
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 5000,
|
||||
}
|
||||
);
|
||||
const { isLoading, error, data, isSuccess } = useQuery(
|
||||
["releases", queryPageIndex, queryPageSize, queryFilters],
|
||||
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 5000
|
||||
}
|
||||
);
|
||||
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
// which has only the rows for the active page
|
||||
// Use the state and functions returned from useTable to build your UI
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
prepareRow,
|
||||
page, // Instead of using 'rows', we'll use page,
|
||||
// which has only the rows for the active page
|
||||
|
||||
// The rest of these things are super handy, too ;)
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize, filters }
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data: data && isSuccess ? data.data : [],
|
||||
initialState: {
|
||||
pageIndex: queryPageIndex,
|
||||
pageSize: queryPageSize,
|
||||
filters: []
|
||||
},
|
||||
manualPagination: true,
|
||||
manualFilters: true,
|
||||
manualSortBy: true,
|
||||
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
||||
autoResetSortBy: false,
|
||||
autoResetExpanded: false,
|
||||
autoResetPage: false
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
);
|
||||
// The rest of these things are super handy, too ;)
|
||||
canPreviousPage,
|
||||
canNextPage,
|
||||
pageOptions,
|
||||
pageCount,
|
||||
gotoPage,
|
||||
nextPage,
|
||||
previousPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize, filters }
|
||||
} = useTable(
|
||||
{
|
||||
columns,
|
||||
data: data && isSuccess ? data.data : [],
|
||||
initialState: {
|
||||
pageIndex: queryPageIndex,
|
||||
pageSize: queryPageSize,
|
||||
filters: []
|
||||
},
|
||||
manualPagination: true,
|
||||
manualFilters: true,
|
||||
manualSortBy: true,
|
||||
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
||||
autoResetSortBy: false,
|
||||
autoResetExpanded: false,
|
||||
autoResetPage: false
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
|
||||
}, [pageIndex]);
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
||||
}, [pageIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
|
||||
gotoPage(0);
|
||||
}, [pageSize, gotoPage]);
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
|
||||
gotoPage(0);
|
||||
}, [pageSize, gotoPage]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data?.count) {
|
||||
dispatch({
|
||||
type: TOTAL_COUNT_CHANGED,
|
||||
payload: data.count,
|
||||
});
|
||||
}
|
||||
}, [data?.count]);
|
||||
React.useEffect(() => {
|
||||
if (data?.count) {
|
||||
dispatch({
|
||||
type: ActionType.TOTAL_COUNT_CHANGED,
|
||||
payload: data.count
|
||||
});
|
||||
}
|
||||
}, [data?.count]);
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: FILTER_CHANGED, payload: filters });
|
||||
}, [filters]);
|
||||
React.useEffect(() => {
|
||||
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
|
||||
}, [filters]);
|
||||
|
||||
if (error)
|
||||
return <p>Error</p>;
|
||||
if (error)
|
||||
return <p>Error</p>;
|
||||
|
||||
if (isLoading)
|
||||
return <p>Loading...</p>;
|
||||
if (isLoading)
|
||||
return <p>Loading...</p>;
|
||||
|
||||
if (!data)
|
||||
return <EmptyListState text="No recent activity" />
|
||||
if (!data)
|
||||
return <EmptyListState text="No recent activity" />;
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex mb-6">
|
||||
{headerGroups.map((headerGroup: { headers: any[] }) =>
|
||||
headerGroup.headers.map((column) => (
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<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">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={rowKey} {...rowRest}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||
return (
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th
|
||||
key={`${rowKey}-${columnKey}`}
|
||||
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"
|
||||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render('Header')}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<Icons.SortDownIcon 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" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row: any) => {
|
||||
prepareRow(row);
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex mb-6">
|
||||
{headerGroups.map((headerGroup) =>
|
||||
headerGroup.headers.map((column) => (
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<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">
|
||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<tr key={rowKey} {...rowRest}>
|
||||
{headerGroup.headers.map((column) => {
|
||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||
return (
|
||||
// Add the sorting props to control sorting. For this example
|
||||
// we can add them into the header props
|
||||
<th
|
||||
key={`${rowKey}-${columnKey}`}
|
||||
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"
|
||||
{...columnRest}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{column.render("Header")}
|
||||
{/* Add a sort direction indicator */}
|
||||
<span>
|
||||
{column.isSorted ? (
|
||||
column.isSortedDesc ? (
|
||||
<Icons.SortDownIcon 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" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
{...getTableBodyProps()}
|
||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||
>
|
||||
{page.map((row) => {
|
||||
prepareRow(row);
|
||||
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell: any) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
key={cellRowKey}
|
||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render('Cell')}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||
return (
|
||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||
{row.cells.map((cell) => {
|
||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||
return (
|
||||
<td
|
||||
key={cellRowKey}
|
||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||
role="cell"
|
||||
{...cellRowRest}
|
||||
>
|
||||
{cell.render("Cell")}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* Pagination */}
|
||||
<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">
|
||||
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
||||
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
{/* Pagination */}
|
||||
<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">
|
||||
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
||||
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
||||
</span>
|
||||
<label>
|
||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||
<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"
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
</span>
|
||||
<label>
|
||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||
<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"
|
||||
value={pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { ReleaseTable } from "./ReleaseTable";
|
||||
|
||||
export const Releases = () => (
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<ReleaseTable />
|
||||
</div>
|
||||
</main>
|
||||
<main>
|
||||
<header className="py-10">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||
<ReleaseTable />
|
||||
</div>
|
||||
</main>
|
||||
);
|
|
@ -1,79 +1,79 @@
|
|||
function ActionSettings() {
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
{/*{addClientIsOpen &&*/}
|
||||
{/*<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">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
{/*{addClientIsOpen &&*/}
|
||||
{/*<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">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Manage actions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
// onClick={toggleAddClient}
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
// onClick={toggleAddClient}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Port
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>empty</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
<div className="flex flex-col mt-6">
|
||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Type
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Port
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
Enabled
|
||||
</th>
|
||||
<th scope="col" className="relative px-6 py-3">
|
||||
<span className="sr-only">Edit</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>empty</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionSettings;
|
|
@ -4,128 +4,127 @@ import { APIClient } from "../../api/APIClient";
|
|||
import { Checkbox } from "../../components/Checkbox";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
|
||||
function ApplicationSettings() {
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const { isLoading, data } = useQuery(
|
||||
['config'],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: err => console.log(err)
|
||||
}
|
||||
);
|
||||
const { isLoading, data } = useQuery(
|
||||
["config"],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: err => console.log(err)
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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">
|
||||
return (
|
||||
<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>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Application settings. Change in config.toml and restart to take effect.
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isLoading && data && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
{!isLoading && data && (
|
||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Host
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 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>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
id="host"
|
||||
value={data.host}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 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 className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<div className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 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>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
id="port"
|
||||
value={data.port}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 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 className="col-span-6 sm:col-span-4">
|
||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||
<div className="col-span-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">
|
||||
Base url
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 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>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="base_url"
|
||||
id="base_url"
|
||||
value={data.base_url}
|
||||
disabled={true}
|
||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 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 className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data?.version ? (
|
||||
<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>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.commit ? (
|
||||
<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>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.date ? (
|
||||
<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>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Debug"
|
||||
description="Enable debug mode to get more logs."
|
||||
value={settings.debug}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
debug: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme."
|
||||
value={settings.darkTheme}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
darkTheme: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 py-5 sm:p-0">
|
||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data?.version ? (
|
||||
<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>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.commit ? (
|
||||
<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>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
{data?.date ? (
|
||||
<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>
|
||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
</div>
|
||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Debug"
|
||||
description="Enable debug mode to get more logs."
|
||||
value={settings.debug}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
debug: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 sm:px-6 py-1">
|
||||
<Checkbox
|
||||
label="Dark theme"
|
||||
description="Switch between dark and light theme."
|
||||
value={settings.darkTheme}
|
||||
setValue={(newValue: boolean) => setSettings({
|
||||
...settings,
|
||||
darkTheme: newValue
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationSettings;
|
|
@ -13,132 +13,132 @@ interface DLSettingsItemProps {
|
|||
}
|
||||
|
||||
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
|
||||
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||
return (
|
||||
<tr key={client.name} className={idx % 2 === 0 ? "light:bg-white" : "light:bg-gray-50"}>
|
||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<Switch
|
||||
checked={client.enabled}
|
||||
onChange={toggleUpdateClient}
|
||||
className={classNames(
|
||||
client.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
client.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</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">{DownloadClientTypeNameMap[client.type]}</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 cursor-pointer" onClick={toggleUpdateClient}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<Switch
|
||||
checked={client.enabled}
|
||||
onChange={toggleUpdateClient}
|
||||
className={classNames(
|
||||
client.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
client.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</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">{DownloadClientTypeNameMap[client.type]}</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 cursor-pointer" onClick={toggleUpdateClient}>
|
||||
Edit
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function DownloadClientSettings() {
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false);
|
||||
|
||||
const { error, data } = useQuery(
|
||||
'downloadClients',
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const { error, data } = useQuery(
|
||||
"downloadClients",
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
return (
|
||||
<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="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage download clients.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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}
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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}
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</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;
|
|
@ -3,147 +3,148 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
|||
import { APIClient } from "../../api/APIClient";
|
||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||
|
||||
import {classNames} from "../../utils";
|
||||
import {Fragment, useRef, useState} from "react";
|
||||
import {toast} from "react-hot-toast";
|
||||
import { classNames } from "../../utils";
|
||||
import { Fragment, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {queryClient} from "../../App";
|
||||
import {DeleteModal} from "../../components/modals";
|
||||
import { queryClient } from "../../App";
|
||||
import { DeleteModal } from "../../components/modals";
|
||||
import {
|
||||
DotsHorizontalIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
TrashIcon
|
||||
DotsHorizontalIcon,
|
||||
PencilAltIcon,
|
||||
SwitchHorizontalIcon,
|
||||
TrashIcon
|
||||
} from "@heroicons/react/outline";
|
||||
import {FeedUpdateForm} from "../../forms/settings/FeedForms";
|
||||
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||
|
||||
function FeedSettings() {
|
||||
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { data } = useQuery<Feed[], Error>("feeds", APIClient.feeds.find,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<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="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
return (
|
||||
<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="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Manage Torznab feeds.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 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>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full relative">
|
||||
<li className="grid grid-cols-12 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 = () => (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
Torznab
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
);
|
||||
|
||||
export const ImplementationMap: any = {
|
||||
"TORZNAB": <ImplementationTorznab/>,
|
||||
export const ImplementationMap: componentMapType = {
|
||||
"TORZNAB": <ImplementationTorznab/>
|
||||
};
|
||||
|
||||
interface ListItemProps {
|
||||
feed: Feed;
|
||||
}
|
||||
|
||||
function ListItem({feed}: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
function ListItem({ feed }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||
|
||||
const [enabled, setEnabled] = useState(feed.enabled)
|
||||
const [enabled, setEnabled] = useState(feed.enabled);
|
||||
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success"
|
||||
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
||||
t={t}/>)
|
||||
const updateMutation = useMutation(
|
||||
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success"
|
||||
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
||||
t={t}/>);
|
||||
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed?.id]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
||||
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
|
||||
const toggleActive = (status: boolean) => {
|
||||
setEnabled(status);
|
||||
updateMutation.mutate(status);
|
||||
};
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={feed.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
feed.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
feed.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{feed.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6">
|
||||
{ImplementationMap[feed.type]}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6">
|
||||
<FeedItemDropdown
|
||||
feed={feed}
|
||||
onToggle={toggleActive}
|
||||
toggleUpdate={toggleUpdateForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
return (
|
||||
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
||||
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={feed.enabled}
|
||||
onChange={toggleActive}
|
||||
className={classNames(
|
||||
feed.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
feed.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{feed.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6">
|
||||
{ImplementationMap[feed.type]}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6">
|
||||
<FeedItemDropdown
|
||||
feed={feed}
|
||||
onToggle={toggleActive}
|
||||
toggleUpdate={toggleUpdateForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
interface FeedItemDropdownProps {
|
||||
|
@ -153,126 +154,126 @@ interface FeedItemDropdownProps {
|
|||
}
|
||||
|
||||
const FeedItemDropdown = ({
|
||||
feed,
|
||||
onToggle,
|
||||
toggleUpdate,
|
||||
}: FeedItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
feed,
|
||||
onToggle,
|
||||
toggleUpdate
|
||||
}: FeedItemDropdownProps) => {
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.feeds.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
queryClient.invalidateQueries(["feeds", feed.id]);
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(
|
||||
(id: number) => APIClient.feeds.delete(id),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["feeds"]);
|
||||
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 (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove feed: ${feed.name}`}
|
||||
text="Are you sure you want to remove this feed? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
return (
|
||||
<Menu as="div">
|
||||
<DeleteModal
|
||||
isOpen={deleteModalIsOpen}
|
||||
toggle={toggleDeleteModal}
|
||||
buttonRef={cancelModalButtonRef}
|
||||
deleteAction={() => {
|
||||
deleteMutation.mutate(feed.id);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
title={`Remove feed: ${feed.name}`}
|
||||
text="Are you sure you want to remove this feed? This action cannot be undone."
|
||||
/>
|
||||
<Menu.Button className="px-4 py-2">
|
||||
<DotsHorizontalIcon
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items
|
||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||
>
|
||||
<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">
|
||||
<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()}
|
||||
>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<PencilAltIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<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={() => onToggle(!feed.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<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={() => onToggle(!feed.enabled)}
|
||||
>
|
||||
<SwitchHorizontalIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-blue-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Toggle
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({active}) => (
|
||||
<button
|
||||
className={classNames(
|
||||
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"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
className={classNames(
|
||||
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"
|
||||
)}
|
||||
onClick={() => toggleDeleteModal()}
|
||||
>
|
||||
<TrashIcon
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-red-500",
|
||||
"w-5 h-5 mr-2"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeedSettings;
|
|
@ -5,148 +5,153 @@ import { Switch } from "@headlessui/react";
|
|||
import { classNames } from "../../utils";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||
|
||||
const ImplementationIRC = () => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
IRC
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
);
|
||||
|
||||
const ImplementationTorznab = () => (
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||
>
|
||||
Torznab
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
);
|
||||
|
||||
const implementationMap: any = {
|
||||
"irc": <ImplementationIRC/>,
|
||||
"torznab": <ImplementationTorznab />,
|
||||
const implementationMap: componentMapType = {
|
||||
"irc": <ImplementationIRC/>,
|
||||
"torznab": <ImplementationTorznab />
|
||||
};
|
||||
|
||||
const ListItem = ({ indexer }: any) => {
|
||||
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}
|
||||
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>
|
||||
)
|
||||
interface ListItemProps {
|
||||
indexer: IndexerDefinition;
|
||||
}
|
||||
|
||||
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() {
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
|
||||
|
||||
const { error, data } = useQuery(
|
||||
'indexer',
|
||||
APIClient.indexers.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
const { error, data } = useQuery(
|
||||
"indexer",
|
||||
APIClient.indexers.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred</p>);
|
||||
if (error)
|
||||
return (<p>An error has occurred</p>);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
return (
|
||||
<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="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Indexer settings.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddIndexer}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddIndexer}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</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;
|
|
@ -1,73 +1,73 @@
|
|||
import { useQuery } from "react-query";
|
||||
|
||||
import {
|
||||
simplifyDate,
|
||||
IsEmptyDate
|
||||
simplifyDate,
|
||||
IsEmptyDate
|
||||
} from "../../utils";
|
||||
import {
|
||||
IrcNetworkAddForm,
|
||||
IrcNetworkUpdateForm
|
||||
IrcNetworkAddForm,
|
||||
IrcNetworkUpdateForm
|
||||
} from "../../forms";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
|
||||
export const IrcSettings = () => {
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
|
||||
|
||||
const { data } = useQuery(
|
||||
"networks",
|
||||
APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
// Refetch every 3 seconds
|
||||
refetchInterval: 3000
|
||||
}
|
||||
);
|
||||
const { data } = useQuery(
|
||||
"networks",
|
||||
APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
// Refetch every 3 seconds
|
||||
refetchInterval: 3000
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNetwork}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm 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"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNetwork}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm 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
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</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 {
|
||||
idx: number;
|
||||
|
@ -75,83 +75,83 @@ interface ListItemProps {
|
|||
}
|
||||
|
||||
const ListItem = ({ idx, network }: ListItemProps) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
const [edit, toggleEdit] = useToggle(false);
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||
const [edit, toggleEdit] = useToggle(false);
|
||||
|
||||
return (
|
||||
return (
|
||||
|
||||
<li key={idx} >
|
||||
<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} />
|
||||
<li key={idx} >
|
||||
<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} />
|
||||
|
||||
<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">
|
||||
{
|
||||
network.enabled ? (
|
||||
network.connected ? (
|
||||
<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="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{network.name}
|
||||
</span>
|
||||
</div>
|
||||
<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">
|
||||
{
|
||||
network.enabled ? (
|
||||
network.connected ? (
|
||||
<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="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{network.name}
|
||||
</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 ? (
|
||||
<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}
|
||||
<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}>
|
||||
<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 ? (
|
||||
<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}
|
||||
<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}>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{edit && (
|
||||
<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">
|
||||
{network.channels.length > 0 ? (
|
||||
<ol>
|
||||
<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">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>
|
||||
</li>
|
||||
{network.channels.map(c => (
|
||||
<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="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
network.enabled ? (
|
||||
c.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="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{c.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{edit && (
|
||||
<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">
|
||||
{network.channels.length > 0 ? (
|
||||
<ol>
|
||||
<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">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>
|
||||
</li>
|
||||
{network.channels.map(c => (
|
||||
<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="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="relative inline-flex items-center">
|
||||
{
|
||||
network.enabled ? (
|
||||
c.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="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||
</span>
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||
}
|
||||
{c.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
|
||||
</div>
|
||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,56 +7,56 @@ import { Switch } from "@headlessui/react";
|
|||
import { classNames } from "../../utils";
|
||||
|
||||
function NotificationSettings() {
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false)
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
|
||||
|
||||
const { data } = useQuery<Notification[], Error>('notifications', APIClient.notifications.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { data } = useQuery<Notification[], Error>("notifications", APIClient.notifications.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Send notifications on events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-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
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</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 {
|
||||
|
@ -64,56 +64,56 @@ interface ListItemProps {
|
|||
}
|
||||
|
||||
function ListItem({ notification }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||
|
||||
return (
|
||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
return (
|
||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
className={classNames(
|
||||
notification.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.type}
|
||||
</div>
|
||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||
{notification.events.map((n, idx) => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<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}>
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
className={classNames(
|
||||
notification.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.enabled ? "translate-x-5" : "translate-x-0",
|
||||
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.type}
|
||||
</div>
|
||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||
{notification.events.map((n, idx) => (
|
||||
<span
|
||||
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"
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<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}>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotificationSettings;
|
|
@ -1,106 +1,105 @@
|
|||
import { useRef, useState } from "react";
|
||||
|
||||
export const RegexPlayground = () => {
|
||||
const regexRef = useRef<HTMLInputElement>(null);
|
||||
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
||||
const regexRef = useRef<HTMLInputElement>(null);
|
||||
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
||||
|
||||
const onInput = (text: string) => {
|
||||
if (!regexRef || !regexRef.current)
|
||||
return;
|
||||
const onInput = (text: string) => {
|
||||
if (!regexRef || !regexRef.current)
|
||||
return;
|
||||
|
||||
const regexp = new RegExp(regexRef.current.value, "g");
|
||||
const regexp = new RegExp(regexRef.current.value, "g");
|
||||
|
||||
const results: Array<React.ReactElement> = [];
|
||||
text.split("\n").forEach((line, index) => {
|
||||
const matches = line.matchAll(regexp);
|
||||
const results: Array<React.ReactElement> = [];
|
||||
text.split("\n").forEach((line, index) => {
|
||||
const matches = line.matchAll(regexp);
|
||||
|
||||
let lastIndex = 0;
|
||||
// @ts-ignore
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined)
|
||||
continue;
|
||||
let lastIndex = 0;
|
||||
for (const match of matches) {
|
||||
if (match.index === undefined)
|
||||
continue;
|
||||
|
||||
if (!match.length)
|
||||
continue;
|
||||
if (!match.length)
|
||||
continue;
|
||||
|
||||
const start = match.index;
|
||||
const start = match.index;
|
||||
|
||||
let length = 0;
|
||||
match.forEach((group) => length += group.length);
|
||||
let length = 0;
|
||||
match.forEach((group) => length += group.length);
|
||||
|
||||
results.push(
|
||||
<span key={`match-${start}`}>
|
||||
{line.substring(lastIndex, start)}
|
||||
<span className="bg-blue-300 text-black font-bold">
|
||||
{line.substring(start, start + length)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
lastIndex = start + length;
|
||||
}
|
||||
results.push(
|
||||
<span key={`match-${start}`}>
|
||||
{line.substring(lastIndex, start)}
|
||||
<span className="bg-blue-300 text-black font-bold">
|
||||
{line.substring(start, start + length)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
lastIndex = start + length;
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
results.push(
|
||||
<span key={`last-${lastIndex + 1}`}>
|
||||
{line.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (lastIndex < line.length) {
|
||||
results.push(
|
||||
<span key={`last-${lastIndex + 1}`}>
|
||||
{line.substring(lastIndex)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (lastIndex > 0)
|
||||
results.push(<br key={`line-delim-${index}`}/>);
|
||||
});
|
||||
if (lastIndex > 0)
|
||||
results.push(<br key={`line-delim-${index}`}/>);
|
||||
});
|
||||
|
||||
setOutput(results);
|
||||
}
|
||||
setOutput(results);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Regex playground. Experiment with your filters here. WIP.
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
};
|
|
@ -9,73 +9,73 @@ import { useToggle } from "../../hooks/hooks";
|
|||
import { DeleteModal } from "../../components/modals";
|
||||
|
||||
function ReleaseSettings() {
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={`All releases was deleted`} t={t}/>
|
||||
));
|
||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => (
|
||||
<Toast type="success" body={"All releases was deleted"} t={t}/>
|
||||
));
|
||||
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries("releases");
|
||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||
queryClient.invalidateQueries("releases");
|
||||
|
||||
toggleDeleteModal()
|
||||
}
|
||||
})
|
||||
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate()
|
||||
toggleDeleteModal();
|
||||
}
|
||||
});
|
||||
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
const deleteAction = () => {
|
||||
deleteMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<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."
|
||||
/>
|
||||
const cancelModalButtonRef = useRef(null);
|
||||
|
||||
<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">
|
||||
return (
|
||||
<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">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</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 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>
|
||||
|
||||
<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">
|
||||
<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
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Delete all releases
|
||||
</button>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
</button>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReleaseSettings;
|
|
@ -1,11 +1,11 @@
|
|||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||
|
||||
module.exports = function(app) {
|
||||
app.use(
|
||||
'/api',
|
||||
createProxyMiddleware({
|
||||
target: 'http://127.0.0.1:7474',
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
"/api",
|
||||
createProxyMiddleware({
|
||||
target: "http://127.0.0.1:7474",
|
||||
changeOrigin: true,
|
||||
})
|
||||
);
|
||||
};
|
|
@ -2,4 +2,4 @@
|
|||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import "@testing-library/jest-dom";
|
||||
|
|
25
web/src/types/Download.d.ts
vendored
25
web/src/types/Download.d.ts
vendored
|
@ -1,11 +1,21 @@
|
|||
type DownloadClientType =
|
||||
'QBITTORRENT' |
|
||||
'DELUGE_V1' |
|
||||
'DELUGE_V2' |
|
||||
'RADARR' |
|
||||
'SONARR' |
|
||||
'LIDARR' |
|
||||
'WHISPARR';
|
||||
"QBITTORRENT" |
|
||||
"DELUGE_V1" |
|
||||
"DELUGE_V2" |
|
||||
"RADARR" |
|
||||
"SONARR" |
|
||||
"LIDARR" |
|
||||
"WHISPARR";
|
||||
|
||||
// export enum DownloadClientTypeEnum {
|
||||
// QBITTORRENT = "QBITTORRENT",
|
||||
// DELUGE_V1 = "DELUGE_V1",
|
||||
// DELUGE_V2 = "DELUGE_V2",
|
||||
// RADARR = "RADARR",
|
||||
// SONARR = "SONARR",
|
||||
// LIDARR = "LIDARR",
|
||||
// WHISPARR = "WHISPARR"
|
||||
// }
|
||||
|
||||
interface DownloadClientRules {
|
||||
enabled: boolean;
|
||||
|
@ -27,7 +37,6 @@ interface DownloadClientSettings {
|
|||
}
|
||||
|
||||
interface DownloadClient {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
type: DownloadClientType;
|
||||
|
|
36
web/src/types/Feed.d.ts
vendored
36
web/src/types/Feed.d.ts
vendored
|
@ -1,23 +1,23 @@
|
|||
interface Feed {
|
||||
id: number;
|
||||
indexer: string;
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
interval: number;
|
||||
api_key: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
id: number;
|
||||
indexer: string;
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
interval: number;
|
||||
api_key: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
interface FeedCreate {
|
||||
indexer: string;
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
interval: number;
|
||||
api_key: string;
|
||||
indexer_id: number;
|
||||
indexer: string;
|
||||
name: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
interval: number;
|
||||
api_key: string;
|
||||
indexer_id: number;
|
||||
}
|
||||
|
|
2
web/src/types/Filter.d.ts
vendored
2
web/src/types/Filter.d.ts
vendored
|
@ -83,4 +83,4 @@ interface Action {
|
|||
client_id?: number;
|
||||
}
|
||||
|
||||
type ActionType = 'TEST' | 'EXEC' | 'WATCH_FOLDER' | 'WEBHOOK' | DownloadClientType;
|
||||
type ActionType = "TEST" | "EXEC" | "WATCH_FOLDER" | "WEBHOOK" | DownloadClientType;
|
||||
|
|
2
web/src/types/Global.d.ts
vendored
2
web/src/types/Global.d.ts
vendored
|
@ -1,3 +1,3 @@
|
|||
interface APP {
|
||||
baseUrl: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
|
2
web/src/types/Indexer.d.ts
vendored
2
web/src/types/Indexer.d.ts
vendored
|
@ -8,7 +8,7 @@ interface Indexer {
|
|||
}
|
||||
|
||||
interface IndexerDefinition {
|
||||
id?: number;
|
||||
id: number;
|
||||
name: string;
|
||||
identifier: string;
|
||||
implementation: string;
|
||||
|
|
60
web/src/types/Irc.d.ts
vendored
60
web/src/types/Irc.d.ts
vendored
|
@ -1,29 +1,29 @@
|
|||
interface IrcNetwork {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
pass: string;
|
||||
invite_command: string;
|
||||
nickserv?: NickServ; // optional
|
||||
channels: IrcChannel[];
|
||||
connected: boolean;
|
||||
connected_since: Time;
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
pass: string;
|
||||
invite_command: string;
|
||||
nickserv?: NickServ; // optional
|
||||
channels: IrcChannel[];
|
||||
connected: boolean;
|
||||
connected_since: string;
|
||||
}
|
||||
|
||||
interface IrcNetworkCreate {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
pass: string;
|
||||
invite_command: string;
|
||||
nickserv?: NickServ; // optional
|
||||
channels: IrcChannel[];
|
||||
connected: boolean;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
port: number;
|
||||
tls: boolean;
|
||||
pass: string;
|
||||
invite_command: string;
|
||||
nickserv?: NickServ; // optional
|
||||
channels: IrcChannel[];
|
||||
connected: boolean;
|
||||
}
|
||||
|
||||
interface IrcChannel {
|
||||
|
@ -61,12 +61,12 @@ interface NickServ {
|
|||
}
|
||||
|
||||
interface Config {
|
||||
host: string;
|
||||
port: number;
|
||||
log_level: string;
|
||||
log_path: string;
|
||||
base_url: string;
|
||||
version: string;
|
||||
commit: string;
|
||||
date: string;
|
||||
host: string;
|
||||
port: number;
|
||||
log_level: string;
|
||||
log_path: string;
|
||||
base_url: string;
|
||||
version: string;
|
||||
commit: string;
|
||||
date: string;
|
||||
}
|
14
web/src/types/Notification.d.ts
vendored
14
web/src/types/Notification.d.ts
vendored
|
@ -1,10 +1,10 @@
|
|||
type NotificationType = 'DISCORD';
|
||||
type NotificationType = "DISCORD";
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
type: NotificationType;
|
||||
events: string[];
|
||||
webhook: string;
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
type: NotificationType;
|
||||
events: string[];
|
||||
webhook: string;
|
||||
}
|
50
web/src/types/Release.d.ts
vendored
50
web/src/types/Release.d.ts
vendored
|
@ -1,38 +1,38 @@
|
|||
interface Release {
|
||||
id: number;
|
||||
filter_status: string;
|
||||
rejections: string[];
|
||||
indexer: string;
|
||||
filter: string;
|
||||
protocol: string;
|
||||
title: string;
|
||||
size: number;
|
||||
raw: string;
|
||||
timestamp: Date
|
||||
action_status: ReleaseActionStatus[]
|
||||
id: number;
|
||||
filter_status: string;
|
||||
rejections: string[];
|
||||
indexer: string;
|
||||
filter: string;
|
||||
protocol: string;
|
||||
title: string;
|
||||
size: number;
|
||||
raw: string;
|
||||
timestamp: Date
|
||||
action_status: ReleaseActionStatus[]
|
||||
}
|
||||
|
||||
interface ReleaseActionStatus {
|
||||
id: number;
|
||||
status: string;
|
||||
action: string;
|
||||
type: string;
|
||||
rejections: string[];
|
||||
timestamp: string
|
||||
id: number;
|
||||
status: string;
|
||||
action: string;
|
||||
type: string;
|
||||
rejections: string[];
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
interface ReleaseFindResponse {
|
||||
data: Release[];
|
||||
next_cursor: number;
|
||||
count: number;
|
||||
data: Release[];
|
||||
next_cursor: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ReleaseStats {
|
||||
total_count: number;
|
||||
filtered_count: number;
|
||||
filter_rejected_count: number;
|
||||
push_approved_count: number;
|
||||
push_rejected_count: number;
|
||||
total_count: number;
|
||||
filtered_count: number;
|
||||
filter_rejected_count: number;
|
||||
push_approved_count: number;
|
||||
push_rejected_count: number;
|
||||
}
|
||||
|
||||
interface ReleaseFilter {
|
||||
|
|
|
@ -19,7 +19,7 @@ export const InitializeGlobalContext = () => {
|
|||
)
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
interface AuthInfo {
|
||||
username: string;
|
||||
|
|
|
@ -2,37 +2,37 @@ import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
|
|||
|
||||
// sleep for x ms
|
||||
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
|
||||
export function baseUrl() {
|
||||
let baseUrl = ""
|
||||
if (window.APP.baseUrl) {
|
||||
if (window.APP.baseUrl === "/") {
|
||||
baseUrl = "/"
|
||||
} else if (window.APP.baseUrl === "{{.BaseUrl}}") {
|
||||
baseUrl = "/"
|
||||
} else if (window.APP.baseUrl === "/autobrr/") {
|
||||
baseUrl = "/autobrr/"
|
||||
} else {
|
||||
baseUrl = window.APP.baseUrl
|
||||
}
|
||||
let baseUrl = "";
|
||||
if (window.APP.baseUrl) {
|
||||
if (window.APP.baseUrl === "/") {
|
||||
baseUrl = "/";
|
||||
} else if (window.APP.baseUrl === "{{.BaseUrl}}") {
|
||||
baseUrl = "/";
|
||||
} else if (window.APP.baseUrl === "/autobrr/") {
|
||||
baseUrl = "/autobrr/";
|
||||
} else {
|
||||
baseUrl = window.APP.baseUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return baseUrl
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
// get sseBaseUrl for SSE
|
||||
export function sseBaseUrl() {
|
||||
if (process.env.NODE_ENV === "development")
|
||||
return `http://localhost:7474/`;
|
||||
if (process.env.NODE_ENV === "development")
|
||||
return "http://localhost:7474/";
|
||||
|
||||
return `${window.location.origin}${baseUrl()}`;
|
||||
return `${window.location.origin}${baseUrl()}`;
|
||||
}
|
||||
|
||||
export function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
// column widths for inputs etc
|
||||
|
@ -40,28 +40,28 @@ export type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
|||
|
||||
// simplify date
|
||||
export function simplifyDate(date: string) {
|
||||
if (date !== "0001-01-01T00:00:00Z") {
|
||||
return formatISO9075(new Date(date))
|
||||
}
|
||||
return "n/a"
|
||||
if (date !== "0001-01-01T00:00:00Z") {
|
||||
return formatISO9075(new Date(date));
|
||||
}
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
// if empty date show as n/a
|
||||
export function IsEmptyDate(date: string) {
|
||||
if (date !== "0001-01-01T00:00:00Z") {
|
||||
return formatDistanceToNowStrict(
|
||||
new Date(date),
|
||||
{ addSuffix: true }
|
||||
)
|
||||
}
|
||||
return "n/a"
|
||||
if (date !== "0001-01-01T00:00:00Z") {
|
||||
return formatDistanceToNowStrict(
|
||||
new Date(date),
|
||||
{ addSuffix: true }
|
||||
);
|
||||
}
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
export function slugify(str: string) {
|
||||
return str
|
||||
.normalize('NFKD')
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.trim()
|
||||
.replace(/[-\s]+/g, '-');
|
||||
return str
|
||||
.normalize("NFKD")
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.trim()
|
||||
.replace(/[-\s]+/g, "-");
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
],
|
||||
"types": [],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"skipLibCheck": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
|
|
|
@ -2088,7 +2088,22 @@
|
|||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.10.2", "@typescript-eslint/eslint-plugin@^5.5.0":
|
||||
"@typescript-eslint/eslint-plugin@^5.18.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8"
|
||||
integrity sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.23.0"
|
||||
"@typescript-eslint/type-utils" "5.23.0"
|
||||
"@typescript-eslint/utils" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
ignore "^5.1.8"
|
||||
regexpp "^3.2.0"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.5.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d"
|
||||
integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==
|
||||
|
@ -2110,7 +2125,17 @@
|
|||
dependencies:
|
||||
"@typescript-eslint/utils" "5.18.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.10.2", "@typescript-eslint/parser@^5.5.0":
|
||||
"@typescript-eslint/parser@^5.18.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.23.0.tgz#443778e1afc9a8ff180f91b5e260ac3bec5e2de1"
|
||||
integrity sha512-V06cYUkqcGqpFjb8ttVgzNF53tgbB/KoQT/iB++DOIExKmzI9vBJKjZKt/6FuV9c+zrDsvJKbJ2DOCYwX91cbw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.23.0"
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/typescript-estree" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
|
||||
"@typescript-eslint/parser@^5.5.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6"
|
||||
integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ==
|
||||
|
@ -2128,6 +2153,14 @@
|
|||
"@typescript-eslint/types" "5.18.0"
|
||||
"@typescript-eslint/visitor-keys" "5.18.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz#4305e61c2c8e3cfa3787d30f54e79430cc17ce1b"
|
||||
integrity sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/visitor-keys" "5.23.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74"
|
||||
|
@ -2137,11 +2170,25 @@
|
|||
debug "^4.3.2"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/type-utils@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz#f852252f2fc27620d5bb279d8fed2a13d2e3685e"
|
||||
integrity sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e"
|
||||
integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==
|
||||
|
||||
"@typescript-eslint/types@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09"
|
||||
integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474"
|
||||
|
@ -2155,6 +2202,19 @@
|
|||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065"
|
||||
integrity sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/visitor-keys" "5.23.0"
|
||||
debug "^4.3.2"
|
||||
globby "^11.0.4"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.5"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.18.0", "@typescript-eslint/utils@^5.13.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855"
|
||||
|
@ -2167,6 +2227,18 @@
|
|||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/utils@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.23.0.tgz#4691c3d1b414da2c53d8943310df36ab1c50648a"
|
||||
integrity sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@typescript-eslint/scope-manager" "5.23.0"
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
"@typescript-eslint/typescript-estree" "5.23.0"
|
||||
eslint-scope "^5.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60"
|
||||
|
@ -2175,6 +2247,14 @@
|
|||
"@typescript-eslint/types" "5.18.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz#057c60a7ca64667a39f991473059377a8067c87b"
|
||||
integrity sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.23.0"
|
||||
eslint-visitor-keys "^3.0.0"
|
||||
|
||||
"@webassemblyjs/ast@1.11.1":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue