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",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"lint": "esw src/ --ext \".ts,.tsx,.js,.jsx\" --color",
|
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx --color",
|
||||||
"lint:watch": "npm run lint -- --watch"
|
"lint:watch": "npm run lint -- --watch"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
@ -59,8 +59,8 @@
|
||||||
"@types/react-dom": "17.0.0",
|
"@types/react-dom": "17.0.0",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-table": "^7.7.7",
|
"@types/react-table": "^7.7.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||||
"@typescript-eslint/parser": "^5.10.2",
|
"@typescript-eslint/parser": "^5.18.0",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.2",
|
||||||
"eslint": "^8.8.0",
|
"eslint": "^8.8.0",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
@ -71,59 +71,5 @@
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"tailwindcss": "^3.0.18",
|
"tailwindcss": "^3.0.18",
|
||||||
"typescript": "^4.1.2"
|
"typescript": "^4.1.2"
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"root": true,
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:import/errors",
|
|
||||||
"plugin:import/warnings",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:react-hooks/recommended"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"react",
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/jsx-uses-react": "off",
|
|
||||||
"react/react-in-jsx-scope": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off"
|
|
||||||
},
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 11,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true,
|
|
||||||
"experimentalObjectRestSpread": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"react": {
|
|
||||||
"version": "detect"
|
|
||||||
},
|
|
||||||
"import/resolver": {
|
|
||||||
"node": {
|
|
||||||
"extensions": [
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".js",
|
|
||||||
".jsx"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"jquery": false
|
|
||||||
},
|
|
||||||
"globals": {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import Toast from "./components/notifications/Toast";
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: { useErrorBoundary: true, },
|
queries: { useErrorBoundary: true },
|
||||||
mutations: {
|
mutations: {
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
// Use a format string to convert the error object to a proper string without much hassle.
|
// Use a format string to convert the error object to a proper string without much hassle.
|
||||||
|
@ -27,8 +27,8 @@ export const queryClient = new QueryClient({
|
||||||
);
|
);
|
||||||
toast.custom((t) => <Toast type="error" body={message} t={t} />);
|
toast.custom((t) => <Toast type="error" body={message} t={t} />);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
|
|
@ -1,157 +1,166 @@
|
||||||
import {baseUrl, sseBaseUrl} from "../utils";
|
import { baseUrl, sseBaseUrl } from "../utils";
|
||||||
import {AuthContext} from "../utils/Context";
|
import { AuthContext } from "../utils/Context";
|
||||||
import {Cookies} from "react-cookie";
|
import { Cookies } from "react-cookie";
|
||||||
|
|
||||||
interface ConfigType {
|
interface ConfigType {
|
||||||
body?: BodyInit | Record<string, unknown> | null;
|
body?: BodyInit | Record<string, unknown> | unknown | null;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostBody = BodyInit | Record<string, unknown> | unknown | null;
|
||||||
|
|
||||||
export async function HttpClient<T>(
|
export async function HttpClient<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: string,
|
method: string,
|
||||||
{ body, ...customConfig }: ConfigType = {}
|
{ body, ...customConfig }: ConfigType = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const config = {
|
const config = {
|
||||||
method: method,
|
method: method,
|
||||||
body: body ? JSON.stringify(body) : null,
|
body: body ? JSON.stringify(body) : null,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
},
|
},
|
||||||
// NOTE: customConfig can override the above defined settings
|
// NOTE: customConfig can override the above defined settings
|
||||||
...customConfig
|
...customConfig
|
||||||
} as RequestInit;
|
} as RequestInit;
|
||||||
|
|
||||||
|
|
||||||
return window.fetch(`${baseUrl()}${endpoint}`, config)
|
return window.fetch(`${baseUrl()}${endpoint}`, config)
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// if 401 consider the session expired and force logout
|
// if 401 consider the session expired and force logout
|
||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
cookies.remove("user_session");
|
cookies.remove("user_session");
|
||||||
AuthContext.reset()
|
AuthContext.reset();
|
||||||
|
|
||||||
return Promise.reject(new Error(response.statusText));
|
return Promise.reject(new Error(response.statusText));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([403, 404].includes(response.status))
|
if ([403, 404].includes(response.status))
|
||||||
return Promise.reject(new Error(response.statusText));
|
return Promise.reject(new Error(response.statusText));
|
||||||
|
|
||||||
// 201 comes from a POST and can contain data
|
// 201 comes from a POST and can contain data
|
||||||
if ([201].includes(response.status))
|
if ([201].includes(response.status))
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
|
||||||
// 204 ok no data
|
// 204 ok no data
|
||||||
if ([204].includes(response.status))
|
if ([204].includes(response.status))
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = await response.text();
|
const errorMessage = await response.text();
|
||||||
return Promise.reject(new Error(errorMessage));
|
return Promise.reject(new Error(errorMessage));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const appClient = {
|
const appClient = {
|
||||||
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
||||||
Post: <T>(endpoint: string, data: any) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
Post: <T>(endpoint: string, data: PostBody) => HttpClient<void | T>(endpoint, "POST", { body: data }),
|
||||||
Put: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
PostBody: <T>(endpoint: string, data: PostBody) => HttpClient<T>(endpoint, "POST", { body: data }),
|
||||||
Patch: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
Put: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||||
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
Patch: (endpoint: string, data: PostBody) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||||
}
|
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
||||||
|
};
|
||||||
|
|
||||||
export const APIClient = {
|
export const APIClient = {
|
||||||
auth: {
|
auth: {
|
||||||
login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }),
|
login: (username: string, password: string) => appClient.Post("api/auth/login", {
|
||||||
logout: () => appClient.Post("api/auth/logout", null),
|
username: username,
|
||||||
validate: () => appClient.Get<void>("api/auth/validate"),
|
password: password
|
||||||
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", { username: username, password: password }),
|
}),
|
||||||
canOnboard: () => appClient.Get("api/auth/onboard"),
|
logout: () => appClient.Post("api/auth/logout", null),
|
||||||
},
|
validate: () => appClient.Get<void>("api/auth/validate"),
|
||||||
actions: {
|
onboard: (username: string, password: string) => appClient.Post("api/auth/onboard", {
|
||||||
create: (action: Action) => appClient.Post("api/actions", action),
|
username: username,
|
||||||
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
|
password: password
|
||||||
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
|
}),
|
||||||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null),
|
canOnboard: () => appClient.Get("api/auth/onboard")
|
||||||
},
|
},
|
||||||
config: {
|
actions: {
|
||||||
get: () => appClient.Get<Config>("api/config")
|
create: (action: Action) => appClient.Post("api/actions", action),
|
||||||
},
|
update: (action: Action) => appClient.Put(`api/actions/${action.id}`, action),
|
||||||
download_clients: {
|
delete: (id: number) => appClient.Delete(`api/actions/${id}`),
|
||||||
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null)
|
||||||
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
|
},
|
||||||
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
|
config: {
|
||||||
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
|
get: () => appClient.Get<Config>("api/config")
|
||||||
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc),
|
},
|
||||||
},
|
download_clients: {
|
||||||
filters: {
|
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
||||||
getAll: () => appClient.Get<Filter[]>("api/filters"),
|
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
|
||||||
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
|
||||||
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
|
||||||
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc)
|
||||||
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
|
},
|
||||||
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
filters: {
|
||||||
delete: (id: number) => appClient.Delete(`api/filters/${id}`),
|
getAll: () => appClient.Get<Filter[]>("api/filters"),
|
||||||
},
|
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
||||||
feeds: {
|
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
||||||
find: () => appClient.Get<Feed[]>("api/feeds"),
|
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
||||||
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
|
duplicate: (id: number) => appClient.Get<Filter>(`api/filters/${id}/duplicate`),
|
||||||
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
|
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
||||||
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
|
delete: (id: number) => appClient.Delete(`api/filters/${id}`)
|
||||||
delete: (id: number) => appClient.Delete(`api/feeds/${id}`),
|
},
|
||||||
},
|
feeds: {
|
||||||
indexers: {
|
find: () => appClient.Get<Feed[]>("api/feeds"),
|
||||||
// returns indexer options for all currently present/enabled indexers
|
create: (feed: FeedCreate) => appClient.Post("api/feeds", feed),
|
||||||
getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
|
toggleEnable: (id: number, enabled: boolean) => appClient.Patch(`api/feeds/${id}/enabled`, { enabled }),
|
||||||
// returns indexer definitions for all currently present/enabled indexers
|
update: (feed: Feed) => appClient.Put(`api/feeds/${feed.id}`, feed),
|
||||||
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
delete: (id: number) => appClient.Delete(`api/feeds/${id}`)
|
||||||
// returns all possible indexer definitions
|
},
|
||||||
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
indexers: {
|
||||||
create: (indexer: Indexer) => appClient.Post<Indexer>("api/indexer", indexer),
|
// returns indexer options for all currently present/enabled indexers
|
||||||
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
|
||||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`),
|
// returns indexer definitions for all currently present/enabled indexers
|
||||||
},
|
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
||||||
irc: {
|
// returns all possible indexer definitions
|
||||||
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
||||||
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
create: (indexer: Indexer) => appClient.PostBody<Indexer>("api/indexer", indexer),
|
||||||
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
||||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
|
delete: (id: number) => appClient.Delete(`api/indexer/${id}`)
|
||||||
},
|
},
|
||||||
events: {
|
irc: {
|
||||||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
getNetworks: () => appClient.Get<IrcNetworkWithHealth[]>("api/irc"),
|
||||||
},
|
createNetwork: (network: IrcNetworkCreate) => appClient.Post("api/irc", network),
|
||||||
notifications: {
|
updateNetwork: (network: IrcNetwork) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||||
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`)
|
||||||
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
},
|
||||||
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
events: {
|
||||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`),
|
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||||
},
|
},
|
||||||
release: {
|
notifications: {
|
||||||
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
||||||
findQuery: (offset?: number, limit?: number, filters?: Array<ReleaseFilter>) => {
|
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
||||||
const params = new URLSearchParams();
|
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
||||||
if (offset !== undefined)
|
delete: (id: number) => appClient.Delete(`api/notification/${id}`)
|
||||||
params.append("offset", offset.toString());
|
},
|
||||||
|
release: {
|
||||||
|
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
||||||
|
findQuery: (offset?: number, limit?: number, filters?: Array<ReleaseFilter>) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (offset !== undefined)
|
||||||
|
params.append("offset", offset.toString());
|
||||||
|
|
||||||
if (limit !== undefined)
|
if (limit !== undefined)
|
||||||
params.append("limit", limit.toString());
|
params.append("limit", limit.toString());
|
||||||
|
|
||||||
filters?.forEach((filter) => {
|
filters?.forEach((filter) => {
|
||||||
if (!filter.value)
|
if (!filter.value)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (filter.id == "indexer")
|
if (filter.id == "indexer")
|
||||||
params.append("indexer", filter.value);
|
params.append("indexer", filter.value);
|
||||||
else if (filter.id === "action_status")
|
else if (filter.id === "action_status")
|
||||||
params.append("push_status", filter.value);
|
params.append("push_status", filter.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`)
|
return appClient.Get<ReleaseFindResponse>(`api/release?${params.toString()}`);
|
||||||
},
|
},
|
||||||
indexerOptions: () => appClient.Get<string[]>(`api/release/indexers`),
|
indexerOptions: () => appClient.Get<string[]>("api/release/indexers"),
|
||||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
stats: () => appClient.Get<ReleaseStats>("api/release/stats"),
|
||||||
delete: () => appClient.Delete(`api/release/all`),
|
delete: () => appClient.Delete("api/release/all")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,27 +8,27 @@ interface CheckboxProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => (
|
export const Checkbox = ({ label, description, value, setValue }: CheckboxProps) => (
|
||||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white" passive>
|
||||||
{label}
|
{label}
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
{description === undefined ? null : (
|
{description === undefined ? null : (
|
||||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{description}
|
{description}
|
||||||
</Switch.Description>
|
</Switch.Description>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={value}
|
checked={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
className={
|
className={
|
||||||
`${value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700'
|
`${value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700"
|
||||||
} ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
|
} ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`${value ? 'translate-x-5' : 'translate-x-0'} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
className={`${value ? "translate-x-5" : "translate-x-0"} inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200`}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Switch.Group>
|
</Switch.Group>
|
||||||
);
|
);
|
|
@ -5,13 +5,13 @@ interface IconProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SortIcon = ({ className }: IconProps) => (
|
export const SortIcon = ({ className }: IconProps) => (
|
||||||
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z"></path></svg>
|
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z"></path></svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SortUpIcon = ({ className }: IconProps) => (
|
export const SortUpIcon = ({ className }: IconProps) => (
|
||||||
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z"></path></svg>
|
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M279 224H41c-21.4 0-32.1-25.9-17-41L143 64c9.4-9.4 24.6-9.4 33.9 0l119 119c15.2 15.1 4.5 41-16.9 41z"></path></svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SortDownIcon = ({ className }: IconProps) => (
|
export const SortDownIcon = ({ className }: IconProps) => (
|
||||||
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z"></path></svg>
|
<svg className={className} stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 320 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41z"></path></svg>
|
||||||
);
|
);
|
||||||
|
|
|
@ -51,7 +51,8 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg className="mr-2 w-5 h-5 text-red-700 dark:text-red-800" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||||
|
@ -77,11 +78,11 @@ export const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
resetErrorBoundary();
|
resetErrorBoundary();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5" />
|
<RefreshIcon className="-ml-0.5 mr-2 h-5 w-5"/>
|
||||||
Reset page state
|
Reset page state
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
|
@ -1,34 +1,37 @@
|
||||||
import { classNames } from "../../utils"
|
import React from "react";
|
||||||
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
children: any;
|
children: React.ReactNode;
|
||||||
[rest: string]: any;
|
disabled?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Button = ({ children, className, ...rest }: ButtonProps) => (
|
export const Button = ({ children, className, disabled, onClick }: ButtonProps) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className ?? "",
|
className ?? "",
|
||||||
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50"
|
"relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-800 text-sm font-medium rounded-md text-gray-700 dark:text-gray-500 bg-white dark:bg-gray-800 hover:bg-gray-50"
|
||||||
)}
|
)}
|
||||||
{...rest}
|
disabled={disabled}
|
||||||
>
|
onClick={onClick}
|
||||||
{children}
|
>
|
||||||
</button>
|
{children}
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const PageButton = ({ children, className, disabled, onClick }: ButtonProps) => (
|
||||||
export const PageButton = ({ children, className, ...rest }: ButtonProps) => (
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={classNames(
|
||||||
className={classNames(
|
className ?? "",
|
||||||
className ?? "",
|
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||||
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600"
|
)}
|
||||||
)}
|
disabled={disabled}
|
||||||
{...rest}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
|
@ -10,24 +10,22 @@ interface CellProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AgeCell = ({ value }: CellProps) => (
|
export const AgeCell = ({ value }: CellProps) => (
|
||||||
<div className="text-sm text-gray-500" title={value}>
|
<div className="text-sm text-gray-500" title={value}>
|
||||||
{formatDistanceToNowStrict(new Date(value), { addSuffix: true })}
|
{formatDistanceToNowStrict(new Date(value), { addSuffix: true })}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TitleCell = ({ value }: CellProps) => (
|
export const TitleCell = ({ value }: CellProps) => (
|
||||||
<div
|
<div
|
||||||
className="text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[128px] sm:max-w-none overflow-auto py-4"
|
className="text-sm font-medium box-content text-gray-900 dark:text-gray-300 max-w-[128px] sm:max-w-none overflow-auto py-4"
|
||||||
title={value}
|
title={value}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface ReleaseStatusCellProps {
|
interface ReleaseStatusCellProps {
|
||||||
value: ReleaseActionStatus[];
|
value: ReleaseActionStatus[];
|
||||||
column: any;
|
|
||||||
row: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusCellMapEntry {
|
interface StatusCellMapEntry {
|
||||||
|
@ -36,22 +34,22 @@ interface StatusCellMapEntry {
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
const StatusCellMap: Record<string, StatusCellMapEntry> = {
|
||||||
"PUSH_ERROR": {
|
"PUSH_ERROR": {
|
||||||
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
|
colors: "bg-pink-100 text-pink-800 hover:bg-pink-300",
|
||||||
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />
|
icon: <ExclamationCircleIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
},
|
},
|
||||||
"PUSH_REJECTED": {
|
"PUSH_REJECTED": {
|
||||||
colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
|
colors: "bg-blue-200 dark:bg-blue-100 text-blue-400 dark:text-blue-800 hover:bg-blue-300 dark:hover:bg-blue-400",
|
||||||
icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
|
icon: <BanIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
},
|
},
|
||||||
"PUSH_APPROVED": {
|
"PUSH_APPROVED": {
|
||||||
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
colors: "bg-green-100 text-green-800 hover:bg-green-300",
|
||||||
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />
|
icon: <CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
},
|
},
|
||||||
"PENDING": {
|
"PENDING": {
|
||||||
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
colors: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
||||||
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />
|
icon: <ClockIcon className="h-5 w-5" aria-hidden="true" />
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
|
const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
|
||||||
|
@ -67,18 +65,18 @@ const GetReleaseStatusString = (releaseAction: ReleaseActionStatus) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
|
export const ReleaseStatusCell = ({ value }: ReleaseStatusCellProps) => (
|
||||||
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
|
<div className="flex text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||||
{value.map((v, idx) => (
|
{value.map((v, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
title={GetReleaseStatusString(v)}
|
title={GetReleaseStatusString(v)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
StatusCellMap[v.status].colors,
|
StatusCellMap[v.status].colors,
|
||||||
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"
|
"mr-1 inline-flex items-center rounded text-xs font-semibold uppercase cursor-pointer"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{StatusCellMap[v.status].icon}
|
{StatusCellMap[v.status].icon}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
import { FC } from "react"
|
import { FC } from "react";
|
||||||
|
|
||||||
interface DebugProps {
|
interface DebugProps {
|
||||||
values: unknown;
|
values: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEBUG: FC<DebugProps> = ({ values }) => {
|
const DEBUG: FC<DebugProps> = ({ values }) => {
|
||||||
if (process.env.NODE_ENV !== "development") {
|
if (process.env.NODE_ENV !== "development") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full p-2 flex flex-col mt-6 bg-gray-100 dark:bg-gray-900">
|
<div className="w-full p-2 flex flex-col mt-12 mb-12 bg-gray-100 dark:bg-gray-900">
|
||||||
<pre className="overflow-x-auto dark:text-gray-400">
|
<pre className="dark:text-gray-400">{JSON.stringify(values, null, 2)}</pre>
|
||||||
{JSON.stringify(values, undefined, 2)}
|
</div>
|
||||||
</pre>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DEBUG;
|
export default DEBUG;
|
||||||
|
|
|
@ -13,43 +13,43 @@ export const EmptySimple = ({
|
||||||
buttonText,
|
buttonText,
|
||||||
buttonAction
|
buttonAction
|
||||||
}: EmptySimpleProps) => (
|
}: EmptySimpleProps) => (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
|
<h3 className="mt-2 text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-200">{subtitle}</p>
|
||||||
{buttonText && buttonAction ? (
|
{buttonText && buttonAction ? (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={buttonAction}
|
onClick={buttonAction}
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
interface EmptyListStateProps {
|
interface EmptyListStateProps {
|
||||||
text: string;
|
text: string;
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
buttonOnClick?: any;
|
buttonOnClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
|
export function EmptyListState({ text, buttonText, buttonOnClick }: EmptyListStateProps) {
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-12 flex flex-col items-center">
|
<div className="px-4 py-12 flex flex-col items-center">
|
||||||
<p className="text-center text-gray-800 dark:text-white">{text}</p>
|
<p className="text-center text-gray-800 dark:text-white">{text}</p>
|
||||||
{buttonText && buttonOnClick && (
|
{buttonText && buttonOnClick && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={buttonOnClick}
|
onClick={buttonOnClick}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
|
@ -6,8 +6,8 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
|
export const TitleSubtitle: FC<Props> = ({ title, subtitle }) => (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">{title}</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
|
@ -1,17 +1,16 @@
|
||||||
import { Field } from "formik";
|
import { Field, FieldProps } from "formik";
|
||||||
|
|
||||||
interface ErrorFieldProps {
|
interface ErrorFieldProps {
|
||||||
name: string;
|
name: string;
|
||||||
classNames?: string;
|
classNames?: string;
|
||||||
subscribe?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ErrorField = ({ name, classNames }: ErrorFieldProps) => (
|
const ErrorField = ({ name, classNames }: ErrorFieldProps) => (
|
||||||
<Field name={name} subscribe={{ touched: true, error: true }}>
|
<Field name={name} subscribe={{ touched: true, error: true }}>
|
||||||
{({ meta: { touched, error } }: any) =>
|
{({ meta: { touched, error } }: FieldProps) =>
|
||||||
touched && error ? <span className={classNames}>{error}</span> : null
|
touched && error ? <span className={classNames}>{error}</span> : null
|
||||||
}
|
}
|
||||||
</Field>
|
</Field>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface CheckboxFieldProps {
|
interface CheckboxFieldProps {
|
||||||
|
@ -21,26 +20,26 @@ interface CheckboxFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CheckboxField = ({
|
const CheckboxField = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
sublabel
|
sublabel
|
||||||
}: CheckboxFieldProps) => (
|
}: CheckboxFieldProps) => (
|
||||||
<div className="relative flex items-start">
|
<div className="relative flex items-start">
|
||||||
<div className="flex items-center h-5">
|
<div className="flex items-center h-5">
|
||||||
<Field
|
<Field
|
||||||
id={name}
|
id={name}
|
||||||
name={name}
|
name={name}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="focus:ring-bkue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
className="focus:ring-bkue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="ml-3 text-sm">
|
|
||||||
<label htmlFor={name} className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
<p className="text-gray-500">{sublabel}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor={name} className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-500">{sublabel}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export { ErrorField, CheckboxField }
|
export { ErrorField, CheckboxField };
|
|
@ -2,6 +2,6 @@ export { ErrorField, CheckboxField } from "./common";
|
||||||
export { TextField, NumberField, PasswordField } from "./input";
|
export { TextField, NumberField, PasswordField } from "./input";
|
||||||
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
export { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "./input_wide";
|
||||||
export { RadioFieldsetWide } from "./radio";
|
export { RadioFieldsetWide } from "./radio";
|
||||||
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect} from "./select";
|
export { MultiSelect, Select, SelectWide, DownloadClientSelect, IndexerMultiSelect } from "./select";
|
||||||
export { SwitchGroup } from "./switch";
|
export { SwitchGroup } from "./switch";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Field } from "formik";
|
import { Field, FieldProps } from "formik";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
|
@ -16,49 +16,49 @@ interface TextFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextField = ({
|
export const TextField = ({
|
||||||
name,
|
name,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
columns,
|
columns,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
hidden,
|
hidden
|
||||||
}: TextFieldProps) => (
|
}: TextFieldProps) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
hidden ? "hidden" : "",
|
hidden ? "hidden" : "",
|
||||||
columns ? `col-span-${columns}` : "col-span-12",
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<Field name={name}>
|
<Field name={name}>
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
meta,
|
meta
|
||||||
}: any) => (
|
}: FieldProps) => (
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
{...field}
|
{...field}
|
||||||
id={name}
|
id={name}
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
autoComplete={autoComplete}
|
autoComplete={autoComplete}
|
||||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
|
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{meta.touched && meta.error && (
|
{meta.touched && meta.error && (
|
||||||
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
interface PasswordFieldProps {
|
interface PasswordFieldProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -72,60 +72,60 @@ interface PasswordFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PasswordField = ({
|
export const PasswordField = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
columns,
|
columns,
|
||||||
autoComplete,
|
autoComplete,
|
||||||
help,
|
help,
|
||||||
required
|
required
|
||||||
}: PasswordFieldProps) => {
|
}: PasswordFieldProps) => {
|
||||||
const [isVisible, toggleVisibility] = useToggle(false)
|
const [isVisible, toggleVisibility] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
columns ? `col-span-${columns}` : "col-span-12"
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<Field name={name} defaultValue={defaultValue}>
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
meta
|
||||||
|
}: FieldProps) => (
|
||||||
|
<div className="sm:col-span-2 relative">
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
type={isVisible ? "text" : "password"}
|
||||||
|
autoComplete={autoComplete}
|
||||||
|
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
{label && (
|
{meta.touched && meta.error && (
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
|
||||||
</label>
|
|
||||||
)}
|
)}
|
||||||
<Field name={name} defaultValue={defaultValue}>
|
</div>
|
||||||
{({
|
)}
|
||||||
field,
|
</Field>
|
||||||
meta,
|
</div>
|
||||||
}: any) => (
|
);
|
||||||
<div className="sm:col-span-2 relative">
|
};
|
||||||
<input
|
|
||||||
{...field}
|
|
||||||
id={name}
|
|
||||||
type={isVisible ? "text" : "password"}
|
|
||||||
autoComplete={autoComplete}
|
|
||||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "mt-2 block w-full dark:bg-gray-800 dark:text-gray-100 rounded-md")}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500" id="email-description">{help}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{meta.touched && meta.error && (
|
|
||||||
<p className="error text-sm text-red-600 mt-1">* {meta.error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NumberFieldProps {
|
interface NumberFieldProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -135,40 +135,40 @@ interface NumberFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberField = ({
|
export const NumberField = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
step,
|
step
|
||||||
}: NumberFieldProps) => (
|
}: NumberFieldProps) => (
|
||||||
<div className="col-span-12 sm:col-span-6">
|
<div className="col-span-12 sm:col-span-6">
|
||||||
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<label htmlFor={name} className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Field name={name} type="number">
|
|
||||||
{({
|
|
||||||
field,
|
|
||||||
meta,
|
|
||||||
}: any) => (
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step={step}
|
|
||||||
{...field}
|
|
||||||
className={classNames(
|
|
||||||
meta.touched && meta.error
|
|
||||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
|
||||||
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
|
|
||||||
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md"
|
|
||||||
)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
{meta.touched && meta.error && (
|
|
||||||
<div className="error">{meta.error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<Field name={name} type="number">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
meta
|
||||||
|
}: FieldProps) => (
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step={step}
|
||||||
|
{...field}
|
||||||
|
className={classNames(
|
||||||
|
meta.touched && meta.error
|
||||||
|
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||||
|
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300",
|
||||||
|
"mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-100 rounded-md"
|
||||||
)}
|
)}
|
||||||
</Field>
|
placeholder={placeholder}
|
||||||
</div>
|
/>
|
||||||
|
{meta.touched && meta.error && (
|
||||||
|
<div className="error">{meta.error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { classNames } from "../../utils";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
import { EyeIcon, EyeOffIcon } from "@heroicons/react/solid";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
import { ErrorField } from "./common"
|
import { ErrorField } from "./common";
|
||||||
|
|
||||||
interface TextFieldWideProps {
|
interface TextFieldWideProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -17,46 +17,46 @@ interface TextFieldWideProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextFieldWide = ({
|
export const TextFieldWide = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
help,
|
help,
|
||||||
placeholder,
|
placeholder,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
required,
|
required,
|
||||||
hidden
|
hidden
|
||||||
}: TextFieldWideProps) => (
|
}: TextFieldWideProps) => (
|
||||||
<div hidden={hidden} className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
<div hidden={hidden} className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
value={defaultValue}
|
|
||||||
required={required}
|
|
||||||
>
|
|
||||||
{({ field, meta }: FieldProps) => (
|
|
||||||
<input
|
|
||||||
{...field}
|
|
||||||
id={name}
|
|
||||||
type="text"
|
|
||||||
value={field.value ? field.value : defaultValue ?? ""}
|
|
||||||
onChange={field.onChange}
|
|
||||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
|
|
||||||
placeholder={placeholder}
|
|
||||||
hidden={hidden}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
|
||||||
)}
|
|
||||||
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className="sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
value={defaultValue}
|
||||||
|
required={required}
|
||||||
|
>
|
||||||
|
{({ field, meta }: FieldProps) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
type="text"
|
||||||
|
value={field.value ? field.value : defaultValue ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
hidden={hidden}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
||||||
|
)}
|
||||||
|
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
interface PasswordFieldWideProps {
|
interface PasswordFieldWideProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -69,53 +69,53 @@ interface PasswordFieldWideProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PasswordFieldWide = ({
|
export const PasswordFieldWide = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
help,
|
help,
|
||||||
required,
|
required,
|
||||||
defaultVisible
|
defaultVisible
|
||||||
}: PasswordFieldWideProps) => {
|
}: PasswordFieldWideProps) => {
|
||||||
const [isVisible, toggleVisibility] = useToggle(defaultVisible)
|
const [isVisible, toggleVisibility] = useToggle(defaultVisible);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
<label htmlFor={name} className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2">
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
>
|
||||||
|
{({ field, meta }: FieldProps) => (
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
value={field.value ? field.value : defaultValue ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
type={isVisible ? "text" : "password"}
|
||||||
|
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full pr-10 dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
||||||
|
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
)}
|
||||||
<Field
|
</Field>
|
||||||
name={name}
|
{help && (
|
||||||
defaultValue={defaultValue}
|
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
||||||
>
|
)}
|
||||||
{({ field, meta }: FieldProps) => (
|
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||||
<div className="relative">
|
</div>
|
||||||
<input
|
</div>
|
||||||
{...field}
|
);
|
||||||
id={name}
|
};
|
||||||
value={field.value ? field.value : defaultValue ?? ""}
|
|
||||||
onChange={field.onChange}
|
|
||||||
type={isVisible ? "text" : "password"}
|
|
||||||
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700", "block w-full pr-10 dark:bg-gray-800 shadow-sm dark:text-gray-100 sm:text-sm rounded-md")}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-y-0 right-0 px-3 flex items-center" onClick={toggleVisibility}>
|
|
||||||
{!isVisible ? <EyeIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" /> : <EyeOffIcon className="h-5 w-5 text-gray-400 hover:text-gray-500" aria-hidden="true" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500" id={`${name}-description`}>{help}</p>
|
|
||||||
)}
|
|
||||||
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NumberFieldWideProps {
|
interface NumberFieldWideProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -127,50 +127,50 @@ interface NumberFieldWideProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NumberFieldWide = ({
|
export const NumberFieldWide = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
help,
|
help,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
required
|
required
|
||||||
}: NumberFieldWideProps) => (
|
}: NumberFieldWideProps) => (
|
||||||
<div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
<div className="px-4 space-y-1 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor={name}
|
htmlFor={name}
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
{label} {required && <span className="text-gray-500">*</span>}
|
{label} {required && <span className="text-gray-500">*</span>}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<Field
|
|
||||||
name={name}
|
|
||||||
defaultValue={defaultValue ?? 0}
|
|
||||||
>
|
|
||||||
{({ field, meta, form }: FieldProps) => (
|
|
||||||
<input
|
|
||||||
{...field}
|
|
||||||
id={name}
|
|
||||||
type="number"
|
|
||||||
value={field.value ? field.value : defaultValue ?? 0}
|
|
||||||
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)) }}
|
|
||||||
className={classNames(
|
|
||||||
meta.touched && meta.error
|
|
||||||
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
|
||||||
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
|
||||||
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
|
|
||||||
)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
{help && (
|
|
||||||
<p className="mt-2 text-sm text-gray-500 dark:text-gray-500" id={`${name}-description`}>{help}</p>
|
|
||||||
)}
|
|
||||||
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Field
|
||||||
|
name={name}
|
||||||
|
defaultValue={defaultValue ?? 0}
|
||||||
|
>
|
||||||
|
{({ field, meta, form }: FieldProps) => (
|
||||||
|
<input
|
||||||
|
{...field}
|
||||||
|
id={name}
|
||||||
|
type="number"
|
||||||
|
value={field.value ? field.value : defaultValue ?? 0}
|
||||||
|
onChange={(e) => { form.setFieldValue(field.name, parseInt(e.target.value)); }}
|
||||||
|
className={classNames(
|
||||||
|
meta.touched && meta.error
|
||||||
|
? "focus:ring-red-500 focus:border-red-500 border-red-500"
|
||||||
|
: "focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-700",
|
||||||
|
"block w-full shadow-sm dark:bg-gray-800 sm:text-sm dark:text-white rounded-md"
|
||||||
|
)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{help && (
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-500" id={`${name}-description`}>{help}</p>
|
||||||
|
)}
|
||||||
|
<ErrorField name={name} classNames="block text-red-500 mt-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface SwitchGroupWideProps {
|
interface SwitchGroupWideProps {
|
||||||
|
@ -182,56 +182,56 @@ interface SwitchGroupWideProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SwitchGroupWide = ({
|
export const SwitchGroupWide = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
defaultValue
|
defaultValue
|
||||||
}: SwitchGroupWideProps) => (
|
}: SwitchGroupWideProps) => (
|
||||||
<ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700">
|
<ul className="mt-2 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
<Switch.Group as="li" className="py-4 flex items-center justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
<Switch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-white"
|
||||||
passive>
|
passive>
|
||||||
{label}
|
{label}
|
||||||
</Switch.Label>
|
</Switch.Label>
|
||||||
{description && (
|
{description && (
|
||||||
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
<Switch.Description className="text-sm text-gray-500 dark:text-gray-700">
|
||||||
{description}
|
{description}
|
||||||
</Switch.Description>
|
</Switch.Description>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
name={name}
|
name={name}
|
||||||
defaultValue={defaultValue as boolean}
|
defaultValue={defaultValue as boolean}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
>
|
>
|
||||||
{({ field, form }: FieldProps) => (
|
{({ field, form }: FieldProps) => (
|
||||||
<Switch
|
<Switch
|
||||||
{...field}
|
{...field}
|
||||||
type="button"
|
type="button"
|
||||||
value={field.value}
|
value={field.value}
|
||||||
checked={field.checked ?? false}
|
checked={field.checked ?? false}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
form.setFieldValue(field?.name ?? '', value)
|
form.setFieldValue(field?.name ?? "", value);
|
||||||
}}
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-500',
|
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-500",
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
field.value ? "translate-x-5" : "translate-x-0",
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</Switch.Group>
|
</Switch.Group>
|
||||||
</ul>
|
</ul>
|
||||||
)
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,101 +15,106 @@ interface props {
|
||||||
options: radioFieldsetOption[];
|
options: radioFieldsetOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface anyObj {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
function RadioFieldsetWide({ name, legend, options }: props) {
|
function RadioFieldsetWide({ name, legend, options }: props) {
|
||||||
const {
|
const {
|
||||||
values,
|
values,
|
||||||
setFieldValue,
|
setFieldValue
|
||||||
} = useFormikContext<any>();
|
} = useFormikContext<anyObj>();
|
||||||
|
|
||||||
const onChange = (value: string) => {
|
|
||||||
setFieldValue(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const onChange = (value: string) => {
|
||||||
<fieldset>
|
setFieldValue(name, value);
|
||||||
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
|
};
|
||||||
<div>
|
|
||||||
<legend className="text-sm font-medium text-gray-900 dark:text-white">
|
return (
|
||||||
{legend}
|
<fieldset>
|
||||||
</legend>
|
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
|
||||||
</div>
|
<div>
|
||||||
<div className="space-y-5 sm:col-span-2">
|
<legend className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<div className="space-y-5 sm:mt-0">
|
{legend}
|
||||||
<Field name={name} type="radio">
|
</legend>
|
||||||
{() => (
|
</div>
|
||||||
<RadioGroup value={values[name]} onChange={onChange}>
|
<div className="space-y-5 sm:col-span-2">
|
||||||
<RadioGroup.Label className="sr-only">
|
<div className="space-y-5 sm:mt-0">
|
||||||
{legend}
|
<Field name={name} type="radio">
|
||||||
</RadioGroup.Label>
|
{() => (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
<RadioGroup value={values[name]} onChange={onChange}>
|
||||||
{options.map((setting, settingIdx) => (
|
<RadioGroup.Label className="sr-only">
|
||||||
<RadioGroup.Option
|
{legend}
|
||||||
key={setting.value}
|
</RadioGroup.Label>
|
||||||
value={setting.value}
|
<div className="bg-white dark:bg-gray-800 rounded-md -space-y-px">
|
||||||
className={({ checked }) =>
|
{options.map((setting, settingIdx) => (
|
||||||
classNames(
|
<RadioGroup.Option
|
||||||
settingIdx === 0
|
key={setting.value}
|
||||||
? "rounded-tl-md rounded-tr-md"
|
value={setting.value}
|
||||||
: "",
|
className={({ checked }) =>
|
||||||
settingIdx === options.length - 1
|
classNames(
|
||||||
? "rounded-bl-md rounded-br-md"
|
settingIdx === 0
|
||||||
: "",
|
? "rounded-tl-md rounded-tr-md"
|
||||||
checked
|
: "",
|
||||||
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
settingIdx === options.length - 1
|
||||||
: "border-gray-200 dark:border-gray-700",
|
? "rounded-bl-md rounded-br-md"
|
||||||
"relative border p-4 flex cursor-pointer focus:outline-none"
|
: "",
|
||||||
)
|
checked
|
||||||
}
|
? "bg-indigo-50 dark:bg-gray-700 border-indigo-200 dark:border-blue-600 z-10"
|
||||||
>
|
: "border-gray-200 dark:border-gray-700",
|
||||||
{({ active, checked }) => (
|
"relative border p-4 flex cursor-pointer focus:outline-none"
|
||||||
<Fragment>
|
)
|
||||||
<span
|
}
|
||||||
className={classNames(
|
>
|
||||||
checked
|
{({ active, checked }) => (
|
||||||
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
<Fragment>
|
||||||
: "bg-white border-gray-300 dark:border-gray-300",
|
<span
|
||||||
active
|
className={classNames(
|
||||||
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
|
checked
|
||||||
: "",
|
? "bg-indigo-600 dark:bg-blue-600 border-transparent"
|
||||||
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
: "bg-white border-gray-300 dark:border-gray-300",
|
||||||
)}
|
active
|
||||||
aria-hidden="true"
|
? "ring-2 ring-offset-2 ring-indigo-500 dark:ring-blue-500"
|
||||||
>
|
: "",
|
||||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
"h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
|
||||||
</span>
|
)}
|
||||||
<div className="ml-3 flex flex-col">
|
aria-hidden="true"
|
||||||
<RadioGroup.Label
|
>
|
||||||
as="span"
|
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||||
className={classNames(
|
</span>
|
||||||
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
<div className="ml-3 flex flex-col">
|
||||||
"block text-sm font-medium"
|
<RadioGroup.Label
|
||||||
)}
|
as="span"
|
||||||
>
|
className={classNames(
|
||||||
{setting.label}
|
checked ? "text-indigo-900 dark:text-blue-500" : "text-gray-900 dark:text-gray-300",
|
||||||
</RadioGroup.Label>
|
"block text-sm font-medium"
|
||||||
<RadioGroup.Description
|
)}
|
||||||
as="span"
|
>
|
||||||
className={classNames(
|
{setting.label}
|
||||||
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
</RadioGroup.Label>
|
||||||
"block text-sm"
|
<RadioGroup.Description
|
||||||
)}
|
as="span"
|
||||||
>
|
className={classNames(
|
||||||
{setting.description}
|
checked ? "text-indigo-700 dark:text-blue-500" : "text-gray-500",
|
||||||
</RadioGroup.Description>
|
"block text-sm"
|
||||||
</div>
|
)}
|
||||||
</Fragment>
|
>
|
||||||
)}
|
{setting.description}
|
||||||
</RadioGroup.Option>
|
</RadioGroup.Description>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</Fragment>
|
||||||
</RadioGroup>
|
)}
|
||||||
)}
|
</RadioGroup.Option>
|
||||||
</Field>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RadioGroup>
|
||||||
</div>
|
)}
|
||||||
</fieldset>
|
</Field>
|
||||||
);
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { RadioFieldsetWide };
|
export { RadioFieldsetWide };
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Field } from "formik";
|
import { Field, FieldProps } from "formik";
|
||||||
import { Transition, Listbox } from "@headlessui/react";
|
import { Transition, Listbox } from "@headlessui/react";
|
||||||
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
import { CheckIcon, SelectorIcon } from "@heroicons/react/solid";
|
||||||
import { MultiSelect as RMSC } from "react-multi-select-component";
|
import { MultiSelect as RMSC } from "react-multi-select-component";
|
||||||
|
@ -7,114 +7,125 @@ import { MultiSelect as RMSC } from "react-multi-select-component";
|
||||||
import { classNames, COL_WIDTHS } from "../../utils";
|
import { classNames, COL_WIDTHS } from "../../utils";
|
||||||
import { SettingsContext } from "../../utils/Context";
|
import { SettingsContext } from "../../utils/Context";
|
||||||
|
|
||||||
|
export interface MultiSelectOption {
|
||||||
|
value: string | number;
|
||||||
|
label: string;
|
||||||
|
key?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface MultiSelectProps {
|
interface MultiSelectProps {
|
||||||
name: string;
|
name: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
options?: [] | any;
|
options: MultiSelectOption[];
|
||||||
columns?: COL_WIDTHS;
|
columns?: COL_WIDTHS;
|
||||||
creatable?: boolean;
|
creatable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MultiSelect = ({
|
export const MultiSelect = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
columns,
|
columns,
|
||||||
creatable,
|
creatable
|
||||||
}: MultiSelectProps) => {
|
}: MultiSelectProps) => {
|
||||||
const settingsContext = SettingsContext.useValue();
|
const settingsContext = SettingsContext.useValue();
|
||||||
|
|
||||||
const handleNewField = (value: string) => ({
|
const handleNewField = (value: string) => ({
|
||||||
value: value.toUpperCase(),
|
value: value.toUpperCase(),
|
||||||
label: value.toUpperCase(),
|
label: value.toUpperCase(),
|
||||||
key: value,
|
key: value
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
columns ? `col-span-${columns}` : "col-span-12"
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
||||||
htmlFor={label}
|
htmlFor={label}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Field name={name} type="select" multiple={true}>
|
<Field name={name} type="select" multiple={true}>
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue }
|
||||||
}: any) => (
|
}: FieldProps) => (
|
||||||
<RMSC
|
<RMSC
|
||||||
{...field}
|
{...field}
|
||||||
type="select"
|
options={[...[...options, ...field.value.map((i: MultiSelectOption) => ({ value: i.value ?? i, label: i.label ?? i }))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
||||||
options={[...[...options, ...field.value.map((i: any) => ({ value: i.value ?? i, label: i.label ?? i}))].reduce((map, obj) => map.set(obj.value, obj), new Map()).values()]}
|
labelledBy={name}
|
||||||
labelledBy={name}
|
isCreatable={creatable}
|
||||||
isCreatable={creatable}
|
onCreateOption={handleNewField}
|
||||||
onCreateOption={handleNewField}
|
value={field.value && field.value.map((item: MultiSelectOption) => ({
|
||||||
value={field.value && field.value.map((item: any) => ({
|
value: item.value ? item.value : item,
|
||||||
value: item.value ? item.value : item,
|
label: item.label ? item.label : item
|
||||||
label: item.label ? item.label : item,
|
}))}
|
||||||
}))}
|
onChange={(values: Array<MultiSelectOption>) => {
|
||||||
onChange={(values: any) => {
|
const am = values && values.map((i) => i.value);
|
||||||
const am = values && values.map((i: any) => i.value);
|
|
||||||
|
|
||||||
setFieldValue(field.name, am);
|
setFieldValue(field.name, am);
|
||||||
}}
|
}}
|
||||||
className={settingsContext.darkTheme ? "dark" : ""}
|
className={settingsContext.darkTheme ? "dark" : ""}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IndexerMultiSelectOption {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IndexerMultiSelect = ({
|
export const IndexerMultiSelect = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
options,
|
options,
|
||||||
columns,
|
columns
|
||||||
}: MultiSelectProps) => {
|
}: MultiSelectProps) => {
|
||||||
const settingsContext = SettingsContext.useValue();
|
const settingsContext = SettingsContext.useValue();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
columns ? `col-span-${columns}` : "col-span-12"
|
columns ? `col-span-${columns}` : "col-span-12"
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
||||||
|
htmlFor={label}
|
||||||
>
|
>
|
||||||
<label
|
{label}
|
||||||
className="block mb-2 text-xs font-bold tracking-wide text-gray-700 uppercase dark:text-gray-200"
|
</label>
|
||||||
htmlFor={label}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<Field name={name} type="select" multiple={true}>
|
<Field name={name} type="select" multiple={true}>
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue }
|
||||||
}: any) => (
|
}: FieldProps) => (
|
||||||
<RMSC
|
<RMSC
|
||||||
{...field}
|
{...field}
|
||||||
type="select"
|
options={options}
|
||||||
options={options}
|
labelledBy={name}
|
||||||
labelledBy={name}
|
value={field.value && field.value.map((item: IndexerMultiSelectOption) => ({
|
||||||
value={field.value && field.value.map((item: any) => options.find((o: any) => o.value?.id === item.id))}
|
value: item.id, label: item.name
|
||||||
onChange={(values: any) => {
|
}))}
|
||||||
const am = values && values.map((i: any) => i.value);
|
onChange={(values: MultiSelectOption[]) => {
|
||||||
setFieldValue(field.name, am);
|
const item = values && values.map((i) => ({ id: i.value, name: i.label }));
|
||||||
}}
|
setFieldValue(field.name, item);
|
||||||
className={settingsContext.darkTheme ? "dark" : ""}
|
}}
|
||||||
itemHeight={50}
|
className={settingsContext.darkTheme ? "dark" : ""}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface DownloadClientSelectProps {
|
interface DownloadClientSelectProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -123,102 +134,102 @@ interface DownloadClientSelectProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DownloadClientSelect({
|
export function DownloadClientSelect({
|
||||||
name,
|
name,
|
||||||
action,
|
action,
|
||||||
clients
|
clients
|
||||||
}: DownloadClientSelectProps) {
|
}: DownloadClientSelectProps) {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-6 sm:col-span-6">
|
<div className="col-span-6 sm:col-span-6">
|
||||||
<Field name={name} type="select">
|
<Field name={name} type="select">
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue }
|
||||||
}: any) => (
|
}: FieldProps) => (
|
||||||
<Listbox
|
<Listbox
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
onChange={(value) => setFieldValue(field?.name, value)}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
Client
|
Client
|
||||||
</Listbox.Label>
|
</Listbox.Label>
|
||||||
<div className="mt-2 relative">
|
<div className="mt-2 relative">
|
||||||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{field.value
|
{field.value
|
||||||
? clients.find((c) => c.id === field.value)!.name
|
? clients.find((c) => c.id === field.value)?.name
|
||||||
: "Choose a client"}
|
: "Choose a client"}
|
||||||
</span>
|
</span>
|
||||||
{/*<span className="block truncate">Choose a client</span>*/}
|
{/*<span className="block truncate">Choose a client</span>*/}
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
<SelectorIcon
|
<SelectorIcon
|
||||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
show={open}
|
show={open}
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
leave="transition ease-in duration-100"
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<Listbox.Options
|
||||||
static
|
static
|
||||||
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
>
|
>
|
||||||
{clients
|
{clients
|
||||||
.filter((c) => c.type === action.type)
|
.filter((c) => c.type === action.type)
|
||||||
.map((client: any) => (
|
.map((client) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={client.id}
|
key={client.id}
|
||||||
className={({ active }) => classNames(
|
className={({ active }) => classNames(
|
||||||
active
|
active
|
||||||
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||||
: "text-gray-900 dark:text-gray-300",
|
: "text-gray-900 dark:text-gray-300",
|
||||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
)}
|
)}
|
||||||
value={client.id}
|
value={client.id}
|
||||||
>
|
>
|
||||||
{({ selected, active }) => (
|
{({ selected, active }) => (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
selected ? "font-semibold" : "font-normal",
|
selected ? "font-semibold" : "font-normal",
|
||||||
"block truncate"
|
"block truncate"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{client.name}
|
{client.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className="h-5 w-5"
|
className="h-5 w-5"
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectFieldOption {
|
interface SelectFieldOption {
|
||||||
|
@ -234,209 +245,209 @@ interface SelectFieldProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Select = ({
|
export const Select = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
optionDefaultText,
|
optionDefaultText,
|
||||||
options
|
options
|
||||||
}: SelectFieldProps) => {
|
}: SelectFieldProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="col-span-6">
|
<div className="col-span-6">
|
||||||
<Field name={name} type="select">
|
<Field name={name} type="select">
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue }
|
||||||
}: any) => (
|
}: FieldProps) => (
|
||||||
<Listbox
|
<Listbox
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
onChange={(value) => setFieldValue(field?.name, value)}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
|
{label}
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="mt-2 relative">
|
||||||
|
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{field.value
|
||||||
|
? options.find((c) => c.value === field.value)?.label
|
||||||
|
: optionDefaultText
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<SelectorIcon
|
||||||
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
static
|
||||||
|
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{options.map((opt) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={opt.value}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active
|
||||||
|
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||||
|
: "text-gray-900 dark:text-gray-300",
|
||||||
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
<>
|
<>
|
||||||
<Listbox.Label className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<span
|
||||||
{label}
|
className={classNames(
|
||||||
</Listbox.Label>
|
selected ? "font-semibold" : "font-normal",
|
||||||
<div className="mt-2 relative">
|
"block truncate"
|
||||||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
)}
|
||||||
<span className="block truncate">
|
>
|
||||||
{field.value
|
{opt.label}
|
||||||
? options.find((c) => c.value === field.value)!.label
|
</span>
|
||||||
: optionDefaultText
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<SelectorIcon
|
|
||||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
{selected ? (
|
||||||
show={open}
|
<span
|
||||||
as={Fragment}
|
className={classNames(
|
||||||
leave="transition ease-in duration-100"
|
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||||
leaveFrom="opacity-100"
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
leaveTo="opacity-0"
|
)}
|
||||||
>
|
>
|
||||||
<Listbox.Options
|
<CheckIcon
|
||||||
static
|
className="h-5 w-5"
|
||||||
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
aria-hidden="true"
|
||||||
>
|
/>
|
||||||
{options.map((opt) => (
|
</span>
|
||||||
<Listbox.Option
|
) : null}
|
||||||
key={opt.value}
|
|
||||||
className={({ active }) =>
|
|
||||||
classNames(
|
|
||||||
active
|
|
||||||
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
|
||||||
: "text-gray-900 dark:text-gray-300",
|
|
||||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={opt.value}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
selected ? "font-semibold" : "font-normal",
|
|
||||||
"block truncate"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox>
|
</Listbox.Option>
|
||||||
)}
|
))}
|
||||||
</Field>
|
</Listbox.Options>
|
||||||
</div>
|
</Transition>
|
||||||
);
|
</div>
|
||||||
}
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const SelectWide = ({
|
export const SelectWide = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
optionDefaultText,
|
optionDefaultText,
|
||||||
options
|
options
|
||||||
}: SelectFieldProps) => {
|
}: SelectFieldProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
|
||||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<Field name={name} type="select">
|
<Field name={name} type="select">
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
form: { setFieldValue },
|
form: { setFieldValue }
|
||||||
}: any) => (
|
}: FieldProps) => (
|
||||||
<Listbox
|
<Listbox
|
||||||
value={field.value}
|
value={field.value}
|
||||||
onChange={(value: any) => setFieldValue(field?.name, value)}
|
onChange={(value) => setFieldValue(field?.name, value)}
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<div className="py-4 flex items-center justify-between">
|
||||||
|
|
||||||
|
<Listbox.Label className="block text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{label}
|
||||||
|
</Listbox.Label>
|
||||||
|
<div className="w-full">
|
||||||
|
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
||||||
|
<span className="block truncate">
|
||||||
|
{field.value
|
||||||
|
? options.find((c) => c.value === field.value)?.label
|
||||||
|
: optionDefaultText
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<SelectorIcon
|
||||||
|
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
<Listbox.Options
|
||||||
<div className="py-4 flex items-center justify-between">
|
static
|
||||||
|
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={opt.value}
|
||||||
|
className={({ active }) =>
|
||||||
|
classNames(
|
||||||
|
active
|
||||||
|
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
||||||
|
: "text-gray-900 dark:text-gray-300",
|
||||||
|
"cursor-default select-none relative py-2 pl-3 pr-9"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
|
{({ selected, active }) => (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
selected ? "font-semibold" : "font-normal",
|
||||||
|
"block truncate"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
<Listbox.Label className="block text-sm font-medium text-gray-900 dark:text-white">
|
{selected ? (
|
||||||
{label}
|
<span
|
||||||
</Listbox.Label>
|
className={classNames(
|
||||||
<div className="w-full">
|
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
||||||
<Listbox.Button className="bg-white dark:bg-gray-800 relative w-full border border-gray-300 dark:border-gray-700 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 dark:text-gray-200 sm:text-sm">
|
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||||
<span className="block truncate">
|
)}
|
||||||
{field.value
|
>
|
||||||
? options.find((c) => c.value === field.value)!.label
|
<CheckIcon
|
||||||
: optionDefaultText
|
className="h-5 w-5"
|
||||||
}
|
aria-hidden="true"
|
||||||
</span>
|
/>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
</span>
|
||||||
<SelectorIcon
|
) : null}
|
||||||
className="h-5 w-5 text-gray-400 dark:text-gray-300"
|
</>
|
||||||
aria-hidden="true"
|
)}
|
||||||
/>
|
</Listbox.Option>
|
||||||
</span>
|
))}
|
||||||
</Listbox.Button>
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
<Transition
|
</div>
|
||||||
show={open}
|
</div>
|
||||||
as={Fragment}
|
)}
|
||||||
leave="transition ease-in duration-100"
|
</Listbox>
|
||||||
leaveFrom="opacity-100"
|
)}
|
||||||
leaveTo="opacity-0"
|
</Field>
|
||||||
>
|
</div>
|
||||||
<Listbox.Options
|
</div>
|
||||||
static
|
);
|
||||||
className="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
};
|
||||||
>
|
|
||||||
{options.map((opt) => (
|
|
||||||
<Listbox.Option
|
|
||||||
key={opt.value}
|
|
||||||
className={({ active }) =>
|
|
||||||
classNames(
|
|
||||||
active
|
|
||||||
? "text-white dark:text-gray-100 bg-indigo-600 dark:bg-gray-800"
|
|
||||||
: "text-gray-900 dark:text-gray-300",
|
|
||||||
"cursor-default select-none relative py-2 pl-3 pr-9"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={opt.value}
|
|
||||||
>
|
|
||||||
{({ selected, active }) => (
|
|
||||||
<>
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
selected ? "font-semibold" : "font-normal",
|
|
||||||
"block truncate"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{selected ? (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
active ? "text-white dark:text-gray-100" : "text-indigo-600 dark:text-gray-700",
|
|
||||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className="h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Listbox>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,69 +1,73 @@
|
||||||
|
import React from "react";
|
||||||
import { Field } from "formik";
|
import { Field } from "formik";
|
||||||
import type {
|
import type {
|
||||||
FieldInputProps,
|
FieldInputProps,
|
||||||
FieldMetaProps,
|
FieldMetaProps,
|
||||||
FieldProps,
|
FieldProps,
|
||||||
FormikProps,
|
FormikProps,
|
||||||
FormikValues
|
FormikValues
|
||||||
} from "formik";
|
} from "formik";
|
||||||
import { Switch as HeadlessSwitch } from "@headlessui/react";
|
import { Switch as HeadlessSwitch } from "@headlessui/react";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
type SwitchProps<V = any> = {
|
type SwitchProps<V = unknown> = {
|
||||||
label: string
|
label?: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
|
value: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onChange: (value: boolean) => void
|
onChange: (value: boolean) => void
|
||||||
field?: FieldInputProps<V>
|
field?: FieldInputProps<V>
|
||||||
form?: FormikProps<FormikValues>
|
form?: FormikProps<FormikValues>
|
||||||
meta?: FieldMetaProps<V>
|
meta?: FieldMetaProps<V>
|
||||||
}
|
children: React.ReactNode
|
||||||
|
className: string
|
||||||
|
};
|
||||||
|
|
||||||
export const Switch = ({
|
export const Switch = ({
|
||||||
label,
|
label,
|
||||||
checked: $checked,
|
checked: $checked,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onChange: $onChange,
|
onChange: $onChange,
|
||||||
field,
|
field,
|
||||||
form,
|
form
|
||||||
}: SwitchProps) => {
|
}: SwitchProps) => {
|
||||||
const checked = field?.checked ?? $checked
|
const checked = field?.checked ?? $checked;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
|
<HeadlessSwitch.Group as="div" className="flex items-center space-x-4">
|
||||||
<HeadlessSwitch.Label>{label}</HeadlessSwitch.Label>
|
<HeadlessSwitch.Label>{label}</HeadlessSwitch.Label>
|
||||||
<HeadlessSwitch
|
<HeadlessSwitch
|
||||||
as="button"
|
as="button"
|
||||||
name={field?.name}
|
name={field?.name}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
form?.setFieldValue(field?.name ?? '', value)
|
form?.setFieldValue(field?.name ?? "", value);
|
||||||
$onChange && $onChange(value)
|
$onChange && $onChange(value);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
checked ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{({ checked }) => (
|
{({ checked }) => (
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
checked ? 'translate-x-5' : 'translate-x-0',
|
checked ? "translate-x-5" : "translate-x-0",
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</HeadlessSwitch>
|
</HeadlessSwitch>
|
||||||
</HeadlessSwitch.Group>
|
</HeadlessSwitch.Group>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SwitchFormikProps = SwitchProps & FieldProps & React.InputHTMLAttributes<HTMLInputElement>;
|
export type SwitchFormikProps = SwitchProps & FieldProps & React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} />
|
export const SwitchFormik = (props: SwitchProps) => <Switch {...props} children={props.children}/>;
|
||||||
|
|
||||||
interface SwitchGroupProps {
|
interface SwitchGroupProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -73,54 +77,54 @@ interface SwitchGroupProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SwitchGroup = ({
|
const SwitchGroup = ({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
description
|
description
|
||||||
}: SwitchGroupProps) => (
|
}: SwitchGroupProps) => (
|
||||||
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
|
<HeadlessSwitch.Group as="ol" className="py-4 flex items-center justify-between">
|
||||||
{label && <div className="flex flex-col">
|
{label && <div className="flex flex-col">
|
||||||
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
<HeadlessSwitch.Label as="p" className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
||||||
passive>
|
passive>
|
||||||
{label}
|
{label}
|
||||||
</HeadlessSwitch.Label>
|
</HeadlessSwitch.Label>
|
||||||
{description && (
|
{description && (
|
||||||
<HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400">
|
<HeadlessSwitch.Description className="text-sm mt-1 text-gray-500 dark:text-gray-400">
|
||||||
{description}
|
{description}
|
||||||
</HeadlessSwitch.Description>
|
</HeadlessSwitch.Description>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Field name={name} type="checkbox">
|
|
||||||
{({
|
|
||||||
field,
|
|
||||||
form: { setFieldValue },
|
|
||||||
}: any) => (
|
|
||||||
<Switch
|
|
||||||
{...field}
|
|
||||||
type="button"
|
|
||||||
value={field.value}
|
|
||||||
checked={field.checked}
|
|
||||||
onChange={value => {
|
|
||||||
setFieldValue(field?.name ?? '', value)
|
|
||||||
}}
|
|
||||||
className={classNames(
|
|
||||||
field.value ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200',
|
|
||||||
'ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={classNames(
|
|
||||||
field.value ? 'translate-x-5' : 'translate-x-0',
|
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
|
<Field name={name} type="checkbox">
|
||||||
|
{({
|
||||||
|
field,
|
||||||
|
form: { setFieldValue }
|
||||||
|
}: FieldProps) => (
|
||||||
|
<Switch
|
||||||
|
{...field}
|
||||||
|
// type="button"
|
||||||
|
value={field.value}
|
||||||
|
checked={field.checked ?? false}
|
||||||
|
onChange={value => {
|
||||||
|
setFieldValue(field?.name ?? "", value);
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
field.value ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200",
|
||||||
|
"ml-4 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
field.value ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
)}
|
)}
|
||||||
</Field>
|
/>
|
||||||
</HeadlessSwitch.Group>
|
</Switch>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</HeadlessSwitch.Group>
|
||||||
);
|
);
|
||||||
|
|
||||||
export { SwitchGroup }
|
export { SwitchGroup };
|
|
@ -1,92 +1,92 @@
|
||||||
import { Fragment, FC } from "react";
|
import React, { Fragment, FC } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { ExclamationIcon } from "@heroicons/react/solid";
|
import { ExclamationIcon } from "@heroicons/react/solid";
|
||||||
|
|
||||||
interface DeleteModalProps {
|
interface DeleteModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
buttonRef: any;
|
buttonRef: React.MutableRefObject<HTMLElement | null> | undefined;
|
||||||
toggle: any;
|
toggle: () => void;
|
||||||
deleteAction: any;
|
deleteAction: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => (
|
export const DeleteModal: FC<DeleteModalProps> = ({ isOpen, buttonRef, toggle, deleteAction, title, text }) => (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog
|
<Dialog
|
||||||
as="div"
|
as="div"
|
||||||
static
|
static
|
||||||
className="fixed z-10 inset-0 overflow-y-auto"
|
className="fixed z-10 inset-0 overflow-y-auto"
|
||||||
initialFocus={buttonRef}
|
initialFocus={buttonRef}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
onClose={toggle}
|
onClose={toggle}
|
||||||
|
>
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
<Dialog.Overlay className="fixed inset-0 bg-gray-700/60 dark:bg-black/60 transition-opacity" />
|
||||||
<Transition.Child
|
</Transition.Child>
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 bg-gray-700/60 dark:bg-black/60 transition-opacity" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
​
|
​
|
||||||
</span>
|
</span>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div className="inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
<div className="inline-block align-bottom rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
<div className="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<ExclamationIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" />
|
<ExclamationIcon className="h-16 w-16 text-red-500 dark:text-red-500" aria-hidden="true" />
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-300">
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (isOpen) {
|
|
||||||
deleteAction();
|
|
||||||
toggle();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
|
||||||
onClick={toggle}
|
|
||||||
ref={buttonRef}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
</Transition.Root>
|
<button
|
||||||
)
|
type="button"
|
||||||
|
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (isOpen) {
|
||||||
|
deleteAction();
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 shadow-sm px-4 py-2 bg-white dark:bg-gray-700 text-base font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
|
onClick={toggle}
|
||||||
|
// ref={buttonRef}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
|
@ -1,30 +1,30 @@
|
||||||
import { FC } from 'react'
|
import { FC } from "react";
|
||||||
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from '@heroicons/react/solid'
|
import { XIcon, CheckCircleIcon, ExclamationIcon, ExclamationCircleIcon } from "@heroicons/react/solid";
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast, Toast as Tooast } from "react-hot-toast";
|
||||||
import { classNames } from '../../utils'
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: 'error' | 'success' | 'warning'
|
type: "error" | "success" | "warning"
|
||||||
body?: string
|
body?: string
|
||||||
t?: any;
|
t?: Tooast;
|
||||||
}
|
};
|
||||||
|
|
||||||
const Toast: FC<Props> = ({ type, body, t }) => (
|
const Toast: FC<Props> = ({ type, body, t }) => (
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
t.visible ? 'animate-enter' : 'animate-leave',
|
t?.visible ? "animate-enter" : "animate-leave",
|
||||||
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
"max-w-sm w-full bg-white dark:bg-gray-800 shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden transition-all")}>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{type === 'success' && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
{type === "success" && <CheckCircleIcon className="h-6 w-6 text-green-400" aria-hidden="true" />}
|
||||||
{type === 'error' && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
{type === "error" && <ExclamationCircleIcon className="h-6 w-6 text-red-400" aria-hidden="true" />}
|
||||||
{type === 'warning' && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
{type === "warning" && <ExclamationIcon className="h-6 w-6 text-yellow-400" aria-hidden="true" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 w-0 flex-1 pt-0.5">
|
<div className="ml-3 w-0 flex-1 pt-0.5">
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||||
{type === 'success' && "Success"}
|
{type === "success" && "Success"}
|
||||||
{type === 'error' && "Error"}
|
{type === "error" && "Error"}
|
||||||
{type === 'warning' && "Warning"}
|
{type === "warning" && "Warning"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{body}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +32,7 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
||||||
<button
|
<button
|
||||||
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="bg-white dark:bg-gray-700 rounded-md inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toast.dismiss(t.id)
|
toast.dismiss(t?.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
|
@ -42,6 +42,6 @@ const Toast: FC<Props> = ({ type, body, t }) => (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export default Toast;
|
export default Toast;
|
|
@ -1,4 +1,4 @@
|
||||||
import { Fragment, useRef } from "react";
|
import React, { Fragment, useRef } from "react";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Form, Formik } from "formik";
|
import { Form, Formik } from "formik";
|
||||||
|
@ -10,7 +10,7 @@ import { classNames } from "../../utils";
|
||||||
interface SlideOverProps<DataType> {
|
interface SlideOverProps<DataType> {
|
||||||
title: string;
|
title: string;
|
||||||
initialValues: DataType;
|
initialValues: DataType;
|
||||||
validate?: (values?: any) => void;
|
validate?: (values: DataType) => void;
|
||||||
onSubmit: (values?: DataType) => void;
|
onSubmit: (values?: DataType) => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
|
@ -30,117 +30,117 @@ function SlideOver<DataType>({
|
||||||
type,
|
type,
|
||||||
children
|
children
|
||||||
}: SlideOverProps<DataType>): React.ReactElement {
|
}: SlideOverProps<DataType>): React.ReactElement {
|
||||||
const cancelModalButtonRef = useRef(null);
|
const cancelModalButtonRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||||
{deleteAction && (
|
{deleteAction && (
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
isOpen={deleteModalIsOpen}
|
isOpen={deleteModalIsOpen}
|
||||||
toggle={toggleDeleteModal}
|
toggle={toggleDeleteModal}
|
||||||
buttonRef={cancelModalButtonRef}
|
buttonRef={cancelModalButtonRef}
|
||||||
deleteAction={deleteAction}
|
deleteAction={deleteAction}
|
||||||
title={`Remove ${title}`}
|
title={`Remove ${title}`}
|
||||||
text={`Are you sure you want to remove this ${title}? This action cannot be undone.`}
|
text={`Are you sure you want to remove this ${title}? This action cannot be undone.`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
<Dialog.Overlay className="absolute inset-0" />
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
enterFrom="translate-x-full"
|
enterFrom="translate-x-full"
|
||||||
enterTo="translate-x-0"
|
enterTo="translate-x-0"
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
leaveFrom="translate-x-0"
|
leaveFrom="translate-x-0"
|
||||||
leaveTo="translate-x-full"
|
leaveTo="translate-x-full"
|
||||||
>
|
>
|
||||||
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
>
|
>
|
||||||
{({ handleSubmit, values }) => (
|
{({ handleSubmit, values }) => (
|
||||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll"
|
||||||
onSubmit={handleSubmit}>
|
onSubmit={handleSubmit}>
|
||||||
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">{type === "CREATE" ? "Create" : "Update"} {title}</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{type === "CREATE" ? "Create" : "Update"} {title}.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-900 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!!values && children !== undefined ? (
|
|
||||||
children(values)
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
|
||||||
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
|
|
||||||
{type === "UPDATE" && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-white bg-red-100 dark:bg-red-700 hover:bg-red-200 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
|
||||||
onClick={toggleDeleteModal}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
||||||
>
|
|
||||||
{type === "CREATE" ? "Create" : "Save"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values} />
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">{type === "CREATE" ? "Create" : "Update"} {title}</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{type === "CREATE" ? "Create" : "Update"} {title}.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-7 flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-900 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close panel</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</Transition.Child>
|
{!!values && children !== undefined ? (
|
||||||
</div>
|
children(values)
|
||||||
</div>
|
) : null}
|
||||||
</Dialog>
|
</div>
|
||||||
</Transition.Root>
|
|
||||||
)
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
|
<div className={classNames(type === "CREATE" ? "justify-end" : "justify-between", "space-x-3 flex")}>
|
||||||
|
{type === "UPDATE" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-white bg-red-100 dark:bg-red-700 hover:bg-red-200 dark:hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
|
onClick={toggleDeleteModal}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="ml-4 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
{type === "CREATE" ? "Create" : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DEBUG values={values} />
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SlideOver };
|
export { SlideOver };
|
|
@ -1,156 +1,158 @@
|
||||||
|
import { MultiSelectOption } from "../components/inputs/select";
|
||||||
|
|
||||||
export const resolutions = [
|
export const resolutions = [
|
||||||
"2160p",
|
"2160p",
|
||||||
"1080p",
|
"1080p",
|
||||||
"1080i",
|
"1080i",
|
||||||
"810p",
|
"810p",
|
||||||
"720p",
|
"720p",
|
||||||
"576p",
|
"576p",
|
||||||
"480p",
|
"480p",
|
||||||
"480i"
|
"480i"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RESOLUTION_OPTIONS = resolutions.map(r => ({ value: r, label: r, key: r}));
|
export const RESOLUTION_OPTIONS: MultiSelectOption[] = resolutions.map(r => ({ value: r, label: r, key: r }));
|
||||||
|
|
||||||
export const codecs = [
|
export const codecs = [
|
||||||
"HEVC",
|
"HEVC",
|
||||||
"H.264",
|
"H.264",
|
||||||
"H.265",
|
"H.265",
|
||||||
"x264",
|
"x264",
|
||||||
"x265",
|
"x265",
|
||||||
"AVC",
|
"AVC",
|
||||||
"VC-1",
|
"VC-1",
|
||||||
"AV1",
|
"AV1",
|
||||||
"XviD"
|
"XviD"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CODECS_OPTIONS = codecs.map(v => ({ value: v, label: v, key: v}));
|
export const CODECS_OPTIONS: MultiSelectOption[] = codecs.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const sources = [
|
export const sources = [
|
||||||
"BluRay",
|
"BluRay",
|
||||||
"UHD.BluRay",
|
"UHD.BluRay",
|
||||||
"WEB-DL",
|
"WEB-DL",
|
||||||
"WEB",
|
"WEB",
|
||||||
"WEBRip",
|
"WEBRip",
|
||||||
"BD5",
|
"BD5",
|
||||||
"BD9",
|
"BD9",
|
||||||
"BDr",
|
"BDr",
|
||||||
"BDRip",
|
"BDRip",
|
||||||
"BRRip",
|
"BRRip",
|
||||||
"CAM",
|
"CAM",
|
||||||
"DVDR",
|
"DVDR",
|
||||||
"DVDRip",
|
"DVDRip",
|
||||||
"DVDScr",
|
"DVDScr",
|
||||||
"HDCAM",
|
"HDCAM",
|
||||||
"HDDVD",
|
"HDDVD",
|
||||||
"HDDVDRip",
|
"HDDVDRip",
|
||||||
"HDTS",
|
"HDTS",
|
||||||
"HDTV",
|
"HDTV",
|
||||||
"Mixed",
|
"Mixed",
|
||||||
"SiteRip",
|
"SiteRip"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SOURCES_OPTIONS = sources.map(v => ({ value: v, label: v, key: v}));
|
export const SOURCES_OPTIONS: MultiSelectOption[] = sources.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const containers = [
|
export const containers = [
|
||||||
"avi",
|
"avi",
|
||||||
"mp4",
|
"mp4",
|
||||||
"mkv",
|
"mkv"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const CONTAINER_OPTIONS = containers.map(v => ({ value: v, label: v, key: v}));
|
export const CONTAINER_OPTIONS: MultiSelectOption[] = containers.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const hdr = [
|
export const hdr = [
|
||||||
"HDR",
|
"HDR",
|
||||||
"HDR10",
|
"HDR10",
|
||||||
"HDR10+",
|
"HDR10+",
|
||||||
"HLG",
|
"HLG",
|
||||||
"DV",
|
"DV",
|
||||||
"DV HDR",
|
"DV HDR",
|
||||||
"DV HDR10",
|
"DV HDR10",
|
||||||
"DV HDR10+",
|
"DV HDR10+",
|
||||||
"DoVi",
|
"DoVi",
|
||||||
"Dolby Vision",
|
"Dolby Vision"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const HDR_OPTIONS = hdr.map(v => ({ value: v, label: v, key: v}));
|
export const HDR_OPTIONS: MultiSelectOption[] = hdr.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const quality_other = [
|
export const quality_other = [
|
||||||
"REMUX",
|
"REMUX",
|
||||||
"HYBRID",
|
"HYBRID",
|
||||||
"REPACK",
|
"REPACK"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v}));
|
export const OTHER_OPTIONS = quality_other.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const formatMusic = [
|
export const formatMusic = [
|
||||||
"MP3",
|
"MP3",
|
||||||
"FLAC",
|
"FLAC",
|
||||||
"Ogg Vorbis",
|
"Ogg Vorbis",
|
||||||
"Ogg",
|
"Ogg",
|
||||||
"AAC",
|
"AAC",
|
||||||
"AC3",
|
"AC3",
|
||||||
"DTS",
|
"DTS"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const FORMATS_OPTIONS = formatMusic.map(r => ({ value: r, label: r, key: r}));
|
export const FORMATS_OPTIONS: MultiSelectOption[] = formatMusic.map(r => ({ value: r, label: r, key: r }));
|
||||||
|
|
||||||
export const sourcesMusic = [
|
export const sourcesMusic = [
|
||||||
"CD",
|
"CD",
|
||||||
"WEB",
|
"WEB",
|
||||||
"DVD",
|
"DVD",
|
||||||
"Vinyl",
|
"Vinyl",
|
||||||
"Soundboard",
|
"Soundboard",
|
||||||
"DAT",
|
"DAT",
|
||||||
"Cassette",
|
"Cassette",
|
||||||
"Blu-Ray",
|
"Blu-Ray",
|
||||||
"SACD",
|
"SACD"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SOURCES_MUSIC_OPTIONS = sourcesMusic.map(v => ({ value: v, label: v, key: v}));
|
export const SOURCES_MUSIC_OPTIONS: MultiSelectOption[] = sourcesMusic.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const qualityMusic = [
|
export const qualityMusic = [
|
||||||
"192",
|
"192",
|
||||||
"256",
|
"256",
|
||||||
"320",
|
"320",
|
||||||
"APS (VBR)",
|
"APS (VBR)",
|
||||||
"APX (VBR)",
|
"APX (VBR)",
|
||||||
"V2 (VBR)",
|
"V2 (VBR)",
|
||||||
"V1 (VBR)",
|
"V1 (VBR)",
|
||||||
"V0 (VBR)",
|
"V0 (VBR)",
|
||||||
"Lossless",
|
"Lossless",
|
||||||
"24bit Lossless",
|
"24bit Lossless"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const QUALITY_MUSIC_OPTIONS = qualityMusic.map(v => ({ value: v, label: v, key: v}));
|
export const QUALITY_MUSIC_OPTIONS: MultiSelectOption[] = qualityMusic.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const releaseTypeMusic = [
|
export const releaseTypeMusic = [
|
||||||
"Album",
|
"Album",
|
||||||
"Single",
|
"Single",
|
||||||
"EP",
|
"EP",
|
||||||
"Soundtrack",
|
"Soundtrack",
|
||||||
"Anthology",
|
"Anthology",
|
||||||
"Compilation",
|
"Compilation",
|
||||||
"Live album",
|
"Live album",
|
||||||
"Remix",
|
"Remix",
|
||||||
"Bootleg",
|
"Bootleg",
|
||||||
"Interview",
|
"Interview",
|
||||||
"Mixtape",
|
"Mixtape",
|
||||||
"Demo",
|
"Demo",
|
||||||
"Concert Recording",
|
"Concert Recording",
|
||||||
"DJ Mix",
|
"DJ Mix",
|
||||||
"Unknown",
|
"Unknown"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RELEASE_TYPE_MUSIC_OPTIONS = releaseTypeMusic.map(v => ({ value: v, label: v, key: v}));
|
export const RELEASE_TYPE_MUSIC_OPTIONS: MultiSelectOption[] = releaseTypeMusic.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export const originOptions = [
|
export const originOptions = [
|
||||||
"P2P",
|
"P2P",
|
||||||
"Internal",
|
"Internal",
|
||||||
"SCENE",
|
"SCENE",
|
||||||
"O-SCENE",
|
"O-SCENE"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v}));
|
export const ORIGIN_OPTIONS = originOptions.map(v => ({ value: v, label: v, key: v }));
|
||||||
|
|
||||||
export interface RadioFieldsetOption {
|
export interface RadioFieldsetOption {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -159,123 +161,128 @@ export interface RadioFieldsetOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
|
export const DownloadClientTypeOptions: RadioFieldsetOption[] = [
|
||||||
{
|
{
|
||||||
label: "qBittorrent",
|
label: "qBittorrent",
|
||||||
description: "Add torrents directly to qBittorrent",
|
description: "Add torrents directly to qBittorrent",
|
||||||
value: "QBITTORRENT"
|
value: "QBITTORRENT"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Deluge",
|
label: "Deluge",
|
||||||
description: "Add torrents directly to Deluge",
|
description: "Add torrents directly to Deluge",
|
||||||
value: "DELUGE_V1"
|
value: "DELUGE_V1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Deluge 2",
|
label: "Deluge 2",
|
||||||
description: "Add torrents directly to Deluge 2",
|
description: "Add torrents directly to Deluge 2",
|
||||||
value: "DELUGE_V2"
|
value: "DELUGE_V2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Radarr",
|
label: "Radarr",
|
||||||
description: "Send to Radarr and let it decide",
|
description: "Send to Radarr and let it decide",
|
||||||
value: "RADARR"
|
value: "RADARR"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Sonarr",
|
label: "Sonarr",
|
||||||
description: "Send to Sonarr and let it decide",
|
description: "Send to Sonarr and let it decide",
|
||||||
value: "SONARR"
|
value: "SONARR"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Lidarr",
|
label: "Lidarr",
|
||||||
description: "Send to Lidarr and let it decide",
|
description: "Send to Lidarr and let it decide",
|
||||||
value: "LIDARR"
|
value: "LIDARR"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Whisparr",
|
label: "Whisparr",
|
||||||
description: "Send to Whisparr and let it decide",
|
description: "Send to Whisparr and let it decide",
|
||||||
value: "WHISPARR"
|
value: "WHISPARR"
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
|
export const DownloadClientTypeNameMap: Record<DownloadClientType | string, string> = {
|
||||||
"DELUGE_V1": "Deluge v1",
|
"DELUGE_V1": "Deluge v1",
|
||||||
"DELUGE_V2": "Deluge v2",
|
"DELUGE_V2": "Deluge v2",
|
||||||
"QBITTORRENT": "qBittorrent",
|
"QBITTORRENT": "qBittorrent",
|
||||||
"RADARR": "Radarr",
|
"RADARR": "Radarr",
|
||||||
"SONARR": "Sonarr",
|
"SONARR": "Sonarr",
|
||||||
"LIDARR": "Lidarr",
|
"LIDARR": "Lidarr",
|
||||||
"WHISPARR": "Whisparr",
|
"WHISPARR": "Whisparr"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActionTypeOptions: RadioFieldsetOption[] = [
|
export const ActionTypeOptions: RadioFieldsetOption[] = [
|
||||||
{label: "Test", description: "A simple action to test a filter.", value: "TEST"},
|
{ label: "Test", description: "A simple action to test a filter.", value: "TEST" },
|
||||||
{label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER"},
|
{ label: "Watch dir", description: "Add filtered torrents to a watch directory", value: "WATCH_FOLDER" },
|
||||||
{label: "Webhook", description: "Run webhook", value: "WEBHOOK"},
|
{ label: "Webhook", description: "Run webhook", value: "WEBHOOK" },
|
||||||
{label: "Exec", description: "Run a custom command after a filter match", value: "EXEC"},
|
{ label: "Exec", description: "Run a custom command after a filter match", value: "EXEC" },
|
||||||
{label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT"},
|
{ label: "qBittorrent", description: "Add torrents directly to qBittorrent", value: "QBITTORRENT" },
|
||||||
{label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1"},
|
{ label: "Deluge", description: "Add torrents directly to Deluge", value: "DELUGE_V1" },
|
||||||
{label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2"},
|
{ label: "Deluge v2", description: "Add torrents directly to Deluge 2", value: "DELUGE_V2" },
|
||||||
{label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR"},
|
{ label: "Radarr", description: "Send to Radarr and let it decide", value: "RADARR" },
|
||||||
{label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR"},
|
{ label: "Sonarr", description: "Send to Sonarr and let it decide", value: "SONARR" },
|
||||||
{label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR"},
|
{ label: "Lidarr", description: "Send to Lidarr and let it decide", value: "LIDARR" },
|
||||||
{label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR"},
|
{ label: "Whisparr", description: "Send to Whisparr and let it decide", value: "WHISPARR" }
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ActionTypeNameMap = {
|
export const ActionTypeNameMap = {
|
||||||
"TEST": "Test",
|
"TEST": "Test",
|
||||||
"WATCH_FOLDER": "Watch folder",
|
"WATCH_FOLDER": "Watch folder",
|
||||||
"WEBHOOK": "Webhook",
|
"WEBHOOK": "Webhook",
|
||||||
"EXEC": "Exec",
|
"EXEC": "Exec",
|
||||||
"DELUGE_V1": "Deluge v1",
|
"DELUGE_V1": "Deluge v1",
|
||||||
"DELUGE_V2": "Deluge v2",
|
"DELUGE_V2": "Deluge v2",
|
||||||
"QBITTORRENT": "qBittorrent",
|
"QBITTORRENT": "qBittorrent",
|
||||||
"RADARR": "Radarr",
|
"RADARR": "Radarr",
|
||||||
"SONARR": "Sonarr",
|
"SONARR": "Sonarr",
|
||||||
"LIDARR": "Lidarr",
|
"LIDARR": "Lidarr",
|
||||||
"WHISPARR": "Whisparr",
|
"WHISPARR": "Whisparr"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PushStatusOptions: any[] = [
|
export interface OptionBasic {
|
||||||
{
|
label: string;
|
||||||
label: "Rejected",
|
value: string;
|
||||||
value: "PUSH_REJECTED",
|
}
|
||||||
},
|
|
||||||
{
|
export const PushStatusOptions: OptionBasic[] = [
|
||||||
label: "Approved",
|
{
|
||||||
value: "PUSH_APPROVED"
|
label: "Rejected",
|
||||||
},
|
value: "PUSH_REJECTED"
|
||||||
{
|
},
|
||||||
label: "Error",
|
{
|
||||||
value: "PUSH_ERROR"
|
label: "Approved",
|
||||||
},
|
value: "PUSH_APPROVED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Error",
|
||||||
|
value: "PUSH_ERROR"
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NotificationTypeOptions: any[] = [
|
export const NotificationTypeOptions: OptionBasic[] = [
|
||||||
{
|
{
|
||||||
label: "Discord",
|
label: "Discord",
|
||||||
value: "DISCORD",
|
value: "DISCORD"
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface SelectOption {
|
export interface SelectOption {
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
value: any;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventOptions: SelectOption[] = [
|
export const EventOptions: SelectOption[] = [
|
||||||
{
|
{
|
||||||
label: "Push Rejected",
|
label: "Push Rejected",
|
||||||
value: "PUSH_REJECTED",
|
value: "PUSH_REJECTED",
|
||||||
description: "On push rejected for the arrs or download client",
|
description: "On push rejected for the arrs or download client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Push Approved",
|
label: "Push Approved",
|
||||||
value: "PUSH_APPROVED",
|
value: "PUSH_APPROVED",
|
||||||
description: "On push approved for the arrs or download client",
|
description: "On push approved for the arrs or download client"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Push Error",
|
label: "Push Error",
|
||||||
value: "PUSH_ERROR",
|
value: "PUSH_ERROR",
|
||||||
description: "On push error for the arrs or download client",
|
description: "On push error for the arrs or download client"
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
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,
|
UseSortByInstanceProps,
|
||||||
UseSortByOptions,
|
UseSortByOptions,
|
||||||
UseSortByState
|
UseSortByState
|
||||||
} from 'react-table'
|
} from "react-table";
|
||||||
|
|
||||||
declare module 'react-table' {
|
declare module "react-table" {
|
||||||
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
||||||
|
|
||||||
export interface TableOptions<D extends Record<string, unknown>>
|
export interface TableOptions<D extends Record<string, unknown>>
|
||||||
|
|
|
@ -3,149 +3,160 @@ import { useMutation } from "react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Field, Form, Formik } from "formik";
|
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from "../../components/notifications/Toast";
|
||||||
|
|
||||||
function FilterAddForm({ isOpen, toggle }: any) {
|
interface filterAddFormProps {
|
||||||
const mutation = useMutation(
|
isOpen: boolean;
|
||||||
(filter: Filter) => APIClient.filters.create(filter),
|
toggle: () => void;
|
||||||
{
|
}
|
||||||
onSuccess: (_, filter) => {
|
|
||||||
queryClient.invalidateQueries("filters");
|
|
||||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
|
|
||||||
|
|
||||||
toggle();
|
function FilterAddForm({ isOpen, toggle }: filterAddFormProps) {
|
||||||
}
|
const mutation = useMutation(
|
||||||
}
|
(filter: Filter) => APIClient.filters.create(filter),
|
||||||
)
|
{
|
||||||
|
onSuccess: (_, filter) => {
|
||||||
|
queryClient.invalidateQueries("filters");
|
||||||
|
toast.custom((t) => <Toast type="success" body={`Filter ${filter.name} was added`} t={t} />);
|
||||||
|
|
||||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
toggle();
|
||||||
const validate = (values: any) => values.name ? {} : { name: "Required" };
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const handleSubmit = (data: unknown) => mutation.mutate(data as Filter);
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
const validate = (values: FormikValues) => {
|
||||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
const errors = {} as FormikErrors<FormikValues>;
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
if (!values.name) {
|
||||||
<Dialog.Overlay className="absolute inset-0" />
|
errors.name = "Required";
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
return (
|
||||||
<Transition.Child
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
as={Fragment}
|
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
enterFrom="translate-x-full"
|
<Dialog.Overlay className="absolute inset-0" />
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
|
||||||
|
|
||||||
<Formik
|
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||||
initialValues={{
|
<Transition.Child
|
||||||
name: "",
|
as={Fragment}
|
||||||
enabled: false,
|
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
resolutions: [],
|
enterFrom="translate-x-full"
|
||||||
codecs: [],
|
enterTo="translate-x-0"
|
||||||
sources: [],
|
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
containers: [],
|
leaveFrom="translate-x-0"
|
||||||
origins: [],
|
leaveTo="translate-x-full"
|
||||||
}}
|
>
|
||||||
onSubmit={handleSubmit}
|
<div className="w-screen max-w-2xl border-l dark:border-gray-700">
|
||||||
validate={validate}
|
|
||||||
>
|
<Formik
|
||||||
{({ values }) => (
|
initialValues={{
|
||||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
name: "",
|
||||||
<div className="flex-1">
|
enabled: false,
|
||||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
resolutions: [],
|
||||||
<div className="flex items-start justify-between space-x-3">
|
codecs: [],
|
||||||
<div className="space-y-1">
|
sources: [],
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
|
containers: [],
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
origins: []
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
validate={validate}
|
||||||
|
>
|
||||||
|
{({ values }) => (
|
||||||
|
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
|
<div className="flex items-start justify-between space-x-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Create filter</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Add new filter.
|
Add new filter.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-7 flex items-center">
|
<div className="h-7 flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="light:bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Close panel</span>
|
<span className="sr-only">Close panel</span>
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<div
|
<div
|
||||||
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="name"
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
className="block text-sm font-medium text-gray-900 dark:text-white sm:mt-px sm:pt-2"
|
||||||
>
|
>
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<Field name="name">
|
<Field name="name">
|
||||||
{({
|
{({
|
||||||
field,
|
field,
|
||||||
meta,
|
meta
|
||||||
}: FieldProps ) => (
|
}: FieldProps ) => (
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<input
|
<input
|
||||||
{...field}
|
{...field}
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
|
className="block w-full shadow-sm dark:bg-gray-800 border-gray-300 dark:border-gray-700 sm:text-sm dark:text-white focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 rounded-md"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{meta.touched && meta.error &&
|
{meta.touched && meta.error &&
|
||||||
<span className="block mt-2 text-red-500">{meta.error}</span>}
|
<span className="block mt-2 text-red-500">{meta.error}</span>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
<div className="space-x-3 flex justify-end">
|
<div className="space-x-3 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-white dark:bg-gray-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="bg-white dark:bg-gray-800 py-2 px-4 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DEBUG values={values} />
|
<DEBUG values={values} />
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilterAddForm;
|
export default FilterAddForm;
|
File diff suppressed because it is too large
Load diff
|
@ -1,115 +1,118 @@
|
||||||
import {useMutation} from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import {APIClient} from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import {queryClient} from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import {toast} from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import {SlideOver} from "../../components/panels";
|
import { SlideOver } from "../../components/panels";
|
||||||
import {NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide} from "../../components/inputs";
|
import { NumberFieldWide, PasswordFieldWide, SwitchGroupWide, TextFieldWide } from "../../components/inputs";
|
||||||
import {ImplementationMap} from "../../screens/settings/Feed";
|
import { ImplementationMap } from "../../screens/settings/Feed";
|
||||||
|
import { componentMapType } from "./DownloadClientForms";
|
||||||
|
|
||||||
interface UpdateProps {
|
interface UpdateProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: any;
|
toggle: () => void;
|
||||||
feed: Feed;
|
feed: Feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeedUpdateForm({isOpen, toggle, feed}: UpdateProps) {
|
export function FeedUpdateForm({ isOpen, toggle, feed }: UpdateProps) {
|
||||||
const mutation = useMutation(
|
const mutation = useMutation(
|
||||||
(feed: Feed) => APIClient.feeds.update(feed),
|
(feed: Feed) => APIClient.feeds.update(feed),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["feeds"]);
|
queryClient.invalidateQueries(["feeds"]);
|
||||||
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t}/>)
|
toast.custom((t) => <Toast type="success" body={`${feed.name} was updated successfully`} t={t}/>);
|
||||||
toggle();
|
toggle();
|
||||||
},
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMutation = useMutation(
|
|
||||||
(feedID: number) => APIClient.feeds.delete(feedID),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["feeds"]);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t}/>)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (formData: any) => {
|
|
||||||
mutation.mutate(formData);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const deleteAction = () => {
|
const deleteMutation = useMutation(
|
||||||
deleteMutation.mutate(feed.id);
|
(feedID: number) => APIClient.feeds.delete(feedID),
|
||||||
};
|
{
|
||||||
|
onSuccess: () => {
|
||||||
const initialValues = {
|
queryClient.invalidateQueries(["feeds"]);
|
||||||
id: feed.id,
|
toast.custom((t) => <Toast type="success" body={`${feed.name} was deleted.`} t={t}/>);
|
||||||
indexer: feed.indexer,
|
}
|
||||||
enabled: feed.enabled,
|
|
||||||
type: feed.type,
|
|
||||||
name: feed.name,
|
|
||||||
url: feed.url,
|
|
||||||
api_key: feed.api_key,
|
|
||||||
interval: feed.interval,
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const onSubmit = (formData: unknown) => {
|
||||||
<SlideOver
|
mutation.mutate(formData as Feed);
|
||||||
type="UPDATE"
|
};
|
||||||
title="Feed"
|
|
||||||
isOpen={isOpen}
|
|
||||||
toggle={toggle}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
initialValues={initialValues}
|
|
||||||
>
|
|
||||||
{(values) => (
|
|
||||||
<div>
|
|
||||||
<TextFieldWide name="name" label="Name" required={true}/>
|
|
||||||
|
|
||||||
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
const deleteAction = () => {
|
||||||
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
deleteMutation.mutate(feed.id);
|
||||||
<div>
|
};
|
||||||
<label
|
|
||||||
htmlFor="type"
|
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
|
||||||
>
|
|
||||||
Type
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end sm:col-span-2">
|
|
||||||
{ImplementationMap[feed.type]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
const initialValues = {
|
||||||
<SwitchGroupWide name="enabled" label="Enabled"/>
|
id: feed.id,
|
||||||
</div>
|
indexer: feed.indexer,
|
||||||
</div>
|
enabled: feed.enabled,
|
||||||
{componentMap[values.type]}
|
type: feed.type,
|
||||||
</div>
|
name: feed.name,
|
||||||
)}
|
url: feed.url,
|
||||||
</SlideOver>
|
api_key: feed.api_key,
|
||||||
)
|
interval: feed.interval
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlideOver
|
||||||
|
type="UPDATE"
|
||||||
|
title="Feed"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
initialValues={initialValues}
|
||||||
|
>
|
||||||
|
{(values) => (
|
||||||
|
<div>
|
||||||
|
<TextFieldWide name="name" label="Name" required={true}/>
|
||||||
|
|
||||||
|
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<div
|
||||||
|
className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="type"
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end sm:col-span-2">
|
||||||
|
{ImplementationMap[feed.type]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroupWide name="enabled" label="Enabled"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{componentMap[values.type]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SlideOver>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormFieldsTorznab() {
|
function FormFieldsTorznab() {
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
<TextFieldWide
|
<TextFieldWide
|
||||||
name="url"
|
name="url"
|
||||||
label="URL"
|
label="URL"
|
||||||
help="Torznab url"
|
help="Torznab url"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordFieldWide name="api_key" label="API key" />
|
<PasswordFieldWide name="api_key" label="API key"/>
|
||||||
|
|
||||||
<NumberFieldWide name="interval" label="Refresh interval" help="Minutes. Recommended 15-30. To low and risk ban." />
|
<NumberFieldWide name="interval" label="Refresh interval"
|
||||||
</div>
|
help="Minutes. Recommended 15-30. To low and risk ban."/>
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentMap: any = {
|
const componentMap: componentMapType = {
|
||||||
TORZNAB: <FormFieldsTorznab/>,
|
TORZNAB: <FormFieldsTorznab/>
|
||||||
};
|
};
|
File diff suppressed because it is too large
Load diff
|
@ -1,87 +1,87 @@
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { XIcon } from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import { Field, FieldArray } from "formik";
|
import { Field, FieldArray, FormikErrors, FormikValues } from "formik";
|
||||||
import type { FieldProps } from "formik";
|
import type { FieldProps } from "formik";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TextFieldWide,
|
TextFieldWide,
|
||||||
PasswordFieldWide,
|
PasswordFieldWide,
|
||||||
SwitchGroupWide,
|
SwitchGroupWide,
|
||||||
NumberFieldWide
|
NumberFieldWide
|
||||||
} from "../../components/inputs/input_wide";
|
} from "../../components/inputs";
|
||||||
import { SlideOver } from "../../components/panels";
|
import { SlideOver } from "../../components/panels";
|
||||||
import Toast from '../../components/notifications/Toast';
|
import Toast from "../../components/notifications/Toast";
|
||||||
|
|
||||||
interface ChannelsFieldArrayProps {
|
interface ChannelsFieldArrayProps {
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
|
const ChannelsFieldArray = ({ channels }: ChannelsFieldArrayProps) => (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<FieldArray name="channels">
|
<FieldArray name="channels">
|
||||||
{({ remove, push }) => (
|
{({ remove, push }) => (
|
||||||
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
<div className="flex flex-col border-2 border-dashed dark:border-gray-700 p-4">
|
||||||
{channels && channels.length > 0 ? (
|
{channels && channels.length > 0 ? (
|
||||||
channels.map((_channel: IrcChannel, index: number) => (
|
channels.map((_channel: IrcChannel, index: number) => (
|
||||||
<div key={index} className="flex justify-between">
|
<div key={index} className="flex justify-between">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Field name={`channels.${index}.name`}>
|
<Field name={`channels.${index}.name`}>
|
||||||
{({ field }: FieldProps) => (
|
{({ field }: FieldProps) => (
|
||||||
<input
|
<input
|
||||||
{...field}
|
{...field}
|
||||||
type="text"
|
type="text"
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
placeholder="#Channel"
|
placeholder="#Channel"
|
||||||
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field name={`channels.${index}.password`}>
|
|
||||||
{({ field }: FieldProps) => (
|
|
||||||
<input
|
|
||||||
{...field}
|
|
||||||
type="text"
|
|
||||||
value={field.value ?? ""}
|
|
||||||
onChange={field.onChange}
|
|
||||||
placeholder="Password"
|
|
||||||
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Remove</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-center text-sm text-grey-darker dark:text-white">
|
|
||||||
No channels!
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<button
|
</Field>
|
||||||
type="button"
|
|
||||||
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
|
<Field name={`channels.${index}.password`}>
|
||||||
onClick={() => push({ name: "", password: "" })}
|
{({ field }: FieldProps) => (
|
||||||
>
|
<input
|
||||||
Add Channel
|
{...field}
|
||||||
</button>
|
type="text"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="Password"
|
||||||
|
className="mr-4 dark:bg-gray-700 focus:ring-indigo-500 dark:focus:ring-blue-500 focus:border-indigo-500 dark:focus:border-blue-500 border-gray-300 dark:border-gray-600 block w-full shadow-sm sm:text-sm dark:text-white rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</FieldArray>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Remove</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-center text-sm text-grey-darker dark:text-white">
|
||||||
|
No channels!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="border dark:border-gray-600 dark:bg-gray-700 my-4 px-4 py-2 text-sm text-gray-700 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-600 rounded self-center text-center"
|
||||||
|
onClick={() => push({ name: "", password: "" })}
|
||||||
|
>
|
||||||
|
Add Channel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FieldArray>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IrcNetworkAddFormValues {
|
interface IrcNetworkAddFormValues {
|
||||||
|
@ -95,105 +95,101 @@ interface IrcNetworkAddFormValues {
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IrcNetworkAddForm({ isOpen, toggle }: any) {
|
interface AddFormProps {
|
||||||
const mutation = useMutation(
|
isOpen: boolean;
|
||||||
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
|
toggle: () => void;
|
||||||
{
|
}
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(['networks']);
|
|
||||||
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />)
|
|
||||||
toggle()
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
export function IrcNetworkAddForm({ isOpen, toggle }: AddFormProps) {
|
||||||
// easy way to split textarea lines into array of strings for each newline.
|
const mutation = useMutation(
|
||||||
// parse on the field didn't really work.
|
(network: IrcNetwork) => APIClient.irc.createNetwork(network),
|
||||||
data.connect_commands = (
|
{
|
||||||
data.connect_commands && data.connect_commands.length > 0 ?
|
onSuccess: () => {
|
||||||
data.connect_commands.replace(/\r\n/g, "\n").split("\n") :
|
queryClient.invalidateQueries(["networks"]);
|
||||||
[]
|
toast.custom((t) => <Toast type="success" body="IRC Network added. Please allow up to 30 seconds for the network to come online." t={t} />);
|
||||||
);
|
toggle();
|
||||||
|
},
|
||||||
mutation.mutate(data);
|
onError: () => {
|
||||||
};
|
toast.custom((t) => <Toast type="error" body="IRC Network could not be added" t={t} />);
|
||||||
|
}
|
||||||
const validate = (values: IrcNetworkAddFormValues) => {
|
|
||||||
const errors = {} as any;
|
|
||||||
if (!values.name)
|
|
||||||
errors.name = "Required";
|
|
||||||
|
|
||||||
if (!values.port)
|
|
||||||
errors.port = "Required";
|
|
||||||
|
|
||||||
if (!values.server)
|
|
||||||
errors.server = "Required";
|
|
||||||
|
|
||||||
if (!values.nickserv || !values.nickserv.account)
|
|
||||||
errors.nickserv = { account: "Required" };
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const initialValues: IrcNetworkAddFormValues = {
|
const onSubmit = (data: unknown) => {
|
||||||
name: "",
|
mutation.mutate(data as IrcNetwork);
|
||||||
enabled: true,
|
};
|
||||||
server: "",
|
const validate = (values: FormikValues) => {
|
||||||
port: 6667,
|
const errors = {} as FormikErrors<FormikValues>;
|
||||||
tls: false,
|
if (!values.name)
|
||||||
pass: "",
|
errors.name = "Required";
|
||||||
nickserv: {
|
|
||||||
account: ""
|
|
||||||
},
|
|
||||||
channels: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
if (!values.port)
|
||||||
<SlideOver
|
errors.port = "Required";
|
||||||
type="CREATE"
|
|
||||||
title="Network"
|
|
||||||
isOpen={isOpen}
|
|
||||||
toggle={toggle}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
initialValues={initialValues}
|
|
||||||
validate={validate}
|
|
||||||
>
|
|
||||||
{(values) => (
|
|
||||||
<>
|
|
||||||
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
if (!values.server)
|
||||||
|
errors.server = "Required";
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
if (!values.nickserv || !values.nickserv.account)
|
||||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
errors.nickserv = { account: "Required" };
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
return errors;
|
||||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
};
|
||||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
const initialValues: IrcNetworkAddFormValues = {
|
||||||
<SwitchGroupWide name="tls" label="TLS" />
|
name: "",
|
||||||
</div>
|
enabled: true,
|
||||||
|
server: "",
|
||||||
|
port: 6667,
|
||||||
|
tls: false,
|
||||||
|
pass: "",
|
||||||
|
nickserv: {
|
||||||
|
account: ""
|
||||||
|
},
|
||||||
|
channels: []
|
||||||
|
};
|
||||||
|
|
||||||
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
return (
|
||||||
|
<SlideOver
|
||||||
|
type="CREATE"
|
||||||
|
title="Network"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
initialValues={initialValues}
|
||||||
|
validate={validate}
|
||||||
|
>
|
||||||
|
{(values) => (
|
||||||
|
<>
|
||||||
|
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
||||||
|
|
||||||
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
|
||||||
|
|
||||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200 dark:sm:divide-gray-700">
|
||||||
</div>
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChannelsFieldArray channels={values.channels} />
|
<div>
|
||||||
</>
|
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||||
)}
|
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||||
</SlideOver>
|
|
||||||
)
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroupWide name="tls" label="TLS" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
||||||
|
|
||||||
|
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
||||||
|
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
||||||
|
|
||||||
|
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChannelsFieldArray channels={values.channels} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SlideOver>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IrcNetworkUpdateFormValues {
|
interface IrcNetworkUpdateFormValues {
|
||||||
|
@ -216,118 +212,113 @@ interface IrcNetworkUpdateFormProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IrcNetworkUpdateForm({
|
export function IrcNetworkUpdateForm({
|
||||||
isOpen,
|
isOpen,
|
||||||
toggle,
|
toggle,
|
||||||
network
|
network
|
||||||
}: IrcNetworkUpdateFormProps) {
|
}: IrcNetworkUpdateFormProps) {
|
||||||
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
|
const mutation = useMutation((network: IrcNetwork) => APIClient.irc.updateNetwork(network), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(['networks']);
|
queryClient.invalidateQueries(["networks"]);
|
||||||
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />)
|
toast.custom((t) => <Toast type="success" body={`${network.name} was updated successfully`} t={t} />);
|
||||||
toggle()
|
toggle();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
const deleteMutation = useMutation((id: number) => APIClient.irc.deleteNetwork(id), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(['networks']);
|
queryClient.invalidateQueries(["networks"]);
|
||||||
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />)
|
toast.custom((t) => <Toast type="success" body={`${network.name} was deleted.`} t={t} />);
|
||||||
|
|
||||||
toggle()
|
toggle();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
const onSubmit = (data: unknown) => {
|
||||||
// easy way to split textarea lines into array of strings for each newline.
|
mutation.mutate(data as IrcNetwork);
|
||||||
// parse on the field didn't really work.
|
};
|
||||||
// TODO fix connect_commands on network update
|
|
||||||
// let cmds = data.connect_commands && data.connect_commands.length > 0 ? data.connect_commands.replace(/\r\n/g,"\n").split("\n") : [];
|
|
||||||
// data.connect_commands = cmds
|
|
||||||
// console.log("formatted", data)
|
|
||||||
|
|
||||||
mutation.mutate(data)
|
const validate = (values: FormikValues) => {
|
||||||
};
|
const errors = {} as FormikErrors<FormikValues>;
|
||||||
|
|
||||||
const validate = (values: any) => {
|
if (!values.name) {
|
||||||
const errors = {} as any;
|
errors.name = "Required";
|
||||||
|
|
||||||
if (!values.name) {
|
|
||||||
errors.name = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.server) {
|
|
||||||
errors.server = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.port) {
|
|
||||||
errors.port = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.nickserv?.account) {
|
|
||||||
errors.nickserv.account = "Required";
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteAction = () => {
|
if (!values.server) {
|
||||||
deleteMutation.mutate(network.id)
|
errors.server = "Required";
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues: IrcNetworkUpdateFormValues = {
|
if (!values.port) {
|
||||||
id: network.id,
|
errors.port = "Required";
|
||||||
name: network.name,
|
|
||||||
enabled: network.enabled,
|
|
||||||
server: network.server,
|
|
||||||
port: network.port,
|
|
||||||
tls: network.tls,
|
|
||||||
nickserv: network.nickserv,
|
|
||||||
pass: network.pass,
|
|
||||||
channels: network.channels,
|
|
||||||
invite_command: network.invite_command
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (!values.nickserv?.account) {
|
||||||
<SlideOver
|
errors.nickserv = {
|
||||||
type="UPDATE"
|
account: "Required"
|
||||||
title="Network"
|
};
|
||||||
isOpen={isOpen}
|
}
|
||||||
toggle={toggle}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
initialValues={initialValues}
|
|
||||||
validate={validate}
|
|
||||||
>
|
|
||||||
{(values) => (
|
|
||||||
<>
|
|
||||||
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
|
||||||
|
|
||||||
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
|
const deleteAction = () => {
|
||||||
<SwitchGroupWide name="enabled" label="Enabled" />
|
deleteMutation.mutate(network.id);
|
||||||
</div>
|
};
|
||||||
|
|
||||||
<div>
|
const initialValues: IrcNetworkUpdateFormValues = {
|
||||||
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
id: network.id,
|
||||||
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
name: network.name,
|
||||||
|
enabled: network.enabled,
|
||||||
|
server: network.server,
|
||||||
|
port: network.port,
|
||||||
|
tls: network.tls,
|
||||||
|
nickserv: network.nickserv,
|
||||||
|
pass: network.pass,
|
||||||
|
channels: network.channels,
|
||||||
|
invite_command: network.invite_command
|
||||||
|
};
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
return (
|
||||||
<SwitchGroupWide name="tls" label="TLS" />
|
<SlideOver
|
||||||
</div>
|
type="UPDATE"
|
||||||
|
title="Network"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
initialValues={initialValues}
|
||||||
|
validate={validate}
|
||||||
|
>
|
||||||
|
{(values) => (
|
||||||
|
<>
|
||||||
|
<TextFieldWide name="name" label="Name" placeholder="Name" required={true} />
|
||||||
|
|
||||||
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
<div className="py-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y dark:divide-gray-700">
|
||||||
|
|
||||||
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0">
|
||||||
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
<SwitchGroupWide name="enabled" label="Enabled" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<PasswordFieldWide name="invite_command" label="Invite command" />
|
<div>
|
||||||
</div>
|
<TextFieldWide name="server" label="Server" placeholder="Address: Eg irc.server.net" required={true} />
|
||||||
</div>
|
<NumberFieldWide name="port" label="Port" placeholder="Eg 6667" required={true} />
|
||||||
|
|
||||||
<ChannelsFieldArray channels={values.channels} />
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
</>
|
<SwitchGroupWide name="tls" label="TLS" />
|
||||||
)}
|
</div>
|
||||||
</SlideOver>
|
|
||||||
)
|
<PasswordFieldWide name="pass" label="Password" help="Network password" />
|
||||||
|
|
||||||
|
<TextFieldWide name="nickserv.account" label="NickServ Account" placeholder="NickServ Account" required={true} />
|
||||||
|
<PasswordFieldWide name="nickserv.password" label="NickServ Password" />
|
||||||
|
|
||||||
|
<PasswordFieldWide name="invite_command" label="Invite command" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChannelsFieldArray channels={values.channels} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SlideOver>
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -1,83 +1,87 @@
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import {Field, Form, Formik} from "formik";
|
import { Field, Form, Formik, FormikErrors, FormikValues } from "formik";
|
||||||
import type {FieldProps} from "formik";
|
import type { FieldProps } from "formik";
|
||||||
import {XIcon} from "@heroicons/react/solid";
|
import { XIcon } from "@heroicons/react/solid";
|
||||||
import Select, {components} from "react-select";
|
import Select, { components, ControlProps, InputProps, MenuProps, OptionProps } from "react-select";
|
||||||
import {
|
import {
|
||||||
SwitchGroupWide,
|
SwitchGroupWide,
|
||||||
TextFieldWide
|
TextFieldWide
|
||||||
} from "../../components/inputs";
|
} from "../../components/inputs";
|
||||||
import DEBUG from "../../components/debug";
|
import DEBUG from "../../components/debug";
|
||||||
import {EventOptions, NotificationTypeOptions} from "../../domain/constants";
|
import { EventOptions, NotificationTypeOptions, SelectOption } from "../../domain/constants";
|
||||||
import {useMutation} from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import {APIClient} from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import {queryClient} from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import {toast} from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import {SlideOver} from "../../components/panels";
|
import { SlideOver } from "../../components/panels";
|
||||||
|
import { componentMapType } from "./DownloadClientForms";
|
||||||
|
|
||||||
|
const Input = (props: InputProps) => {
|
||||||
|
return (
|
||||||
|
<components.Input
|
||||||
|
{...props}
|
||||||
|
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||||
|
className="text-gray-400 dark:text-gray-100"
|
||||||
|
children={props.children}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Input = (props: any) => {
|
const Control = (props: ControlProps) => {
|
||||||
return (
|
return (
|
||||||
<components.Input
|
<components.Control
|
||||||
{...props}
|
{...props}
|
||||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||||
className="text-gray-400 dark:text-gray-100"
|
children={props.children}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const Control = (props: any) => {
|
const Menu = (props: MenuProps) => {
|
||||||
return (
|
return (
|
||||||
<components.Control
|
<components.Menu
|
||||||
{...props}
|
{...props}
|
||||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||||
/>
|
children={props.children}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Menu = (props: any) => {
|
const Option = (props: OptionProps) => {
|
||||||
return (
|
return (
|
||||||
<components.Menu
|
<components.Option
|
||||||
{...props}
|
{...props}
|
||||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||||
/>
|
children={props.children}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
const Option = (props: any) => {
|
|
||||||
return (
|
|
||||||
<components.Option
|
|
||||||
{...props}
|
|
||||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function FormFieldsDiscord() {
|
function FormFieldsDiscord() {
|
||||||
return (
|
return (
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
{/*<div className="px-6 space-y-1">*/}
|
{/*<div className="px-6 space-y-1">*/}
|
||||||
{/* <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Credentials</Dialog.Title>*/}
|
{/* <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Credentials</Dialog.Title>*/}
|
||||||
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
|
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
|
||||||
{/* Api keys etc*/}
|
{/* Api keys etc*/}
|
||||||
{/* </p>*/}
|
{/* </p>*/}
|
||||||
{/*</div>*/}
|
{/*</div>*/}
|
||||||
|
|
||||||
<TextFieldWide
|
<TextFieldWide
|
||||||
name="webhook"
|
name="webhook"
|
||||||
label="Webhook URL"
|
label="Webhook URL"
|
||||||
help="Discord channel webhook url"
|
help="Discord channel webhook url"
|
||||||
placeholder="https://discordapp.com/api/webhooks/xx/xx"
|
placeholder="https://discordapp.com/api/webhooks/xx/xx"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentMap: any = {
|
const componentMap: componentMapType = {
|
||||||
DISCORD: <FormFieldsDiscord/>,
|
DISCORD: <FormFieldsDiscord/>
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NotificationAddFormValues {
|
interface NotificationAddFormValues {
|
||||||
|
@ -87,349 +91,353 @@ interface NotificationAddFormValues {
|
||||||
|
|
||||||
interface AddProps {
|
interface AddProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: any;
|
toggle: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
export function NotificationAddForm({ isOpen, toggle }: AddProps) {
|
||||||
const mutation = useMutation(
|
const mutation = useMutation(
|
||||||
(notification: Notification) => APIClient.notifications.create(notification),
|
(notification: Notification) => APIClient.notifications.create(notification),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(['notifications']);
|
queryClient.invalidateQueries(["notifications"]);
|
||||||
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />)
|
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />);
|
||||||
toggle()
|
toggle();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />)
|
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />);
|
||||||
},
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (formData: any) => {
|
|
||||||
mutation.mutate(formData)
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const validate = (values: NotificationAddFormValues) => {
|
const onSubmit = (formData: unknown) => {
|
||||||
const errors = {} as any;
|
mutation.mutate(formData as Notification);
|
||||||
if (!values.name)
|
};
|
||||||
errors.name = "Required";
|
|
||||||
|
|
||||||
return errors;
|
const validate = (values: NotificationAddFormValues) => {
|
||||||
}
|
const errors = {} as FormikErrors<FormikValues>;
|
||||||
|
if (!values.name)
|
||||||
|
errors.name = "Required";
|
||||||
|
|
||||||
return (
|
return errors;
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
};
|
||||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
|
||||||
<div className="absolute inset-0 overflow-hidden">
|
|
||||||
<Dialog.Overlay className="absolute inset-0"/>
|
|
||||||
|
|
||||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
return (
|
||||||
<Transition.Child
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
as={Fragment}
|
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
enterFrom="translate-x-full"
|
<Dialog.Overlay className="absolute inset-0"/>
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="translate-x-full"
|
|
||||||
>
|
|
||||||
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
|
||||||
<Formik
|
|
||||||
enableReinitialize={true}
|
|
||||||
initialValues={{
|
|
||||||
enabled: true,
|
|
||||||
type: "",
|
|
||||||
name: "",
|
|
||||||
webhook: "",
|
|
||||||
events: [],
|
|
||||||
}}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
validate={validate}
|
|
||||||
>
|
|
||||||
{({values}) => (
|
|
||||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
|
||||||
<div className="flex items-start justify-between space-x-3">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Add
|
|
||||||
Notifications</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-200">
|
|
||||||
Trigger notifications on different events.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-7 flex items-center">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close panel</span>
|
|
||||||
<XIcon className="h-6 w-6" aria-hidden="true"/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextFieldWide name="name" label="Name" required={true}/>
|
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||||
|
<Transition.Child
|
||||||
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
as={Fragment}
|
||||||
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
<div>
|
enterFrom="translate-x-full"
|
||||||
<label
|
enterTo="translate-x-0"
|
||||||
htmlFor="type"
|
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
leaveFrom="translate-x-0"
|
||||||
>
|
leaveTo="translate-x-full"
|
||||||
Type
|
>
|
||||||
</label>
|
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
||||||
</div>
|
<Formik
|
||||||
<div className="sm:col-span-2">
|
enableReinitialize={true}
|
||||||
<Field name="type" type="select">
|
initialValues={{
|
||||||
{({
|
enabled: true,
|
||||||
field,
|
type: "",
|
||||||
form: {setFieldValue, resetForm}
|
name: "",
|
||||||
}: FieldProps) => (
|
webhook: "",
|
||||||
<Select {...field}
|
events: []
|
||||||
isClearable={true}
|
}}
|
||||||
isSearchable={true}
|
onSubmit={onSubmit}
|
||||||
components={{Input, Control, Menu, Option}}
|
validate={validate}
|
||||||
placeholder="Choose a type"
|
>
|
||||||
styles={{
|
{({ values }) => (
|
||||||
singleValue: (base) => ({
|
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||||
...base,
|
<div className="flex-1">
|
||||||
color: "unset"
|
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||||
})
|
<div className="flex items-start justify-between space-x-3">
|
||||||
}}
|
<div className="space-y-1">
|
||||||
theme={(theme) => ({
|
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">
|
||||||
...theme,
|
Add Notifications
|
||||||
spacing: {
|
</Dialog.Title>
|
||||||
...theme.spacing,
|
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||||
controlHeight: 30,
|
Trigger notifications on different events.
|
||||||
baseUnit: 2,
|
</p>
|
||||||
}
|
|
||||||
})}
|
|
||||||
value={field?.value && field.value.value}
|
|
||||||
onChange={(option: any) => {
|
|
||||||
resetForm()
|
|
||||||
// setFieldValue("name", option?.label ?? "")
|
|
||||||
setFieldValue(field.name, option?.value ?? "")
|
|
||||||
}}
|
|
||||||
options={NotificationTypeOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
|
||||||
<SwitchGroupWide name="enabled" label="Enabled"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
|
||||||
<div className="px-6 space-y-1">
|
|
||||||
<Dialog.Title
|
|
||||||
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Select what events to trigger on
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<EventCheckBoxes />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{componentMap[values.type]}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
|
||||||
<div className="space-x-3 flex justify-end">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DEBUG values={values}/>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-7 flex items-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close panel</span>
|
||||||
|
<XIcon className="h-6 w-6" aria-hidden="true"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</Transition.Child>
|
<TextFieldWide name="name" label="Name" required={true}/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const EventCheckBoxes = () => (
|
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<fieldset className="space-y-5">
|
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<legend className="sr-only">Notifications</legend>
|
|
||||||
{EventOptions.map((e, idx) => (
|
|
||||||
<div key={idx} className="relative flex items-start">
|
|
||||||
<div className="flex items-center h-5">
|
|
||||||
<Field
|
|
||||||
id={`events-${e.value}`}
|
|
||||||
aria-describedby={`events-${e.value}-description`}
|
|
||||||
name="events"
|
|
||||||
type="checkbox"
|
|
||||||
value={e.value}
|
|
||||||
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ml-3 text-sm">
|
|
||||||
<label htmlFor={`events-${e.value}`}
|
|
||||||
className="font-medium text-gray-900 dark:text-gray-100">
|
|
||||||
{e.label}
|
|
||||||
</label>
|
|
||||||
{e.description && (
|
|
||||||
<p className="text-gray-500">{e.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</fieldset>
|
|
||||||
)
|
|
||||||
|
|
||||||
interface UpdateProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
toggle: any;
|
|
||||||
notification: Notification;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationUpdateForm({isOpen, toggle, notification}: UpdateProps) {
|
|
||||||
const mutation = useMutation(
|
|
||||||
(notification: Notification) => APIClient.notifications.update(notification),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["notifications"]);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>)
|
|
||||||
toggle();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const deleteMutation = useMutation(
|
|
||||||
(notificationID: number) => APIClient.notifications.delete(notificationID),
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries(["notifications"]);
|
|
||||||
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSubmit = (formData: any) => {
|
|
||||||
mutation.mutate(formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate(notification.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialValues = {
|
|
||||||
id: notification.id,
|
|
||||||
enabled: notification.enabled,
|
|
||||||
type: notification.type,
|
|
||||||
name: notification.name,
|
|
||||||
webhook: notification.webhook,
|
|
||||||
events: notification.events || [],
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SlideOver
|
|
||||||
type="UPDATE"
|
|
||||||
title="Notification"
|
|
||||||
isOpen={isOpen}
|
|
||||||
toggle={toggle}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
initialValues={initialValues}
|
|
||||||
>
|
|
||||||
{(values) => (
|
|
||||||
<div>
|
|
||||||
<TextFieldWide name="name" label="Name" required={true}/>
|
|
||||||
|
|
||||||
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="type"
|
htmlFor="type"
|
||||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
className="block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
>
|
>
|
||||||
Type
|
Type
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<Field name="type" type="select">
|
<Field name="type" type="select">
|
||||||
{({field, form: {setFieldValue, resetForm}}: FieldProps) => (
|
{({
|
||||||
<Select {...field}
|
field,
|
||||||
isClearable={true}
|
form: { setFieldValue, resetForm }
|
||||||
isSearchable={true}
|
}: FieldProps) => (
|
||||||
components={{Input, Control, Menu, Option}}
|
<Select {...field}
|
||||||
|
isClearable={true}
|
||||||
|
isSearchable={true}
|
||||||
|
components={{ Input, Control, Menu, Option }}
|
||||||
|
placeholder="Choose a type"
|
||||||
|
styles={{
|
||||||
|
singleValue: (base) => ({
|
||||||
|
...base,
|
||||||
|
color: "unset"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
theme={(theme) => ({
|
||||||
|
...theme,
|
||||||
|
spacing: {
|
||||||
|
...theme.spacing,
|
||||||
|
controlHeight: 30,
|
||||||
|
baseUnit: 2
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
value={field?.value && field.value.value}
|
||||||
|
onChange={(option: unknown) => {
|
||||||
|
resetForm();
|
||||||
|
|
||||||
|
const opt = option as SelectOption;
|
||||||
|
// setFieldValue("name", option?.label ?? "")
|
||||||
|
setFieldValue(field.name, opt.value ?? "");
|
||||||
|
}}
|
||||||
|
options={NotificationTypeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
placeholder="Choose a type"
|
|
||||||
styles={{
|
|
||||||
singleValue: (base) => ({
|
|
||||||
...base,
|
|
||||||
color: "unset"
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
theme={(theme) => ({
|
|
||||||
...theme,
|
|
||||||
spacing: {
|
|
||||||
...theme.spacing,
|
|
||||||
controlHeight: 30,
|
|
||||||
baseUnit: 2,
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
|
|
||||||
onChange={(option: any) => {
|
|
||||||
resetForm()
|
|
||||||
setFieldValue(field.name, option?.value ?? "")
|
|
||||||
}}
|
|
||||||
options={NotificationTypeOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
<SwitchGroupWide name="enabled" label="Enabled"/>
|
<SwitchGroupWide name="enabled" label="Enabled"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
<div className="px-6 space-y-1">
|
<div className="px-6 space-y-1">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
|
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
Select what events to trigger on
|
Select what events to trigger on
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
|
||||||
<EventCheckBoxes />
|
<EventCheckBoxes />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{componentMap[values.type]}
|
||||||
{componentMap[values.type]}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||||
</SlideOver>
|
<div className="space-x-3 flex justify-end">
|
||||||
)
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DEBUG values={values}/>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventCheckBoxes = () => (
|
||||||
|
<fieldset className="space-y-5">
|
||||||
|
<legend className="sr-only">Notifications</legend>
|
||||||
|
{EventOptions.map((e, idx) => (
|
||||||
|
<div key={idx} className="relative flex items-start">
|
||||||
|
<div className="flex items-center h-5">
|
||||||
|
<Field
|
||||||
|
id={`events-${e.value}`}
|
||||||
|
aria-describedby={`events-${e.value}-description`}
|
||||||
|
name="events"
|
||||||
|
type="checkbox"
|
||||||
|
value={e.value}
|
||||||
|
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 text-sm">
|
||||||
|
<label htmlFor={`events-${e.value}`}
|
||||||
|
className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{e.label}
|
||||||
|
</label>
|
||||||
|
{e.description && (
|
||||||
|
<p className="text-gray-500">{e.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface UpdateProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
notification: Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationUpdateForm({ isOpen, toggle, notification }: UpdateProps) {
|
||||||
|
const mutation = useMutation(
|
||||||
|
(notification: Notification) => APIClient.notifications.update(notification),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["notifications"]);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${notification.name} was updated successfully`} t={t}/>);
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteMutation = useMutation(
|
||||||
|
(notificationID: number) => APIClient.notifications.delete(notificationID),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["notifications"]);
|
||||||
|
toast.custom((t) => <Toast type="success" body={`${notification.name} was deleted.`} t={t}/>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (formData: unknown) => {
|
||||||
|
mutation.mutate(formData as Notification);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteAction = () => {
|
||||||
|
deleteMutation.mutate(notification.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
id: notification.id,
|
||||||
|
enabled: notification.enabled,
|
||||||
|
type: notification.type,
|
||||||
|
name: notification.name,
|
||||||
|
webhook: notification.webhook,
|
||||||
|
events: notification.events || []
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SlideOver
|
||||||
|
type="UPDATE"
|
||||||
|
title="Notification"
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggle={toggle}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
initialValues={initialValues}
|
||||||
|
>
|
||||||
|
{(values) => (
|
||||||
|
<div>
|
||||||
|
<TextFieldWide name="name" label="Name" required={true}/>
|
||||||
|
|
||||||
|
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="type"
|
||||||
|
className="block text-sm font-medium text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<Field name="type" type="select">
|
||||||
|
{({ field, form: { setFieldValue, resetForm } }: FieldProps) => (
|
||||||
|
<Select {...field}
|
||||||
|
isClearable={true}
|
||||||
|
isSearchable={true}
|
||||||
|
components={{ Input, Control, Menu, Option }}
|
||||||
|
|
||||||
|
placeholder="Choose a type"
|
||||||
|
styles={{
|
||||||
|
singleValue: (base) => ({
|
||||||
|
...base,
|
||||||
|
color: "unset"
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
theme={(theme) => ({
|
||||||
|
...theme,
|
||||||
|
spacing: {
|
||||||
|
...theme.spacing,
|
||||||
|
controlHeight: 30,
|
||||||
|
baseUnit: 2
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
value={field?.value && NotificationTypeOptions.find(o => o.value == field?.value)}
|
||||||
|
onChange={(option: unknown) => {
|
||||||
|
resetForm();
|
||||||
|
const opt = option as SelectOption;
|
||||||
|
setFieldValue(field.name, opt.value ?? "");
|
||||||
|
}}
|
||||||
|
options={NotificationTypeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||||
|
<SwitchGroupWide name="enabled" label="Enabled"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||||
|
<div className="px-6 space-y-1">
|
||||||
|
<Dialog.Title
|
||||||
|
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Select what events to trigger on
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
|
||||||
|
<EventCheckBoxes />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{componentMap[values.type]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SlideOver>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export function useToggle(initialValue = false): [boolean, () => void] {
|
export function useToggle(initialValue = false): [boolean, () => void] {
|
||||||
const [value, setValue] = useState(initialValue);
|
const [value, setValue] = useState(initialValue);
|
||||||
const toggle = () => setValue(v => !v);
|
const toggle = () => setValue(v => !v);
|
||||||
|
|
||||||
return [value, toggle];
|
return [value, toggle];
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,8 @@ window.APP = window.APP || {};
|
||||||
InitializeGlobalContext();
|
InitializeGlobalContext();
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
document.getElementById("root")
|
document.getElementById("root")
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { ReportHandler } from "web-vitals";
|
||||||
|
|
||||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
getCLS(onPerfEntry);
|
getCLS(onPerfEntry);
|
||||||
getFID(onPerfEntry);
|
getFID(onPerfEntry);
|
||||||
getFCP(onPerfEntry);
|
getFCP(onPerfEntry);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { NavLink, Link, Route, Switch } from "react-router-dom";
|
|
||||||
import type { match } from "react-router-dom";
|
import type { match } from "react-router-dom";
|
||||||
|
import { Link, NavLink, Route, Switch } from "react-router-dom";
|
||||||
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
import { Disclosure, Menu, Transition } from "@headlessui/react";
|
||||||
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
import { ExternalLinkIcon } from "@heroicons/react/solid";
|
||||||
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
import { ChevronDownIcon, MenuIcon, XIcon } from "@heroicons/react/outline";
|
||||||
|
@ -11,9 +11,9 @@ import { Logs } from "./Logs";
|
||||||
import { Releases } from "./releases";
|
import { Releases } from "./releases";
|
||||||
import { Dashboard } from "./dashboard";
|
import { Dashboard } from "./dashboard";
|
||||||
import { FilterDetails, Filters } from "./filters";
|
import { FilterDetails, Filters } from "./filters";
|
||||||
import { AuthContext } from '../utils/Context';
|
import { AuthContext } from "../utils/Context";
|
||||||
|
|
||||||
import logo from '../logo.png';
|
import logo from "../logo.png";
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -21,229 +21,230 @@ interface NavItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
function classNames(...classes: string[]) {
|
function classNames(...classes: string[]) {
|
||||||
return classes.filter(Boolean).join(' ')
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActiveMatcher = (
|
const isActiveMatcher = (
|
||||||
match: match<any> | null,
|
match: match | null,
|
||||||
location: { pathname: string },
|
location: { pathname: string },
|
||||||
item: NavItem
|
item: NavItem
|
||||||
) => {
|
) => {
|
||||||
if (!match)
|
if (!match)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
|
if (match?.url === "/" && item.path === "/" && location.pathname === "/")
|
||||||
return true
|
return true;
|
||||||
|
|
||||||
if (match.url === "/")
|
if (match.url === "/")
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Base() {
|
export default function Base() {
|
||||||
const authContext = AuthContext.useValue();
|
const authContext = AuthContext.useValue();
|
||||||
const nav: Array<NavItem> = [
|
const nav: Array<NavItem> = [
|
||||||
{ name: 'Dashboard', path: "/" },
|
{ name: "Dashboard", path: "/" },
|
||||||
{ name: 'Filters', path: "/filters" },
|
{ name: "Filters", path: "/filters" },
|
||||||
{ name: 'Releases', path: "/releases" },
|
{ name: "Releases", path: "/releases" },
|
||||||
{ name: "Settings", path: "/settings" },
|
{ name: "Settings", path: "/settings" },
|
||||||
{ name: "Logs", path: "/logs" }
|
{ name: "Logs", path: "/logs" }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Disclosure
|
<Disclosure
|
||||||
as="nav"
|
as="nav"
|
||||||
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
|
className="bg-gradient-to-b from-gray-100 dark:from-[#141414]"
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="max-w-screen-xl mx-auto sm:px-6 lg:px-8">
|
<div className="max-w-screen-xl mx-auto sm:px-6 lg:px-8">
|
||||||
<div className="border-b border-gray-300 dark:border-gray-700">
|
<div className="border-b border-gray-300 dark:border-gray-700">
|
||||||
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
<div className="flex items-center justify-between h-16 px-4 sm:px-0">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-shrink-0 flex items-center">
|
<div className="flex-shrink-0 flex items-center">
|
||||||
<img
|
<img
|
||||||
className="block lg:hidden h-10 w-auto"
|
className="block lg:hidden h-10 w-auto"
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="hidden lg:block h-10 w-auto"
|
className="hidden lg:block h-10 w-auto"
|
||||||
src={logo}
|
src={logo}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:ml-3 hidden sm:block">
|
<div className="sm:ml-3 hidden sm:block">
|
||||||
<div className="flex items-baseline space-x-4">
|
<div className="flex items-baseline space-x-4">
|
||||||
{nav.map((item, itemIdx) =>
|
{nav.map((item, itemIdx) =>
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.name + itemIdx}
|
key={item.name + itemIdx}
|
||||||
to={item.path}
|
to={item.path}
|
||||||
strict
|
strict
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
||||||
"transition-colors duration-200"
|
"transition-colors duration-200"
|
||||||
)}
|
)}
|
||||||
activeClassName="text-black dark:text-gray-50 !font-bold"
|
activeClassName="text-black dark:text-gray-50 !font-bold"
|
||||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
<a
|
<a
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://autobrr.com/docs/configuration/indexers"
|
href="https://autobrr.com/docs/configuration/indexers"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
"text-gray-600 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white px-3 py-2 rounded-2xl text-sm font-medium",
|
||||||
"transition-colors duration-200 flex items-center justify-center"
|
"transition-colors duration-200 flex items-center justify-center"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Docs
|
Docs
|
||||||
<ExternalLinkIcon className="inline ml-1 h-5 w-5" aria-hidden="true" />
|
<ExternalLinkIcon className="inline ml-1 h-5 w-5"
|
||||||
</a>
|
aria-hidden="true"/>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block">
|
</div>
|
||||||
<div className="ml-4 flex items-center sm:ml-6">
|
<div className="hidden sm:block">
|
||||||
<Menu as="div" className="ml-3 relative">
|
<div className="ml-4 flex items-center sm:ml-6">
|
||||||
{({ open }) => (
|
<Menu as="div" className="ml-3 relative">
|
||||||
<>
|
{({ open }) => (
|
||||||
<Menu.Button
|
<>
|
||||||
className={classNames(
|
<Menu.Button
|
||||||
open ? "bg-gray-200 dark:bg-gray-800" : "",
|
className={classNames(
|
||||||
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
|
open ? "bg-gray-200 dark:bg-gray-800" : "",
|
||||||
"max-w-xs rounded-full flex items-center text-sm px-3 py-2",
|
"text-gray-800 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-800",
|
||||||
"transition-colors duration-200"
|
"max-w-xs rounded-full flex items-center text-sm px-3 py-2",
|
||||||
)}
|
"transition-colors duration-200"
|
||||||
>
|
)}
|
||||||
<span className="hidden text-sm font-medium sm:block">
|
>
|
||||||
<span className="sr-only">Open user menu for </span>
|
<span className="hidden text-sm font-medium sm:block">
|
||||||
{authContext.username}
|
<span className="sr-only">Open user menu for </span>
|
||||||
</span>
|
{authContext.username}
|
||||||
<ChevronDownIcon
|
</span>
|
||||||
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block"
|
<ChevronDownIcon
|
||||||
aria-hidden="true"
|
className="hidden flex-shrink-0 ml-1 h-5 w-5 text-gray-800 dark:text-gray-300 sm:block"
|
||||||
/>
|
aria-hidden="true"
|
||||||
</Menu.Button>
|
/>
|
||||||
<Transition
|
</Menu.Button>
|
||||||
show={open}
|
<Transition
|
||||||
as={Fragment}
|
show={open}
|
||||||
enter="transition ease-out duration-100"
|
as={Fragment}
|
||||||
enterFrom="transform opacity-0 scale-95"
|
enter="transition ease-out duration-100"
|
||||||
enterTo="transform opacity-100 scale-100"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
leave="transition ease-in duration-75"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leave="transition ease-in duration-75"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
>
|
leaveTo="transform opacity-0 scale-95"
|
||||||
<Menu.Items
|
>
|
||||||
static
|
<Menu.Items
|
||||||
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
static
|
||||||
>
|
className="origin-top-right absolute right-0 mt-2 w-48 z-10 rounded-md shadow-lg py-1 bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
<Menu.Item>
|
>
|
||||||
{({ active }) => (
|
<Menu.Item>
|
||||||
<Link
|
{({ active }) => (
|
||||||
to="/settings"
|
<Link
|
||||||
className={classNames(
|
to="/settings"
|
||||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
className={classNames(
|
||||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||||
)}
|
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||||
>
|
)}
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<Link
|
|
||||||
to="/logout"
|
|
||||||
className={classNames(
|
|
||||||
active ? 'bg-gray-100 dark:bg-gray-600' : '',
|
|
||||||
'block px-4 py-2 text-sm text-gray-700 dark:text-gray-200'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="-mr-2 flex sm:hidden">
|
|
||||||
{/* Mobile menu button */}
|
|
||||||
<Disclosure.Button
|
|
||||||
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
{open ? (
|
|
||||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</Disclosure.Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
|
|
||||||
<div className="px-2 py-3 space-y-1 sm:px-3">
|
|
||||||
{nav.map((item) =>
|
|
||||||
<NavLink
|
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
|
||||||
strict
|
|
||||||
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
|
||||||
activeClassName="font-bold bg-gray-300 text-black"
|
|
||||||
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
|
||||||
>
|
>
|
||||||
{item.name}
|
Settings
|
||||||
</NavLink>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link
|
</Menu.Item>
|
||||||
to="/logout"
|
<Menu.Item>
|
||||||
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
{({ active }) => (
|
||||||
>
|
<Link
|
||||||
Logout
|
to="/logout"
|
||||||
</Link>
|
className={classNames(
|
||||||
</div>
|
active ? "bg-gray-100 dark:bg-gray-600" : "",
|
||||||
|
"block px-4 py-2 text-sm text-gray-700 dark:text-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="-mr-2 flex sm:hidden">
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<Disclosure.Button
|
||||||
|
className="bg-gray-200 dark:bg-gray-800 inline-flex items-center justify-center p-2 rounded-md text-gray-600 dark:text-gray-400 hover:text-white hover:bg-gray-700">
|
||||||
|
<span className="sr-only">Open main menu</span>
|
||||||
|
{open ? (
|
||||||
|
<XIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||||
|
) : (
|
||||||
|
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
|
||||||
|
)}
|
||||||
|
</Disclosure.Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</Disclosure.Panel>
|
<Disclosure.Panel className="border-b border-gray-300 dark:border-gray-700 md:hidden">
|
||||||
</>
|
<div className="px-2 py-3 space-y-1 sm:px-3">
|
||||||
|
{nav.map((item) =>
|
||||||
|
<NavLink
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
strict
|
||||||
|
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
activeClassName="font-bold bg-gray-300 text-black"
|
||||||
|
isActive={(match, location) => isActiveMatcher(match, location, item)}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
<Link
|
||||||
|
to="/logout"
|
||||||
|
className="dark:bg-gray-900 dark:text-white block px-3 py-2 rounded-md text-base font-medium"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Switch>
|
</Disclosure.Panel>
|
||||||
<Route path="/logs">
|
</>
|
||||||
<Logs />
|
)}
|
||||||
</Route>
|
</Disclosure>
|
||||||
|
|
||||||
<Route path="/settings">
|
<Switch>
|
||||||
<Settings />
|
<Route path="/logs">
|
||||||
</Route>
|
<Logs/>
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="/releases">
|
<Route path="/settings">
|
||||||
<Releases />
|
<Settings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route exact={true} path="/filters">
|
<Route path="/releases">
|
||||||
<Filters />
|
<Releases/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/filters/:filterId">
|
<Route exact={true} path="/filters">
|
||||||
<FilterDetails />
|
<Filters/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route exact path="/">
|
<Route path="/filters/:filterId">
|
||||||
<Dashboard />
|
<FilterDetails/>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
|
||||||
</div>
|
<Route exact path="/">
|
||||||
)
|
<Dashboard/>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const LogColors: Record<LogLevel, string> = {
|
||||||
"TRACE": "text-purple-300",
|
"TRACE": "text-purple-300",
|
||||||
"DEBUG": "text-yellow-500",
|
"DEBUG": "text-yellow-500",
|
||||||
"INFO": "text-green-500",
|
"INFO": "text-green-500",
|
||||||
"ERROR": "text-red-500",
|
"ERROR": "text-red-500"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Logs = () => {
|
export const Logs = () => {
|
||||||
|
@ -29,7 +29,7 @@ export const Logs = () => {
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const es = APIClient.events.logs();
|
const es = APIClient.events.logs();
|
||||||
|
@ -40,7 +40,7 @@ export const Logs = () => {
|
||||||
|
|
||||||
if (settings.scrollOnNewLog)
|
if (settings.scrollOnNewLog)
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
};
|
||||||
|
|
||||||
return () => es.close();
|
return () => es.close();
|
||||||
}, [setLogs, settings]);
|
}, [setLogs, settings]);
|
||||||
|
@ -96,7 +96,7 @@ export const Logs = () => {
|
||||||
key={idx}
|
key={idx}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
settings.indentLogLines ? "grid justify-start grid-flow-col" : "",
|
||||||
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : "",
|
settings.hideWrappedText ? "truncate hover:text-ellipsis hover:whitespace-normal" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
@ -112,7 +112,7 @@ export const Logs = () => {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{a.level}
|
{a.level}
|
||||||
{' '}
|
{" "}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="ml-2 text-black dark:text-gray-300">
|
<span className="ml-2 text-black dark:text-gray-300">
|
||||||
|
@ -125,5 +125,5 @@ export const Logs = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,120 +1,137 @@
|
||||||
import {BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon} from '@heroicons/react/outline'
|
import { BellIcon, ChatAlt2Icon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon, RssIcon } from "@heroicons/react/outline";
|
||||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
import { NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch } from "react-router-dom";
|
||||||
|
|
||||||
import { classNames } from "../utils";
|
import { classNames } from "../utils";
|
||||||
import IndexerSettings from "./settings/Indexer";
|
import IndexerSettings from "./settings/Indexer";
|
||||||
import { IrcSettings } from "./settings/Irc";
|
import { IrcSettings } from "./settings/Irc";
|
||||||
import ApplicationSettings from "./settings/Application";
|
import ApplicationSettings from "./settings/Application";
|
||||||
import DownloadClientSettings from "./settings/DownloadClient";
|
import DownloadClientSettings from "./settings/DownloadClient";
|
||||||
import { RegexPlayground } from './settings/RegexPlayground';
|
import { RegexPlayground } from "./settings/RegexPlayground";
|
||||||
import ReleaseSettings from "./settings/Releases";
|
import ReleaseSettings from "./settings/Releases";
|
||||||
import NotificationSettings from "./settings/Notifications";
|
import NotificationSettings from "./settings/Notifications";
|
||||||
import FeedSettings from "./settings/Feed";
|
import FeedSettings from "./settings/Feed";
|
||||||
|
|
||||||
const subNavigation = [
|
interface NavTabType {
|
||||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
name: string;
|
||||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
href: string;
|
||||||
{name: 'IRC', href: 'irc', icon: ChatAlt2Icon, current: false},
|
icon: typeof CogIcon;
|
||||||
{name: 'Feeds', href: 'feeds', icon: RssIcon, current: false},
|
current: boolean;
|
||||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
|
||||||
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
|
||||||
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false},
|
|
||||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
|
||||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
|
||||||
]
|
|
||||||
|
|
||||||
function SubNavLink({item, url}: any) {
|
|
||||||
const location = useLocation();
|
|
||||||
const { pathname } = location;
|
|
||||||
|
|
||||||
const splitLocation = pathname.split("/");
|
|
||||||
|
|
||||||
// we need to clean the / if it's a base root path
|
|
||||||
const too = item.href ? `${url}/${item.href}` : url
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
key={item.name}
|
|
||||||
to={too}
|
|
||||||
exact={true}
|
|
||||||
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
|
|
||||||
className={classNames(
|
|
||||||
'border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium'
|
|
||||||
)}
|
|
||||||
aria-current={splitLocation[2] === item.href ? 'page' : undefined}
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className="text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="truncate">{item.name}</span>
|
|
||||||
</NavLink>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarNav({subNavigation, url}: any) {
|
const subNavigation: NavTabType[] = [
|
||||||
return (
|
{ name: "Application", href: "", icon: CogIcon, current: true },
|
||||||
<aside className="py-2 lg:col-span-3">
|
{ name: "Indexers", href: "indexers", icon: KeyIcon, current: false },
|
||||||
<nav className="space-y-1">
|
{ name: "IRC", href: "irc", icon: ChatAlt2Icon, current: false },
|
||||||
{subNavigation.map((item: any) => (
|
{ name: "Feeds", href: "feeds", icon: RssIcon, current: false },
|
||||||
<SubNavLink item={item} url={url} key={item.href}/>
|
{ name: "Clients", href: "clients", icon: DownloadIcon, current: false },
|
||||||
))}
|
{ name: "Notifications", href: "notifications", icon: BellIcon, current: false },
|
||||||
</nav>
|
{ name: "Releases", href: "releases", icon: CollectionIcon, current: false }
|
||||||
</aside>
|
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||||
)
|
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface NavLinkProps {
|
||||||
|
item: NavTabType;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubNavLink({ item, url }: NavLinkProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const { pathname } = location;
|
||||||
|
|
||||||
|
const splitLocation = pathname.split("/");
|
||||||
|
|
||||||
|
// we need to clean the / if it's a base root path
|
||||||
|
const too = item.href ? `${url}/${item.href}` : url;
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={item.name}
|
||||||
|
to={too}
|
||||||
|
exact={true}
|
||||||
|
activeClassName="bg-teal-50 dark:bg-gray-700 border-teal-500 dark:border-blue-500 text-teal-700 dark:text-white hover:bg-teal-50 dark:hover:bg-gray-500 hover:text-teal-700 dark:hover:text-gray-200"
|
||||||
|
className={classNames(
|
||||||
|
"border-transparent text-gray-900 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-300 group border-l-4 px-3 py-2 flex items-center text-sm font-medium"
|
||||||
|
)}
|
||||||
|
aria-current={splitLocation[2] === item.href ? "page" : undefined}
|
||||||
|
>
|
||||||
|
<item.icon
|
||||||
|
className="text-gray-400 group-hover:text-gray-500 dark:group-hover:text-gray-300 flex-shrink-0 -ml-1 mr-3 h-6 w-6"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="truncate">{item.name}</span>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarNavProps {
|
||||||
|
subNavigation: NavTabType[];
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarNav({ subNavigation, url }: SidebarNavProps) {
|
||||||
|
return (
|
||||||
|
<aside className="py-2 lg:col-span-3">
|
||||||
|
<nav className="space-y-1">
|
||||||
|
{subNavigation.map((item) => (
|
||||||
|
<SubNavLink item={item} url={url} key={item.href}/>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
const { url } = useRouteMatch();
|
const { url } = useRouteMatch();
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<header className="py-10">
|
<header className="py-10">
|
||||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<h1 className="text-3xl font-bold text-black dark:text-white">Settings</h1>
|
<h1 className="text-3xl font-bold text-black dark:text-white">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg">
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:grid lg:grid-cols-12 lg:divide-y-0 lg:divide-x">
|
||||||
<SidebarNav url={url} subNavigation={subNavigation}/>
|
<SidebarNav url={url} subNavigation={subNavigation}/>
|
||||||
|
|
||||||
<RouteSwitch>
|
<RouteSwitch>
|
||||||
<Route exact path={url}>
|
<Route exact path={url}>
|
||||||
<ApplicationSettings/>
|
<ApplicationSettings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/indexers`}>
|
<Route path={`${url}/indexers`}>
|
||||||
<IndexerSettings/>
|
<IndexerSettings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/feeds`}>
|
<Route path={`${url}/feeds`}>
|
||||||
<FeedSettings/>
|
<FeedSettings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/irc`}>
|
<Route path={`${url}/irc`}>
|
||||||
<IrcSettings/>
|
<IrcSettings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/clients`}>
|
<Route path={`${url}/clients`}>
|
||||||
<DownloadClientSettings/>
|
<DownloadClientSettings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/notifications`}>
|
<Route path={`${url}/notifications`}>
|
||||||
<NotificationSettings />
|
<NotificationSettings />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/releases`}>
|
<Route path={`${url}/releases`}>
|
||||||
<ReleaseSettings/>
|
<ReleaseSettings/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path={`${url}/regex-playground`}>
|
<Route path={`${url}/regex-playground`}>
|
||||||
<RegexPlayground />
|
<RegexPlayground />
|
||||||
</Route>
|
</Route>
|
||||||
</RouteSwitch>
|
</RouteSwitch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,11 +34,11 @@ export const Login = () => {
|
||||||
isLoggedIn: true
|
isLoggedIn: true
|
||||||
});
|
});
|
||||||
history.push("/");
|
history.push("/");
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = (data: any) => mutation.mutate(data);
|
const handleSubmit = (data: LoginData) => mutation.mutate(data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
<div className="min-h-screen flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
|
@ -75,4 +75,4 @@ export const Login = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -29,4 +29,4 @@ export const Logout = () => {
|
||||||
<p>Logged out</p>
|
<p>Logged out</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -27,7 +27,7 @@ export const Onboarding = () => {
|
||||||
if (values.password1 !== values.password2)
|
if (values.password1 !== values.password2)
|
||||||
obj.password2 = "Passwords don't match!";
|
obj.password2 = "Passwords don't match!";
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -37,7 +37,7 @@ export const Onboarding = () => {
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
history.push("/login");
|
history.push("/login");
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -81,5 +81,5 @@ export const Onboarding = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
useFilters,
|
useFilters,
|
||||||
useGlobalFilter,
|
useGlobalFilter,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
usePagination
|
usePagination, FilterProps, Column
|
||||||
} from "react-table";
|
} from "react-table";
|
||||||
|
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
|
@ -17,17 +17,17 @@ import * as DataTable from "../../components/data-table";
|
||||||
// This is a custom filter UI for selecting
|
// This is a custom filter UI for selecting
|
||||||
// a unique option from a list
|
// a unique option from a list
|
||||||
function SelectColumnFilter({
|
function SelectColumnFilter({
|
||||||
column: { filterValue, setFilter, preFilteredRows, id, render },
|
column: { filterValue, setFilter, preFilteredRows, id, render }
|
||||||
}: any) {
|
}: FilterProps<object>) {
|
||||||
// Calculate the options for filtering
|
// Calculate the options for filtering
|
||||||
// using the preFilteredRows
|
// using the preFilteredRows
|
||||||
const options = React.useMemo(() => {
|
const options = React.useMemo(() => {
|
||||||
const options: any = new Set()
|
const options = new Set<string>();
|
||||||
preFilteredRows.forEach((row: { values: { [x: string]: unknown } }) => {
|
preFilteredRows.forEach((row: { values: { [x: string]: string } }) => {
|
||||||
options.add(row.values[id])
|
options.add(row.values[id]);
|
||||||
})
|
});
|
||||||
return [...options.values()]
|
return [...options.values()];
|
||||||
}, [id, preFilteredRows])
|
}, [id, preFilteredRows]);
|
||||||
|
|
||||||
// Render a multi-select box
|
// Render a multi-select box
|
||||||
return (
|
return (
|
||||||
|
@ -39,7 +39,7 @@ function SelectColumnFilter({
|
||||||
id={id}
|
id={id}
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setFilter(e.target.value || undefined)
|
setFilter(e.target.value || undefined);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
|
@ -50,17 +50,22 @@ function SelectColumnFilter({
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Table({ columns, data }: any) {
|
interface TableProps {
|
||||||
|
columns: Column[];
|
||||||
|
data: Release[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Table({ columns, data }: TableProps) {
|
||||||
// Use the state and functions returned from useTable to build your UI
|
// Use the state and functions returned from useTable to build your UI
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
headerGroups,
|
headerGroups,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
page, // Instead of using 'rows', we'll use page,
|
page // Instead of using 'rows', we'll use page,
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{ columns, data },
|
{ columns, data },
|
||||||
useFilters,
|
useFilters,
|
||||||
|
@ -94,7 +99,7 @@ function Table({ columns, data }: any) {
|
||||||
{...columnRest}
|
{...columnRest}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{column.render('Header')}
|
{column.render("Header")}
|
||||||
{/* Add a sort direction indicator */}
|
{/* Add a sort direction indicator */}
|
||||||
<span>
|
<span>
|
||||||
{column.isSorted ? (
|
{column.isSorted ? (
|
||||||
|
@ -119,12 +124,12 @@ function Table({ columns, data }: any) {
|
||||||
{...getTableBodyProps()}
|
{...getTableBodyProps()}
|
||||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||||
>
|
>
|
||||||
{page.map((row: any) => {
|
{page.map((row) => {
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||||
return (
|
return (
|
||||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||||
{row.cells.map((cell: any) => {
|
{row.cells.map((cell) => {
|
||||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
|
@ -133,12 +138,12 @@ function Table({ columns, data }: any) {
|
||||||
role="cell"
|
role="cell"
|
||||||
{...cellRowRest}
|
{...cellRowRest}
|
||||||
>
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render("Cell")}
|
||||||
</td>
|
</td>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -151,30 +156,30 @@ export const ActivityTable = () => {
|
||||||
const columns = React.useMemo(() => [
|
const columns = React.useMemo(() => [
|
||||||
{
|
{
|
||||||
Header: "Age",
|
Header: "Age",
|
||||||
accessor: 'timestamp',
|
accessor: "timestamp",
|
||||||
Cell: DataTable.AgeCell,
|
Cell: DataTable.AgeCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Release",
|
Header: "Release",
|
||||||
accessor: 'torrent_name',
|
accessor: "torrent_name",
|
||||||
Cell: DataTable.TitleCell,
|
Cell: DataTable.TitleCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Actions",
|
Header: "Actions",
|
||||||
accessor: 'action_status',
|
accessor: "action_status",
|
||||||
Cell: DataTable.ReleaseStatusCell,
|
Cell: DataTable.ReleaseStatusCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Indexer",
|
Header: "Indexer",
|
||||||
accessor: 'indexer',
|
accessor: "indexer",
|
||||||
Cell: DataTable.TitleCell,
|
Cell: DataTable.TitleCell,
|
||||||
Filter: SelectColumnFilter,
|
Filter: SelectColumnFilter,
|
||||||
filter: 'includes',
|
filter: "includes"
|
||||||
},
|
}
|
||||||
], [])
|
], []);
|
||||||
|
|
||||||
const { isLoading, data } = useQuery(
|
const { isLoading, data } = useQuery(
|
||||||
'dash_release',
|
"dash_release",
|
||||||
() => APIClient.release.find("?limit=10"),
|
() => APIClient.release.find("?limit=10"),
|
||||||
{ refetchOnWindowFocus: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
@ -188,7 +193,7 @@ export const ActivityTable = () => {
|
||||||
Recent activity
|
Recent activity
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<Table columns={columns} data={data?.data} />
|
<Table columns={columns} data={data?.data ?? []} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
|
@ -8,41 +8,41 @@ interface StatsItemProps {
|
||||||
|
|
||||||
const StatsItem = ({ name, value }: StatsItemProps) => (
|
const StatsItem = ({ name, value }: StatsItemProps) => (
|
||||||
<div
|
<div
|
||||||
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
|
className="relative px-4 py-5 overflow-hidden bg-white rounded-lg shadow-lg dark:bg-gray-800"
|
||||||
title="All time"
|
title="All time"
|
||||||
>
|
>
|
||||||
<dt>
|
<dt>
|
||||||
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
|
<p className="pb-1 text-sm font-medium text-gray-500 truncate">{name}</p>
|
||||||
</dt>
|
</dt>
|
||||||
|
|
||||||
<dd className="flex items-baseline">
|
<dd className="flex items-baseline">
|
||||||
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
<p className="text-3xl font-extrabold text-gray-900 dark:text-gray-200">{value}</p>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const Stats = () => {
|
export const Stats = () => {
|
||||||
const { isLoading, data } = useQuery(
|
const { isLoading, data } = useQuery(
|
||||||
"dash_release_stats",
|
"dash_release_stats",
|
||||||
() => APIClient.release.stats(),
|
() => APIClient.release.stats(),
|
||||||
{ refetchOnWindowFocus: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
<h3 className="text-2xl font-medium leading-6 text-gray-900 dark:text-gray-200">
|
||||||
Stats
|
Stats
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3">
|
<dl className="grid grid-cols-1 gap-5 mt-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<StatsItem name="Filtered Releases" value={data?.filtered_count} />
|
<StatsItem name="Filtered Releases" value={data?.filtered_count} />
|
||||||
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
|
{/* <StatsItem name="Filter Rejected Releases" stat={data?.filter_rejected_count} /> */}
|
||||||
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
|
<StatsItem name="Rejected Pushes" value={data?.push_rejected_count} />
|
||||||
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
<StatsItem name="Approved Pushes" value={data?.push_approved_count} />
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
|
@ -2,10 +2,10 @@ import { Stats } from "./Stats";
|
||||||
import { ActivityTable } from "./ActivityTable";
|
import { ActivityTable } from "./ActivityTable";
|
||||||
|
|
||||||
export const Dashboard = () => (
|
export const Dashboard = () => (
|
||||||
<main className="py-10">
|
<main className="py-10">
|
||||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||||
<Stats />
|
<Stats />
|
||||||
<ActivityTable />
|
<ActivityTable />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
File diff suppressed because it is too large
Load diff
|
@ -4,10 +4,10 @@ import { toast } from "react-hot-toast";
|
||||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import {
|
import {
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
SwitchHorizontalIcon,
|
SwitchHorizontalIcon,
|
||||||
DotsHorizontalIcon, DuplicateIcon,
|
DotsHorizontalIcon, DuplicateIcon
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/outline";
|
||||||
|
|
||||||
import { queryClient } from "../../App";
|
import { queryClient } from "../../App";
|
||||||
|
@ -20,50 +20,50 @@ import { EmptyListState } from "../../components/emptystates";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
|
||||||
export default function Filters() {
|
export default function Filters() {
|
||||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false);
|
||||||
|
|
||||||
const { isLoading, error, data } = useQuery(
|
const { isLoading, error, data } = useQuery(
|
||||||
["filters"],
|
["filters"],
|
||||||
APIClient.filters.getAll,
|
APIClient.filters.getAll,
|
||||||
{ refetchOnWindowFocus: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (<p>An error has occurred: </p>);
|
return (<p>An error has occurred: </p>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
<FilterAddForm isOpen={createFilterIsOpen} toggle={toggleCreateFilter} />
|
||||||
|
|
||||||
<header className="py-10">
|
<header className="py-10">
|
||||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||||
<h1 className="text-3xl font-bold text-black dark:text-white">
|
<h1 className="text-3xl font-bold text-black dark:text-white">
|
||||||
Filters
|
Filters
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
onClick={toggleCreateFilter}
|
onClick={toggleCreateFilter}
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
<div className="max-w-screen-xl mx-auto pb-12 px-4 sm:px-6 lg:px-8 relative">
|
||||||
{data && data.length > 0 ? (
|
{data && data.length > 0 ? (
|
||||||
<FilterList filters={data} />
|
<FilterList filters={data} />
|
||||||
) : (
|
) : (
|
||||||
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
<EmptyListState text="No filters here.." buttonText="Add new" buttonOnClick={toggleCreateFilter} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterListProps {
|
interface FilterListProps {
|
||||||
|
@ -71,33 +71,33 @@ interface FilterListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterList({ filters }: FilterListProps) {
|
function FilterList({ filters }: FilterListProps) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
<div className="overflow-x-auto align-middle min-w-full rounded-lg shadow-lg">
|
||||||
<table className="min-w-full">
|
<table className="min-w-full">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
{["Enabled", "Name", "Indexers"].map((label) => (
|
{["Enabled", "Name", "Indexers"].map((label) => (
|
||||||
<th
|
<th
|
||||||
key={`th-${label}`}
|
key={`th-${label}`}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
className="px-6 py-2.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
<th scope="col" className="relative px-6 py-3">
|
<th scope="col" className="relative px-6 py-3">
|
||||||
<span className="sr-only">Edit</span>
|
<span className="sr-only">Edit</span>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||||
{filters.map((filter: Filter, idx) => (
|
{filters.map((filter: Filter, idx) => (
|
||||||
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
<FilterListItem filter={filter} key={filter.id} idx={idx} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterItemDropdownProps {
|
interface FilterItemDropdownProps {
|
||||||
|
@ -106,157 +106,157 @@ interface FilterItemDropdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterItemDropdown = ({
|
const FilterItemDropdown = ({
|
||||||
filter,
|
filter,
|
||||||
onToggle
|
onToggle
|
||||||
}: FilterItemDropdownProps) => {
|
}: FilterItemDropdownProps) => {
|
||||||
const cancelModalButtonRef = useRef(null);
|
const cancelModalButtonRef = useRef(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
const deleteMutation = useMutation(
|
const deleteMutation = useMutation(
|
||||||
(id: number) => APIClient.filters.delete(id),
|
(id: number) => APIClient.filters.delete(id),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["filters"]);
|
queryClient.invalidateQueries(["filters"]);
|
||||||
queryClient.invalidateQueries(["filters", filter.id]);
|
queryClient.invalidateQueries(["filters", filter.id]);
|
||||||
|
|
||||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
|
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} was deleted`} t={t} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const duplicateMutation = useMutation(
|
const duplicateMutation = useMutation(
|
||||||
(id: number) => APIClient.filters.duplicate(id),
|
(id: number) => APIClient.filters.duplicate(id),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["filters"]);
|
queryClient.invalidateQueries(["filters"]);
|
||||||
|
|
||||||
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
|
toast.custom((t) => <Toast type="success" body={`Filter ${filter?.name} duplicated`} t={t} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div">
|
<Menu as="div">
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
isOpen={deleteModalIsOpen}
|
isOpen={deleteModalIsOpen}
|
||||||
toggle={toggleDeleteModal}
|
toggle={toggleDeleteModal}
|
||||||
buttonRef={cancelModalButtonRef}
|
buttonRef={cancelModalButtonRef}
|
||||||
deleteAction={() => {
|
deleteAction={() => {
|
||||||
deleteMutation.mutate(filter.id);
|
deleteMutation.mutate(filter.id);
|
||||||
toggleDeleteModal();
|
toggleDeleteModal();
|
||||||
}}
|
}}
|
||||||
title={`Remove filter: ${filter.name}`}
|
title={`Remove filter: ${filter.name}`}
|
||||||
text="Are you sure you want to remove this filter? This action cannot be undone."
|
text="Are you sure you want to remove this filter? This action cannot be undone."
|
||||||
/>
|
/>
|
||||||
<Menu.Button className="px-4 py-2">
|
<Menu.Button className="px-4 py-2">
|
||||||
<DotsHorizontalIcon
|
<DotsHorizontalIcon
|
||||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-100"
|
enter="transition ease-out duration-100"
|
||||||
enterFrom="transform opacity-0 scale-95"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
enterTo="transform opacity-100 scale-100"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leave="transition ease-in duration-75"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="px-1 py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<Link
|
||||||
|
to={`filters/${filter.id.toString()}`}
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="px-1 py-1">
|
<PencilAltIcon
|
||||||
<Menu.Item>
|
className={classNames(
|
||||||
{({ active }) => (
|
active ? "text-white" : "text-blue-500",
|
||||||
<Link
|
"w-5 h-5 mr-2"
|
||||||
to={`filters/${filter.id.toString()}`}
|
)}
|
||||||
className={classNames(
|
aria-hidden="true"
|
||||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
/>
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<PencilAltIcon
|
|
||||||
className={classNames(
|
|
||||||
active ? "text-white" : "text-blue-500",
|
|
||||||
"w-5 h-5 mr-2"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggle(!filter.enabled)}
|
onClick={() => onToggle(!filter.enabled)}
|
||||||
>
|
>
|
||||||
<SwitchHorizontalIcon
|
<SwitchHorizontalIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Toggle
|
Toggle
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
onClick={() => duplicateMutation.mutate(filter.id)}
|
onClick={() => duplicateMutation.mutate(filter.id)}
|
||||||
>
|
>
|
||||||
<DuplicateIcon
|
<DuplicateIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Duplicate
|
Duplicate
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-1 py-1">
|
<div className="px-1 py-1">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleDeleteModal()}
|
onClick={() => toggleDeleteModal()}
|
||||||
>
|
>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-red-500",
|
active ? "text-white" : "text-red-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface FilterListItemProps {
|
interface FilterListItemProps {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
|
@ -264,83 +264,83 @@ interface FilterListItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
function FilterListItem({ filter, idx }: FilterListItemProps) {
|
||||||
const [enabled, setEnabled] = useState(filter.enabled)
|
const [enabled, setEnabled] = useState(filter.enabled);
|
||||||
|
|
||||||
const updateMutation = useMutation(
|
const updateMutation = useMutation(
|
||||||
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
(status: boolean) => APIClient.filters.toggleEnable(filter.id, status),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />)
|
toast.custom((t) => <Toast type="success" body={`${filter.name} was ${enabled ? "disabled" : "enabled"} successfully`} t={t} />);
|
||||||
|
|
||||||
// We need to invalidate both keys here.
|
// We need to invalidate both keys here.
|
||||||
// The filters key is used on the /filters page,
|
// The filters key is used on the /filters page,
|
||||||
// while the ["filter", filter.id] key is used on the details page.
|
// while the ["filter", filter.id] key is used on the details page.
|
||||||
queryClient.invalidateQueries(["filters"]);
|
queryClient.invalidateQueries(["filters"]);
|
||||||
queryClient.invalidateQueries(["filters", filter?.id]);
|
queryClient.invalidateQueries(["filters", filter?.id]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleActive = (status: boolean) => {
|
|
||||||
setEnabled(status);
|
|
||||||
updateMutation.mutate(status);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const toggleActive = (status: boolean) => {
|
||||||
<tr
|
setEnabled(status);
|
||||||
key={filter.id}
|
updateMutation.mutate(status);
|
||||||
className={classNames(
|
};
|
||||||
idx % 2 === 0 ?
|
|
||||||
"bg-white dark:bg-[#2e2e31]" :
|
return (
|
||||||
"bg-gray-50 dark:bg-gray-800",
|
<tr
|
||||||
"hover:bg-gray-100 dark:hover:bg-[#222225]"
|
key={filter.id}
|
||||||
)}
|
className={classNames(
|
||||||
|
idx % 2 === 0 ?
|
||||||
|
"bg-white dark:bg-[#2e2e31]" :
|
||||||
|
"bg-gray-50 dark:bg-gray-800",
|
||||||
|
"hover:bg-gray-100 dark:hover:bg-[#222225]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={toggleActive}
|
||||||
|
className={classNames(
|
||||||
|
enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-700",
|
||||||
|
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<td
|
<span className="sr-only">Use setting</span>
|
||||||
className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-100"
|
<span
|
||||||
>
|
aria-hidden="true"
|
||||||
<Switch
|
className={classNames(
|
||||||
checked={enabled}
|
enabled ? "translate-x-5" : "translate-x-0",
|
||||||
onChange={toggleActive}
|
"inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
className={classNames(
|
)}
|
||||||
enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-700',
|
/>
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
</Switch>
|
||||||
)}
|
</td>
|
||||||
>
|
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||||
<span className="sr-only">Use setting</span>
|
<Link
|
||||||
<span
|
to={`filters/${filter.id.toString()}`}
|
||||||
aria-hidden="true"
|
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
||||||
className={classNames(
|
>
|
||||||
enabled ? 'translate-x-5' : 'translate-x-0',
|
{filter.name}
|
||||||
'inline-block h-5 w-5 rounded-full bg-white dark:bg-gray-200 shadow transform ring-0 transition ease-in-out duration-200'
|
</Link>
|
||||||
)}
|
</td>
|
||||||
/>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
</Switch>
|
{filter.indexers && filter.indexers.map((t) => (
|
||||||
</td>
|
<span
|
||||||
<td className="px-6 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
|
key={t.id}
|
||||||
<Link
|
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||||
to={`filters/${filter.id.toString()}`}
|
>
|
||||||
className="hover:text-black dark:hover:text-gray-300 w-full py-4 flex"
|
{t.name}
|
||||||
>
|
</span>
|
||||||
{filter.name}
|
))}
|
||||||
</Link>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<FilterItemDropdown
|
||||||
{filter.indexers && filter.indexers.map((t) => (
|
filter={filter}
|
||||||
<span
|
onToggle={toggleActive}
|
||||||
key={t.id}
|
/>
|
||||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
</td>
|
||||||
>
|
</tr>
|
||||||
{t.name}
|
);
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<FilterItemDropdown
|
|
||||||
filter={filter}
|
|
||||||
onToggle={toggleActive}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
}
|
|
@ -2,90 +2,91 @@ import * as React from "react";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDownIcon,
|
ChevronDownIcon
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
|
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import { PushStatusOptions } from "../../domain/constants";
|
import { PushStatusOptions } from "../../domain/constants";
|
||||||
|
import { FilterProps } from "react-table";
|
||||||
|
|
||||||
interface ListboxFilterProps {
|
interface ListboxFilterProps {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
currentValue: string;
|
currentValue: string;
|
||||||
onChange: (newValue: string) => void;
|
onChange: (newValue: string) => void;
|
||||||
children: any;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListboxFilter = ({
|
const ListboxFilter = ({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
currentValue,
|
currentValue,
|
||||||
onChange,
|
onChange,
|
||||||
children
|
children
|
||||||
}: ListboxFilterProps) => (
|
}: ListboxFilterProps) => (
|
||||||
<div className="w-48">
|
<div className="w-48">
|
||||||
<Listbox
|
<Listbox
|
||||||
refName={id}
|
refName={id}
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm">
|
||||||
|
<span className="block truncate">{label}</span>
|
||||||
|
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="relative mt-1">
|
<Listbox.Options
|
||||||
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white dark:bg-gray-800 rounded-lg shadow-md cursor-default dark:text-gray-400 sm:text-sm">
|
className="absolute w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
|
||||||
<span className="block truncate">{label}</span>
|
>
|
||||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
<FilterOption label="All" />
|
||||||
<ChevronDownIcon
|
{children}
|
||||||
className="w-5 h-5 ml-2 -mr-1 text-gray-600 hover:text-gray-600"
|
</Listbox.Options>
|
||||||
aria-hidden="true"
|
</Transition>
|
||||||
/>
|
</div>
|
||||||
</span>
|
</Listbox>
|
||||||
</Listbox.Button>
|
</div>
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Listbox.Options
|
|
||||||
className="absolute w-full mt-1 overflow-auto text-base bg-white dark:bg-gray-800 rounded-md shadow-lg max-h-60 border border-opacity-5 border-black dark:border-gray-700 dark:border-opacity-40 focus:outline-none sm:text-sm"
|
|
||||||
>
|
|
||||||
<FilterOption label="All" />
|
|
||||||
{children}
|
|
||||||
</Listbox.Options>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
</Listbox>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// a unique option from a list
|
// a unique option from a list
|
||||||
export const IndexerSelectColumnFilter = ({
|
export const IndexerSelectColumnFilter = ({
|
||||||
column: { filterValue, setFilter, id }
|
column: { filterValue, setFilter, id }
|
||||||
}: any) => {
|
}: FilterProps<object>) => {
|
||||||
const { data, isSuccess } = useQuery(
|
const { data, isSuccess } = useQuery(
|
||||||
"release_indexers",
|
"release_indexers",
|
||||||
() => APIClient.release.indexerOptions(),
|
() => APIClient.release.indexerOptions(),
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Render a multi-select box
|
// Render a multi-select box
|
||||||
return (
|
return (
|
||||||
<ListboxFilter
|
<ListboxFilter
|
||||||
id={id}
|
id={id}
|
||||||
label={filterValue ?? "Indexer"}
|
label={filterValue ?? "Indexer"}
|
||||||
currentValue={filterValue}
|
currentValue={filterValue}
|
||||||
onChange={setFilter}
|
onChange={setFilter}
|
||||||
>
|
>
|
||||||
{isSuccess && data?.map((indexer, idx) => (
|
{isSuccess && data?.map((indexer, idx) => (
|
||||||
<FilterOption key={idx} label={indexer} value={indexer} />
|
<FilterOption key={idx} label={indexer} value={indexer} />
|
||||||
))}
|
))}
|
||||||
</ListboxFilter>
|
</ListboxFilter>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface FilterOptionProps {
|
interface FilterOptionProps {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -93,50 +94,48 @@ interface FilterOptionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilterOption = ({ label, value }: FilterOptionProps) => (
|
const FilterOption = ({ label, value }: FilterOptionProps) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
className={({ active }) => classNames(
|
className={({ active }) => classNames(
|
||||||
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
"cursor-pointer select-none relative py-2 pl-10 pr-4",
|
||||||
active ? 'text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900' : 'text-gray-700 dark:text-gray-400'
|
active ? "text-black dark:text-gray-200 bg-gray-100 dark:bg-gray-900" : "text-gray-700 dark:text-gray-400"
|
||||||
)}
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"block truncate",
|
"block truncate",
|
||||||
selected ? "font-medium text-black dark:text-white" : "font-normal"
|
selected ? "font-medium text-black dark:text-white" : "font-normal"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
|
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500 dark:text-gray-400">
|
||||||
<CheckIcon className="w-5 h-5" aria-hidden="true" />
|
<CheckIcon className="w-5 h-5" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const PushStatusSelectColumnFilter = ({
|
export const PushStatusSelectColumnFilter = ({
|
||||||
column: { filterValue, setFilter, id }
|
column: { filterValue, setFilter, id }
|
||||||
}: any) => (
|
}: FilterProps<object>) => {
|
||||||
|
const label = filterValue ? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label : "Push status";
|
||||||
|
return (
|
||||||
<div className="mr-3">
|
<div className="mr-3">
|
||||||
<ListboxFilter
|
<ListboxFilter
|
||||||
id={id}
|
id={id}
|
||||||
label={
|
label={label ?? "Push status"}
|
||||||
filterValue
|
currentValue={filterValue}
|
||||||
? PushStatusOptions.find((o) => o.value === filterValue && o.value)?.label
|
onChange={setFilter}
|
||||||
: "Push status"
|
>
|
||||||
}
|
{PushStatusOptions.map((status, idx) => (
|
||||||
currentValue={filterValue}
|
<FilterOption key={idx} value={status.value} label={status.label} />
|
||||||
onChange={setFilter}
|
))}
|
||||||
>
|
</ListboxFilter>
|
||||||
{PushStatusOptions.map((status, idx) => (
|
|
||||||
<FilterOption key={idx} value={status.value} label={status.label} />
|
|
||||||
))}
|
|
||||||
</ListboxFilter>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);};
|
|
@ -1,17 +1,17 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
import {
|
import {
|
||||||
useTable,
|
useTable,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
usePagination,
|
usePagination,
|
||||||
useFilters,
|
useFilters,
|
||||||
Column
|
Column
|
||||||
} from "react-table";
|
} from "react-table";
|
||||||
import {
|
import {
|
||||||
ChevronDoubleLeftIcon,
|
ChevronDoubleLeftIcon,
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ChevronDoubleRightIcon
|
ChevronDoubleRightIcon
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
|
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
|
@ -21,295 +21,310 @@ import * as Icons from "../../components/Icons";
|
||||||
import * as DataTable from "../../components/data-table";
|
import * as DataTable from "../../components/data-table";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IndexerSelectColumnFilter,
|
IndexerSelectColumnFilter,
|
||||||
PushStatusSelectColumnFilter
|
PushStatusSelectColumnFilter
|
||||||
} from "./Filters";
|
} from "./Filters";
|
||||||
|
|
||||||
const initialState = {
|
type TableState = {
|
||||||
queryPageIndex: 0,
|
queryPageIndex: number;
|
||||||
queryPageSize: 10,
|
queryPageSize: number;
|
||||||
totalCount: null,
|
totalCount: number;
|
||||||
queryFilters: []
|
queryFilters: ReleaseFilter[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const PAGE_CHANGED = 'PAGE_CHANGED';
|
const initialState: TableState = {
|
||||||
const PAGE_SIZE_CHANGED = 'PAGE_SIZE_CHANGED';
|
queryPageIndex: 0,
|
||||||
const TOTAL_COUNT_CHANGED = 'TOTAL_COUNT_CHANGED';
|
queryPageSize: 10,
|
||||||
const FILTER_CHANGED = 'FILTER_CHANGED';
|
totalCount: 0,
|
||||||
|
queryFilters: []
|
||||||
|
};
|
||||||
|
|
||||||
const TableReducer = (state: any, { type, payload }: any) => {
|
enum ActionType {
|
||||||
switch (type) {
|
PAGE_CHANGED = "PAGE_CHANGED",
|
||||||
case PAGE_CHANGED:
|
PAGE_SIZE_CHANGED = "PAGE_SIZE_CHANGED",
|
||||||
return { ...state, queryPageIndex: payload };
|
TOTAL_COUNT_CHANGED = "TOTAL_COUNT_CHANGED",
|
||||||
case PAGE_SIZE_CHANGED:
|
FILTER_CHANGED = "FILTER_CHANGED"
|
||||||
return { ...state, queryPageSize: payload };
|
}
|
||||||
case TOTAL_COUNT_CHANGED:
|
|
||||||
return { ...state, totalCount: payload };
|
type Actions =
|
||||||
case FILTER_CHANGED:
|
| { type: ActionType.FILTER_CHANGED; payload: ReleaseFilter[]; }
|
||||||
return { ...state, queryFilters: payload };
|
| { type: ActionType.PAGE_CHANGED; payload: number; }
|
||||||
default:
|
| { type: ActionType.PAGE_SIZE_CHANGED; payload: number; }
|
||||||
throw new Error(`Unhandled action type: ${type}`);
|
| { type: ActionType.TOTAL_COUNT_CHANGED; payload: number; };
|
||||||
}
|
|
||||||
|
const TableReducer = (state: TableState, action: Actions): TableState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionType.PAGE_CHANGED:
|
||||||
|
return { ...state, queryPageIndex: action.payload };
|
||||||
|
case ActionType.PAGE_SIZE_CHANGED:
|
||||||
|
return { ...state, queryPageSize: action.payload };
|
||||||
|
case ActionType.FILTER_CHANGED:
|
||||||
|
return { ...state, queryFilters: action.payload };
|
||||||
|
case ActionType.TOTAL_COUNT_CHANGED:
|
||||||
|
return { ...state, totalCount: action.payload };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled action type: ${action}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReleaseTable = () => {
|
export const ReleaseTable = () => {
|
||||||
const columns = React.useMemo(() => [
|
const columns = React.useMemo(() => [
|
||||||
{
|
{
|
||||||
Header: "Age",
|
Header: "Age",
|
||||||
accessor: 'timestamp',
|
accessor: "timestamp",
|
||||||
Cell: DataTable.AgeCell,
|
Cell: DataTable.AgeCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Release",
|
Header: "Release",
|
||||||
accessor: 'torrent_name',
|
accessor: "torrent_name",
|
||||||
Cell: DataTable.TitleCell,
|
Cell: DataTable.TitleCell
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Actions",
|
Header: "Actions",
|
||||||
accessor: 'action_status',
|
accessor: "action_status",
|
||||||
Cell: DataTable.ReleaseStatusCell,
|
Cell: DataTable.ReleaseStatusCell,
|
||||||
Filter: PushStatusSelectColumnFilter,
|
Filter: PushStatusSelectColumnFilter
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: "Indexer",
|
Header: "Indexer",
|
||||||
accessor: 'indexer',
|
accessor: "indexer",
|
||||||
Cell: DataTable.TitleCell,
|
Cell: DataTable.TitleCell,
|
||||||
Filter: IndexerSelectColumnFilter,
|
Filter: IndexerSelectColumnFilter,
|
||||||
filter: 'equal',
|
filter: "equal"
|
||||||
},
|
}
|
||||||
] as Column<Release>[], [])
|
] as Column<Release>[], []);
|
||||||
|
|
||||||
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
const [{ queryPageIndex, queryPageSize, totalCount, queryFilters }, dispatch] =
|
||||||
React.useReducer(TableReducer, initialState);
|
React.useReducer(TableReducer, initialState);
|
||||||
|
|
||||||
const { isLoading, error, data, isSuccess } = useQuery(
|
const { isLoading, error, data, isSuccess } = useQuery(
|
||||||
['releases', queryPageIndex, queryPageSize, queryFilters],
|
["releases", queryPageIndex, queryPageSize, queryFilters],
|
||||||
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
() => APIClient.release.findQuery(queryPageIndex * queryPageSize, queryPageSize, queryFilters),
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
staleTime: 5000,
|
staleTime: 5000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use the state and functions returned from useTable to build your UI
|
// Use the state and functions returned from useTable to build your UI
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
headerGroups,
|
headerGroups,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
page, // Instead of using 'rows', we'll use page,
|
page, // Instead of using 'rows', we'll use page,
|
||||||
// which has only the rows for the active page
|
// which has only the rows for the active page
|
||||||
|
|
||||||
// The rest of these things are super handy, too ;)
|
// The rest of these things are super handy, too ;)
|
||||||
canPreviousPage,
|
canPreviousPage,
|
||||||
canNextPage,
|
canNextPage,
|
||||||
pageOptions,
|
pageOptions,
|
||||||
pageCount,
|
pageCount,
|
||||||
gotoPage,
|
gotoPage,
|
||||||
nextPage,
|
nextPage,
|
||||||
previousPage,
|
previousPage,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
state: { pageIndex, pageSize, filters }
|
state: { pageIndex, pageSize, filters }
|
||||||
} = useTable(
|
} = useTable(
|
||||||
{
|
{
|
||||||
columns,
|
columns,
|
||||||
data: data && isSuccess ? data.data : [],
|
data: data && isSuccess ? data.data : [],
|
||||||
initialState: {
|
initialState: {
|
||||||
pageIndex: queryPageIndex,
|
pageIndex: queryPageIndex,
|
||||||
pageSize: queryPageSize,
|
pageSize: queryPageSize,
|
||||||
filters: []
|
filters: []
|
||||||
},
|
},
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
manualFilters: true,
|
manualFilters: true,
|
||||||
manualSortBy: true,
|
manualSortBy: true,
|
||||||
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
pageCount: isSuccess ? Math.ceil(totalCount / queryPageSize) : 0,
|
||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
autoResetExpanded: false,
|
autoResetExpanded: false,
|
||||||
autoResetPage: false
|
autoResetPage: false
|
||||||
},
|
},
|
||||||
useFilters,
|
useFilters,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
usePagination,
|
usePagination
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch({ type: PAGE_CHANGED, payload: pageIndex });
|
dispatch({ type: ActionType.PAGE_CHANGED, payload: pageIndex });
|
||||||
}, [pageIndex]);
|
}, [pageIndex]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch({ type: PAGE_SIZE_CHANGED, payload: pageSize });
|
dispatch({ type: ActionType.PAGE_SIZE_CHANGED, payload: pageSize });
|
||||||
gotoPage(0);
|
gotoPage(0);
|
||||||
}, [pageSize, gotoPage]);
|
}, [pageSize, gotoPage]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (data?.count) {
|
if (data?.count) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TOTAL_COUNT_CHANGED,
|
type: ActionType.TOTAL_COUNT_CHANGED,
|
||||||
payload: data.count,
|
payload: data.count
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data?.count]);
|
}, [data?.count]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dispatch({ type: FILTER_CHANGED, payload: filters });
|
dispatch({ type: ActionType.FILTER_CHANGED, payload: filters });
|
||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return <p>Error</p>;
|
return <p>Error</p>;
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return <p>Loading...</p>;
|
return <p>Loading...</p>;
|
||||||
|
|
||||||
if (!data)
|
if (!data)
|
||||||
return <EmptyListState text="No recent activity" />
|
return <EmptyListState text="No recent activity" />;
|
||||||
|
|
||||||
// Render the UI for your table
|
// Render the UI for your table
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex mb-6">
|
<div className="flex mb-6">
|
||||||
{headerGroups.map((headerGroup: { headers: any[] }) =>
|
{headerGroups.map((headerGroup) =>
|
||||||
headerGroup.headers.map((column) => (
|
headerGroup.headers.map((column) => (
|
||||||
column.Filter ? (
|
column.Filter ? (
|
||||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||||
{column.render("Filter")}
|
{column.render("Filter")}
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg">
|
<div className="overflow-auto bg-white shadow-lg dark:bg-gray-800 rounded-lg">
|
||||||
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
<table {...getTableProps()} className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800">
|
<thead className="bg-gray-50 dark:bg-gray-800">
|
||||||
{headerGroups.map((headerGroup) => {
|
{headerGroups.map((headerGroup) => {
|
||||||
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
const { key: rowKey, ...rowRest } = headerGroup.getHeaderGroupProps();
|
||||||
return (
|
return (
|
||||||
<tr key={rowKey} {...rowRest}>
|
<tr key={rowKey} {...rowRest}>
|
||||||
{headerGroup.headers.map((column) => {
|
{headerGroup.headers.map((column) => {
|
||||||
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
const { key: columnKey, ...columnRest } = column.getHeaderProps(column.getSortByToggleProps());
|
||||||
return (
|
return (
|
||||||
// Add the sorting props to control sorting. For this example
|
// Add the sorting props to control sorting. For this example
|
||||||
// we can add them into the header props
|
// we can add them into the header props
|
||||||
<th
|
<th
|
||||||
key={`${rowKey}-${columnKey}`}
|
key={`${rowKey}-${columnKey}`}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
className="first:pl-5 pl-3 pr-3 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase group"
|
||||||
{...columnRest}
|
{...columnRest}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{column.render('Header')}
|
{column.render("Header")}
|
||||||
{/* Add a sort direction indicator */}
|
{/* Add a sort direction indicator */}
|
||||||
<span>
|
<span>
|
||||||
{column.isSorted ? (
|
{column.isSorted ? (
|
||||||
column.isSortedDesc ? (
|
column.isSortedDesc ? (
|
||||||
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
<Icons.SortDownIcon className="w-4 h-4 text-gray-400" />
|
||||||
) : (
|
) : (
|
||||||
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
<Icons.SortUpIcon className="w-4 h-4 text-gray-400" />
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
<Icons.SortIcon className="w-4 h-4 text-gray-400 opacity-0 group-hover:opacity-100" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</thead>
|
</thead>
|
||||||
<tbody
|
<tbody
|
||||||
{...getTableBodyProps()}
|
{...getTableBodyProps()}
|
||||||
className="divide-y divide-gray-200 dark:divide-gray-700"
|
className="divide-y divide-gray-200 dark:divide-gray-700"
|
||||||
>
|
>
|
||||||
{page.map((row: any) => {
|
{page.map((row) => {
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
|
|
||||||
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
const { key: bodyRowKey, ...bodyRowRest } = row.getRowProps();
|
||||||
return (
|
return (
|
||||||
<tr key={bodyRowKey} {...bodyRowRest}>
|
<tr key={bodyRowKey} {...bodyRowRest}>
|
||||||
{row.cells.map((cell: any) => {
|
{row.cells.map((cell) => {
|
||||||
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
const { key: cellRowKey, ...cellRowRest } = cell.getCellProps();
|
||||||
return (
|
return (
|
||||||
<td
|
<td
|
||||||
key={cellRowKey}
|
key={cellRowKey}
|
||||||
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
className="first:pl-5 pl-3 pr-3 whitespace-nowrap"
|
||||||
role="cell"
|
role="cell"
|
||||||
{...cellRowRest}
|
{...cellRowRest}
|
||||||
>
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render("Cell")}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
<div className="flex justify-between flex-1 sm:hidden">
|
<div className="flex justify-between flex-1 sm:hidden">
|
||||||
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
<DataTable.Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</DataTable.Button>
|
||||||
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
<DataTable.Button onClick={() => nextPage()} disabled={!canNextPage}>Next</DataTable.Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||||
<div className="flex items-baseline gap-x-2">
|
<div className="flex items-baseline gap-x-2">
|
||||||
<span className="text-sm text-gray-700">
|
<span className="text-sm text-gray-700">
|
||||||
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
Page <span className="font-medium">{pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
||||||
</span>
|
</span>
|
||||||
<label>
|
<label>
|
||||||
<span className="sr-only bg-gray-700">Items Per Page</span>
|
<span className="sr-only bg-gray-700">Items Per Page</span>
|
||||||
<select
|
<select
|
||||||
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||||
value={pageSize}
|
value={pageSize}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
setPageSize(Number(e.target.value))
|
setPageSize(Number(e.target.value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[5, 10, 20, 50].map(pageSize => (
|
{[5, 10, 20, 50].map(pageSize => (
|
||||||
<option key={pageSize} value={pageSize}>
|
<option key={pageSize} value={pageSize}>
|
||||||
Show {pageSize}
|
Show {pageSize}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
|
||||||
<DataTable.PageButton
|
|
||||||
className="rounded-l-md"
|
|
||||||
onClick={() => gotoPage(0)}
|
|
||||||
disabled={!canPreviousPage}
|
|
||||||
>
|
|
||||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
|
|
||||||
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
||||||
</DataTable.PageButton>
|
|
||||||
<DataTable.PageButton
|
|
||||||
onClick={() => previousPage()}
|
|
||||||
disabled={!canPreviousPage}
|
|
||||||
>
|
|
||||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
|
|
||||||
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
||||||
</DataTable.PageButton>
|
|
||||||
<DataTable.PageButton
|
|
||||||
onClick={() => nextPage()}
|
|
||||||
disabled={!canNextPage}>
|
|
||||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
|
|
||||||
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
||||||
</DataTable.PageButton>
|
|
||||||
<DataTable.PageButton
|
|
||||||
className="rounded-r-md"
|
|
||||||
onClick={() => gotoPage(pageCount - 1)}
|
|
||||||
disabled={!canNextPage}
|
|
||||||
>
|
|
||||||
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
|
|
||||||
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
|
||||||
</DataTable.PageButton>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
|
<DataTable.PageButton
|
||||||
|
className="rounded-l-md"
|
||||||
|
onClick={() => gotoPage(0)}
|
||||||
|
disabled={!canPreviousPage}
|
||||||
|
>
|
||||||
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">First</span>
|
||||||
|
<ChevronDoubleLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||||
|
</DataTable.PageButton>
|
||||||
|
<DataTable.PageButton
|
||||||
|
onClick={() => previousPage()}
|
||||||
|
disabled={!canPreviousPage}
|
||||||
|
>
|
||||||
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Previous</span>
|
||||||
|
<ChevronLeftIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||||
|
</DataTable.PageButton>
|
||||||
|
<DataTable.PageButton
|
||||||
|
onClick={() => nextPage()}
|
||||||
|
disabled={!canNextPage}>
|
||||||
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Next</span>
|
||||||
|
<ChevronRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||||
|
</DataTable.PageButton>
|
||||||
|
<DataTable.PageButton
|
||||||
|
className="rounded-r-md"
|
||||||
|
onClick={() => gotoPage(pageCount - 1)}
|
||||||
|
disabled={!canNextPage}
|
||||||
|
>
|
||||||
|
<span className="sr-only text-gray-400 dark:text-gray-500 dark:bg-gray-700">Last</span>
|
||||||
|
<ChevronDoubleRightIcon className="w-4 h-4 text-gray-400 dark:text-gray-500" aria-hidden="true" />
|
||||||
|
</DataTable.PageButton>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { ReleaseTable } from "./ReleaseTable";
|
import { ReleaseTable } from "./ReleaseTable";
|
||||||
|
|
||||||
export const Releases = () => (
|
export const Releases = () => (
|
||||||
<main>
|
<main>
|
||||||
<header className="py-10">
|
<header className="py-10">
|
||||||
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
<div className="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between">
|
||||||
<h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1>
|
<h1 className="text-3xl font-bold text-black dark:text-white">Releases</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
<div className="max-w-screen-xl mx-auto pb-6 px-4 sm:px-6 lg:pb-16 lg:px-8">
|
||||||
<ReleaseTable />
|
<ReleaseTable />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
|
@ -1,79 +1,79 @@
|
||||||
function ActionSettings() {
|
function ActionSettings() {
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
|
||||||
|
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
{/*{addClientIsOpen &&*/}
|
{/*{addClientIsOpen &&*/}
|
||||||
{/*<AddNewClientForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>*/}
|
{/*<AddNewClientForm isOpen={addClientIsOpen} toggle={toggleAddClient}/>*/}
|
||||||
{/*}*/}
|
{/*}*/}
|
||||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
<div className="ml-4 mt-4">
|
<div className="ml-4 mt-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Actions</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
Manage actions.
|
Manage actions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 mt-4 flex-shrink-0">
|
<div className="ml-4 mt-4 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
// onClick={toggleAddClient}
|
// onClick={toggleAddClient}
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col mt-6">
|
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
|
||||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Type
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Port
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Enabled
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="relative px-6 py-3">
|
|
||||||
<span className="sr-only">Edit</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>empty</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
<div className="flex flex-col mt-6">
|
||||||
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Port
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative px-6 py-3">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>empty</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ActionSettings;
|
export default ActionSettings;
|
|
@ -4,128 +4,127 @@ import { APIClient } from "../../api/APIClient";
|
||||||
import { Checkbox } from "../../components/Checkbox";
|
import { Checkbox } from "../../components/Checkbox";
|
||||||
import { SettingsContext } from "../../utils/Context";
|
import { SettingsContext } from "../../utils/Context";
|
||||||
|
|
||||||
|
|
||||||
function ApplicationSettings() {
|
function ApplicationSettings() {
|
||||||
const [settings, setSettings] = SettingsContext.use();
|
const [settings, setSettings] = SettingsContext.use();
|
||||||
|
|
||||||
const { isLoading, data } = useQuery(
|
const { isLoading, data } = useQuery(
|
||||||
['config'],
|
["config"],
|
||||||
() => APIClient.config.get(),
|
() => APIClient.config.get(),
|
||||||
{
|
{
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
onError: err => console.log(err)
|
onError: err => console.log(err)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Application settings. Change in config.toml and restart to take effect.
|
Application settings. Change in config.toml and restart to take effect.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isLoading && data && (
|
{!isLoading && data && (
|
||||||
<div className="mt-6 grid grid-cols-12 gap-6">
|
<div className="mt-6 grid grid-cols-12 gap-6">
|
||||||
<div className="col-span-6 sm:col-span-4">
|
<div className="col-span-6 sm:col-span-4">
|
||||||
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<label htmlFor="host" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
Host
|
Host
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="host"
|
name="host"
|
||||||
id="host"
|
id="host"
|
||||||
value={data.host}
|
value={data.host}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-6 sm:col-span-4">
|
<div className="col-span-6 sm:col-span-4">
|
||||||
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<label htmlFor="port" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
Port
|
Port
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="port"
|
name="port"
|
||||||
id="port"
|
id="port"
|
||||||
value={data.port}
|
value={data.port}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-6 sm:col-span-4">
|
<div className="col-span-6 sm:col-span-4">
|
||||||
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
<label htmlFor="base_url" className="block text-xs font-bold text-gray-700 dark:text-gray-200 uppercase tracking-wide">
|
||||||
Base url
|
Base url
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="base_url"
|
name="base_url"
|
||||||
id="base_url"
|
id="base_url"
|
||||||
value={data.base_url}
|
value={data.base_url}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
className="mt-2 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div className="px-4 py-5 sm:p-0">
|
<div className="px-4 py-5 sm:p-0">
|
||||||
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
<dl className="sm:divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{data?.version ? (
|
{data?.version ? (
|
||||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||||
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
|
<dt className="font-medium text-gray-500 dark:text-white">Version:</dt>
|
||||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.version}</dd>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{data?.commit ? (
|
{data?.commit ? (
|
||||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||||
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
|
<dt className="font-medium text-gray-500 dark:text-white">Commit:</dt>
|
||||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data.commit}</dd>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{data?.date ? (
|
{data?.date ? (
|
||||||
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-4 sm:gap-4 sm:px-6">
|
||||||
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
|
<dt className="font-medium text-gray-500 dark:text-white">Date:</dt>
|
||||||
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
<dd className="mt-1 text-gray-900 dark:text-white sm:mt-0 sm:col-span-2">{data?.date}</dd>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
<ul className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
<div className="px-4 sm:px-6 py-1">
|
<div className="px-4 sm:px-6 py-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Debug"
|
label="Debug"
|
||||||
description="Enable debug mode to get more logs."
|
description="Enable debug mode to get more logs."
|
||||||
value={settings.debug}
|
value={settings.debug}
|
||||||
setValue={(newValue: boolean) => setSettings({
|
setValue={(newValue: boolean) => setSettings({
|
||||||
...settings,
|
...settings,
|
||||||
debug: newValue
|
debug: newValue
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4 sm:px-6 py-1">
|
<div className="px-4 sm:px-6 py-1">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Dark theme"
|
label="Dark theme"
|
||||||
description="Switch between dark and light theme."
|
description="Switch between dark and light theme."
|
||||||
value={settings.darkTheme}
|
value={settings.darkTheme}
|
||||||
setValue={(newValue: boolean) => setSettings({
|
setValue={(newValue: boolean) => setSettings({
|
||||||
...settings,
|
...settings,
|
||||||
darkTheme: newValue
|
darkTheme: newValue
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplicationSettings;
|
export default ApplicationSettings;
|
|
@ -13,132 +13,132 @@ interface DLSettingsItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
||||||
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false)
|
const [updateClientIsOpen, toggleUpdateClient] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={client.name} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
<tr key={client.name} className={idx % 2 === 0 ? "light:bg-white" : "light:bg-gray-50"}>
|
||||||
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
<DownloadClientUpdateForm client={client} isOpen={updateClientIsOpen} toggle={toggleUpdateClient} />
|
||||||
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
<Switch
|
<Switch
|
||||||
checked={client.enabled}
|
checked={client.enabled}
|
||||||
onChange={toggleUpdateClient}
|
onChange={toggleUpdateClient}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
client.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
client.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
client.enabled ? 'translate-x-5' : 'translate-x-0',
|
client.enabled ? "translate-x-5" : "translate-x-0",
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{client.name}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{client.name}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{client.host}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{client.host}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{DownloadClientTypeNameMap[client.type]}</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateClient}>
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DownloadClientSettings() {
|
function DownloadClientSettings() {
|
||||||
const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
const [addClientIsOpen, toggleAddClient] = useToggle(false);
|
||||||
|
|
||||||
const { error, data } = useQuery(
|
const { error, data } = useQuery(
|
||||||
'downloadClients',
|
"downloadClients",
|
||||||
APIClient.download_clients.getAll,
|
APIClient.download_clients.getAll,
|
||||||
{ refetchOnWindowFocus: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (<p>An error has occurred: </p>);
|
return (<p>An error has occurred: </p>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
|
||||||
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
<DownloadClientAddForm isOpen={addClientIsOpen} toggle={toggleAddClient} />
|
||||||
|
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
<div className="ml-4 mt-4">
|
<div className="ml-4 mt-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Clients</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Manage download clients.
|
Manage download clients.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 mt-4 flex-shrink-0">
|
<div className="ml-4 mt-4 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
onClick={toggleAddClient}
|
onClick={toggleAddClient}
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col mt-6">
|
|
||||||
{data && data.length > 0 ?
|
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
|
||||||
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<thead className="light:bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Enabled
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Host
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Type
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="relative px-6 py-3">
|
|
||||||
<span className="sr-only">Edit</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{data && data.map((client, idx) => (
|
|
||||||
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
<div className="flex flex-col mt-6">
|
||||||
|
{data && data.length > 0 ?
|
||||||
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 sm:rounded-lg">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead className="light:bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Host
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative px-6 py-3">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{data && data.map((client, idx) => (
|
||||||
|
<DownloadClientSettingsListItem client={client} idx={idx} key={idx} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <EmptySimple title="No download clients" subtitle="Add a new client" buttonText="New client" buttonAction={toggleAddClient} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DownloadClientSettings;
|
export default DownloadClientSettings;
|
|
@ -3,147 +3,148 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import { Menu, Switch, Transition } from "@headlessui/react";
|
import { Menu, Switch, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
import {classNames} from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import {Fragment, useRef, useState} from "react";
|
import { Fragment, useRef, useState } from "react";
|
||||||
import {toast} from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import Toast from "../../components/notifications/Toast";
|
import Toast from "../../components/notifications/Toast";
|
||||||
import {queryClient} from "../../App";
|
import { queryClient } from "../../App";
|
||||||
import {DeleteModal} from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
import {
|
import {
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
SwitchHorizontalIcon,
|
SwitchHorizontalIcon,
|
||||||
TrashIcon
|
TrashIcon
|
||||||
} from "@heroicons/react/outline";
|
} from "@heroicons/react/outline";
|
||||||
import {FeedUpdateForm} from "../../forms/settings/FeedForms";
|
import { FeedUpdateForm } from "../../forms/settings/FeedForms";
|
||||||
import { EmptySimple } from "../../components/emptystates";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
|
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||||
|
|
||||||
function FeedSettings() {
|
function FeedSettings() {
|
||||||
const {data} = useQuery<Feed[], Error>('feeds', APIClient.feeds.find,
|
const { data } = useQuery<Feed[], Error>("feeds", APIClient.feeds.find,
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
<div className="ml-4 mt-4">
|
<div className="ml-4 mt-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Feeds</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Manage Torznab feeds.
|
Manage Torznab feeds.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{data && data.length > 0 ?
|
|
||||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
|
||||||
<ol className="min-w-full relative">
|
|
||||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div
|
|
||||||
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="col-span-6 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type
|
|
||||||
</div>
|
|
||||||
{/*<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>*/}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{data && data.map((f) => (
|
|
||||||
<ListItem key={f.id} feed={f}/>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
{data && data.length > 0 ?
|
||||||
|
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||||
|
<ol className="min-w-full relative">
|
||||||
|
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div
|
||||||
|
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="col-span-6 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type
|
||||||
|
</div>
|
||||||
|
{/*<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>*/}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{data && data.map((f) => (
|
||||||
|
<ListItem key={f.id} feed={f}/>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
: <EmptySimple title="No feeds" subtitle="Setup via indexers" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImplementationTorznab = () => (
|
const ImplementationTorznab = () => (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||||
>
|
>
|
||||||
Torznab
|
Torznab
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
|
|
||||||
export const ImplementationMap: any = {
|
export const ImplementationMap: componentMapType = {
|
||||||
"TORZNAB": <ImplementationTorznab/>,
|
"TORZNAB": <ImplementationTorznab/>
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
feed: Feed;
|
feed: Feed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListItem({feed}: ListItemProps) {
|
function ListItem({ feed }: ListItemProps) {
|
||||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||||
|
|
||||||
const [enabled, setEnabled] = useState(feed.enabled)
|
const [enabled, setEnabled] = useState(feed.enabled);
|
||||||
|
|
||||||
const updateMutation = useMutation(
|
const updateMutation = useMutation(
|
||||||
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
(status: boolean) => APIClient.feeds.toggleEnable(feed.id, status),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.custom((t) => <Toast type="success"
|
toast.custom((t) => <Toast type="success"
|
||||||
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
body={`${feed.name} was ${enabled ? "disabled" : "enabled"} successfully`}
|
||||||
t={t}/>)
|
t={t}/>);
|
||||||
|
|
||||||
queryClient.invalidateQueries(["feeds"]);
|
queryClient.invalidateQueries(["feeds"]);
|
||||||
queryClient.invalidateQueries(["feeds", feed?.id]);
|
queryClient.invalidateQueries(["feeds", feed?.id]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleActive = (status: boolean) => {
|
|
||||||
setEnabled(status);
|
|
||||||
updateMutation.mutate(status);
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
const toggleActive = (status: boolean) => {
|
||||||
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
setEnabled(status);
|
||||||
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
|
updateMutation.mutate(status);
|
||||||
|
};
|
||||||
|
|
||||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
return (
|
||||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
<li key={feed.id} className="text-gray-500 dark:text-gray-400">
|
||||||
<Switch
|
<FeedUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} feed={feed}/>
|
||||||
checked={feed.enabled}
|
|
||||||
onChange={toggleActive}
|
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||||
className={classNames(
|
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||||
feed.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
<Switch
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
checked={feed.enabled}
|
||||||
)}
|
onChange={toggleActive}
|
||||||
>
|
className={classNames(
|
||||||
<span className="sr-only">Use setting</span>
|
feed.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||||
<span
|
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
aria-hidden="true"
|
)}
|
||||||
className={classNames(
|
>
|
||||||
feed.enabled ? 'translate-x-5' : 'translate-x-0',
|
<span className="sr-only">Use setting</span>
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
<span
|
||||||
)}
|
aria-hidden="true"
|
||||||
/>
|
className={classNames(
|
||||||
</Switch>
|
feed.enabled ? "translate-x-5" : "translate-x-0",
|
||||||
</div>
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
|
)}
|
||||||
{feed.name}
|
/>
|
||||||
</div>
|
</Switch>
|
||||||
<div className="col-span-2 flex items-center sm:px-6">
|
</div>
|
||||||
{ImplementationMap[feed.type]}
|
<div className="col-span-6 flex items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
</div>
|
{feed.name}
|
||||||
<div className="col-span-1 flex items-center sm:px-6">
|
</div>
|
||||||
<FeedItemDropdown
|
<div className="col-span-2 flex items-center sm:px-6">
|
||||||
feed={feed}
|
{ImplementationMap[feed.type]}
|
||||||
onToggle={toggleActive}
|
</div>
|
||||||
toggleUpdate={toggleUpdateForm}
|
<div className="col-span-1 flex items-center sm:px-6">
|
||||||
/>
|
<FeedItemDropdown
|
||||||
</div>
|
feed={feed}
|
||||||
</div>
|
onToggle={toggleActive}
|
||||||
</li>
|
toggleUpdate={toggleUpdateForm}
|
||||||
)
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeedItemDropdownProps {
|
interface FeedItemDropdownProps {
|
||||||
|
@ -153,126 +154,126 @@ interface FeedItemDropdownProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeedItemDropdown = ({
|
const FeedItemDropdown = ({
|
||||||
feed,
|
feed,
|
||||||
onToggle,
|
onToggle,
|
||||||
toggleUpdate,
|
toggleUpdate
|
||||||
}: FeedItemDropdownProps) => {
|
}: FeedItemDropdownProps) => {
|
||||||
const cancelModalButtonRef = useRef(null);
|
const cancelModalButtonRef = useRef(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
const deleteMutation = useMutation(
|
const deleteMutation = useMutation(
|
||||||
(id: number) => APIClient.feeds.delete(id),
|
(id: number) => APIClient.feeds.delete(id),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(["feeds"]);
|
queryClient.invalidateQueries(["feeds"]);
|
||||||
queryClient.invalidateQueries(["feeds", feed.id]);
|
queryClient.invalidateQueries(["feeds", feed.id]);
|
||||||
|
|
||||||
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
|
toast.custom((t) => <Toast type="success" body={`Feed ${feed?.name} was deleted`} t={t}/>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div">
|
<Menu as="div">
|
||||||
<DeleteModal
|
<DeleteModal
|
||||||
isOpen={deleteModalIsOpen}
|
isOpen={deleteModalIsOpen}
|
||||||
toggle={toggleDeleteModal}
|
toggle={toggleDeleteModal}
|
||||||
buttonRef={cancelModalButtonRef}
|
buttonRef={cancelModalButtonRef}
|
||||||
deleteAction={() => {
|
deleteAction={() => {
|
||||||
deleteMutation.mutate(feed.id);
|
deleteMutation.mutate(feed.id);
|
||||||
toggleDeleteModal();
|
toggleDeleteModal();
|
||||||
}}
|
}}
|
||||||
title={`Remove feed: ${feed.name}`}
|
title={`Remove feed: ${feed.name}`}
|
||||||
text="Are you sure you want to remove this feed? This action cannot be undone."
|
text="Are you sure you want to remove this feed? This action cannot be undone."
|
||||||
/>
|
/>
|
||||||
<Menu.Button className="px-4 py-2">
|
<Menu.Button className="px-4 py-2">
|
||||||
<DotsHorizontalIcon
|
<DotsHorizontalIcon
|
||||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
className="w-5 h-5 text-gray-700 hover:text-gray-900 dark:text-gray-100 dark:hover:text-gray-400"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="transition ease-out duration-100"
|
enter="transition ease-out duration-100"
|
||||||
enterFrom="transform opacity-0 scale-95"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
enterTo="transform opacity-100 scale-100"
|
enterTo="transform opacity-100 scale-100"
|
||||||
leave="transition ease-in duration-75"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
className="absolute right-0 w-56 mt-2 origin-top-right bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700 rounded-md shadow-lg ring-1 ring-black ring-opacity-10 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="px-1 py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
onClick={() => toggleUpdate()}
|
||||||
>
|
>
|
||||||
<div className="px-1 py-1">
|
<PencilAltIcon
|
||||||
<Menu.Item>
|
className={classNames(
|
||||||
{({active}) => (
|
active ? "text-white" : "text-blue-500",
|
||||||
<button
|
"w-5 h-5 mr-2"
|
||||||
className={classNames(
|
)}
|
||||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
aria-hidden="true"
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
/>
|
||||||
)}
|
|
||||||
onClick={() => toggleUpdate()}
|
|
||||||
>
|
|
||||||
<PencilAltIcon
|
|
||||||
className={classNames(
|
|
||||||
active ? "text-white" : "text-blue-500",
|
|
||||||
"w-5 h-5 mr-2"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({active}) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
active ? "bg-blue-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggle(!feed.enabled)}
|
onClick={() => onToggle(!feed.enabled)}
|
||||||
>
|
>
|
||||||
<SwitchHorizontalIcon
|
<SwitchHorizontalIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-blue-500",
|
active ? "text-white" : "text-blue-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Toggle
|
Toggle
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-1 py-1">
|
<div className="px-1 py-1">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({active}) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
active ? "bg-red-600 text-white" : "text-gray-900 dark:text-gray-300",
|
||||||
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
"font-medium group flex rounded-md items-center w-full px-2 py-2 text-sm"
|
||||||
)}
|
)}
|
||||||
onClick={() => toggleDeleteModal()}
|
onClick={() => toggleDeleteModal()}
|
||||||
>
|
>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
active ? "text-white" : "text-red-500",
|
active ? "text-white" : "text-red-500",
|
||||||
"w-5 h-5 mr-2"
|
"w-5 h-5 mr-2"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FeedSettings;
|
export default FeedSettings;
|
|
@ -5,148 +5,153 @@ import { Switch } from "@headlessui/react";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
import { EmptySimple } from "../../components/emptystates";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
|
import { componentMapType } from "../../forms/settings/DownloadClientForms";
|
||||||
|
|
||||||
const ImplementationIRC = () => (
|
const ImplementationIRC = () => (
|
||||||
<span
|
<span
|
||||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-green-200 dark:bg-green-400 text-green-800 dark:text-green-800"
|
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-green-200 dark:bg-green-400 text-green-800 dark:text-green-800"
|
||||||
>
|
>
|
||||||
IRC
|
IRC
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
|
|
||||||
const ImplementationTorznab = () => (
|
const ImplementationTorznab = () => (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
className="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-orange-200 dark:bg-orange-400 text-orange-800 dark:text-orange-800"
|
||||||
>
|
>
|
||||||
Torznab
|
Torznab
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
|
|
||||||
const implementationMap: any = {
|
const implementationMap: componentMapType = {
|
||||||
"irc": <ImplementationIRC/>,
|
"irc": <ImplementationIRC/>,
|
||||||
"torznab": <ImplementationTorznab />,
|
"torznab": <ImplementationTorznab />
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListItem = ({ indexer }: any) => {
|
interface ListItemProps {
|
||||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
indexer: IndexerDefinition;
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={indexer.name}>
|
|
||||||
<IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
|
|
||||||
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<Switch
|
|
||||||
checked={indexer.enabled}
|
|
||||||
onChange={toggleUpdate}
|
|
||||||
className={classNames(
|
|
||||||
indexer.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Enable</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className={classNames(
|
|
||||||
indexer.enabled ? 'translate-x-5' : 'translate-x-0',
|
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{indexer.name}</td>
|
|
||||||
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{implementationMap[indexer.implementation]}</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
||||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 dark:hover:text-blue-500 cursor-pointer" onClick={toggleUpdate}>
|
|
||||||
Edit
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ListItem = ({ indexer }: ListItemProps) => {
|
||||||
|
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={indexer.name}>
|
||||||
|
<IndexerUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} indexer={indexer} />
|
||||||
|
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<Switch
|
||||||
|
checked={indexer.enabled ?? false}
|
||||||
|
onChange={toggleUpdate}
|
||||||
|
className={classNames(
|
||||||
|
indexer.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||||
|
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Enable</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={classNames(
|
||||||
|
indexer.enabled ? "translate-x-5" : "translate-x-0",
|
||||||
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{indexer.name}</td>
|
||||||
|
<td className="px-6 py-4 w-full whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{implementationMap[indexer.implementation]}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 dark:hover:text-blue-500 cursor-pointer" onClick={toggleUpdate}>
|
||||||
|
Edit
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
function IndexerSettings() {
|
function IndexerSettings() {
|
||||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false);
|
||||||
|
|
||||||
const { error, data } = useQuery(
|
const { error, data } = useQuery(
|
||||||
'indexer',
|
"indexer",
|
||||||
APIClient.indexers.getAll,
|
APIClient.indexers.getAll,
|
||||||
{ refetchOnWindowFocus: false }
|
{ refetchOnWindowFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (<p>An error has occurred</p>);
|
return (<p>An error has occurred</p>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
|
|
||||||
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />
|
<IndexerAddForm isOpen={addIndexerIsOpen} toggle={toggleAddIndexer} />
|
||||||
|
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
<div className="ml-4 mt-4">
|
<div className="ml-4 mt-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Indexers</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Indexer settings.
|
Indexer settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 mt-4 flex-shrink-0">
|
<div className="ml-4 mt-4 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleAddIndexer}
|
onClick={toggleAddIndexer}
|
||||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col mt-6">
|
|
||||||
{data && data.length > 0 ?
|
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
|
||||||
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 dark:border-gray-700 sm:rounded-lg">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
<thead className="light:bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Enabled
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
|
||||||
>
|
|
||||||
Implementation
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="relative px-6 py-3">
|
|
||||||
<span className="sr-only">Edit</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{data && data.map((indexer: IndexerDefinition, idx: number) => (
|
|
||||||
<ListItem indexer={indexer} key={idx} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
<div className="flex flex-col mt-6">
|
||||||
|
{data && data.length > 0 ?
|
||||||
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
|
<div className="light:shadow overflow-hidden light:border-b light:border-gray-200 dark:border-gray-700 sm:rounded-lg">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<thead className="light:bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Enabled
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Implementation
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative px-6 py-3">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{data && data.map((indexer: IndexerDefinition, idx: number) => (
|
||||||
|
<ListItem indexer={indexer} key={idx} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
: <EmptySimple title="No indexers" subtitle="Add a new indexer" buttonText="New indexer" buttonAction={toggleAddIndexer} />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IndexerSettings;
|
export default IndexerSettings;
|
|
@ -1,73 +1,73 @@
|
||||||
import { useQuery } from "react-query";
|
import { useQuery } from "react-query";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
simplifyDate,
|
simplifyDate,
|
||||||
IsEmptyDate
|
IsEmptyDate
|
||||||
} from "../../utils";
|
} from "../../utils";
|
||||||
import {
|
import {
|
||||||
IrcNetworkAddForm,
|
IrcNetworkAddForm,
|
||||||
IrcNetworkUpdateForm
|
IrcNetworkUpdateForm
|
||||||
} from "../../forms";
|
} from "../../forms";
|
||||||
import { useToggle } from "../../hooks/hooks";
|
import { useToggle } from "../../hooks/hooks";
|
||||||
import { APIClient } from "../../api/APIClient";
|
import { APIClient } from "../../api/APIClient";
|
||||||
import { EmptySimple } from "../../components/emptystates";
|
import { EmptySimple } from "../../components/emptystates";
|
||||||
|
|
||||||
export const IrcSettings = () => {
|
export const IrcSettings = () => {
|
||||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false);
|
||||||
|
|
||||||
const { data } = useQuery(
|
const { data } = useQuery(
|
||||||
"networks",
|
"networks",
|
||||||
APIClient.irc.getNetworks,
|
APIClient.irc.getNetworks,
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
// Refetch every 3 seconds
|
// Refetch every 3 seconds
|
||||||
refetchInterval: 3000
|
refetchInterval: 3000
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
<IrcNetworkAddForm isOpen={addNetworkIsOpen} toggle={toggleAddNetwork} />
|
||||||
|
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
<div className="ml-4 mt-4">
|
<div className="ml-4 mt-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">IRC</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
IRC networks and channels. Click on a network to view channel status.
|
IRC networks and channels. Click on a network to view channel status.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 mt-4 flex-shrink-0">
|
<div className="ml-4 mt-4 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleAddNetwork}
|
onClick={toggleAddNetwork}
|
||||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{data && data.length > 0 ? (
|
|
||||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
|
||||||
<ol className="min-w-full">
|
|
||||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */}
|
|
||||||
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div>
|
|
||||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div>
|
|
||||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{data && data.map((network, idx) => (
|
|
||||||
<ListItem key={idx} idx={idx} network={network} />
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
}
|
{data && data.length > 0 ? (
|
||||||
|
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||||
|
<ol className="min-w-full">
|
||||||
|
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
{/* <div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div> */}
|
||||||
|
<div className="col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Network</div>
|
||||||
|
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Server</div>
|
||||||
|
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nick</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{data && data.map((network, idx) => (
|
||||||
|
<ListItem key={idx} idx={idx} network={network} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
) : <EmptySimple title="No networks" subtitle="Add a new network" buttonText="New network" buttonAction={toggleAddNetwork} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
idx: number;
|
idx: number;
|
||||||
|
@ -75,83 +75,83 @@ interface ListItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem = ({ idx, network }: ListItemProps) => {
|
const ListItem = ({ idx, network }: ListItemProps) => {
|
||||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
const [updateIsOpen, toggleUpdate] = useToggle(false);
|
||||||
const [edit, toggleEdit] = useToggle(false);
|
const [edit, toggleEdit] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<li key={idx} >
|
<li key={idx} >
|
||||||
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
|
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
|
||||||
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
|
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
|
||||||
|
|
||||||
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}>
|
<div className="col-span-3 items-center sm:px-6 text-sm font-medium text-gray-900 dark:text-white cursor-pointer" onClick={toggleEdit}>
|
||||||
<span className="relative inline-flex items-center">
|
<span className="relative inline-flex items-center">
|
||||||
{
|
{
|
||||||
network.enabled ? (
|
network.enabled ? (
|
||||||
network.connected ? (
|
network.connected ? (
|
||||||
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}>
|
<span className="mr-3 flex h-3 w-3 relative" title={`Connected since: ${simplifyDate(network.connected_since)}`}>
|
||||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||||
</span>
|
</span>
|
||||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||||
}
|
}
|
||||||
{network.name}
|
{network.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
|
<div className="col-span-4 flex justify-between items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.server}:{network.port} {network.tls && <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-300 text-green-800 dark:text-green-900">TLS</span>}</div>
|
||||||
{network.nickserv && network.nickserv.account ? (
|
{network.nickserv && network.nickserv.account ? (
|
||||||
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div>
|
<div className="col-span-4 items-center sm:px-6 text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onClick={toggleEdit}>{network.nickserv.account}</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
|
<div className="col-span-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
|
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdate}>
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{edit && (
|
{edit && (
|
||||||
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
<div className="px-4 py-4 flex border-b border-x-0 dark:border-gray-600 dark:bg-gray-700">
|
||||||
<div className="min-w-full">
|
<div className="min-w-full">
|
||||||
{network.channels.length > 0 ? (
|
{network.channels.length > 0 ? (
|
||||||
<ol>
|
<ol>
|
||||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div>
|
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Channel</div>
|
||||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div>
|
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Monitoring since</div>
|
||||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div>
|
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last announce</div>
|
||||||
</li>
|
</li>
|
||||||
{network.channels.map(c => (
|
{network.channels.map(c => (
|
||||||
<li key={c.id} className="text-gray-500 dark:text-gray-400">
|
<li key={c.id} className="text-gray-500 dark:text-gray-400">
|
||||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||||
<span className="relative inline-flex items-center">
|
<span className="relative inline-flex items-center">
|
||||||
{
|
{
|
||||||
network.enabled ? (
|
network.enabled ? (
|
||||||
c.monitoring ? (
|
c.monitoring ? (
|
||||||
<span className="mr-3 flex h-3 w-3 relative" title="monitoring">
|
<span className="mr-3 flex h-3 w-3 relative" title="monitoring">
|
||||||
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
<span className="animate-ping inline-flex h-full w-full rounded-full bg-green-400 opacity-75"/>
|
||||||
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
<span className="inline-flex absolute rounded-full h-3 w-3 bg-green-500"/>
|
||||||
</span>
|
</span>
|
||||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-red-400" />
|
||||||
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
) : <span className="mr-3 flex h-3 w-3 rounded-full opacity-75 bg-gray-500" />
|
||||||
}
|
}
|
||||||
{c.name}
|
{c.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||||
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
|
<span className="" title={simplifyDate(c.monitoring_since)}>{IsEmptyDate(c.monitoring_since)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-4 flex items-center sm:px-6 ">
|
<div className="col-span-4 flex items-center sm:px-6 ">
|
||||||
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
|
<span className="" title={simplifyDate(c.last_announce)}>{IsEmptyDate(c.last_announce)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
)}
|
))}
|
||||||
</li>
|
</ol>
|
||||||
)
|
) : <div className="flex text-center justify-center py-4 dark:text-gray-500"><p>No channels!</p></div>}
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -7,56 +7,56 @@ import { Switch } from "@headlessui/react";
|
||||||
import { classNames } from "../../utils";
|
import { classNames } from "../../utils";
|
||||||
|
|
||||||
function NotificationSettings() {
|
function NotificationSettings() {
|
||||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false)
|
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false);
|
||||||
|
|
||||||
const { data } = useQuery<Notification[], Error>('notifications', APIClient.notifications.getAll,
|
const { data } = useQuery<Notification[], Error>("notifications", APIClient.notifications.getAll,
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||||
|
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||||
<div className="ml-4 mt-4">
|
<div className="ml-4 mt-4">
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Send notifications on events.
|
Send notifications on events.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 mt-4 flex-shrink-0">
|
<div className="ml-4 mt-4 flex-shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleAddNotifications}
|
onClick={toggleAddNotifications}
|
||||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
Add new
|
Add new
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{data && data.length > 0 ?
|
|
||||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
|
||||||
<ol className="min-w-full">
|
|
||||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
|
||||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
|
||||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
|
||||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{data && data.map((n: Notification) => (
|
|
||||||
<ListItem key={n.id} notification={n} />
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
{data && data.length > 0 ?
|
||||||
|
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||||
|
<ol className="min-w-full">
|
||||||
|
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||||
|
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||||
|
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||||
|
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{data && data.map((n: Notification) => (
|
||||||
|
<ListItem key={n.id} notification={n} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListItemProps {
|
interface ListItemProps {
|
||||||
|
@ -64,56 +64,56 @@ interface ListItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListItem({ notification }: ListItemProps) {
|
function ListItem({ notification }: ListItemProps) {
|
||||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||||
|
|
||||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||||
<Switch
|
<Switch
|
||||||
checked={notification.enabled}
|
checked={notification.enabled}
|
||||||
onChange={toggleUpdateForm}
|
onChange={toggleUpdateForm}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
notification.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
notification.enabled ? "bg-teal-500 dark:bg-blue-500" : "bg-gray-200 dark:bg-gray-600",
|
||||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
"relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Use setting</span>
|
<span className="sr-only">Use setting</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
notification.enabled ? 'translate-x-5' : 'translate-x-0',
|
notification.enabled ? "translate-x-5" : "translate-x-0",
|
||||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
"inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||||
{notification.name}
|
{notification.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||||
{notification.type}
|
{notification.type}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||||
{notification.events.map((n, idx) => (
|
{notification.events.map((n, idx) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{n}
|
{n}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
|
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
|
||||||
Edit
|
Edit
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotificationSettings;
|
export default NotificationSettings;
|
|
@ -1,106 +1,105 @@
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
|
||||||
export const RegexPlayground = () => {
|
export const RegexPlayground = () => {
|
||||||
const regexRef = useRef<HTMLInputElement>(null);
|
const regexRef = useRef<HTMLInputElement>(null);
|
||||||
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
const [output, setOutput] = useState<Array<React.ReactElement>>();
|
||||||
|
|
||||||
const onInput = (text: string) => {
|
const onInput = (text: string) => {
|
||||||
if (!regexRef || !regexRef.current)
|
if (!regexRef || !regexRef.current)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const regexp = new RegExp(regexRef.current.value, "g");
|
const regexp = new RegExp(regexRef.current.value, "g");
|
||||||
|
|
||||||
const results: Array<React.ReactElement> = [];
|
const results: Array<React.ReactElement> = [];
|
||||||
text.split("\n").forEach((line, index) => {
|
text.split("\n").forEach((line, index) => {
|
||||||
const matches = line.matchAll(regexp);
|
const matches = line.matchAll(regexp);
|
||||||
|
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
// @ts-ignore
|
for (const match of matches) {
|
||||||
for (const match of matches) {
|
if (match.index === undefined)
|
||||||
if (match.index === undefined)
|
continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!match.length)
|
if (!match.length)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const start = match.index;
|
const start = match.index;
|
||||||
|
|
||||||
let length = 0;
|
let length = 0;
|
||||||
match.forEach((group) => length += group.length);
|
match.forEach((group) => length += group.length);
|
||||||
|
|
||||||
results.push(
|
results.push(
|
||||||
<span key={`match-${start}`}>
|
<span key={`match-${start}`}>
|
||||||
{line.substring(lastIndex, start)}
|
{line.substring(lastIndex, start)}
|
||||||
<span className="bg-blue-300 text-black font-bold">
|
<span className="bg-blue-300 text-black font-bold">
|
||||||
{line.substring(start, start + length)}
|
{line.substring(start, start + length)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
lastIndex = start + length;
|
lastIndex = start + length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < line.length) {
|
if (lastIndex < line.length) {
|
||||||
results.push(
|
results.push(
|
||||||
<span key={`last-${lastIndex + 1}`}>
|
<span key={`last-${lastIndex + 1}`}>
|
||||||
{line.substring(lastIndex)}
|
{line.substring(lastIndex)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex > 0)
|
if (lastIndex > 0)
|
||||||
results.push(<br key={`line-delim-${index}`}/>);
|
results.push(<br key={`line-delim-${index}`}/>);
|
||||||
});
|
});
|
||||||
|
|
||||||
setOutput(results);
|
setOutput(results);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
<div className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9">
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Application</h2>
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Regex playground. Experiment with your filters here. WIP.
|
Regex playground. Experiment with your filters here. WIP.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
<label
|
|
||||||
htmlFor="input-regex"
|
|
||||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
RegExp filter
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={regexRef}
|
|
||||||
id="input-regex"
|
|
||||||
type="text"
|
|
||||||
autoComplete="true"
|
|
||||||
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="input-lines"
|
|
||||||
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
|
||||||
>
|
|
||||||
Lines to match
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
id="input-lines"
|
|
||||||
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
|
||||||
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
|
|
||||||
contentEditable
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div className="py-4 px-4 sm:p-6 lg:pb-8">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
|
|
||||||
Matches
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
|
|
||||||
{output}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
<div className="px-6 py-4">
|
||||||
|
<label
|
||||||
|
htmlFor="input-regex"
|
||||||
|
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
RegExp filter
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
ref={regexRef}
|
||||||
|
id="input-regex"
|
||||||
|
type="text"
|
||||||
|
autoComplete="true"
|
||||||
|
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="input-lines"
|
||||||
|
className="block text-sm font-medium text-gray-600 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
Lines to match
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
id="input-lines"
|
||||||
|
className="mt-1 mb-4 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||||
|
onInput={(e) => onInput(e.currentTarget.innerText ?? "")}
|
||||||
|
contentEditable
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="py-4 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md leading-6 font-medium text-gray-900 dark:text-white">
|
||||||
|
Matches
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-lg text-gray-500 dark:text-gray-400">
|
||||||
|
{output}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -9,73 +9,73 @@ import { useToggle } from "../../hooks/hooks";
|
||||||
import { DeleteModal } from "../../components/modals";
|
import { DeleteModal } from "../../components/modals";
|
||||||
|
|
||||||
function ReleaseSettings() {
|
function ReleaseSettings() {
|
||||||
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false);
|
||||||
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
const deleteMutation = useMutation(() => APIClient.release.delete(), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.custom((t) => (
|
toast.custom((t) => (
|
||||||
<Toast type="success" body={`All releases was deleted`} t={t}/>
|
<Toast type="success" body={"All releases was deleted"} t={t}/>
|
||||||
));
|
));
|
||||||
|
|
||||||
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
// Invalidate filters just in case, most likely not necessary but can't hurt.
|
||||||
queryClient.invalidateQueries("releases");
|
queryClient.invalidateQueries("releases");
|
||||||
|
|
||||||
toggleDeleteModal()
|
toggleDeleteModal();
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteAction = () => {
|
|
||||||
deleteMutation.mutate()
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const cancelModalButtonRef = useRef(null);
|
const deleteAction = () => {
|
||||||
|
deleteMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const cancelModalButtonRef = useRef(null);
|
||||||
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
|
||||||
<DeleteModal
|
|
||||||
isOpen={deleteModalIsOpen}
|
|
||||||
toggle={toggleDeleteModal}
|
|
||||||
buttonRef={cancelModalButtonRef}
|
|
||||||
deleteAction={deleteAction}
|
|
||||||
title={`Delete all releases`}
|
|
||||||
text="Are you sure you want to delete all releases? This action cannot be undone."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
return (
|
||||||
<div>
|
<form className="divide-y divide-gray-200 dark:divide-gray-700 lg:col-span-9" action="#" method="POST">
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2>
|
<DeleteModal
|
||||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
isOpen={deleteModalIsOpen}
|
||||||
|
toggle={toggleDeleteModal}
|
||||||
|
buttonRef={cancelModalButtonRef}
|
||||||
|
deleteAction={deleteAction}
|
||||||
|
title={"Delete all releases"}
|
||||||
|
text="Are you sure you want to delete all releases? This action cannot be undone."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Releases</h2>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
Release settings. Reset state.
|
Release settings. Reset state.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
<div className="px-4 py-5 sm:p-0">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pb-6 divide-y divide-gray-200 dark:divide-gray-700">
|
<ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg">
|
||||||
<div className="px-4 py-5 sm:p-0">
|
<div className="flex justify-between items-center py-2">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Danger Zone</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="p-4 mt-6 divide-y divide-gray-200 dark:divide-gray-700 border-red-500 border rounded-lg">
|
|
||||||
<div className="flex justify-between items-center py-2">
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
Delete all releases
|
Delete all releases
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleDeleteModal}
|
onClick={toggleDeleteModal}
|
||||||
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 dark:text-red-100 bg-red-100 dark:bg-red-500 hover:bg-red-200 dark:hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:text-sm"
|
||||||
>
|
>
|
||||||
Delete all releases
|
Delete all releases
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReleaseSettings;
|
export default ReleaseSettings;
|
|
@ -1,11 +1,11 @@
|
||||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
const { createProxyMiddleware } = require("http-proxy-middleware");
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
app.use(
|
app.use(
|
||||||
'/api',
|
"/api",
|
||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
target: 'http://127.0.0.1:7474',
|
target: "http://127.0.0.1:7474",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -2,4 +2,4 @@
|
||||||
// allows you to do things like:
|
// allows you to do things like:
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
import '@testing-library/jest-dom';
|
import "@testing-library/jest-dom";
|
||||||
|
|
25
web/src/types/Download.d.ts
vendored
25
web/src/types/Download.d.ts
vendored
|
@ -1,11 +1,21 @@
|
||||||
type DownloadClientType =
|
type DownloadClientType =
|
||||||
'QBITTORRENT' |
|
"QBITTORRENT" |
|
||||||
'DELUGE_V1' |
|
"DELUGE_V1" |
|
||||||
'DELUGE_V2' |
|
"DELUGE_V2" |
|
||||||
'RADARR' |
|
"RADARR" |
|
||||||
'SONARR' |
|
"SONARR" |
|
||||||
'LIDARR' |
|
"LIDARR" |
|
||||||
'WHISPARR';
|
"WHISPARR";
|
||||||
|
|
||||||
|
// export enum DownloadClientTypeEnum {
|
||||||
|
// QBITTORRENT = "QBITTORRENT",
|
||||||
|
// DELUGE_V1 = "DELUGE_V1",
|
||||||
|
// DELUGE_V2 = "DELUGE_V2",
|
||||||
|
// RADARR = "RADARR",
|
||||||
|
// SONARR = "SONARR",
|
||||||
|
// LIDARR = "LIDARR",
|
||||||
|
// WHISPARR = "WHISPARR"
|
||||||
|
// }
|
||||||
|
|
||||||
interface DownloadClientRules {
|
interface DownloadClientRules {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
@ -27,7 +37,6 @@ interface DownloadClientSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadClient {
|
interface DownloadClient {
|
||||||
id?: number;
|
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
type: DownloadClientType;
|
type: DownloadClientType;
|
||||||
|
|
36
web/src/types/Feed.d.ts
vendored
36
web/src/types/Feed.d.ts
vendored
|
@ -1,23 +1,23 @@
|
||||||
interface Feed {
|
interface Feed {
|
||||||
id: number;
|
id: number;
|
||||||
indexer: string;
|
indexer: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
interval: number;
|
interval: number;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeedCreate {
|
interface FeedCreate {
|
||||||
indexer: string;
|
indexer: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
interval: number;
|
interval: number;
|
||||||
api_key: string;
|
api_key: string;
|
||||||
indexer_id: number;
|
indexer_id: number;
|
||||||
}
|
}
|
||||||
|
|
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;
|
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 {
|
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 {
|
interface IndexerDefinition {
|
||||||
id?: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
implementation: 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 {
|
interface IrcNetwork {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
server: string;
|
server: string;
|
||||||
port: number;
|
port: number;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
pass: string;
|
pass: string;
|
||||||
invite_command: string;
|
invite_command: string;
|
||||||
nickserv?: NickServ; // optional
|
nickserv?: NickServ; // optional
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
connected_since: Time;
|
connected_since: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IrcNetworkCreate {
|
interface IrcNetworkCreate {
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
server: string;
|
server: string;
|
||||||
port: number;
|
port: number;
|
||||||
tls: boolean;
|
tls: boolean;
|
||||||
pass: string;
|
pass: string;
|
||||||
invite_command: string;
|
invite_command: string;
|
||||||
nickserv?: NickServ; // optional
|
nickserv?: NickServ; // optional
|
||||||
channels: IrcChannel[];
|
channels: IrcChannel[];
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IrcChannel {
|
interface IrcChannel {
|
||||||
|
@ -61,12 +61,12 @@ interface NickServ {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
log_level: string;
|
log_level: string;
|
||||||
log_path: string;
|
log_path: string;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
version: string;
|
version: string;
|
||||||
commit: string;
|
commit: string;
|
||||||
date: string;
|
date: string;
|
||||||
}
|
}
|
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 {
|
interface Notification {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
type: NotificationType;
|
type: NotificationType;
|
||||||
events: string[];
|
events: string[];
|
||||||
webhook: string;
|
webhook: string;
|
||||||
}
|
}
|
50
web/src/types/Release.d.ts
vendored
50
web/src/types/Release.d.ts
vendored
|
@ -1,38 +1,38 @@
|
||||||
interface Release {
|
interface Release {
|
||||||
id: number;
|
id: number;
|
||||||
filter_status: string;
|
filter_status: string;
|
||||||
rejections: string[];
|
rejections: string[];
|
||||||
indexer: string;
|
indexer: string;
|
||||||
filter: string;
|
filter: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
title: string;
|
title: string;
|
||||||
size: number;
|
size: number;
|
||||||
raw: string;
|
raw: string;
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
action_status: ReleaseActionStatus[]
|
action_status: ReleaseActionStatus[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReleaseActionStatus {
|
interface ReleaseActionStatus {
|
||||||
id: number;
|
id: number;
|
||||||
status: string;
|
status: string;
|
||||||
action: string;
|
action: string;
|
||||||
type: string;
|
type: string;
|
||||||
rejections: string[];
|
rejections: string[];
|
||||||
timestamp: string
|
timestamp: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReleaseFindResponse {
|
interface ReleaseFindResponse {
|
||||||
data: Release[];
|
data: Release[];
|
||||||
next_cursor: number;
|
next_cursor: number;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReleaseStats {
|
interface ReleaseStats {
|
||||||
total_count: number;
|
total_count: number;
|
||||||
filtered_count: number;
|
filtered_count: number;
|
||||||
filter_rejected_count: number;
|
filter_rejected_count: number;
|
||||||
push_approved_count: number;
|
push_approved_count: number;
|
||||||
push_rejected_count: number;
|
push_rejected_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReleaseFilter {
|
interface ReleaseFilter {
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const InitializeGlobalContext = () => {
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
interface AuthInfo {
|
interface AuthInfo {
|
||||||
username: string;
|
username: string;
|
||||||
|
|
|
@ -2,37 +2,37 @@ import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
|
||||||
|
|
||||||
// sleep for x ms
|
// sleep for x ms
|
||||||
export function sleep(ms: number) {
|
export function sleep(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
// get baseUrl sent from server rendered index template
|
// get baseUrl sent from server rendered index template
|
||||||
export function baseUrl() {
|
export function baseUrl() {
|
||||||
let baseUrl = ""
|
let baseUrl = "";
|
||||||
if (window.APP.baseUrl) {
|
if (window.APP.baseUrl) {
|
||||||
if (window.APP.baseUrl === "/") {
|
if (window.APP.baseUrl === "/") {
|
||||||
baseUrl = "/"
|
baseUrl = "/";
|
||||||
} else if (window.APP.baseUrl === "{{.BaseUrl}}") {
|
} else if (window.APP.baseUrl === "{{.BaseUrl}}") {
|
||||||
baseUrl = "/"
|
baseUrl = "/";
|
||||||
} else if (window.APP.baseUrl === "/autobrr/") {
|
} else if (window.APP.baseUrl === "/autobrr/") {
|
||||||
baseUrl = "/autobrr/"
|
baseUrl = "/autobrr/";
|
||||||
} else {
|
} else {
|
||||||
baseUrl = window.APP.baseUrl
|
baseUrl = window.APP.baseUrl;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return baseUrl
|
return baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get sseBaseUrl for SSE
|
// get sseBaseUrl for SSE
|
||||||
export function sseBaseUrl() {
|
export function sseBaseUrl() {
|
||||||
if (process.env.NODE_ENV === "development")
|
if (process.env.NODE_ENV === "development")
|
||||||
return `http://localhost:7474/`;
|
return "http://localhost:7474/";
|
||||||
|
|
||||||
return `${window.location.origin}${baseUrl()}`;
|
return `${window.location.origin}${baseUrl()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function classNames(...classes: string[]) {
|
export function classNames(...classes: string[]) {
|
||||||
return classes.filter(Boolean).join(' ')
|
return classes.filter(Boolean).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// column widths for inputs etc
|
// column widths for inputs etc
|
||||||
|
@ -40,28 +40,28 @@ export type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||||
|
|
||||||
// simplify date
|
// simplify date
|
||||||
export function simplifyDate(date: string) {
|
export function simplifyDate(date: string) {
|
||||||
if (date !== "0001-01-01T00:00:00Z") {
|
if (date !== "0001-01-01T00:00:00Z") {
|
||||||
return formatISO9075(new Date(date))
|
return formatISO9075(new Date(date));
|
||||||
}
|
}
|
||||||
return "n/a"
|
return "n/a";
|
||||||
}
|
}
|
||||||
|
|
||||||
// if empty date show as n/a
|
// if empty date show as n/a
|
||||||
export function IsEmptyDate(date: string) {
|
export function IsEmptyDate(date: string) {
|
||||||
if (date !== "0001-01-01T00:00:00Z") {
|
if (date !== "0001-01-01T00:00:00Z") {
|
||||||
return formatDistanceToNowStrict(
|
return formatDistanceToNowStrict(
|
||||||
new Date(date),
|
new Date(date),
|
||||||
{ addSuffix: true }
|
{ addSuffix: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
return "n/a"
|
return "n/a";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slugify(str: string) {
|
export function slugify(str: string) {
|
||||||
return str
|
return str
|
||||||
.normalize('NFKD')
|
.normalize("NFKD")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^\w\s-]/g, '')
|
.replace(/[^\w\s-]/g, "")
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/[-\s]+/g, '-');
|
.replace(/[-\s]+/g, "-");
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
],
|
],
|
||||||
"types": [],
|
"types": [],
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": false,
|
||||||
"esModuleInterop": false,
|
"esModuleInterop": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
|
@ -2088,7 +2088,22 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^5.10.2", "@typescript-eslint/eslint-plugin@^5.5.0":
|
"@typescript-eslint/eslint-plugin@^5.18.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz#bc4cbcf91fbbcc2e47e534774781b82ae25cc3d8"
|
||||||
|
integrity sha512-hEcSmG4XodSLiAp1uxv/OQSGsDY6QN3TcRU32gANp+19wGE1QQZLRS8/GV58VRUoXhnkuJ3ZxNQ3T6Z6zM59DA==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/scope-manager" "5.23.0"
|
||||||
|
"@typescript-eslint/type-utils" "5.23.0"
|
||||||
|
"@typescript-eslint/utils" "5.23.0"
|
||||||
|
debug "^4.3.2"
|
||||||
|
functional-red-black-tree "^1.0.1"
|
||||||
|
ignore "^5.1.8"
|
||||||
|
regexpp "^3.2.0"
|
||||||
|
semver "^7.3.5"
|
||||||
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/eslint-plugin@^5.5.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.18.0.tgz#950df411cec65f90d75d6320a03b2c98f6c3af7d"
|
||||||
integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==
|
integrity sha512-tzrmdGMJI/uii9/V6lurMo4/o+dMTKDH82LkNjhJ3adCW22YQydoRs5MwTiqxGF9CSYxPxQ7EYb4jLNlIs+E+A==
|
||||||
|
@ -2110,7 +2125,17 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@typescript-eslint/utils" "5.18.0"
|
"@typescript-eslint/utils" "5.18.0"
|
||||||
|
|
||||||
"@typescript-eslint/parser@^5.10.2", "@typescript-eslint/parser@^5.5.0":
|
"@typescript-eslint/parser@^5.18.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.23.0.tgz#443778e1afc9a8ff180f91b5e260ac3bec5e2de1"
|
||||||
|
integrity sha512-V06cYUkqcGqpFjb8ttVgzNF53tgbB/KoQT/iB++DOIExKmzI9vBJKjZKt/6FuV9c+zrDsvJKbJ2DOCYwX91cbw==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/scope-manager" "5.23.0"
|
||||||
|
"@typescript-eslint/types" "5.23.0"
|
||||||
|
"@typescript-eslint/typescript-estree" "5.23.0"
|
||||||
|
debug "^4.3.2"
|
||||||
|
|
||||||
|
"@typescript-eslint/parser@^5.5.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.18.0.tgz#2bcd4ff21df33621df33e942ccb21cb897f004c6"
|
||||||
integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ==
|
integrity sha512-+08nYfurBzSSPndngnHvFw/fniWYJ5ymOrn/63oMIbgomVQOvIDhBoJmYZ9lwQOCnQV9xHGvf88ze3jFGUYooQ==
|
||||||
|
@ -2128,6 +2153,14 @@
|
||||||
"@typescript-eslint/types" "5.18.0"
|
"@typescript-eslint/types" "5.18.0"
|
||||||
"@typescript-eslint/visitor-keys" "5.18.0"
|
"@typescript-eslint/visitor-keys" "5.18.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager@5.23.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.23.0.tgz#4305e61c2c8e3cfa3787d30f54e79430cc17ce1b"
|
||||||
|
integrity sha512-EhjaFELQHCRb5wTwlGsNMvzK9b8Oco4aYNleeDlNuL6qXWDF47ch4EhVNPh8Rdhf9tmqbN4sWDk/8g+Z/J8JVw==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types" "5.23.0"
|
||||||
|
"@typescript-eslint/visitor-keys" "5.23.0"
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@5.18.0":
|
"@typescript-eslint/type-utils@5.18.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.18.0.tgz#62dbfc8478abf36ba94a90ddf10be3cc8e471c74"
|
||||||
|
@ -2137,11 +2170,25 @@
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
tsutils "^3.21.0"
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/type-utils@5.23.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.23.0.tgz#f852252f2fc27620d5bb279d8fed2a13d2e3685e"
|
||||||
|
integrity sha512-iuI05JsJl/SUnOTXA9f4oI+/4qS/Zcgk+s2ir+lRmXI+80D8GaGwoUqs4p+X+4AxDolPpEpVUdlEH4ADxFy4gw==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/utils" "5.23.0"
|
||||||
|
debug "^4.3.2"
|
||||||
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
"@typescript-eslint/types@5.18.0":
|
"@typescript-eslint/types@5.18.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.18.0.tgz#4f0425d85fdb863071680983853c59a62ce9566e"
|
||||||
integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==
|
integrity sha512-bhV1+XjM+9bHMTmXi46p1Led5NP6iqQcsOxgx7fvk6gGiV48c6IynY0apQb7693twJDsXiVzNXTflhplmaiJaw==
|
||||||
|
|
||||||
|
"@typescript-eslint/types@5.23.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.23.0.tgz#8733de0f58ae0ed318dbdd8f09868cdbf9f9ad09"
|
||||||
|
integrity sha512-NfBsV/h4dir/8mJwdZz7JFibaKC3E/QdeMEDJhiAE3/eMkoniZ7MjbEMCGXw6MZnZDMN3G9S0mH/6WUIj91dmw==
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@5.18.0":
|
"@typescript-eslint/typescript-estree@5.18.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.18.0.tgz#6498e5ee69a32e82b6e18689e2f72e4060986474"
|
||||||
|
@ -2155,6 +2202,19 @@
|
||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
tsutils "^3.21.0"
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree@5.23.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.23.0.tgz#dca5f10a0a85226db0796e8ad86addc9aee52065"
|
||||||
|
integrity sha512-xE9e0lrHhI647SlGMl+m+3E3CKPF1wzvvOEWnuE3CCjjT7UiRnDGJxmAcVKJIlFgK6DY9RB98eLr1OPigPEOGg==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types" "5.23.0"
|
||||||
|
"@typescript-eslint/visitor-keys" "5.23.0"
|
||||||
|
debug "^4.3.2"
|
||||||
|
globby "^11.0.4"
|
||||||
|
is-glob "^4.0.3"
|
||||||
|
semver "^7.3.5"
|
||||||
|
tsutils "^3.21.0"
|
||||||
|
|
||||||
"@typescript-eslint/utils@5.18.0", "@typescript-eslint/utils@^5.13.0":
|
"@typescript-eslint/utils@5.18.0", "@typescript-eslint/utils@^5.13.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.18.0.tgz#27fc84cf95c1a96def0aae31684cb43a37e76855"
|
||||||
|
@ -2167,6 +2227,18 @@
|
||||||
eslint-scope "^5.1.1"
|
eslint-scope "^5.1.1"
|
||||||
eslint-utils "^3.0.0"
|
eslint-utils "^3.0.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/utils@5.23.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.23.0.tgz#4691c3d1b414da2c53d8943310df36ab1c50648a"
|
||||||
|
integrity sha512-dbgaKN21drqpkbbedGMNPCtRPZo1IOUr5EI9Jrrh99r5UW5Q0dz46RKXeSBoPV+56R6dFKpbrdhgUNSJsDDRZA==
|
||||||
|
dependencies:
|
||||||
|
"@types/json-schema" "^7.0.9"
|
||||||
|
"@typescript-eslint/scope-manager" "5.23.0"
|
||||||
|
"@typescript-eslint/types" "5.23.0"
|
||||||
|
"@typescript-eslint/typescript-estree" "5.23.0"
|
||||||
|
eslint-scope "^5.1.1"
|
||||||
|
eslint-utils "^3.0.0"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@5.18.0":
|
"@typescript-eslint/visitor-keys@5.18.0":
|
||||||
version "5.18.0"
|
version "5.18.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.18.0.tgz#c7c07709823804171d569017f3b031ced7253e60"
|
||||||
|
@ -2175,6 +2247,14 @@
|
||||||
"@typescript-eslint/types" "5.18.0"
|
"@typescript-eslint/types" "5.18.0"
|
||||||
eslint-visitor-keys "^3.0.0"
|
eslint-visitor-keys "^3.0.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys@5.23.0":
|
||||||
|
version "5.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.23.0.tgz#057c60a7ca64667a39f991473059377a8067c87b"
|
||||||
|
integrity sha512-Vd4mFNchU62sJB8pX19ZSPog05B0Y0CE2UxAZPT5k4iqhRYjPnqyY3woMxCd0++t9OTqkgjST+1ydLBi7e2Fvg==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types" "5.23.0"
|
||||||
|
eslint-visitor-keys "^3.0.0"
|
||||||
|
|
||||||
"@webassemblyjs/ast@1.11.1":
|
"@webassemblyjs/ast@1.11.1":
|
||||||
version "1.11.1"
|
version "1.11.1"
|
||||||
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue