Finish alerts implementation
This commit is contained in:
parent
72bf572981
commit
7e9625ecf3
|
|
@ -7,5 +7,9 @@
|
||||||
"path": "client"
|
"path": "client"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {}
|
"settings": {
|
||||||
|
"cSpell.words": [
|
||||||
|
"tgbotapi"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"prettier.useTabs": false,
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.insertSpaces": false
|
||||||
|
}
|
||||||
|
|
@ -13,14 +13,19 @@ export type AlertInfo = {
|
||||||
|
|
||||||
export const getAlerts = () => request<AlertInfo[]>('/api/alerts')
|
export const getAlerts = () => request<AlertInfo[]>('/api/alerts')
|
||||||
|
|
||||||
export const createAlert = (data: Omit<AlertInfo, 'id'>) =>
|
export const createAlert = (
|
||||||
|
data: Omit<AlertInfo, 'id' | 'lastStatus' | 'lastStatusAt'>
|
||||||
|
) =>
|
||||||
request<AlertInfo>('/api/alerts', {
|
request<AlertInfo>('/api/alerts', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ data }),
|
body: JSON.stringify(data),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateAlert = ({ id, ...body }: AlertInfo) =>
|
export const updateAlert = ({
|
||||||
|
id,
|
||||||
|
...body
|
||||||
|
}: Omit<AlertInfo, 'lastStatus' | 'lastStatusAt'>) =>
|
||||||
request<AlertInfo>(`/api/alerts/${id}`, {
|
request<AlertInfo>(`/api/alerts/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,14 @@ export type ContactPointInfo = {
|
||||||
export const getContactPoints = () =>
|
export const getContactPoints = () =>
|
||||||
request<ContactPointInfo[]>('/api/contact-points')
|
request<ContactPointInfo[]>('/api/contact-points')
|
||||||
|
|
||||||
export const createContactPoint = (data: {
|
export const createContactPoint = (data: Omit<ContactPointInfo, 'id'>) =>
|
||||||
name: string
|
|
||||||
type: string
|
|
||||||
config: string
|
|
||||||
}) =>
|
|
||||||
request<ContactPointInfo>('/api/contact-points', {
|
request<ContactPointInfo>('/api/contact-points', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({ data }),
|
body: JSON.stringify(data),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateContactPoint = ({
|
export const updateContactPoint = ({ id, ...body }: ContactPointInfo) =>
|
||||||
id,
|
|
||||||
...body
|
|
||||||
}: {
|
|
||||||
id: number
|
|
||||||
type: string
|
|
||||||
config: string
|
|
||||||
}) =>
|
|
||||||
request<ContactPointInfo>(`/api/contact-points/${id}`, {
|
request<ContactPointInfo>(`/api/contact-points/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
|
|
@ -41,3 +30,10 @@ export const deleteContactPoint = (id: number) =>
|
||||||
{ method: 'DELETE' },
|
{ method: 'DELETE' },
|
||||||
'void'
|
'void'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const testContactPoint = (body: { type: string; typeConfig: string }) =>
|
||||||
|
request<ContactPointInfo>(`/api/contact-points/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,7 @@
|
||||||
border-bottom: 1px solid var(--box-border-color);
|
border-bottom: 1px solid var(--box-border-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box-shadow {
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
.data-table {
|
||||||
|
border: 1px solid var(--box-border-color);
|
||||||
|
background-color: var(--box-bg-color);
|
||||||
|
|
||||||
|
.data-table-header,
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table-body {
|
||||||
|
.row {
|
||||||
|
border-bottom: 1px solid var(--box-border-color);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
.alerts-page .content {
|
||||||
|
max-width: 50rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
@import "themes/basic";
|
@import 'themes/basic';
|
||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
|
|
@ -38,14 +38,16 @@ a {
|
||||||
stroke: currentColor;
|
stroke: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "components/button";
|
@import 'components/button';
|
||||||
@import "components/input";
|
@import 'components/input';
|
||||||
@import "components/modal";
|
@import 'components/modal';
|
||||||
@import "components/form";
|
@import 'components/form';
|
||||||
@import "components/grid-sensors";
|
@import 'components/grid-sensors';
|
||||||
@import "components/user-layout";
|
@import 'components/user-layout';
|
||||||
@import "components/box";
|
@import 'components/box';
|
||||||
|
@import 'components/data-table';
|
||||||
|
|
||||||
@import "pages/login-page";
|
@import 'pages/login-page';
|
||||||
@import "pages/sensors-page";
|
@import 'pages/sensors-page';
|
||||||
@import "pages/dashboard-page";
|
@import 'pages/dashboard-page';
|
||||||
|
@import 'pages/alerts-page';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { cn } from '@/utils/cn'
|
||||||
|
import { ComponentChild } from 'preact'
|
||||||
|
|
||||||
|
type Props<TRow> = {
|
||||||
|
data: TRow[]
|
||||||
|
columns: DataTableColumn<TRow>[]
|
||||||
|
hideHeader?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type DataTableColumn<TRow> = {
|
||||||
|
key: keyof TRow | string
|
||||||
|
title?: ComponentChild
|
||||||
|
render: (row: TRow, rowIndex: number, colIndex: number) => ComponentChild
|
||||||
|
className?: string
|
||||||
|
} & ({ scale: number } | { width: string })
|
||||||
|
|
||||||
|
export const DataTable = <TRow,>({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
hideHeader,
|
||||||
|
}: Props<TRow>) => {
|
||||||
|
return (
|
||||||
|
<div className="data-table">
|
||||||
|
{!hideHeader && (
|
||||||
|
<div className="data-table-header">
|
||||||
|
{columns.map((col) => (
|
||||||
|
<div
|
||||||
|
key={col.key}
|
||||||
|
className={cn('col', 'head', col.className)}
|
||||||
|
style={
|
||||||
|
'width' in col
|
||||||
|
? { width: col.width, flexShrink: 0, flexGrow: 0 }
|
||||||
|
: { flex: col.scale }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{col.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="data-table-body">
|
||||||
|
{data.map((row, rowIndex) => (
|
||||||
|
<div className="row" key={rowIndex}>
|
||||||
|
{columns.map((col, colIndex) => (
|
||||||
|
<div
|
||||||
|
key={col.key}
|
||||||
|
className={cn('col', col.className)}
|
||||||
|
style={
|
||||||
|
'width' in col ? { width: col.width } : { flex: col.scale }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{col.render(row, rowIndex, colIndex)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,19 @@
|
||||||
import { PlusIcon } from '@/icons'
|
|
||||||
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
||||||
|
import { ContactPointsTable } from './components/ContactPointsTable/ContactPointsTable'
|
||||||
|
import { AlertsTable } from './components/AlertsTable/AlertsTable'
|
||||||
|
|
||||||
export const AlertsPage = () => {
|
export const AlertsPage = () => {
|
||||||
return (
|
return (
|
||||||
<UserLayout
|
<UserLayout
|
||||||
header={
|
header={
|
||||||
<div className="sensors-head">
|
<div className="alerts-head">
|
||||||
<div>Alerts</div>
|
<div>Alerts</div>
|
||||||
<button>
|
|
||||||
<PlusIcon /> Add alert
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
className="sensors-page"
|
className="alerts-page"
|
||||||
>
|
>
|
||||||
TEST
|
<ContactPointsTable />
|
||||||
|
<AlertsTable />
|
||||||
</UserLayout>
|
</UserLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { AlertInfo, deleteAlert, getAlerts } from '@/api/alerts'
|
||||||
|
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||||
|
import { DataTable } from '@/components/DataTable'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { useMutation, useQuery } from 'react-query'
|
||||||
|
import { AlertFormModal } from './components/AlertFormModal'
|
||||||
|
|
||||||
|
export const AlertsTable = () => {
|
||||||
|
const alerts = useQuery('/alerts', () => getAlerts())
|
||||||
|
|
||||||
|
const deleteMutation = useMutation(deleteAlert, {
|
||||||
|
onSuccess: () => {
|
||||||
|
alerts.refetch()
|
||||||
|
setShowDelete(undefined)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
const [edited, setEdited] = useState<AlertInfo>()
|
||||||
|
const [showDelete, setShowDelete] = useState<AlertInfo>()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="alerts">
|
||||||
|
<div className="section-title">
|
||||||
|
<h2>Alerts</h2>
|
||||||
|
<button onClick={() => alerts.refetch()}>Refresh</button>
|
||||||
|
<button onClick={() => setShowNew(true)}>Add alert</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box-shadow">
|
||||||
|
<DataTable
|
||||||
|
data={alerts.data ?? []}
|
||||||
|
columns={[
|
||||||
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
|
{
|
||||||
|
key: 'lastStatus',
|
||||||
|
title: 'Last status',
|
||||||
|
render: (c) => c.lastStatus,
|
||||||
|
width: '10rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
title: 'Actions',
|
||||||
|
width: '10rem',
|
||||||
|
className: 'actions',
|
||||||
|
render: (c) => (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setEdited(c)}>Edit</button>
|
||||||
|
<button onClick={() => setShowDelete(c)}>Delete</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(showNew || edited) && (
|
||||||
|
<AlertFormModal
|
||||||
|
open
|
||||||
|
alert={edited}
|
||||||
|
onClose={() => {
|
||||||
|
setEdited(undefined)
|
||||||
|
setShowNew(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDelete && (
|
||||||
|
<ConfirmModal
|
||||||
|
open
|
||||||
|
onConfirm={() => deleteMutation.mutate(showDelete.id)}
|
||||||
|
onCancel={() => setShowDelete(undefined)}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete {showDelete.name} alert?
|
||||||
|
</ConfirmModal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
import { AlertInfo, createAlert, updateAlert } from '@/api/alerts'
|
||||||
|
import { getContactPoints } from '@/api/contactPoints'
|
||||||
|
import { getSensors } from '@/api/sensors'
|
||||||
|
import { Modal } from '@/components/Modal'
|
||||||
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
|
import { tryParseAlertCondition } from '@/utils/tryParseAlertCondition'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
alert?: AlertInfo
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlertFormModal = ({ alert, open, onClose }: Props) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const createMutation = useMutation(createAlert)
|
||||||
|
const updateMutation = useMutation(updateAlert)
|
||||||
|
|
||||||
|
const contactPoints = useQuery('/contact-points', () => getContactPoints())
|
||||||
|
const sensors = useQuery('/sensors', () => getSensors())
|
||||||
|
|
||||||
|
const isLoading = createMutation.isLoading || updateMutation.isLoading
|
||||||
|
|
||||||
|
const parsedCondition = alert && tryParseAlertCondition(alert.condition)
|
||||||
|
|
||||||
|
const { handleSubmit, register, watch } = useForm({
|
||||||
|
defaultValue: () => ({
|
||||||
|
name: '',
|
||||||
|
customMessage: '',
|
||||||
|
triggerInterval: 0,
|
||||||
|
...(alert && {
|
||||||
|
name: alert.name,
|
||||||
|
contactPointId: alert.contactPointId,
|
||||||
|
customMessage: alert.customMessage,
|
||||||
|
triggerInterval: alert.triggerInterval,
|
||||||
|
}),
|
||||||
|
...(parsedCondition && {
|
||||||
|
conditionType: parsedCondition.type,
|
||||||
|
...(parsedCondition.type === 'sensor_value' && {
|
||||||
|
sensorValueSensorId: parsedCondition.sensorId,
|
||||||
|
sensorValueCondition: parsedCondition.condition,
|
||||||
|
sensorValueValue: parsedCondition.value,
|
||||||
|
}),
|
||||||
|
...(parsedCondition.type === 'sensor_last_contact' && {
|
||||||
|
sensorLastContactSensorId: parsedCondition.sensorId,
|
||||||
|
sensorLastContactValue: parsedCondition.value,
|
||||||
|
sensorLastContactValueUnit: parsedCondition.valueUnit,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onSubmit: async (v) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!v.contactPointId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConditionData = () => {
|
||||||
|
switch (v.conditionType) {
|
||||||
|
case 'sensor_value':
|
||||||
|
return {
|
||||||
|
type: 'sensor_value',
|
||||||
|
sensorId: v.sensorValueSensorId,
|
||||||
|
condition: v.sensorValueCondition,
|
||||||
|
value: v.sensorValueValue,
|
||||||
|
}
|
||||||
|
case 'sensor_last_contact':
|
||||||
|
return {
|
||||||
|
type: 'sensor_last_contact',
|
||||||
|
sensorId: v.sensorLastContactSensorId,
|
||||||
|
value: v.sensorLastContactValue,
|
||||||
|
valueUnit: v.sensorLastContactValueUnit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const condition = JSON.stringify(getConditionData())
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: v.name,
|
||||||
|
contactPointId: v.contactPointId,
|
||||||
|
customMessage: v.customMessage,
|
||||||
|
triggerInterval: v.triggerInterval,
|
||||||
|
condition,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert) {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
id: alert.id,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries('/alerts')
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const conditionType = watch('conditionType')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="input">
|
||||||
|
<label>Name</label>
|
||||||
|
<input required minLength={1} autoFocus {...register('name')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Contact Point</label>
|
||||||
|
<select required {...register('contactPointId', { type: 'integer' })}>
|
||||||
|
{contactPoints.data?.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Custom Message</label>
|
||||||
|
<textarea {...register('customMessage')} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Trigger After (seconds)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
required
|
||||||
|
{...register('triggerInterval', { type: 'integer' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Condition Type</label>
|
||||||
|
<select required {...register('conditionType')}>
|
||||||
|
<option value="sensor_value">Sensor Value</option>
|
||||||
|
<option value="sensor_last_contact">Sensor Last Contact</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{conditionType === 'sensor_value' && (
|
||||||
|
<>
|
||||||
|
<div className="input">
|
||||||
|
<label>Sensor</label>
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
{...register('sensorValueSensorId', { type: 'integer' })}
|
||||||
|
>
|
||||||
|
{sensors.data?.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Condition</label>
|
||||||
|
<select required {...register('sensorValueCondition')}>
|
||||||
|
<option value="less">Less</option>
|
||||||
|
<option value="more">More</option>
|
||||||
|
<option value="equal">Equal</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Value</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
{...register('sensorValueValue', { type: 'number' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{conditionType === 'sensor_last_contact' && (
|
||||||
|
<>
|
||||||
|
<div className="input">
|
||||||
|
<label>Sensor</label>
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
{...register('sensorLastContactSensorId', { type: 'integer' })}
|
||||||
|
>
|
||||||
|
{sensors.data?.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Value</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
{...register('sensorLastContactValue', { type: 'number' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Value Unit</label>
|
||||||
|
<select required {...register('sensorLastContactValueUnit')}>
|
||||||
|
<option value="s">Seconds</option>
|
||||||
|
<option value="m">Minutes</option>
|
||||||
|
<option value="h">Hours</option>
|
||||||
|
<option value="d">Days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<button className="cancel" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button disabled={isLoading}>{alert ? 'Update' : 'Create'}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import {
|
||||||
|
ContactPointInfo,
|
||||||
|
deleteContactPoint,
|
||||||
|
getContactPoints,
|
||||||
|
} from '@/api/contactPoints'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { useMutation, useQuery } from 'react-query'
|
||||||
|
import { ContactPointFormModal } from './components/ContactPointFormModal'
|
||||||
|
import { DataTable } from '@/components/DataTable'
|
||||||
|
import { ConfirmModal } from '@/components/ConfirmModal'
|
||||||
|
|
||||||
|
export const ContactPointsTable = () => {
|
||||||
|
const contactPoints = useQuery('/contact-points', () => getContactPoints())
|
||||||
|
|
||||||
|
const deleteMutation = useMutation(deleteContactPoint, {
|
||||||
|
onSuccess: () => {
|
||||||
|
contactPoints.refetch()
|
||||||
|
setShowDelete(undefined)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
const [edited, setEdited] = useState<ContactPointInfo>()
|
||||||
|
const [showDelete, setShowDelete] = useState<ContactPointInfo>()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="contact-points">
|
||||||
|
<div className="section-title">
|
||||||
|
<h2>Contact Points</h2>
|
||||||
|
<button onClick={() => setShowNew(true)}>Add contact point</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box-shadow">
|
||||||
|
<DataTable
|
||||||
|
data={contactPoints.data ?? []}
|
||||||
|
columns={[
|
||||||
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
|
{
|
||||||
|
key: 'type',
|
||||||
|
title: 'Type',
|
||||||
|
render: (c) => c.type,
|
||||||
|
width: '5rem',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
title: 'Actions',
|
||||||
|
width: '10rem',
|
||||||
|
className: 'actions',
|
||||||
|
render: (c) => (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setEdited(c)}>Edit</button>
|
||||||
|
<button onClick={() => setShowDelete(c)}>Delete</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(showNew || edited) && (
|
||||||
|
<ContactPointFormModal
|
||||||
|
open
|
||||||
|
contactPoint={edited}
|
||||||
|
onClose={() => {
|
||||||
|
setShowNew(false)
|
||||||
|
setEdited(undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDelete && (
|
||||||
|
<ConfirmModal
|
||||||
|
open
|
||||||
|
onConfirm={() => deleteMutation.mutate(showDelete.id)}
|
||||||
|
onCancel={() => setShowDelete(undefined)}
|
||||||
|
confirmText="Delete"
|
||||||
|
>
|
||||||
|
Do you want to delete {showDelete.name} contact point?
|
||||||
|
</ConfirmModal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import {
|
||||||
|
ContactPointInfo,
|
||||||
|
createContactPoint,
|
||||||
|
testContactPoint,
|
||||||
|
updateContactPoint,
|
||||||
|
} from '@/api/contactPoints'
|
||||||
|
import { Modal } from '@/components/Modal'
|
||||||
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
|
import { tryParseContactPointConfig } from '@/utils/tryParseContactPointConfig'
|
||||||
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contactPoint?: ContactPointInfo
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
telegramApiKey?: string
|
||||||
|
telegramTargetChannel?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ContactPointFormModal = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
contactPoint,
|
||||||
|
}: Props) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const createMutation = useMutation(createContactPoint)
|
||||||
|
const updateMutation = useMutation(updateContactPoint)
|
||||||
|
const testMutation = useMutation(testContactPoint)
|
||||||
|
|
||||||
|
const isLoading = createMutation.isLoading || updateMutation.isLoading
|
||||||
|
|
||||||
|
const parsedTypeConfig =
|
||||||
|
contactPoint && tryParseContactPointConfig(contactPoint)
|
||||||
|
|
||||||
|
const getTypeConfig = (v: FormValues) => {
|
||||||
|
if (v.type === 'telegram') {
|
||||||
|
return {
|
||||||
|
apiKey: v.telegramApiKey,
|
||||||
|
targetChannel: v.telegramTargetChannel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handleSubmit, register, watch, getValues } = useForm({
|
||||||
|
defaultValue: () => ({
|
||||||
|
name: contactPoint?.name ?? '',
|
||||||
|
type: contactPoint?.type ?? 'telegram',
|
||||||
|
...(parsedTypeConfig?.type === 'telegram' && {
|
||||||
|
telegramApiKey: parsedTypeConfig.apiKey ?? '',
|
||||||
|
telegramTargetChannel: parsedTypeConfig.targetChannel,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onSubmit: async (v) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeConfig = JSON.stringify(getTypeConfig(v))
|
||||||
|
|
||||||
|
if (contactPoint) {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
id: contactPoint.id,
|
||||||
|
name: v.name,
|
||||||
|
type: v.type,
|
||||||
|
typeConfig: typeConfig,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
name: v.name,
|
||||||
|
type: v.type,
|
||||||
|
typeConfig: typeConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries('/contact-points')
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const type = watch('type')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="input">
|
||||||
|
<label>Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
minLength={1}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
{...register('name')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Type</label>
|
||||||
|
<select required {...register('type')}>
|
||||||
|
<option value="telegram">Telegram</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type === 'telegram' && (
|
||||||
|
<>
|
||||||
|
<div className="input">
|
||||||
|
<label>API Key</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
minLength={1}
|
||||||
|
required
|
||||||
|
{...register('telegramApiKey')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input">
|
||||||
|
<label>Target Channel</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
minLength={1}
|
||||||
|
required
|
||||||
|
{...register('telegramTargetChannel', { type: 'integer' })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<button
|
||||||
|
className="test"
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
testMutation.mutate({
|
||||||
|
type: getValues().type,
|
||||||
|
typeConfig: JSON.stringify(getTypeConfig(getValues())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
<button className="cancel" type="button" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button disabled={isLoading}>Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,391 @@
|
||||||
|
import { ComponentChild, createContext } from 'preact'
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'preact/hooks'
|
||||||
|
import { ConditionalKeys } from 'type-fest'
|
||||||
|
|
||||||
|
type FormValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| Array<string | number | boolean>
|
||||||
|
|
||||||
|
type BaseValue = Record<string, FormValue>
|
||||||
|
|
||||||
|
type Props<TValue extends BaseValue> = {
|
||||||
|
onSubmit?: (value: TValue) => void
|
||||||
|
onChange?: (value: TValue) => void
|
||||||
|
defaultValue: TValue | (() => TValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReferencedElement =
|
||||||
|
| HTMLInputElement
|
||||||
|
| HTMLSelectElement
|
||||||
|
| HTMLTextAreaElement
|
||||||
|
| null
|
||||||
|
|
||||||
|
type InternalState<TValue extends BaseValue> = {
|
||||||
|
onSubmit?: (value: TValue) => void
|
||||||
|
onChange?: (value: TValue) => void
|
||||||
|
subscribers: ((value: TValue) => void)[]
|
||||||
|
value: TValue
|
||||||
|
refs: Record<string, ReferencedElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputOptions = {
|
||||||
|
type?: 'string' | 'number' | 'integer' | 'boolean'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegisterCallback<TValue extends BaseValue = BaseValue> = <
|
||||||
|
TName extends Extract<keyof TValue, string>
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
options?: InputOptions
|
||||||
|
) => {
|
||||||
|
name: TName
|
||||||
|
ref: (ref: ReferencedElement) => void
|
||||||
|
onChange: (e: Event) => void
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RegisterArrayCallback<TValue extends BaseValue = BaseValue> = <
|
||||||
|
TName extends ConditionalKeys<TValue, Array<unknown>>,
|
||||||
|
TArrayValue = TValue[TName] extends Array<infer T> ? T : unknown
|
||||||
|
>(
|
||||||
|
name: TName
|
||||||
|
) => {
|
||||||
|
register: (index: number) => ReturnType<RegisterCallback>
|
||||||
|
append: (value: TArrayValue) => number
|
||||||
|
remove: (index: number) => void
|
||||||
|
values: TArrayValue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WatchCallback<TValue extends BaseValue = BaseValue> = <
|
||||||
|
TName extends Extract<keyof TValue, string>
|
||||||
|
>(
|
||||||
|
name: TName
|
||||||
|
) => TValue[TName]
|
||||||
|
|
||||||
|
export type SetValueCallback<TValue extends BaseValue = BaseValue> = <
|
||||||
|
TName extends Extract<keyof TValue, string>
|
||||||
|
>(
|
||||||
|
name: TName,
|
||||||
|
value: TValue[TName],
|
||||||
|
skipUpdate?: boolean
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export type GetValues<TValue extends BaseValue = BaseValue> = () => TValue
|
||||||
|
|
||||||
|
export type FormInterface<TValue extends BaseValue = BaseValue> = {
|
||||||
|
register: RegisterCallback<TValue>
|
||||||
|
registerArray: RegisterArrayCallback<TValue>
|
||||||
|
watch: WatchCallback<TValue>
|
||||||
|
setValue: SetValueCallback<TValue>
|
||||||
|
handleSubmit: (e: Event) => void
|
||||||
|
getValues: GetValues<TValue>
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormContext = createContext<FormInterface | null>(null)
|
||||||
|
|
||||||
|
const INDEXED_NAME_REGEX = /(.*)\[[0-9]+\]/
|
||||||
|
|
||||||
|
export const useForm = <TValue extends BaseValue = BaseValue>({
|
||||||
|
defaultValue,
|
||||||
|
onSubmit,
|
||||||
|
onChange,
|
||||||
|
}: Props<TValue>) => {
|
||||||
|
const firstRenderRef = useRef(true)
|
||||||
|
|
||||||
|
const internalRef = useRef(
|
||||||
|
(firstRenderRef.current
|
||||||
|
? {
|
||||||
|
onSubmit,
|
||||||
|
onChange,
|
||||||
|
subscribers: [],
|
||||||
|
value:
|
||||||
|
typeof defaultValue === 'function'
|
||||||
|
? (defaultValue as () => TValue)()
|
||||||
|
: defaultValue,
|
||||||
|
refs: {},
|
||||||
|
}
|
||||||
|
: {}) as InternalState<TValue>
|
||||||
|
)
|
||||||
|
|
||||||
|
internalRef.current = {
|
||||||
|
onSubmit,
|
||||||
|
onChange,
|
||||||
|
subscribers: internalRef.current.subscribers,
|
||||||
|
value: internalRef.current.value,
|
||||||
|
refs: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
firstRenderRef.current = false
|
||||||
|
|
||||||
|
const getCurrentValueByName = useCallback((name: string): FormValue => {
|
||||||
|
const current = internalRef.current.value
|
||||||
|
|
||||||
|
if (name.includes('[')) {
|
||||||
|
const match = INDEXED_NAME_REGEX.exec(name)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const arrayValue = current[match[1]] as any[]
|
||||||
|
|
||||||
|
return arrayValue?.[+match[2]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current[name]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setCurrentValueByName = useCallback(
|
||||||
|
(name: string, value: FormValue) => {
|
||||||
|
const current = internalRef.current.value
|
||||||
|
|
||||||
|
if (name.includes('[')) {
|
||||||
|
const match = INDEXED_NAME_REGEX.exec(name)
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let arrayValue = current[match[1]] as any[]
|
||||||
|
|
||||||
|
if (!Array.isArray(arrayValue)) {
|
||||||
|
arrayValue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
arrayValue[+match[2]] = value
|
||||||
|
|
||||||
|
internalRef.current.value = {
|
||||||
|
...internalRef.current.value,
|
||||||
|
[match[1]]: arrayValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internalRef.current.value = {
|
||||||
|
...internalRef.current.value,
|
||||||
|
[name]: value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setRefValue = useCallback((name: string, value: FormValue) => {
|
||||||
|
const ref = internalRef.current.refs[name]
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
if (ref.type === 'checkbox') {
|
||||||
|
const input = ref as HTMLInputElement
|
||||||
|
input.checked = !!value
|
||||||
|
} else {
|
||||||
|
ref.value = String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleInputRef = useCallback(
|
||||||
|
(
|
||||||
|
ref: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | null
|
||||||
|
) => {
|
||||||
|
if (ref) {
|
||||||
|
const name = ref.name
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
console.warn('Element has no name!', ref)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const predefinedValue = getCurrentValueByName(name)
|
||||||
|
|
||||||
|
internalRef.current.refs[name] = ref
|
||||||
|
|
||||||
|
if (predefinedValue !== undefined && predefinedValue !== null) {
|
||||||
|
setRefValue(name, predefinedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const setValue: SetValueCallback<TValue> = useCallback(
|
||||||
|
<TName extends Extract<keyof TValue, string>>(
|
||||||
|
name: TName,
|
||||||
|
value: TValue[TName],
|
||||||
|
skipUpdate = false
|
||||||
|
) => {
|
||||||
|
setCurrentValueByName(name, value)
|
||||||
|
|
||||||
|
if (!skipUpdate) {
|
||||||
|
setRefValue(name as string, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
internalRef.current.onChange?.(internalRef.current.value)
|
||||||
|
|
||||||
|
internalRef.current.subscribers.forEach((s) =>
|
||||||
|
s(internalRef.current.value)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(e: Event, options?: InputOptions) => {
|
||||||
|
const target = e.target as ReferencedElement
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: FormValue = target.value
|
||||||
|
|
||||||
|
if (target instanceof HTMLInputElement && target.type == 'checkbox') {
|
||||||
|
value = target.checked
|
||||||
|
} else if (options?.type === 'integer') {
|
||||||
|
const parsed = parseInt(value, 10)
|
||||||
|
value = isNaN(parsed) ? null : parsed
|
||||||
|
} else if (options?.type === 'number') {
|
||||||
|
const parsed = parseFloat(value)
|
||||||
|
value = isNaN(parsed) ? null : parsed
|
||||||
|
} else if (options?.type === 'boolean') {
|
||||||
|
value = target.value === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
setValue(target.name as any, value as any, true)
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
)
|
||||||
|
|
||||||
|
const register: RegisterCallback<TValue> = useCallback(
|
||||||
|
<TName extends Extract<keyof TValue, string>>(
|
||||||
|
name: TName,
|
||||||
|
options?: InputOptions
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
ref: handleInputRef,
|
||||||
|
onChange: (e: Event) => handleInputChange(e, options),
|
||||||
|
value: `${internalRef.current.value[name] ?? ''}`,
|
||||||
|
...(options?.type === 'boolean' && {
|
||||||
|
value: 'true',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleInputChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const registerArray: RegisterArrayCallback<TValue> = useCallback(
|
||||||
|
<
|
||||||
|
TName extends ConditionalKeys<TValue, Array<unknown>>,
|
||||||
|
TArrayValue = TValue[TName] extends Array<infer T> ? T : unknown
|
||||||
|
>(
|
||||||
|
name: TName
|
||||||
|
) => {
|
||||||
|
const [values, setValues] = useState([] as TArrayValue[])
|
||||||
|
|
||||||
|
return {
|
||||||
|
register: (index: number, options?: InputOptions) => {
|
||||||
|
return {
|
||||||
|
name: `${String(name)}[${index}]`,
|
||||||
|
ref: handleInputRef,
|
||||||
|
onChange: (e: Event) => handleInputChange(e, options),
|
||||||
|
value: `${internalRef.current.value[name] ?? ''}`,
|
||||||
|
...(options?.type === 'boolean' && {
|
||||||
|
value: 'true',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
append: (value: TArrayValue) => {
|
||||||
|
setValues((v) => [...v, value])
|
||||||
|
|
||||||
|
return values.length
|
||||||
|
},
|
||||||
|
remove: (index: number) => {
|
||||||
|
setValues((v) => {
|
||||||
|
const res = v.slice()
|
||||||
|
res.splice(index, 1)
|
||||||
|
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
},
|
||||||
|
values,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[register]
|
||||||
|
)
|
||||||
|
|
||||||
|
const watch: WatchCallback<TValue> = useCallback(
|
||||||
|
<TName extends keyof TValue>(name: TName) => {
|
||||||
|
const [value, setValue] = useState<FormValue>(
|
||||||
|
internalRef.current.value[name] ?? ''
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cb = (v: TValue) => setValue(v[name])
|
||||||
|
|
||||||
|
internalRef.current.subscribers.push(cb)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const index = internalRef.current.subscribers.indexOf(cb)
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
internalRef.current.subscribers.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return value as NonNullable<TValue[TName]>
|
||||||
|
},
|
||||||
|
[handleInputChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getValues: GetValues<TValue> = useCallback(
|
||||||
|
() => internalRef.current.value,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSubmit = useCallback((e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
internalRef.current.onSubmit?.(internalRef.current.value)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const formInterface = useMemo(() => {
|
||||||
|
const ctx = {
|
||||||
|
register,
|
||||||
|
registerArray,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
handleSubmit,
|
||||||
|
getValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContextProvider = ({ children }: { children: ComponentChild }) => (
|
||||||
|
<FormContext.Provider value={ctx as unknown as FormInterface<BaseValue>}>
|
||||||
|
{children}
|
||||||
|
</FormContext.Provider>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ...ctx, ContextProvider }
|
||||||
|
}, [register, registerArray, watch, setValue, handleSubmit])
|
||||||
|
|
||||||
|
return formInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFormContext = <TValues extends BaseValue = BaseValue>() => {
|
||||||
|
const form = useContext(FormContext) as FormInterface<TValues> | null
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
throw new Error('Failed to load context')
|
||||||
|
}
|
||||||
|
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { tryParseJson } from './tryParseJson'
|
||||||
|
|
||||||
|
type SensorValueAlertCondition = {
|
||||||
|
type: 'sensor_value'
|
||||||
|
sensorId: number
|
||||||
|
condition: 'less' | 'more' | 'equal'
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type SensorLastContactAlertCondition = {
|
||||||
|
type: 'sensor_last_contact'
|
||||||
|
sensorId: number
|
||||||
|
value: number
|
||||||
|
valueUnit: 's' | 'm' | 'h' | 'd'
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertCondition =
|
||||||
|
| SensorValueAlertCondition
|
||||||
|
| SensorLastContactAlertCondition
|
||||||
|
|
||||||
|
export const tryParseAlertCondition = (
|
||||||
|
condition: string
|
||||||
|
): AlertCondition | null => {
|
||||||
|
const data = tryParseJson(condition)
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'sensor_value') {
|
||||||
|
return data as SensorValueAlertCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.type === 'sensor_last_contact') {
|
||||||
|
return data as SensorLastContactAlertCondition
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { ContactPointInfo } from '@/api/contactPoints'
|
||||||
|
import { tryParseJson } from './tryParseJson'
|
||||||
|
|
||||||
|
type ContactPointTelegramConfig = {
|
||||||
|
type: 'telegram'
|
||||||
|
apiKey: string
|
||||||
|
targetChannel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParsedContactPointConfig = ContactPointTelegramConfig
|
||||||
|
|
||||||
|
export const tryParseContactPointConfig = (
|
||||||
|
contactPoint: ContactPointInfo
|
||||||
|
): ParsedContactPointConfig | null => {
|
||||||
|
const data = tryParseJson(contactPoint.typeConfig)
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contactPoint.type === 'telegram') {
|
||||||
|
return {
|
||||||
|
type: 'telegram',
|
||||||
|
apiKey: data.apiKey,
|
||||||
|
targetChannel: data.targetChannel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const tryParseJson = (json: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(json)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
.env
|
.env
|
||||||
basic-sensor-receiver.exe
|
basic-sensor-receiver.exe
|
||||||
basic-sensor-receiver
|
basic-sensor-receiver
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
tmp/
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func (s *Server) StartAlerts() {
|
func (s *Server) StartAlerts() {
|
||||||
ticker := time.NewTicker(time.Second * 10)
|
ticker := time.NewTicker(time.Second * 10)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
s.Services.Alerts.EvaluateAlerts()
|
err := s.Services.Alerts.EvaluateAlerts()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error evaluating alerts: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
<-ticker.C
|
<-ticker.C
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package integrations
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
|
@ -40,12 +41,16 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri
|
||||||
text = data.Alert.CustomMessage
|
text = data.Alert.CustomMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
text = strings.Replace(text, "{value}", fmt.Sprintf("%f", data.LastValue), -1)
|
text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1)
|
||||||
|
|
||||||
msg := tgbotapi.NewMessage(config.TargetChannel, text)
|
msg := tgbotapi.NewMessage(config.TargetChannel, text)
|
||||||
msg.ParseMode = "Markdown"
|
msg.ParseMode = "Markdown"
|
||||||
_, err = bot.Send(msg)
|
_, err = bot.Send(msg)
|
||||||
return err
|
return err
|
||||||
|
case ContactPointEventTest:
|
||||||
|
msg := tgbotapi.NewMessage(config.TargetChannel, "Test message from Basic Sensor Receiver")
|
||||||
|
_, err = bot.Send(msg)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ func main() {
|
||||||
loginProtected.GET("/api/contact-points/:contactPointId", routes.GetContactPoint(server))
|
loginProtected.GET("/api/contact-points/:contactPointId", routes.GetContactPoint(server))
|
||||||
loginProtected.PUT("/api/contact-points/:contactPointId", routes.PutContactPoint(server))
|
loginProtected.PUT("/api/contact-points/:contactPointId", routes.PutContactPoint(server))
|
||||||
loginProtected.DELETE("/api/contact-points/:contactPointId", routes.DeleteContactPoint(server))
|
loginProtected.DELETE("/api/contact-points/:contactPointId", routes.DeleteContactPoint(server))
|
||||||
|
loginProtected.POST("/api/contact-points/test", routes.TestContactPoint(server))
|
||||||
loginProtected.POST("/api/logout", routes.Logout(server))
|
loginProtected.POST("/api/logout", routes.Logout(server))
|
||||||
|
|
||||||
// Routes accessible using auth key
|
// Routes accessible using auth key
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ type AlertConditionSensorValue struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertConditionSensorLastContact struct {
|
type AlertConditionSensorLastContact struct {
|
||||||
SensorId int64 `json:"sensorId"`
|
SensorId int64 `json:"sensorId"`
|
||||||
Value int64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
ValueUnit string `json:"valueUnit"`
|
ValueUnit string `json:"valueUnit"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type postAlertsBody struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
TriggerInterval int64 `json:"triggerInterval"`
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
|
CustomMessage string `json:"customMessage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type putAlertsBody struct {
|
type putAlertsBody struct {
|
||||||
|
|
@ -20,6 +21,7 @@ type putAlertsBody struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Condition string `json:"condition"`
|
Condition string `json:"condition"`
|
||||||
TriggerInterval int64 `json:"triggerInterval"`
|
TriggerInterval int64 `json:"triggerInterval"`
|
||||||
|
CustomMessage string `json:"customMessage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlerts(s *app.Server) gin.HandlerFunc {
|
func GetAlerts(s *app.Server) gin.HandlerFunc {
|
||||||
|
|
@ -44,7 +46,7 @@ func PostAlerts(s *app.Server) gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alert, err := s.Services.Alerts.Create(body.ContactPointId, body.Name, body.Condition, body.TriggerInterval)
|
alert, err := s.Services.Alerts.Create(body.ContactPointId, body.Name, body.Condition, body.TriggerInterval, body.CustomMessage)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
|
@ -71,7 +73,7 @@ func PutAlert(s *app.Server) gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alert, err := s.Services.Alerts.Update(alertId, body.ContactPointId, body.Name, body.Condition, body.TriggerInterval)
|
alert, err := s.Services.Alerts.Update(alertId, body.ContactPointId, body.Name, body.Condition, body.TriggerInterval, body.CustomMessage)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(http.StatusInternalServerError, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ type putContactPointsBody struct {
|
||||||
TypeConfig string `json:"typeConfig"`
|
TypeConfig string `json:"typeConfig"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testContactPointBody struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
TypeConfig string `json:"typeConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
func GetContactPoints(s *app.Server) gin.HandlerFunc {
|
func GetContactPoints(s *app.Server) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
contactPoints, err := s.Services.ContactPoints.GetList()
|
contactPoints, err := s.Services.ContactPoints.GetList()
|
||||||
|
|
@ -116,7 +121,27 @@ func DeleteContactPoint(s *app.Server) gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{})
|
c.AbortWithStatus(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContactPoint(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
body := testContactPointBody{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Services.ContactPoints.Test(body.Type, body.TypeConfig)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatus(http.StatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ func (s *AlertsService) EvaluateAlerts() error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unitToSeconds(unit string) int64 {
|
func unitToSeconds(unit string) float64 {
|
||||||
switch unit {
|
switch unit {
|
||||||
case "s":
|
case "s":
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -76,7 +76,7 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
||||||
case "sensor_value":
|
case "sensor_value":
|
||||||
{
|
{
|
||||||
sensorValueCondition = models.AlertConditionSensorValue{
|
sensorValueCondition = models.AlertConditionSensorValue{
|
||||||
SensorId: condition["sensorId"].(int64),
|
SensorId: int64(condition["sensorId"].(float64)),
|
||||||
Condition: condition["condition"].(string),
|
Condition: condition["condition"].(string),
|
||||||
Value: condition["value"].(float64),
|
Value: condition["value"].(float64),
|
||||||
}
|
}
|
||||||
|
|
@ -97,8 +97,8 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
||||||
case "sensor_last_contact":
|
case "sensor_last_contact":
|
||||||
{
|
{
|
||||||
sensorLastContactCondition := models.AlertConditionSensorLastContact{
|
sensorLastContactCondition := models.AlertConditionSensorLastContact{
|
||||||
SensorId: condition["sensorId"].(int64),
|
SensorId: int64(condition["sensorId"].(float64)),
|
||||||
Value: condition["value"].(int64),
|
Value: condition["value"].(float64),
|
||||||
ValueUnit: condition["valueUnit"].(string),
|
ValueUnit: condition["valueUnit"].(string),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +109,7 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conditionInSec := sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit)
|
conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit))
|
||||||
|
|
||||||
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec
|
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec
|
||||||
|
|
||||||
|
|
@ -119,46 +119,56 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if conditionMet {
|
if conditionMet {
|
||||||
if newStatus == "good" {
|
if alert.LastStatus == "good" {
|
||||||
newStatus = "alerting"
|
newStatus = "pending"
|
||||||
} else if newStatus == "pending" {
|
} else if alert.LastStatus == "pending" {
|
||||||
if time.Now().Unix()-alert.LastStatusAt > alert.TriggerInterval {
|
if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval {
|
||||||
newStatus = "alerting"
|
newStatus = "alerting"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if alert.LastStatus == "alerting" {
|
||||||
|
newStatus = "pending"
|
||||||
|
} else if alert.LastStatus == "pending" {
|
||||||
|
if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval {
|
||||||
|
newStatus = "good"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newStatus != alert.LastStatus {
|
if newStatus != alert.LastStatus {
|
||||||
s.ctx.DB.Exec("UPDATE alerts SET last_status = ?, last_status_at = ? WHERE id = ?", newStatus, time.Now().Unix(), alert.Id)
|
s.ctx.DB.Exec("UPDATE alerts SET last_status = ?, last_status_at = ? WHERE id = ?", newStatus, time.Now().Unix(), alert.Id)
|
||||||
|
|
||||||
sensor, err := s.ctx.Services.Sensors.GetById(sensorId)
|
if newStatus == "alerting" {
|
||||||
if err != nil {
|
sensor, err := s.ctx.Services.Sensors.GetById(sensorId)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId)
|
contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchService, err := contactPoint.getService(s.ctx)
|
dispatchService, err := contactPoint.getService(s.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{
|
err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{
|
||||||
Type: "alert",
|
Type: integrations.ContactPointEventAlertTriggered,
|
||||||
AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{
|
AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{
|
||||||
Alert: alert,
|
Alert: alert,
|
||||||
Sensor: sensor,
|
Sensor: sensor,
|
||||||
SensorValueCondition: &sensorValueCondition,
|
SensorValueCondition: &sensorValueCondition,
|
||||||
SensorLastContactCondition: &sensorLastContactCondition,
|
SensorLastContactCondition: &sensorLastContactCondition,
|
||||||
LastValue: lastValue,
|
LastValue: lastValue,
|
||||||
},
|
},
|
||||||
}, contactPoint.TypeConfig)
|
}, contactPoint.TypeConfig)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,7 +176,7 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertsService) GetList() ([]*models.AlertItem, error) {
|
func (s *AlertsService) GetList() ([]*models.AlertItem, error) {
|
||||||
rows, err := s.ctx.DB.Query("SELECT id, name, condition, trigger_interval, last_status, last_status_at FROM alerts ORDER BY name ASC")
|
rows, err := s.ctx.DB.Query("SELECT id, contact_point_id, name, condition, trigger_interval, last_status, last_status_at FROM alerts ORDER BY name ASC")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -179,7 +189,7 @@ func (s *AlertsService) GetList() ([]*models.AlertItem, error) {
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
alert := models.AlertItem{}
|
alert := models.AlertItem{}
|
||||||
|
|
||||||
err := rows.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
err := rows.Scan(&alert.Id, &alert.ContactPointId, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -199,8 +209,8 @@ func (s *AlertsService) GetList() ([]*models.AlertItem, error) {
|
||||||
func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
||||||
alert := models.AlertItem{}
|
alert := models.AlertItem{}
|
||||||
|
|
||||||
row := s.ctx.DB.QueryRow("SELECT id, name, condition, trigger_interval, last_status, last_status_at FROM alerts WHERE id = ?", id)
|
row := s.ctx.DB.QueryRow("SELECT id, contact_point_id, name, condition, trigger_interval, last_status, last_status_at FROM alerts WHERE id = ?", id)
|
||||||
err := row.Scan(&alert.Id, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
err := row.Scan(&alert.Id, &alert.ContactPointId, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -209,19 +219,20 @@ func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
||||||
return &alert, nil
|
return &alert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64) (*models.AlertItem, error) {
|
func (s *AlertsService) Create(contactPointId int64, name string, condition string, triggerInterval int64, customMessage string) (*models.AlertItem, error) {
|
||||||
alert := models.AlertItem{
|
alert := models.AlertItem{
|
||||||
ContactPointId: contactPointId,
|
ContactPointId: contactPointId,
|
||||||
Name: name,
|
Name: name,
|
||||||
Condition: condition,
|
Condition: condition,
|
||||||
TriggerInterval: triggerInterval,
|
TriggerInterval: triggerInterval,
|
||||||
|
CustomMessage: customMessage,
|
||||||
LastStatus: "good",
|
LastStatus: "good",
|
||||||
LastStatusAt: 0,
|
LastStatusAt: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.ctx.DB.Exec(
|
res, err := s.ctx.DB.Exec(
|
||||||
"INSERT INTO alerts (contact_point_id, name, condition, trigger_interval, last_status, last_status_at) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO alerts (contact_point_id, name, condition, trigger_interval, last_status, last_status_at, custom_message) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.LastStatus, alert.LastStatusAt,
|
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.LastStatus, alert.LastStatusAt, alert.CustomMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -247,18 +258,19 @@ func (s *AlertsService) DeleteById(id int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64) (*models.AlertItem, error) {
|
func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64, customMessage string) (*models.AlertItem, error) {
|
||||||
alert := models.AlertItem{
|
alert := models.AlertItem{
|
||||||
Id: id,
|
Id: id,
|
||||||
ContactPointId: contactPointId,
|
ContactPointId: contactPointId,
|
||||||
Name: name,
|
Name: name,
|
||||||
Condition: condition,
|
Condition: condition,
|
||||||
TriggerInterval: triggerInterval,
|
TriggerInterval: triggerInterval,
|
||||||
|
CustomMessage: customMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.ctx.DB.Exec(
|
_, err := s.ctx.DB.Exec(
|
||||||
"UPDATE alerts SET contact_point_id = ?, name = ?, condition = ?, trigger_interval = ? WHERE id = ?",
|
"UPDATE alerts SET contact_point_id = ?, name = ?, condition = ?, trigger_interval = ?, custom_message = ? WHERE id = ?",
|
||||||
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.Id,
|
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.CustomMessage, alert.Id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,12 @@ func (s *ContactPointsService) Delete(id int64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ContactPointsService) Test(item *ContactPointItem) error {
|
func (s *ContactPointsService) Test(contactPointType string, typeConfig string) error {
|
||||||
|
item := ContactPointItem{
|
||||||
|
Type: contactPointType,
|
||||||
|
TypeConfig: typeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
service, err := item.getTestService(s.ctx)
|
service, err := item.getTestService(s.ctx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue