feat: add webui

This commit is contained in:
Ludvig Lundgren 2021-08-11 15:27:48 +02:00
parent a838d994a6
commit 773e57afe6
59 changed files with 19794 additions and 0 deletions

View file

@ -0,0 +1,24 @@
import React from "react";
interface props {
text: string;
buttonText?: string;
buttonOnClick?: any;
}
export function EmptyListState({ text, buttonText, buttonOnClick }: props) {
return (
<div className="px-4 py-12 flex flex-col items-center">
<p className="text-center text-gray-500">{text}</p>
{buttonText && buttonOnClick && (
<button
type="button"
className="relative inline-flex items-center px-4 py-2 mt-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={buttonOnClick}
>
{buttonText}
</button>
)}
</div>
)
}

View file

@ -0,0 +1,665 @@
import {Action, DownloadClient} from "../domain/interfaces";
import React, {Fragment, useEffect, useRef } from "react";
import {Dialog, Listbox, Switch, Transition} from '@headlessui/react'
import {classNames} from "../styles/utils";
import {CheckIcon, ChevronRightIcon, ExclamationIcon, SelectorIcon,} from "@heroicons/react/solid";
import {useToggle} from "../hooks/hooks";
import {useMutation} from "react-query";
import {queryClient} from "..";
import {Field, Form} from "react-final-form";
import {TextField} from "./inputs";
import DEBUG from "./debug";
import APIClient from "../api/APIClient";
interface radioFieldsetOption {
label: string;
value: string;
}
const actionTypeOptions: radioFieldsetOption[] = [
{label: "Test", value: "TEST"},
{label: "Watch dir", value: "WATCH_FOLDER"},
{label: "Exec", value: "EXEC"},
{label: "qBittorrent", value: "QBITTORRENT"},
{label: "Deluge", value: "DELUGE"},
];
interface FilterListProps {
actions: Action[];
clients: DownloadClient[];
filterID: number;
}
export function FilterActionList({actions, clients, filterID}: FilterListProps) {
useEffect(() => {
// console.log("render list")
}, [])
return (
<div className="bg-white shadow overflow-hidden sm:rounded-md">
<ul className="divide-y divide-gray-200">
{actions.map((action, idx) => (
<ListItem action={action} clients={clients} filterID={filterID} key={action.id} idx={idx} />
))}
</ul>
</div>
)
}
interface ListItemProps {
action: Action;
clients: DownloadClient[];
filterID: number;
idx: number;
}
function ListItem({action, clients, filterID, idx}: ListItemProps) {
const [deleteModalIsOpen, toggleDeleteModal] = useToggle(false)
const [edit, toggleEdit] = useToggle(false)
const deleteMutation = useMutation((actionID: number) => APIClient.actions.delete(actionID), {
onSuccess: () => {
queryClient.invalidateQueries(['filter',filterID]);
toggleDeleteModal()
}
})
const enabledMutation = useMutation((actionID: number) => APIClient.actions.toggleEnable(actionID), {
onSuccess: () => {
queryClient.invalidateQueries(['filter',filterID]);
}
})
const updateMutation = useMutation((action: Action) => APIClient.actions.update(action), {
onSuccess: () => {
queryClient.invalidateQueries(['filter',filterID]);
}
})
const toggleActive = () => {
enabledMutation.mutate(action.id)
}
useEffect(() => {
}, [action])
const cancelButtonRef = useRef(null)
const deleteAction = () => {
deleteMutation.mutate(action.id)
}
const onSubmit = (action: Action) => {
// TODO clear data depending on type
updateMutation.mutate(action)
};
const TypeForm = (action: Action) => {
switch (action.type) {
case "TEST":
return (
<div className="py-4">
<div className="rounded-md bg-yellow-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationIcon className="h-5 w-5 text-yellow-400" aria-hidden="true"/>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-yellow-800">Notice</h3>
<div className="mt-2 text-sm text-yellow-700">
<p>
The test action does nothing except to show if the filter works.
</p>
</div>
</div>
</div>
</div>
</div>
)
case "EXEC":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField name="exec_cmd" label="Command" columns={6} placeholder="Path to program eg. /bin/test"/>
<TextField name="exec_args" label="Arguments" columns={6} placeholder="Arguments eg. --test"/>
</div>
</div>
)
case "WATCH_FOLDER":
return (
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField name="watch_folder" label="Watch folder" columns={6} placeholder="Watch directory eg. /home/user/rwatch"/>
</div>
)
case "QBITTORRENT":
return (
<div className="w-full">
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6 sm:col-span-6">
<Field
name="client_id"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-xs font-bold text-gray-700 uppercase tracking-wide">Client</Listbox.Label>
<div className="mt-2 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span
className="block truncate">{input.value ? clients.find(c => c.id === input.value)!.name : "Choose a client"}</span>
{/*<span className="block truncate">Choose a client</span>*/}
<span
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" 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 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{clients.filter((c) => c.type === action.type).map((client: any) => (
<Listbox.Option
key={client.id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={client.id}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{client.name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'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>
)}/>
</div>
<div className="col-span-6 sm:col-span-6">
<TextField name="save_path" label="Save path" columns={6}/>
</div>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<TextField name="category" label="Category" columns={6}/>
<TextField name="tags" label="Tags" columns={6}/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700">
Limit upload speed (kb/s)
</label>
<Field name="limit_upload_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="col-span-12 sm:col-span-6">
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700">
Limit download speed (kb/s)
</label>
<Field name="limit_download_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
</div>
)
case "DELUGE":
return (
<div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<Field
name="client_id"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-xs font-bold text-gray-700 uppercase tracking-wide">Client</Listbox.Label>
<div className="mt-2 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span
className="block truncate">{input.value ? clients.find(c => c.id === input.value)!.name : "Choose a client"}</span>
{/*<span className="block truncate">Choose a client</span>*/}
<span
className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" 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 shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
>
{clients.filter((c) => c.type === action.type).map((client: any) => (
<Listbox.Option
key={client.id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={client.id}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}>
{client.name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'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>
)}/>
</div>
<div className="col-span-12 sm:col-span-6">
<TextField name="save_path" label="Save path" columns={6}/>
</div>
</div>
<div className="mt-6 col-span-12 sm:col-span-6">
<TextField name="label" label="Label" columns={6}/>
</div>
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-12 sm:col-span-6">
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700">
Limit upload speed (kb/s)
</label>
<Field name="limit_upload_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
<div className="col-span-12 sm:col-span-6">
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700">
Limit download speed (kb/s)
</label>
<Field name="limit_download_speed">
{({input, meta}) => (
<div className="sm:col-span-2">
<input
type="number"
{...input}
className="block w-full shadow-sm sm:text-sm focus:ring-indigo-500 focus:border-indigo-500 border-gray-300 rounded-md"
/>
{meta.touched && meta.error &&
<span>{meta.error}</span>}
</div>
)}
</Field>
</div>
</div>
</div>
)
default:
return <p>default</p>
}
}
return (
<li key={action.id}>
<div className={classNames(idx % 2 === 0 ? 'bg-white' : 'bg-gray-50', "flex items-center sm:px-6 hover:bg-gray-50")}>
<Switch
checked={action.enabled}
onChange={toggleActive}
className={classNames(
action.enabled ? 'bg-teal-500' : 'bg-gray-200',
'z-10 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-light-blue-500'
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={classNames(
action.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>
<button className="px-4 py-4 w-full flex block" onClick={toggleEdit}>
<div className="min-w-0 flex-1 sm:flex sm:items-center sm:justify-between">
<div className="truncate">
<div className="flex text-sm">
<p className="ml-4 font-medium text-indigo-600 truncate">{action.name}</p>
</div>
</div>
<div className="mt-4 flex-shrink-0 sm:mt-0 sm:ml-5">
<div className="flex overflow-hidden -space-x-1">
<span className="text-sm font-normal text-gray-500">{action.type}</span>
</div>
</div>
</div>
<div className="ml-5 flex-shrink-0">
<ChevronRightIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</div>
</button>
</div>
{edit &&
<div className="px-4 py-4 flex items-center sm:px-6">
<Transition.Root show={deleteModalIsOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={cancelButtonRef}
open={deleteModalIsOpen}
onClose={toggleDeleteModal}
>
<div
className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
className="inline-block align-bottom bg-white 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 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true"/>
</div>
<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">
Remove filter action
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to remove this action?
This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 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={deleteAction}
>
Remove
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={toggleDeleteModal}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
<Form
initialValues={{
id: action.id,
name: action.name,
enabled: action.enabled,
type: action.type,
watch_folder: action.watch_folder,
exec_cmd: action.exec_cmd,
exec_args: action.exec_args,
category: action.category,
tags: action.tags,
label: action.label,
save_path: action.save_path,
paused: action.paused,
ignore_rules: action.ignore_rules,
limit_upload_speed: action.limit_upload_speed || 0,
limit_download_speed: action.limit_download_speed || 0,
filter_id: action.filter_id,
client_id: action.client_id,
}}
onSubmit={onSubmit}
>
{({handleSubmit, values}) => {
return (
<form onSubmit={handleSubmit} className="w-full">
<div className="mt-6 grid grid-cols-12 gap-6">
<div className="col-span-6">
<Field
name="type"
type="select"
render={({input}) => (
<Listbox value={input.value} onChange={input.onChange}>
{({open}) => (
<>
<Listbox.Label
className="block text-xs font-bold text-gray-700 uppercase tracking-wide">Type</Listbox.Label>
<div className="mt-2 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span
className="block truncate">{input.value ? actionTypeOptions.find(c => c.value === input.value)!.label : "Choose a type"}</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" 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 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"
>
{actionTypeOptions.map((opt) => (
<Listbox.Option
key={opt.value}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'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' : 'text-indigo-600',
'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>
)}/>
</div>
<TextField name="name" label="Name" columns={6}/>
</div>
{TypeForm(values)}
<div className="pt-6 divide-y divide-gray-200">
<div className="mt-4 pt-4 flex justify-between">
<button
type="button"
className="inline-flex items-center justify-center px-4 py-2 border border-transparent font-medium rounded-md text-red-700 bg-red-100 hover:bg-red-200 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 border border-gray-300 rounded-md shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-light-blue-500"
>
Cancel
</button>
<button
type="submit"
className="ml-4 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"
>
Save
</button>
</div>
</div>
</div>
<DEBUG values={values}/>
</form>
)
}}
</Form>
</div>
}
</li>
)
}

View file

@ -0,0 +1,15 @@
import React from "react";
const DEBUG = ({ values }: any) => {
if (process.env.NODE_ENV !== "development") {
return null;
}
return (
<div className="w-1/2 mx-auto mt-2 flex flex-col mt-12 mb-12">
<pre className="mt-2">{JSON.stringify(values, 0 as any, 2)}</pre>
</div>
);
};
export default DEBUG;

View file

@ -0,0 +1,27 @@
import {PlusIcon} from "@heroicons/react/solid";
interface props {
title: string;
subtitle: string;
buttonText: string;
buttonAction: any;
}
const EmptySimple = ({ title, subtitle, buttonText, buttonAction}: props) => (
<div className="text-center py-8">
<h3 className="mt-2 text-sm font-medium text-gray-900">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
<div className="mt-6">
<button
type="button"
onClick={buttonAction}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<PlusIcon className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{buttonText}
</button>
</div>
</div>
)
export default EmptySimple;

View file

@ -0,0 +1,15 @@
import React from "react";
interface Props {
title: string;
subtitle: string;
}
const TitleSubtitle: React.FC<Props> = ({ title, subtitle }) => (
<div>
<h2 className="text-lg leading-6 font-medium text-gray-900">{title}</h2>
<p className="mt-1 text-sm text-gray-500">{subtitle}</p>
</div>
)
export default TitleSubtitle;

View file

@ -0,0 +1,20 @@
import React from "react";
import { Field } from "react-final-form";
interface Props {
name: string;
classNames?: string;
subscribe?: any;
}
const Error: React.FC<Props> = ({ name, classNames }) => (
<Field
name={name}
subscribe={{ touched: true, error: true }}
render={({ meta: { touched, error } }) =>
touched && error ? <span className={classNames}>{error}</span> : null
}
/>
);
export default Error;

View file

@ -0,0 +1,50 @@
import React from "react";
import {Field} from "react-final-form";
import MultiSelect from "react-multi-select-component";
import {classNames, COL_WIDTHS} from "../../styles/utils";
interface Props {
label?: string;
options?: [] | any;
name: string;
className?: string;
columns?: COL_WIDTHS;
}
const MultiSelectField: React.FC<Props> = ({
name,
label,
options,
className,
columns
}) => (
<div
className={classNames(
columns ? `col-span-${columns}` : "col-span-12"
)}
>
<label
className="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2"
htmlFor={label}
>
{label}
</label>
<Field
name={name}
parse={val => val && val.map((item: any) => item.value)}
format={val =>
val &&
val.map((item: any) => options.find((o: any) => o.value === item))
}
render={({input, meta}) => (
<MultiSelect
{...input}
options={options}
labelledBy={name}
/>
)}
/>
</div>
);
export default MultiSelectField;

View file

@ -0,0 +1,60 @@
import React from "react";
import {Field} from "react-final-form";
export interface radioFieldsetOption {
label: string;
description: string;
value: string;
}
interface props {
name: string;
legend: string;
options: radioFieldsetOption[];
}
const RadioFieldset: React.FC<props> = ({ name, legend,options }) => (
<fieldset>
<div className="space-y-2 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:px-6 sm:py-5">
<div>
<legend className="text-sm font-medium text-gray-900">{legend}</legend>
</div>
<div className="space-y-5 sm:col-span-2">
<div className="space-y-5 sm:mt-0">
{options.map((opt, idx) => (
<div className="relative flex items-start" key={idx}>
<div className="absolute flex items-center h-5">
<Field
name={name}
type="radio"
render={({input}) => (
<input
{...input}
id={name}
value={opt.value}
// type="radio"
checked={input.checked}
className="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300"
/>
)}
/>
</div>
<div className="pl-7 text-sm">
<label htmlFor={opt.value} className="font-medium text-gray-900">
{opt.label}
</label>
<p id={opt.value+"_description"} className="text-gray-500">
{opt.description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</fieldset>
)
export default RadioFieldset;

View file

@ -0,0 +1,55 @@
import React from "react";
import {Switch} from "@headlessui/react";
import {Field} from "react-final-form";
import {classNames} from "../../styles/utils";
interface Props {
name: string;
label: string;
description?: string;
className?: string;
}
const SwitchGroup: React.FC<Props> = ({name, label, description}) => (
<ul className="mt-2 divide-y divide-gray-200">
<Switch.Group as="li" className="py-4 flex items-center justify-between">
<div className="flex flex-col">
<Switch.Label as="p" className="text-sm font-medium text-gray-900"
passive>
{label}
</Switch.Label>
{description && (
<Switch.Description className="text-sm text-gray-500">
{description}
</Switch.Description>
)}
</div>
<Field
name={name}
render={({input: {onChange, checked, value}}) => (
<Switch
value={value}
checked={value}
onChange={onChange}
className={classNames(
value ? 'bg-teal-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-light-blue-500'
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={classNames(
value ? 'translate-x-5' : 'translate-x-0',
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
)}
/>
</Switch>
)}
/>
</Switch.Group>
</ul>
)
export default SwitchGroup;

View file

@ -0,0 +1,40 @@
import {Field} from "react-final-form";
import React from "react";
import Error from "./Error";
import {classNames} from "../../styles/utils";
interface Props {
name: string;
label?: string;
placeholder?: string;
className?: string;
required?: boolean;
}
const TextAreaWide: React.FC<Props> = ({name, label, placeholder, required, className}) => (
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>}
</label>
</div>
<div className="sm:col-span-2">
<Field
name={name}
render={({input, meta}) => (
<textarea
{...input}
id={name}
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")}
placeholder={placeholder}
/>
)}
/>
<Error name={name} classNames="block text-red-500 mt-2"/>
</div>
</div>
)
export default TextAreaWide;

View file

@ -0,0 +1,45 @@
import { Field } from "react-final-form";
import React from "react";
import Error from "./Error";
import {classNames} from "../../styles/utils";
type COL_WIDTHS = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
interface Props {
name: string;
label?: string;
placeholder?: string;
columns?: COL_WIDTHS;
className?: string;
}
const TextField: React.FC<Props> = ({ name, label, placeholder, columns , className}) => (
<div
className={classNames(
columns ? `col-span-${columns}` : "col-span-12"
)}
>
{label && (
<label htmlFor={name} className="block text-xs font-bold text-gray-700 uppercase tracking-wide">
{label}
</label>
)}
<Field
name={name}
render={({input, meta}) => (
<input
{...input}
id={name}
type="text"
className="mt-2 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-light-blue-500 focus:border-light-blue-500 sm:text-sm"
placeholder={placeholder}
/>
)}
/>
<div>
<Error name={name} classNames="text-red mt-2" />
</div>
</div>
)
export default TextField;

View file

@ -0,0 +1,41 @@
import {Field} from "react-final-form";
import React from "react";
import Error from "./Error";
import {classNames} from "../../styles/utils";
interface Props {
name: string;
label?: string;
placeholder?: string;
className?: string;
required?: boolean;
}
const TextFieldWide: React.FC<Props> = ({name, label, placeholder, required, className}) => (
<div
className="space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
<div>
<label htmlFor={name} className="block text-sm font-medium text-gray-900 sm:mt-px sm:pt-2">
{label} {required && <span className="text-gray-500">*</span>}
</label>
</div>
<div className="sm:col-span-2">
<Field
name={name}
render={({input, meta}) => (
<input
{...input}
id={name}
type="text"
className={classNames(meta.touched && meta.error ? "focus:ring-red-500 focus:border-red-500 border-red-500" : "focus:ring-indigo-500 focus:border-indigo-500 border-gray-300", "block w-full shadow-sm sm:text-sm rounded-md")}
placeholder={placeholder}
/>
)}
/>
<Error name={name} classNames="block text-red-500 mt-2"/>
</div>
</div>
)
export default TextFieldWide;

View file

@ -0,0 +1,6 @@
export { default as TextField } from "./TextField";
export { default as TextFieldWide } from "./TextFieldWide";
export { default as TextAreaWide } from "./TextAreaWide";
export { default as MultiSelectField } from "./MultiSelectField";
export { default as RadioFieldset } from "./RadioFieldset";
export { default as SwitchGroup } from "./SwitchGroup";

View file

@ -0,0 +1,92 @@
import {Fragment} from "react";
import {Dialog, Transition} from "@headlessui/react";
import {ExclamationIcon} from "@heroicons/react/solid";
interface props {
isOpen: boolean;
buttonRef: any;
toggle: any;
deleteAction: any;
title: string;
text: string;
}
const DeleteModal = ({ isOpen, buttonRef, toggle, deleteAction, title, text }: props) => (
<Transition.Root show={isOpen} as={Fragment}>
<Dialog
as="div"
static
className="fixed z-10 inset-0 overflow-y-auto"
initialFocus={buttonRef}
open={isOpen}
onClose={toggle}
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div className="inline-block align-bottom bg-white 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 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
</div>
<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">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
{text}
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 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={deleteAction}
>
Remove
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-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>
)
export default DeleteModal;

View file

@ -0,0 +1 @@
export { default as DeleteModal } from "./Delete";