mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 16:59:12 +00:00
feat: add notifications (#216)
* feat: initial notifications support * chore: update deps
This commit is contained in:
parent
3185832708
commit
431742fd94
20 changed files with 1632 additions and 36 deletions
|
@ -108,6 +108,12 @@ export const APIClient = {
|
|||
events: {
|
||||
logs: () => new EventSource(`${sseBaseUrl()}api/events?stream=logs`, { withCredentials: true })
|
||||
},
|
||||
notifications: {
|
||||
getAll: () => appClient.Get<Notification[]>("api/notification"),
|
||||
create: (notification: Notification) => appClient.Post("api/notification", notification),
|
||||
update: (notification: Notification) => appClient.Put(`api/notification/${notification.id}`, notification),
|
||||
delete: (id: number) => appClient.Delete(`api/notification/${id}`),
|
||||
},
|
||||
release: {
|
||||
find: (query?: string) => appClient.Get<ReleaseFindResponse>(`api/release${query}`),
|
||||
findQuery: (offset?: number, limit?: number, filters?: Array<ReleaseFilter>) => {
|
||||
|
|
|
@ -227,4 +227,35 @@ export const PushStatusOptions: any[] = [
|
|||
label: "Error",
|
||||
value: "PUSH_ERROR"
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
export const NotificationTypeOptions: any[] = [
|
||||
{
|
||||
label: "Discord",
|
||||
value: "DISCORD",
|
||||
},
|
||||
];
|
||||
|
||||
export interface SelectOption {
|
||||
label: string;
|
||||
description: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export const EventOptions: SelectOption[] = [
|
||||
{
|
||||
label: "Push Rejected",
|
||||
value: "PUSH_REJECTED",
|
||||
description: "On push rejected for the arrs or download client",
|
||||
},
|
||||
{
|
||||
label: "Push Approved",
|
||||
value: "PUSH_APPROVED",
|
||||
description: "On push approved for the arrs or download client",
|
||||
},
|
||||
{
|
||||
label: "Push Error",
|
||||
value: "PUSH_ERROR",
|
||||
description: "On push error for the arrs or download client",
|
||||
},
|
||||
];
|
||||
|
|
435
web/src/forms/settings/NotifiactionForms.tsx
Normal file
435
web/src/forms/settings/NotifiactionForms.tsx
Normal file
|
@ -0,0 +1,435 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { Fragment } from "react";
|
||||
import {Field, Form, Formik} from "formik";
|
||||
import type {FieldProps} from "formik";
|
||||
import {XIcon} from "@heroicons/react/solid";
|
||||
import Select, {components} from "react-select";
|
||||
import {
|
||||
SwitchGroupWide,
|
||||
TextFieldWide
|
||||
} from "../../components/inputs";
|
||||
import DEBUG from "../../components/debug";
|
||||
import {EventOptions, NotificationTypeOptions} from "../../domain/constants";
|
||||
import {useMutation} from "react-query";
|
||||
import {APIClient} from "../../api/APIClient";
|
||||
import {queryClient} from "../../App";
|
||||
import {toast} from "react-hot-toast";
|
||||
import Toast from "../../components/notifications/Toast";
|
||||
import {SlideOver} from "../../components/panels";
|
||||
|
||||
|
||||
const Input = (props: any) => {
|
||||
return (
|
||||
<components.Input
|
||||
{...props}
|
||||
inputClassName="outline-none border-none shadow-none focus:ring-transparent"
|
||||
className="text-gray-400 dark:text-gray-100"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Control = (props: any) => {
|
||||
return (
|
||||
<components.Control
|
||||
{...props}
|
||||
className="p-1 block w-full dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:text-gray-100 sm:text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Menu = (props: any) => {
|
||||
return (
|
||||
<components.Menu
|
||||
{...props}
|
||||
className="dark:bg-gray-800 border border-gray-300 dark:border-gray-700 dark:text-gray-400 rounded-md shadow-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const Option = (props: any) => {
|
||||
return (
|
||||
<components.Option
|
||||
{...props}
|
||||
className="dark:text-gray-400 dark:bg-gray-800 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function FormFieldsDiscord() {
|
||||
return (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
{/*<div className="px-6 space-y-1">*/}
|
||||
{/* <Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Credentials</Dialog.Title>*/}
|
||||
{/* <p className="text-sm text-gray-500 dark:text-gray-400">*/}
|
||||
{/* Api keys etc*/}
|
||||
{/* </p>*/}
|
||||
{/*</div>*/}
|
||||
|
||||
<TextFieldWide
|
||||
name="webhook"
|
||||
label="Webhook URL"
|
||||
help="Discord channel webhook url"
|
||||
placeholder="https://discordapp.com/api/webhooks/xx/xx"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const componentMap: any = {
|
||||
DISCORD: <FormFieldsDiscord/>,
|
||||
};
|
||||
|
||||
interface NotificationAddFormValues {
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface AddProps {
|
||||
isOpen: boolean;
|
||||
toggle: any;
|
||||
}
|
||||
|
||||
export function NotificationAddForm({isOpen, toggle}: AddProps) {
|
||||
const mutation = useMutation(
|
||||
(notification: Notification) => APIClient.notifications.create(notification),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['notifications']);
|
||||
toast.custom((t) => <Toast type="success" body="Notification added!" t={t} />)
|
||||
toggle()
|
||||
},
|
||||
onError: () => {
|
||||
toast.custom((t) => <Toast type="error" body="Notification could not be added" t={t} />)
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const onSubmit = (formData: any) => {
|
||||
mutation.mutate(formData)
|
||||
}
|
||||
|
||||
const validate = (values: NotificationAddFormValues) => {
|
||||
const errors = {} as any;
|
||||
if (!values.name)
|
||||
errors.name = "Required";
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" static className="fixed inset-0 overflow-hidden" open={isOpen} onClose={toggle}>
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<Dialog.Overlay className="absolute inset-0"/>
|
||||
|
||||
<div className="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
enterFrom="translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transform transition ease-in-out duration-500 sm:duration-700"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="translate-x-full"
|
||||
>
|
||||
<div className="w-screen max-w-2xl dark:border-gray-700 border-l">
|
||||
<Formik
|
||||
enableReinitialize={true}
|
||||
initialValues={{
|
||||
enabled: true,
|
||||
type: "",
|
||||
name: "",
|
||||
webhook: "",
|
||||
events: [],
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
validate={validate}
|
||||
>
|
||||
{({values}) => (
|
||||
<Form className="h-full flex flex-col bg-white dark:bg-gray-800 shadow-xl overflow-y-scroll">
|
||||
<div className="flex-1">
|
||||
<div className="px-4 py-6 bg-gray-50 dark:bg-gray-900 sm:px-6">
|
||||
<div className="flex items-start justify-between space-x-3">
|
||||
<div className="space-y-1">
|
||||
<Dialog.Title className="text-lg font-medium text-gray-900 dark:text-white">Add
|
||||
Notifications</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-200">
|
||||
Trigger notifications on different events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="h-7 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white dark:bg-gray-700 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
onClick={toggle}
|
||||
>
|
||||
<span className="sr-only">Close panel</span>
|
||||
<XIcon className="h-6 w-6" aria-hidden="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextFieldWide name="name" label="Name" required={true}/>
|
||||
|
||||
<div className="space-y-4 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="py-4 flex items-center justify-between space-y-1 px-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 sm:py-5">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="type"
|
||||
className="block text-sm font-medium text-gray-900 dark:text-white"
|
||||
>
|
||||
Type
|
||||
</label>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Field name="type" type="select">
|
||||
{({
|
||||
field,
|
||||
form: {setFieldValue, resetForm}
|
||||
}: FieldProps) => (
|
||||
<Select {...field}
|
||||
isClearable={true}
|
||||
isSearchable={true}
|
||||
components={{Input, Control, Menu, Option}}
|
||||
placeholder="Choose a type"
|
||||
styles={{
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
color: "unset"
|
||||
})
|
||||
}}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
spacing: {
|
||||
...theme.spacing,
|
||||
controlHeight: 30,
|
||||
baseUnit: 2,
|
||||
}
|
||||
})}
|
||||
value={field?.value && field.value.value}
|
||||
onChange={(option: any) => {
|
||||
resetForm()
|
||||
// setFieldValue("name", option?.label ?? "")
|
||||
setFieldValue(field.name, option?.value ?? "")
|
||||
}}
|
||||
options={NotificationTypeOptions}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-6 px-6 space-y-6 sm:py-0 sm:space-y-0 sm:divide-y sm:divide-gray-200">
|
||||
<SwitchGroupWide name="enabled" label="Enabled"/>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 py-5">
|
||||
<div className="px-6 space-y-1">
|
||||
<Dialog.Title
|
||||
className="text-lg font-medium text-gray-900 dark:text-white">Events</Dialog.Title>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Select what events to trigger on
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 px-4 sm:space-y-0 sm:grid sm:gap-4 sm:px-6 sm:py-5">
|
||||
<EventCheckBoxes />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{componentMap[values.type]}
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 px-4 border-t border-gray-200 dark:border-gray-700 py-5 sm:px-6">
|
||||
<div className="space-x-3 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-white dark:bg-gray-700 py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
onClick={toggle}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-blue-500"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DEBUG values={values}/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
|
||||
</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: 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>
|
||||
<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: any) => {
|
||||
resetForm()
|
||||
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>
|
||||
)}
|
||||
</SlideOver>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import {CogIcon, CollectionIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
|
||||
import {BellIcon, CogIcon, CollectionIcon, DownloadIcon, KeyIcon} from '@heroicons/react/outline'
|
||||
import {NavLink, Route, Switch as RouteSwitch, useLocation, useRouteMatch} from "react-router-dom";
|
||||
|
||||
import { classNames } from "../utils";
|
||||
|
@ -8,17 +8,18 @@ import ApplicationSettings from "./settings/Application";
|
|||
import DownloadClientSettings from "./settings/DownloadClient";
|
||||
import { RegexPlayground } from './settings/RegexPlayground';
|
||||
import ReleaseSettings from "./settings/Releases";
|
||||
import NotificationSettings from "./settings/Notifications";
|
||||
|
||||
const subNavigation = [
|
||||
{name: 'Application', href: '', icon: CogIcon, current: true},
|
||||
{name: 'Indexers', href: 'indexers', icon: KeyIcon, current: false},
|
||||
{name: 'IRC', href: 'irc', icon: KeyIcon, current: false},
|
||||
{name: 'Clients', href: 'clients', icon: DownloadIcon, current: false},
|
||||
{name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
{name: 'Releases', href: 'releases', icon: CollectionIcon, current: false},
|
||||
// {name: 'Regex Playground', href: 'regex-playground', icon: CogIcon, current: false}
|
||||
// {name: 'Actions', href: 'actions', icon: PlayIcon, current: false},
|
||||
// {name: 'Rules', href: 'rules', icon: ClipboardCheckIcon, current: false},
|
||||
// {name: 'Notifications', href: 'notifications', icon: BellIcon, current: false},
|
||||
]
|
||||
|
||||
function SubNavLink({item, url}: any) {
|
||||
|
@ -93,6 +94,10 @@ export default function Settings() {
|
|||
<DownloadClientSettings/>
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/notifications`}>
|
||||
<NotificationSettings />
|
||||
</Route>
|
||||
|
||||
<Route path={`${url}/releases`}>
|
||||
<ReleaseSettings/>
|
||||
</Route>
|
||||
|
|
119
web/src/screens/settings/Notifications.tsx
Normal file
119
web/src/screens/settings/Notifications.tsx
Normal file
|
@ -0,0 +1,119 @@
|
|||
import { useQuery } from "react-query";
|
||||
import { APIClient } from "../../api/APIClient";
|
||||
import { EmptySimple } from "../../components/emptystates";
|
||||
import { useToggle } from "../../hooks/hooks";
|
||||
import { NotificationAddForm, NotificationUpdateForm } from "../../forms/settings/NotifiactionForms";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
function NotificationSettings() {
|
||||
const [addNotificationsIsOpen, toggleAddNotifications] = useToggle(false)
|
||||
|
||||
const { data } = useQuery<Notification[], Error>('notifications', APIClient.notifications.getAll,
|
||||
{
|
||||
refetchOnWindowFocus: false
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-200 lg:col-span-9">
|
||||
<NotificationAddForm isOpen={addNotificationsIsOpen} toggle={toggleAddNotifications} />
|
||||
|
||||
<div className="py-6 px-4 sm:p-6 lg:pb-8">
|
||||
<div className="-ml-4 -mt-4 flex justify-between items-center flex-wrap sm:flex-nowrap">
|
||||
<div className="ml-4 mt-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900 dark:text-white">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Send notifications on events
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-4 mt-4 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAddNotifications}
|
||||
className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 dark:bg-blue-600 hover:bg-indigo-700 dark:hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Add new
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data && data.length > 0 ?
|
||||
<section className="mt-6 light:bg-white dark:bg-gray-800 light:shadow sm:rounded-md">
|
||||
<ol className="min-w-full">
|
||||
<li className="grid grid-cols-12 gap-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="col-span-1 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Enabled</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</div>
|
||||
<div className="col-span-2 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</div>
|
||||
<div className="col-span-4 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Events</div>
|
||||
</li>
|
||||
|
||||
{data && data.map((n: Notification) => (
|
||||
<ListItem key={n.id} notification={n} />
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
: <EmptySimple title="No notifications setup" subtitle="Add a new notification" buttonText="New notification" buttonAction={toggleAddNotifications} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ListItemProps {
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
function ListItem({ notification }: ListItemProps) {
|
||||
const [updateFormIsOpen, toggleUpdateForm] = useToggle(false)
|
||||
|
||||
return (
|
||||
<li key={notification.id} className="text-gray-500 dark:text-gray-400">
|
||||
<NotificationUpdateForm isOpen={updateFormIsOpen} toggle={toggleUpdateForm} notification={notification} />
|
||||
|
||||
<div className="grid grid-cols-12 gap-4 items-center py-4">
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<Switch
|
||||
checked={notification.enabled}
|
||||
onChange={toggleUpdateForm}
|
||||
className={classNames(
|
||||
notification.enabled ? 'bg-teal-500 dark:bg-blue-500' : 'bg-gray-200 dark:bg-gray-600',
|
||||
'relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500'
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
notification.enabled ? 'translate-x-5' : 'translate-x-0',
|
||||
'inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.name}
|
||||
</div>
|
||||
<div className="col-span-2 flex items-center sm:px-6 ">
|
||||
{notification.type}
|
||||
</div>
|
||||
<div className="col-span-5 flex items-center sm:px-6 ">
|
||||
{notification.events.map((n, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="mr-2 inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-medium bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-400"
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center sm:px-6 ">
|
||||
<span className="text-indigo-600 dark:text-gray-300 hover:text-indigo-900 cursor-pointer" onClick={toggleUpdateForm}>
|
||||
Edit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationSettings;
|
10
web/src/types/Notification.d.ts
vendored
Normal file
10
web/src/types/Notification.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
type NotificationType = 'DISCORD';
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
type: NotificationType;
|
||||
events: string[];
|
||||
webhook: string;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue