mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
refactor: web api client and cleanup (#128)
refactor: refactored APIClient.ts with a new fetch wrapper and changed it into an explicit-import. chore: modified package.json not to start browser on "npm run start" chore: cleaned up code, deleted 2mo+ useless old comments. fix: fixed parameter collision in screens/filters/details.tsx fix: override react-select's Select component style to make it consistent. addresses #116 Co-authored-by: anonymous <anonymous>
This commit is contained in:
parent
6d68a5c3b7
commit
b60e5f61c6
17 changed files with 381 additions and 793 deletions
|
@ -23,7 +23,7 @@
|
|||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "BROWSER=none react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
|
|
|
@ -1,72 +1,55 @@
|
|||
import {baseUrl, sseBaseUrl} from "../utils";
|
||||
|
||||
function baseClient(endpoint: string, method: string, { body, ...customConfig}: any = {}) {
|
||||
const baseURL = baseUrl()
|
||||
interface ConfigType {
|
||||
body?: BodyInit | Record<string, unknown> | null;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
const headers = {'content-type': 'application/json'}
|
||||
export async function HttpClient<T>(
|
||||
endpoint: string,
|
||||
method: string,
|
||||
{ body, ...customConfig }: ConfigType = {}
|
||||
): Promise<T> {
|
||||
const config = {
|
||||
method: method,
|
||||
...customConfig,
|
||||
body: body ? JSON.stringify(body) : null,
|
||||
headers: {
|
||||
...headers,
|
||||
...customConfig.headers,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
}
|
||||
// NOTE: customConfig can override the above defined settings
|
||||
...customConfig
|
||||
} as RequestInit;
|
||||
|
||||
if (body) {
|
||||
config.body = JSON.stringify(body)
|
||||
}
|
||||
|
||||
return window.fetch(`${baseURL}${endpoint}`, config)
|
||||
return window.fetch(`${baseUrl()}${endpoint}`, config)
|
||||
.then(async response => {
|
||||
if (response.status === 401) {
|
||||
// unauthorized
|
||||
// window.location.assign(window.location)
|
||||
if ([401, 403, 404].includes(response.status))
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
|
||||
return Promise.reject(new Error(response.statusText))
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
// window.location.assign("/login")
|
||||
return Promise.reject(new Error(response.statusText))
|
||||
// return
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
return Promise.reject(new Error(response.statusText))
|
||||
}
|
||||
|
||||
if (response.status === 201) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return ""
|
||||
}
|
||||
if ([201, 204].includes(response.status))
|
||||
return Promise.resolve(response);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json()
|
||||
return await response.json();
|
||||
} else {
|
||||
const errorMessage = await response.text()
|
||||
|
||||
return Promise.reject(new Error(errorMessage))
|
||||
const errorMessage = await response.text();
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
const appClient = {
|
||||
Get: (endpoint: string) => baseClient(endpoint, "GET"),
|
||||
Post: (endpoint: string, data: any) => baseClient(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: any) => baseClient(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: any) => baseClient(endpoint, "PATCH", { body: data }),
|
||||
Delete: (endpoint: string) => baseClient(endpoint, "DELETE"),
|
||||
Get: <T>(endpoint: string) => HttpClient<T>(endpoint, "GET"),
|
||||
Post: (endpoint: string, data: any) => HttpClient<void>(endpoint, "POST", { body: data }),
|
||||
Put: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PUT", { body: data }),
|
||||
Patch: (endpoint: string, data: any) => HttpClient<void>(endpoint, "PATCH", { body: data }),
|
||||
Delete: (endpoint: string) => HttpClient<void>(endpoint, "DELETE")
|
||||
}
|
||||
|
||||
const APIClient = {
|
||||
export const APIClient = {
|
||||
auth: {
|
||||
login: (username: string, password: string) => appClient.Post("api/auth/login", { username: username, password: password }),
|
||||
logout: () => appClient.Post(`api/auth/logout`, null),
|
||||
test: () => appClient.Get(`api/auth/test`),
|
||||
logout: () => appClient.Post("api/auth/logout", null),
|
||||
test: () => appClient.Get<void>("api/auth/test"),
|
||||
},
|
||||
actions: {
|
||||
create: (action: Action) => appClient.Post("api/actions", action),
|
||||
|
@ -75,34 +58,37 @@ const APIClient = {
|
|||
toggleEnable: (id: number) => appClient.Patch(`api/actions/${id}/toggleEnabled`, null),
|
||||
},
|
||||
config: {
|
||||
get: () => appClient.Get("api/config")
|
||||
get: () => appClient.Get<Config>("api/config")
|
||||
},
|
||||
download_clients: {
|
||||
getAll: () => appClient.Get("api/download_clients"),
|
||||
create: (dc: DownloadClient) => appClient.Post(`api/download_clients`, dc),
|
||||
update: (dc: DownloadClient) => appClient.Put(`api/download_clients`, dc),
|
||||
getAll: () => appClient.Get<DownloadClient[]>("api/download_clients"),
|
||||
create: (dc: DownloadClient) => appClient.Post("api/download_clients", dc),
|
||||
update: (dc: DownloadClient) => appClient.Put("api/download_clients", dc),
|
||||
delete: (id: number) => appClient.Delete(`api/download_clients/${id}`),
|
||||
test: (dc: DownloadClient) => appClient.Post(`api/download_clients/test`, dc),
|
||||
test: (dc: DownloadClient) => appClient.Post("api/download_clients/test", dc),
|
||||
},
|
||||
filters: {
|
||||
getAll: () => appClient.Get("api/filters"),
|
||||
getByID: (id: number) => appClient.Get(`api/filters/${id}`),
|
||||
create: (filter: Filter) => appClient.Post(`api/filters`, filter),
|
||||
getAll: () => appClient.Get<Filter[]>("api/filters"),
|
||||
getByID: (id: number) => appClient.Get<Filter>(`api/filters/${id}`),
|
||||
create: (filter: Filter) => appClient.Post("api/filters", filter),
|
||||
update: (filter: Filter) => appClient.Put(`api/filters/${filter.id}`, filter),
|
||||
toggleEnable: (id: number, enabled: boolean) => appClient.Put(`api/filters/${id}/enabled`, { enabled }),
|
||||
delete: (id: number) => appClient.Delete(`api/filters/${id}`),
|
||||
},
|
||||
indexers: {
|
||||
getOptions: () => appClient.Get("api/indexer/options"),
|
||||
getAll: () => appClient.Get("api/indexer"),
|
||||
getSchema: () => appClient.Get("api/indexer/schema"),
|
||||
create: (indexer: Indexer) => appClient.Post(`api/indexer`, indexer),
|
||||
update: (indexer: Indexer) => appClient.Put(`api/indexer`, indexer),
|
||||
// returns indexer options for all currently present/enabled indexers
|
||||
getOptions: () => appClient.Get<Indexer[]>("api/indexer/options"),
|
||||
// returns indexer definitions for all currently present/enabled indexers
|
||||
getAll: () => appClient.Get<IndexerDefinition[]>("api/indexer"),
|
||||
// returns all possible indexer definitions
|
||||
getSchema: () => appClient.Get<IndexerDefinition[]>("api/indexer/schema"),
|
||||
create: (indexer: Indexer) => appClient.Post("api/indexer", indexer),
|
||||
update: (indexer: Indexer) => appClient.Put("api/indexer", indexer),
|
||||
delete: (id: number) => appClient.Delete(`api/indexer/${id}`),
|
||||
},
|
||||
irc: {
|
||||
getNetworks: () => appClient.Get("api/irc"),
|
||||
createNetwork: (network: Network) => appClient.Post(`api/irc`, network),
|
||||
getNetworks: () => appClient.Get<IrcNetwork[]>("api/irc"),
|
||||
createNetwork: (network: Network) => appClient.Post("api/irc", network),
|
||||
updateNetwork: (network: Network) => appClient.Put(`api/irc/network/${network.id}`, network),
|
||||
deleteNetwork: (id: number) => appClient.Delete(`api/irc/network/${id}`),
|
||||
},
|
||||
|
@ -110,9 +96,7 @@ const APIClient = {
|
|||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||
},
|
||||
release: {
|
||||
find: (query?: string) => appClient.Get(`api/release${query}`),
|
||||
stats: () => appClient.Get(`api/release/stats`)
|
||||
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
||||
stats: () => appClient.Get<ReleaseStats>("api/release/stats")
|
||||
}
|
||||
}
|
||||
|
||||
export default APIClient;
|
||||
};
|
|
@ -7,7 +7,7 @@ import { Field, Form, Formik } from "formik";
|
|||
import type { FieldProps } from "formik";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import DEBUG from "../../components/debug";
|
||||
import Toast from '../../components/notifications/Toast';
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { sleep, classNames } from "../../utils";
|
|||
import { Form, Formik, useFormikContext } from "formik";
|
||||
import DEBUG from "../../components/debug";
|
||||
import { queryClient } from "../../App";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { DownloadClientTypeOptions } from "../../domain/constants";
|
||||
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||
import { sleep } from "../../utils";
|
||||
import { queryClient } from "../../App";
|
||||
import DEBUG from "../../components/debug";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import {
|
||||
TextFieldWide,
|
||||
PasswordFieldWide,
|
||||
|
@ -48,13 +48,22 @@ const Menu = (props: any) => {
|
|||
);
|
||||
}
|
||||
|
||||
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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
}
|
||||
|
||||
export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
||||
const { data } = useQuery<IndexerDefinition[], Error>('indexerDefinition', APIClient.indexers.getSchema,
|
||||
const { data } = useQuery('indexerDefinition', APIClient.indexers.getSchema,
|
||||
{
|
||||
enabled: isOpen,
|
||||
refetchOnWindowFocus: false
|
||||
|
@ -237,8 +246,14 @@ export function IndexerAddForm({ isOpen, toggle }: AddProps) {
|
|||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{ Input, Control, Menu }}
|
||||
components={{ Input, Control, Menu, Option }}
|
||||
placeholder="Choose an indexer"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
color: "unset"
|
||||
})
|
||||
}}
|
||||
value={field?.value && field.value.value}
|
||||
onChange={(option: any) => {
|
||||
setFieldValue("name", option?.label ?? "")
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Field, FieldArray } from "formik";
|
|||
import type { FieldProps } from "formik";
|
||||
|
||||
import { queryClient } from "../../App";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
||||
import {
|
||||
TextFieldWide,
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
usePagination
|
||||
} from "react-table";
|
||||
|
||||
import APIClient from "../api/APIClient";
|
||||
import { APIClient } from "../api/APIClient";
|
||||
import { EmptyListState } from "../components/emptystates";
|
||||
import { ReleaseStatusCell } from "./Releases";
|
||||
|
||||
|
@ -18,7 +18,7 @@ export function Dashboard() {
|
|||
<main className="py-10 -mt-48">
|
||||
<div className="px-4 pb-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<Stats />
|
||||
<DataTablee />
|
||||
<DataTable />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
|
@ -40,15 +40,14 @@ const StatsItem = ({ name, stat }: any) => (
|
|||
)
|
||||
|
||||
function Stats() {
|
||||
const { isLoading, data } = useQuery<ReleaseStats, Error>('dash_release_staats', () => APIClient.release.stats(),
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { isLoading, data } = useQuery(
|
||||
'dash_release_stats',
|
||||
() => APIClient.release.stats(),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
if (isLoading)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -64,255 +63,6 @@ function Stats() {
|
|||
)
|
||||
}
|
||||
|
||||
/* function RecentActivity() {
|
||||
let data: any[] = [
|
||||
{
|
||||
id: 1,
|
||||
status: "FILTERED",
|
||||
created_at: "2021-10-16 20:25:26",
|
||||
indexer: "tl",
|
||||
title: "That movie 2019 1080p x264-GROUP",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: "PUSH_APPROVED",
|
||||
created_at: "2021-10-15 16:16:23",
|
||||
indexer: "tl",
|
||||
title: "That great movie 2009 1080p x264-1GROUP",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
status: "FILTER_REJECTED",
|
||||
created_at: "2021-10-15 10:16:23",
|
||||
indexer: "tl",
|
||||
title: "Movie 1 2002 720p x264-1GROUP",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
status: "PUSH_APPROVED",
|
||||
created_at: "2021-10-14 16:16:23",
|
||||
indexer: "tl",
|
||||
title: "That bad movie 2019 2160p x265-1GROUP",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
status: "PUSH_REJECTED",
|
||||
created_at: "2021-10-13 16:16:23",
|
||||
indexer: "tl",
|
||||
title: "That really bad movie 20010 1080p x264-GROUP2",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-12">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">Recent activity</h3>
|
||||
|
||||
<div className="mt-3 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full py-2 sm:px-6 lg:px-8">
|
||||
<div className="overflow-hidden light:shadow 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 dark:bg-gray-800">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Age
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Release
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-400"
|
||||
>
|
||||
Indexer
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-gray-800 divide-y divide-gray-200 light:bg-white dark:divide-gray-700">
|
||||
{data && data.length > 0 ?
|
||||
data.map((release: any, idx) => (
|
||||
<ListItem key={idx} idx={idx} release={release} />
|
||||
))
|
||||
: <span>No recent activity</span>}
|
||||
</tbody>
|
||||
</table>
|
||||
<nav
|
||||
className="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200 dark:bg-gray-800 dark:border-gray-700 sm:px-6"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<div className="hidden sm:block">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-500">
|
||||
Showing <span className="font-medium">1</span> to <span className="font-medium">10</span> of{' '}
|
||||
<span className="font-medium">20</span> results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between flex-1 sm:justify-end">
|
||||
<p className="relative text-sm text-gray-700 dark:text-gray-500">
|
||||
Show <span className="font-medium">10</span>
|
||||
</p>
|
||||
<Menu as="div" className="relative text-left">
|
||||
<Menu.Button className="flex items-center text-sm font-medium text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-600">
|
||||
<span>Show</span>
|
||||
<ChevronDownIcon className="w-5 h-5 ml-1 text-gray-500" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-30 w-40 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
{[5, 10, 25, 50].map((child) => (
|
||||
<Menu.Item key={child}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
// href={child.href}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700'
|
||||
)}
|
||||
>
|
||||
{child}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<a
|
||||
href="#"
|
||||
// className="px-4 py-2 mr-4 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm dark:bg-gray-700 dark:border-gray-600 hover:bg-gray-50 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 ml-5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
>
|
||||
Previous
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
// className="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
className="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md dark:border-gray-600 dark:text-gray-400 dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600"
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
} */
|
||||
|
||||
/* const ListItem = ({ idx, release }: any) => {
|
||||
|
||||
const formatDate = formatDistanceToNowStrict(
|
||||
new Date(release.created_at),
|
||||
{ addSuffix: true }
|
||||
)
|
||||
|
||||
return (
|
||||
<tr key={release.id} className={idx % 2 === 0 ? 'light:bg-white' : 'light:bg-gray-50'}>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap dark:text-gray-400" title={release.created_at}>{formatDate}</td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-gray-900 whitespace-nowrap dark:text-gray-300">{release.title}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap dark:text-gray-300">{statusMap[release.status]}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap dark:text-gray-300">{release.indexer}</td>
|
||||
</tr>
|
||||
|
||||
)
|
||||
} */
|
||||
/*
|
||||
const getData = () => {
|
||||
|
||||
const data: any[] = [
|
||||
{
|
||||
id: 1,
|
||||
status: "FILTERED",
|
||||
created_at: "2021-10-16 20:25:26",
|
||||
indexer: "tl",
|
||||
title: "That movie 2019 1080p x264-GROUP",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: "PUSH_APPROVED",
|
||||
created_at: "2021-10-15 16:16:23",
|
||||
indexer: "tl",
|
||||
title: "That great movie 2009 1080p x264-1GROUP",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
status: "FILTER_REJECTED",
|
||||
created_at: "2021-10-15 10:16:23",
|
||||
indexer: "tl",
|
||||
title: "Movie 1 2002 720p x264-1GROUP",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
status: "PUSH_APPROVED",
|
||||
created_at: "2021-10-14 16:16:23",
|
||||
indexer: "tl",
|
||||
title: "That bad movie 2019 2160p x265-1GROUP",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
status: "PUSH_REJECTED",
|
||||
created_at: "2021-10-13 16:16:23",
|
||||
indexer: "tl",
|
||||
title: "That really bad movie 20010 1080p x264-GROUP2",
|
||||
},
|
||||
]
|
||||
|
||||
return [...data, ...data, ...data]
|
||||
} */
|
||||
|
||||
// Define a default UI for filtering
|
||||
/* function GlobalFilter({
|
||||
preGlobalFilteredRows,
|
||||
globalFilter,
|
||||
setGlobalFilter,
|
||||
}: any) {
|
||||
const count = preGlobalFilteredRows.length
|
||||
const [value, setValue] = React.useState(globalFilter)
|
||||
const onChange = useAsyncDebounce((value: any) => {
|
||||
setGlobalFilter(value || undefined)
|
||||
}, 200)
|
||||
|
||||
return (
|
||||
<label className="flex items-baseline gap-x-2">
|
||||
<span className="text-gray-700">Search: </span>
|
||||
<input
|
||||
type="text"
|
||||
className="border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
||||
value={value || ""}
|
||||
onChange={e => {
|
||||
setValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
placeholder={`${count} records...`}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
} */
|
||||
|
||||
// This is a custom filter UI for selecting
|
||||
// a unique option from a list
|
||||
export function SelectColumnFilter({
|
||||
|
@ -352,26 +102,6 @@ export function SelectColumnFilter({
|
|||
)
|
||||
}
|
||||
|
||||
// export function StatusPill({ value }: any) {
|
||||
|
||||
// const status = value ? value.toLowerCase() : "unknown";
|
||||
|
||||
// return (
|
||||
// <span
|
||||
// className={
|
||||
// classNames(
|
||||
// "px-3 py-1 uppercase leading-wide font-bold text-xs rounded-full shadow-sm",
|
||||
// status.startsWith("active") ? "bg-green-100 text-green-800" : "",
|
||||
// status.startsWith("inactive") ? "bg-yellow-100 text-yellow-800" : "",
|
||||
// status.startsWith("offline") ? "bg-red-100 text-red-800" : "",
|
||||
// )
|
||||
// }
|
||||
// >
|
||||
// {status}
|
||||
// </span>
|
||||
// );
|
||||
// };
|
||||
|
||||
export function StatusPill({ value }: any) {
|
||||
const statusMap: any = {
|
||||
"FILTER_APPROVED": <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase bg-blue-100 text-blue-800 ">Approved</span>,
|
||||
|
@ -439,28 +169,13 @@ function Table({ columns, data }: any) {
|
|||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination, // new
|
||||
)
|
||||
);
|
||||
|
||||
if (!page.length)
|
||||
return <EmptyListState text="No recent activity" />;
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<>
|
||||
<div className="sm:flex sm:gap-x-2">
|
||||
{/* <GlobalFilter
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
globalFilter={state.globalFilter}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
/> */}
|
||||
{/* {headerGroups.map((headerGroup: { headers: any[] }) =>
|
||||
headerGroup.headers.map((column) =>
|
||||
column.Filter ? (
|
||||
<div className="mt-2 sm:mt-0" key={column.id}>
|
||||
{column.render("Filter")}
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
)} */}
|
||||
</div>
|
||||
{page.length > 0 ?
|
||||
<div className="flex flex-col mt-4">
|
||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
|
@ -534,81 +249,11 @@ function Table({ columns, data }: any) {
|
|||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
{/* Pagination */}
|
||||
{/* <div className="flex items-center justify-between px-6 py-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex justify-between flex-1 sm:hidden">
|
||||
<Button onClick={() => previousPage()} disabled={!canPreviousPage}>Previous</Button>
|
||||
<Button onClick={() => nextPage()} disabled={!canNextPage}>Next</Button>
|
||||
</div>
|
||||
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-sm text-gray-700">
|
||||
Page <span className="font-medium">{state.pageIndex + 1}</span> of <span className="font-medium">{pageOptions.length}</span>
|
||||
</span>
|
||||
<label>
|
||||
<span className="sr-only">Items Per Page</span>
|
||||
<select
|
||||
className="block w-full border-gray-300 rounded-md shadow-sm cursor-pointer dark:bg-gray-800 dark:border-gray-800 dark:text-gray-600 dark:hover:text-gray-500 focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
|
||||
value={state.pageSize}
|
||||
onChange={e => {
|
||||
setPageSize(Number(e.target.value))
|
||||
}}
|
||||
>
|
||||
{[5, 10, 20].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<nav className="relative z-0 inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
<PageButton
|
||||
className="rounded-l-md"
|
||||
onClick={() => gotoPage(0)}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only">First</span>
|
||||
<ChevronDoubleLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</PageButton>
|
||||
<PageButton
|
||||
onClick={() => previousPage()}
|
||||
disabled={!canPreviousPage}
|
||||
>
|
||||
<span className="sr-only">Previous</span>
|
||||
<ChevronLeftIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</PageButton>
|
||||
<PageButton
|
||||
onClick={() => nextPage()}
|
||||
disabled={!canNextPage
|
||||
}>
|
||||
<span className="sr-only">Next</span>
|
||||
<ChevronRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</PageButton>
|
||||
<PageButton
|
||||
className="rounded-r-md"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
disabled={!canNextPage}
|
||||
>
|
||||
<span className="sr-only">Last</span>
|
||||
<ChevronDoubleRightIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</PageButton>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
: <EmptyListState text="No recent activity"/>}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SortIcon({ className }: any) {
|
||||
|
@ -629,40 +274,7 @@ function SortDownIcon({ className }: any) {
|
|||
)
|
||||
}
|
||||
|
||||
/* function Button({ children, className, ...rest }: any) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
classNames(
|
||||
"relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function PageButton({ children, className, ...rest }: any) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={
|
||||
classNames(
|
||||
"relative inline-flex items-center px-2 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
} */
|
||||
|
||||
function DataTablee() {
|
||||
|
||||
function DataTable() {
|
||||
const columns = React.useMemo(() => [
|
||||
{
|
||||
Header: "Age",
|
||||
|
@ -674,11 +286,6 @@ function DataTablee() {
|
|||
accessor: 'torrent_name',
|
||||
Cell: ReleaseCell,
|
||||
},
|
||||
// {
|
||||
// Header: "Filter Status",
|
||||
// accessor: 'filter_status',
|
||||
// Cell: StatusPill,
|
||||
// },
|
||||
{
|
||||
Header: "Actions",
|
||||
accessor: 'action_status',
|
||||
|
@ -693,23 +300,22 @@ function DataTablee() {
|
|||
},
|
||||
], [])
|
||||
|
||||
// const data = React.useMemo(() => getData(), [])
|
||||
const { isLoading, data } = useQuery(
|
||||
'dash_release',
|
||||
() => APIClient.release.find("?limit=10"),
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const { isLoading, data } = useQuery<ReleaseFindResponse, Error>('dash_release', () => APIClient.release.find("?limit=10"),
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
return null
|
||||
}
|
||||
if (isLoading)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-12">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">Recent activity</h3>
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-600">
|
||||
Recent activity
|
||||
</h3>
|
||||
|
||||
<Table columns={columns} data={data?.data} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import APIClient from "../api/APIClient";
|
||||
import { APIClient } from "../api/APIClient";
|
||||
|
||||
type LogEvent = {
|
||||
time: string;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
import { useTable, useSortBy, usePagination } from "react-table";
|
||||
import { useTable, useSortBy, usePagination, Column } from "react-table";
|
||||
import {
|
||||
ClockIcon,
|
||||
BanIcon,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
CheckIcon
|
||||
} from "@heroicons/react/solid";
|
||||
|
||||
import APIClient from "../api/APIClient";
|
||||
import { APIClient } from "../api/APIClient";
|
||||
import { EmptyListState } from "../components/emptystates";
|
||||
import { classNames, simplifyDate } from "../utils";
|
||||
|
||||
|
@ -222,7 +222,7 @@ function Table() {
|
|||
Filter: SelectColumnFilter, // new
|
||||
filter: 'includes',
|
||||
},
|
||||
], [])
|
||||
] as Column<Release>[], [])
|
||||
|
||||
const [{ queryPageIndex, queryPageSize, totalCount }, dispatch] =
|
||||
React.useReducer(reducer, initialState);
|
||||
|
@ -260,7 +260,7 @@ function Table() {
|
|||
// setGlobalFilter,
|
||||
} = useTable({
|
||||
columns,
|
||||
data: isSuccess ? data.data : [],
|
||||
data: data && isSuccess ? data.data : [],
|
||||
initialState: {
|
||||
pageIndex: queryPageIndex,
|
||||
pageSize: queryPageSize,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useHistory } from "react-router-dom";
|
|||
import { useMutation } from "react-query";
|
||||
import { Form, Formik } from "formik";
|
||||
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { TextField, PasswordField } from "../../components/inputs";
|
||||
|
||||
import logo from "../../logo.png";
|
||||
|
|
|
@ -2,7 +2,7 @@ import {useEffect} from "react";
|
|||
import {useCookies} from "react-cookie";
|
||||
import {useHistory} from "react-router-dom";
|
||||
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { AuthContext } from "../../utils/Context";
|
||||
|
||||
function Logout() {
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
RELEASE_TYPE_MUSIC_OPTIONS
|
||||
} from "../../domain/constants";
|
||||
import { queryClient } from "../../App";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { buildPath, classNames } from "../../utils";
|
||||
|
||||
|
@ -134,7 +134,7 @@ export default function FilterDetails() {
|
|||
const { url } = useRouteMatch();
|
||||
const { filterId } = useParams<{ filterId: string }>();
|
||||
|
||||
const { isLoading, data: filter } = useQuery<Filter, Error>(
|
||||
const { isLoading, data: filter } = useQuery(
|
||||
['filter', +filterId],
|
||||
() => APIClient.filters.getByID(parseInt(filterId)),
|
||||
{
|
||||
|
@ -144,14 +144,15 @@ export default function FilterDetails() {
|
|||
}
|
||||
);
|
||||
|
||||
const updateMutation = useMutation((filter: Filter) => APIClient.filters.update(filter), {
|
||||
onSuccess: (filter) => {
|
||||
// queryClient.setQueryData(['filter', filter.id], data)
|
||||
toast.custom((t) => <Toast type="success" body={`${filter.name} was updated successfully`} t={t} />)
|
||||
|
||||
queryClient.invalidateQueries(["filter", filter.id]);
|
||||
const updateMutation = useMutation(
|
||||
(filter: Filter) => APIClient.filters.update(filter),
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.custom((t) => <Toast type="success" body={`${filter?.name} was updated successfully`} t={t} />)
|
||||
queryClient.invalidateQueries(["filter", filter?.id]);
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const deleteMutation = useMutation((id: number) => APIClient.filters.delete(id), {
|
||||
onSuccess: () => {
|
||||
|
@ -315,11 +316,11 @@ export default function FilterDetails() {
|
|||
}
|
||||
|
||||
function General() {
|
||||
const { isLoading, data: indexers } = useQuery<Indexer[], Error>(["filter", "indexer_list"], APIClient.indexers.getOptions,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { isLoading, data: indexers } = useQuery(
|
||||
["filter", "indexer_list"],
|
||||
APIClient.indexers.getOptions,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const opts = indexers && indexers.length > 0 ? indexers.map(v => ({
|
||||
label: v.name,
|
||||
|
@ -592,11 +593,11 @@ interface FilterActionsProps {
|
|||
}
|
||||
|
||||
function FilterActions({ filter, values }: FilterActionsProps) {
|
||||
const { data } = useQuery<DownloadClient[], Error>(['filter', 'download_clients'], APIClient.download_clients.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { data } = useQuery(
|
||||
['filter', 'download_clients'],
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
const newAction = {
|
||||
name: "new action",
|
||||
|
|
|
@ -8,17 +8,17 @@ import { queryClient } from "../../App";
|
|||
import { classNames } from "../../utils";
|
||||
import { FilterAddForm } from "../../forms";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import { EmptyListState } from "../../components/emptystates";
|
||||
|
||||
export default function Filters() {
|
||||
const [createFilterIsOpen, toggleCreateFilter] = useToggle(false)
|
||||
|
||||
const { isLoading, error, data } = useQuery<Filter[], Error>('filter', APIClient.filters.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
const { isLoading, error, data } = useQuery(
|
||||
'filter',
|
||||
APIClient.filters.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery } from "react-query";
|
||||
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { Checkbox } from "../../components/Checkbox";
|
||||
import { SettingsContext } from "../../utils/Context";
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { SettingsContext } from "../../utils/Context";
|
|||
function ApplicationSettings() {
|
||||
const [settings, setSettings] = SettingsContext.use();
|
||||
|
||||
const { isLoading, data } = useQuery<Config, Error>(
|
||||
const { isLoading, data } = useQuery(
|
||||
['config'],
|
||||
() => APIClient.config.get(),
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useQuery } from "react-query";
|
|||
import { classNames } from "../../utils";
|
||||
import { DownloadClientAddForm, DownloadClientUpdateForm } from "../../forms";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { DownloadClientTypeNameMap } from "../../domain/constants";
|
||||
|
||||
interface DLSettingsItemProps {
|
||||
|
@ -53,10 +53,11 @@ function DownloadClientSettingsListItem({ client, idx }: DLSettingsItemProps) {
|
|||
function DownloadClientSettings() {
|
||||
const [addClientIsOpen, toggleAddClient] = useToggle(false)
|
||||
|
||||
const { error, data } = useQuery<DownloadClient[], Error>('downloadClients', APIClient.download_clients.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
})
|
||||
const { error, data } = useQuery(
|
||||
'downloadClients',
|
||||
APIClient.download_clients.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred: </p>);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { IndexerAddForm, IndexerUpdateForm } from "../../forms";
|
|||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
|
||||
const ListItem = ({ indexer }: any) => {
|
||||
const [updateIsOpen, toggleUpdate] = useToggle(false)
|
||||
|
@ -45,11 +45,11 @@ const ListItem = ({ indexer }: any) => {
|
|||
function IndexerSettings() {
|
||||
const [addIndexerIsOpen, toggleAddIndexer] = useToggle(false)
|
||||
|
||||
const { error, data } = useQuery<any[], Error>('indexer', APIClient.indexers.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { error, data } = useQuery(
|
||||
'indexer',
|
||||
APIClient.indexers.getAll,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
if (error)
|
||||
return (<p>An error has occurred</p>);
|
||||
|
@ -104,7 +104,7 @@ function IndexerSettings() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody className="light:bg-white divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{data && data.map((indexer: Indexer, idx: number) => (
|
||||
{data && data.map((indexer: IndexerDefinition, idx: number) => (
|
||||
<ListItem indexer={indexer} key={idx} />
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useQuery } from "react-query";
|
||||
import { formatDistanceToNowStrict, formatISO9075 } from "date-fns";
|
||||
|
||||
import APIClient from "../../api/APIClient";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { IrcNetworkAddForm, IrcNetworkUpdateForm } from "../../forms";
|
||||
|
@ -27,11 +27,11 @@ function simplifyDate(date: string) {
|
|||
function IrcSettings() {
|
||||
const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false)
|
||||
|
||||
const { data } = useQuery<IrcNetwork[], Error>('networks', APIClient.irc.getNetworks,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
const { data } = useQuery(
|
||||
'networks',
|
||||
APIClient.irc.getNetworks,
|
||||
{ refetchOnWindowFocus: false }
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
|
@ -90,27 +90,8 @@ const LiItem = ({ idx, network }: LiItemProps) => {
|
|||
|
||||
<li key={idx} >
|
||||
<div className="grid grid-cols-12 gap-4 items-center hover:bg-gray-50 dark:hover:bg-gray-700 py-4">
|
||||
|
||||
<IrcNetworkUpdateForm isOpen={updateIsOpen} toggle={toggleUpdate} network={network} />
|
||||
{/* <div className="col-span-1 flex items-center sm:px-6">
|
||||
<Switch
|
||||
checked={network.enabled}
|
||||
onChange={toggleUpdate}
|
||||
className={classNames(
|
||||
network.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(
|
||||
network.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div> */}
|
||||
|
||||
<div className="col-span-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">
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue