Finish alerts implementation
This commit is contained in:
parent
72bf572981
commit
7e9625ecf3
|
|
@ -7,5 +7,9 @@
|
|||
"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 createAlert = (data: Omit<AlertInfo, 'id'>) =>
|
||||
export const createAlert = (
|
||||
data: Omit<AlertInfo, 'id' | 'lastStatus' | 'lastStatusAt'>
|
||||
) =>
|
||||
request<AlertInfo>('/api/alerts', {
|
||||
method: 'POST',
|
||||
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}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
|
|
|
|||
|
|
@ -10,25 +10,14 @@ export type ContactPointInfo = {
|
|||
export const getContactPoints = () =>
|
||||
request<ContactPointInfo[]>('/api/contact-points')
|
||||
|
||||
export const createContactPoint = (data: {
|
||||
name: string
|
||||
type: string
|
||||
config: string
|
||||
}) =>
|
||||
export const createContactPoint = (data: Omit<ContactPointInfo, 'id'>) =>
|
||||
request<ContactPointInfo>('/api/contact-points', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ data }),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
export const updateContactPoint = ({
|
||||
id,
|
||||
...body
|
||||
}: {
|
||||
id: number
|
||||
type: string
|
||||
config: string
|
||||
}) =>
|
||||
export const updateContactPoint = ({ id, ...body }: ContactPointInfo) =>
|
||||
request<ContactPointInfo>(`/api/contact-points/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
|
|
@ -41,3 +30,10 @@ export const deleteContactPoint = (id: number) =>
|
|||
{ method: 'DELETE' },
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
.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,
|
||||
html {
|
||||
|
|
@ -38,14 +38,16 @@ a {
|
|||
stroke: currentColor;
|
||||
}
|
||||
|
||||
@import "components/button";
|
||||
@import "components/input";
|
||||
@import "components/modal";
|
||||
@import "components/form";
|
||||
@import "components/grid-sensors";
|
||||
@import "components/user-layout";
|
||||
@import "components/box";
|
||||
@import 'components/button';
|
||||
@import 'components/input';
|
||||
@import 'components/modal';
|
||||
@import 'components/form';
|
||||
@import 'components/grid-sensors';
|
||||
@import 'components/user-layout';
|
||||
@import 'components/box';
|
||||
@import 'components/data-table';
|
||||
|
||||
@import "pages/login-page";
|
||||
@import "pages/sensors-page";
|
||||
@import "pages/dashboard-page";
|
||||
@import 'pages/login-page';
|
||||
@import 'pages/sensors-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 { ContactPointsTable } from './components/ContactPointsTable/ContactPointsTable'
|
||||
import { AlertsTable } from './components/AlertsTable/AlertsTable'
|
||||
|
||||
export const AlertsPage = () => {
|
||||
return (
|
||||
<UserLayout
|
||||
header={
|
||||
<div className="sensors-head">
|
||||
<div className="alerts-head">
|
||||
<div>Alerts</div>
|
||||
<button>
|
||||
<PlusIcon /> Add alert
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
className="sensors-page"
|
||||
className="alerts-page"
|
||||
>
|
||||
TEST
|
||||
<ContactPointsTable />
|
||||
<AlertsTable />
|
||||
</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
|
||||
basic-sensor-receiver.exe
|
||||
basic-sensor-receiver
|
||||
*.sqlite3
|
||||
*.sqlite3
|
||||
tmp/
|
||||
|
|
@ -1,13 +1,20 @@
|
|||
package app
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Server) StartAlerts() {
|
||||
ticker := time.NewTicker(time.Second * 10)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
s.Services.Alerts.EvaluateAlerts()
|
||||
err := s.Services.Alerts.EvaluateAlerts()
|
||||
if err != nil {
|
||||
fmt.Println("Error evaluating alerts: ", err)
|
||||
}
|
||||
|
||||
<-ticker.C
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package integrations
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
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 = 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.ParseMode = "Markdown"
|
||||
_, err = bot.Send(msg)
|
||||
return err
|
||||
case ContactPointEventTest:
|
||||
msg := tgbotapi.NewMessage(config.TargetChannel, "Test message from Basic Sensor Receiver")
|
||||
_, err = bot.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ func main() {
|
|||
loginProtected.GET("/api/contact-points/:contactPointId", routes.GetContactPoint(server))
|
||||
loginProtected.PUT("/api/contact-points/:contactPointId", routes.PutContactPoint(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))
|
||||
|
||||
// Routes accessible using auth key
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type AlertConditionSensorValue struct {
|
|||
}
|
||||
|
||||
type AlertConditionSensorLastContact struct {
|
||||
SensorId int64 `json:"sensorId"`
|
||||
Value int64 `json:"value"`
|
||||
ValueUnit string `json:"valueUnit"`
|
||||
SensorId int64 `json:"sensorId"`
|
||||
Value float64 `json:"value"`
|
||||
ValueUnit string `json:"valueUnit"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type postAlertsBody struct {
|
|||
Name string `json:"name"`
|
||||
Condition string `json:"condition"`
|
||||
TriggerInterval int64 `json:"triggerInterval"`
|
||||
CustomMessage string `json:"customMessage"`
|
||||
}
|
||||
|
||||
type putAlertsBody struct {
|
||||
|
|
@ -20,6 +21,7 @@ type putAlertsBody struct {
|
|||
Name string `json:"name"`
|
||||
Condition string `json:"condition"`
|
||||
TriggerInterval int64 `json:"triggerInterval"`
|
||||
CustomMessage string `json:"customMessage"`
|
||||
}
|
||||
|
||||
func GetAlerts(s *app.Server) gin.HandlerFunc {
|
||||
|
|
@ -44,7 +46,7 @@ func PostAlerts(s *app.Server) gin.HandlerFunc {
|
|||
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 {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
|
@ -71,7 +73,7 @@ func PutAlert(s *app.Server) gin.HandlerFunc {
|
|||
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 {
|
||||
c.AbortWithError(http.StatusInternalServerError, err)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,11 @@ type putContactPointsBody struct {
|
|||
TypeConfig string `json:"typeConfig"`
|
||||
}
|
||||
|
||||
type testContactPointBody struct {
|
||||
Type string `json:"type"`
|
||||
TypeConfig string `json:"typeConfig"`
|
||||
}
|
||||
|
||||
func GetContactPoints(s *app.Server) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
contactPoints, err := s.Services.ContactPoints.GetList()
|
||||
|
|
@ -116,7 +121,27 @@ func DeleteContactPoint(s *app.Server) gin.HandlerFunc {
|
|||
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 {
|
||||
case "s":
|
||||
return 1
|
||||
|
|
@ -76,7 +76,7 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
|||
case "sensor_value":
|
||||
{
|
||||
sensorValueCondition = models.AlertConditionSensorValue{
|
||||
SensorId: condition["sensorId"].(int64),
|
||||
SensorId: int64(condition["sensorId"].(float64)),
|
||||
Condition: condition["condition"].(string),
|
||||
Value: condition["value"].(float64),
|
||||
}
|
||||
|
|
@ -97,8 +97,8 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
|||
case "sensor_last_contact":
|
||||
{
|
||||
sensorLastContactCondition := models.AlertConditionSensorLastContact{
|
||||
SensorId: condition["sensorId"].(int64),
|
||||
Value: condition["value"].(int64),
|
||||
SensorId: int64(condition["sensorId"].(float64)),
|
||||
Value: condition["value"].(float64),
|
||||
ValueUnit: condition["valueUnit"].(string),
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
|||
return err
|
||||
}
|
||||
|
||||
conditionInSec := sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit)
|
||||
conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit))
|
||||
|
||||
conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec
|
||||
|
||||
|
|
@ -119,46 +119,56 @@ func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error {
|
|||
}
|
||||
|
||||
if conditionMet {
|
||||
if newStatus == "good" {
|
||||
newStatus = "alerting"
|
||||
} else if newStatus == "pending" {
|
||||
if time.Now().Unix()-alert.LastStatusAt > alert.TriggerInterval {
|
||||
if alert.LastStatus == "good" {
|
||||
newStatus = "pending"
|
||||
} else if alert.LastStatus == "pending" {
|
||||
if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval {
|
||||
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 {
|
||||
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 err != nil {
|
||||
return err
|
||||
}
|
||||
if newStatus == "alerting" {
|
||||
sensor, err := s.ctx.Services.Sensors.GetById(sensorId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dispatchService, err := contactPoint.getService(s.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dispatchService, err := contactPoint.getService(s.ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{
|
||||
Type: "alert",
|
||||
AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{
|
||||
Alert: alert,
|
||||
Sensor: sensor,
|
||||
SensorValueCondition: &sensorValueCondition,
|
||||
SensorLastContactCondition: &sensorLastContactCondition,
|
||||
LastValue: lastValue,
|
||||
},
|
||||
}, contactPoint.TypeConfig)
|
||||
err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{
|
||||
Type: integrations.ContactPointEventAlertTriggered,
|
||||
AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{
|
||||
Alert: alert,
|
||||
Sensor: sensor,
|
||||
SensorValueCondition: &sensorValueCondition,
|
||||
SensorLastContactCondition: &sensorLastContactCondition,
|
||||
LastValue: lastValue,
|
||||
},
|
||||
}, contactPoint.TypeConfig)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +176,7 @@ func (s *AlertsService) EvaluateAlert(alert *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 {
|
||||
return nil, err
|
||||
|
|
@ -179,7 +189,7 @@ func (s *AlertsService) GetList() ([]*models.AlertItem, error) {
|
|||
for rows.Next() {
|
||||
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 {
|
||||
return nil, err
|
||||
|
|
@ -199,8 +209,8 @@ func (s *AlertsService) GetList() ([]*models.AlertItem, error) {
|
|||
func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
||||
alert := models.AlertItem{}
|
||||
|
||||
row := s.ctx.DB.QueryRow("SELECT 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)
|
||||
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.ContactPointId, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -209,19 +219,20 @@ func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) {
|
|||
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{
|
||||
ContactPointId: contactPointId,
|
||||
Name: name,
|
||||
Condition: condition,
|
||||
TriggerInterval: triggerInterval,
|
||||
CustomMessage: customMessage,
|
||||
LastStatus: "good",
|
||||
LastStatusAt: 0,
|
||||
}
|
||||
|
||||
res, err := s.ctx.DB.Exec(
|
||||
"INSERT INTO alerts (contact_point_id, name, condition, trigger_interval, last_status, last_status_at) VALUES (?, ?, ?, ?, ?)",
|
||||
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.LastStatus, alert.LastStatusAt,
|
||||
"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.CustomMessage,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -247,18 +258,19 @@ func (s *AlertsService) DeleteById(id int64) error {
|
|||
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{
|
||||
Id: id,
|
||||
ContactPointId: contactPointId,
|
||||
Name: name,
|
||||
Condition: condition,
|
||||
TriggerInterval: triggerInterval,
|
||||
CustomMessage: customMessage,
|
||||
}
|
||||
|
||||
_, err := s.ctx.DB.Exec(
|
||||
"UPDATE alerts SET contact_point_id = ?, name = ?, condition = ?, trigger_interval = ? WHERE id = ?",
|
||||
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.Id,
|
||||
"UPDATE alerts SET contact_point_id = ?, name = ?, condition = ?, trigger_interval = ?, custom_message = ? WHERE id = ?",
|
||||
alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.CustomMessage, alert.Id,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -123,7 +123,12 @@ func (s *ContactPointsService) Delete(id int64) error {
|
|||
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)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Reference in New Issue