Add MQTT brokers section
This commit is contained in:
parent
9232a9a0d6
commit
0a54980cf1
|
|
@ -1,21 +0,0 @@
|
||||||
import { request } from './request'
|
|
||||||
|
|
||||||
export const publishMqttMessage = (body: {
|
|
||||||
server: string
|
|
||||||
clientId?: string
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
qos?: number
|
|
||||||
retain?: boolean
|
|
||||||
message: string
|
|
||||||
topic: string
|
|
||||||
}) =>
|
|
||||||
request(
|
|
||||||
`/api/mqtt/publish`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'content-type': 'application/json' },
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
},
|
|
||||||
'void'
|
|
||||||
)
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { request } from './request'
|
||||||
|
|
||||||
|
export type MQTTBrokerInfo = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
address: string
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
clientId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMQTTBrokers = () =>
|
||||||
|
request<MQTTBrokerInfo[]>('/api/mqtt/brokers')
|
||||||
|
|
||||||
|
export const createMQTTBroker = (data: Omit<MQTTBrokerInfo, 'id'>) =>
|
||||||
|
request<MQTTBrokerInfo>('/api/mqtt/brokers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateMQTTBroker = ({
|
||||||
|
id,
|
||||||
|
...body
|
||||||
|
}: Omit<MQTTBrokerInfo, 'lastStatus' | 'lastStatusAt'>) =>
|
||||||
|
request<MQTTBrokerInfo>(`/api/mqtt/brokers/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const deleteMQTTBroker = (id: number) =>
|
||||||
|
request<MQTTBrokerInfo, 'void'>(
|
||||||
|
`/api/mqtt/brokers/${id}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
'void'
|
||||||
|
)
|
||||||
|
|
||||||
|
export const publishMqttBroker = ({
|
||||||
|
mqttBrokerId,
|
||||||
|
...body
|
||||||
|
}: {
|
||||||
|
mqttBrokerId: number
|
||||||
|
qos?: number
|
||||||
|
retain?: boolean
|
||||||
|
message: string
|
||||||
|
topic: string
|
||||||
|
}) =>
|
||||||
|
request(
|
||||||
|
`/api/mqtt/brokers/${mqttBrokerId}/publish`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
'void'
|
||||||
|
)
|
||||||
|
|
@ -54,17 +54,16 @@ textarea {
|
||||||
|
|
||||||
.color-input {
|
.color-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
|
|
||||||
.current-color {
|
.current-color {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 1.5rem;
|
|
||||||
border: 1px solid var(--input-border-color);
|
border: 1px solid var(--input-border-color);
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-picker {
|
.color-picker {
|
||||||
margin-top: -280px;
|
margin-top: -240px;
|
||||||
|
|
||||||
> .title {
|
> .title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export const UserMenu = ({ shown, popup, onHide }: Props) => {
|
||||||
<NavLink href="/">📈 Dashboards</NavLink>
|
<NavLink href="/">📈 Dashboards</NavLink>
|
||||||
<NavLink href="/sensors">⌚ Sensors</NavLink>
|
<NavLink href="/sensors">⌚ Sensors</NavLink>
|
||||||
<NavLink href="/alerts">🚨 Alerts</NavLink>
|
<NavLink href="/alerts">🚨 Alerts</NavLink>
|
||||||
|
<NavLink href="/mqtt/brokers">⚙️ MQTT Brokers</NavLink>
|
||||||
{/*<a href="#">⚙️ Settings</a>*/}
|
{/*<a href="#">⚙️ Settings</a>*/}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { AlertsPage } from './alerts/AlertsPage'
|
||||||
import { NewDashboardPage } from './dashboard/NewDashboardPage'
|
import { NewDashboardPage } from './dashboard/NewDashboardPage'
|
||||||
import { LoginPage } from './login/LoginPage'
|
import { LoginPage } from './login/LoginPage'
|
||||||
import { SensorsPage } from './sensors/SensorsPage'
|
import { SensorsPage } from './sensors/SensorsPage'
|
||||||
|
import { MqttBrokersPage } from './mqttBrokers/MqttBrokersPage'
|
||||||
|
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
const { loggedIn } = useAppContext()
|
const { loggedIn } = useAppContext()
|
||||||
|
|
@ -22,6 +23,9 @@ export const Router = () => {
|
||||||
<Route path="/alerts">
|
<Route path="/alerts">
|
||||||
<AlertsPage />
|
<AlertsPage />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/mqtt/brokers">
|
||||||
|
<MqttBrokersPage />
|
||||||
|
</Route>
|
||||||
</Wouter>
|
</Wouter>
|
||||||
)}
|
)}
|
||||||
{!loggedIn && <LoginPage />}
|
{!loggedIn && <LoginPage />}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { getMQTTBrokers } from '@/api/mqttBrokers'
|
||||||
import { CssColorInput } from '@/components/CssColorInput'
|
import { CssColorInput } from '@/components/CssColorInput'
|
||||||
import { FormCheckboxField } from '@/components/FormCheckboxField'
|
import { FormCheckboxField } from '@/components/FormCheckboxField'
|
||||||
import { FormField } from '@/components/FormField'
|
import { FormField } from '@/components/FormField'
|
||||||
import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard'
|
import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard'
|
||||||
import { useForm } from '@/utils/hooks/useForm'
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
import { omit } from '@/utils/omit'
|
import { omit } from '@/utils/omit'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value?: DashboardMQTTButtonData
|
value?: DashboardMQTTButtonData
|
||||||
|
|
@ -11,9 +13,10 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MQTTButtonSettings = ({ value, onChange }: Props) => {
|
export const MQTTButtonSettings = ({ value, onChange }: Props) => {
|
||||||
|
const brokers = useQuery('/mqtt/brokers', getMQTTBrokers)
|
||||||
|
|
||||||
const { register } = useForm({
|
const { register } = useForm({
|
||||||
defaultValue: () => ({
|
defaultValue: () => ({
|
||||||
server: '',
|
|
||||||
topic: '',
|
topic: '',
|
||||||
message: '',
|
message: '',
|
||||||
retain: false,
|
retain: false,
|
||||||
|
|
@ -24,21 +27,14 @@ export const MQTTButtonSettings = ({ value, onChange }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField name="mqttBrokerId" label="MQTT Broker">
|
||||||
name="server"
|
<select {...register('mqttBrokerId', { type: 'integer' })}>
|
||||||
label="MQTT Broker Address"
|
{brokers.data?.map((b) => (
|
||||||
hint="Example: tcp://10.10.1.1:1883"
|
<option key={b.id} value={b.id}>
|
||||||
>
|
{b.name}
|
||||||
<input required {...register('server')} />
|
</option>
|
||||||
</FormField>
|
))}
|
||||||
<FormField name="clientId" label="Client ID">
|
</select>
|
||||||
<input {...register('clientId')} />
|
|
||||||
</FormField>
|
|
||||||
<FormField name="username" label="Username">
|
|
||||||
<input {...register('username')} />
|
|
||||||
</FormField>
|
|
||||||
<FormField name="password" label="Password">
|
|
||||||
<input type="password" {...register('password')} />
|
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField name="qoc" label="QoS">
|
<FormField name="qoc" label="QoS">
|
||||||
<select {...register('qos', { type: 'integer' })}>
|
<select {...register('qos', { type: 'integer' })}>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,28 @@
|
||||||
import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard'
|
import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard'
|
||||||
import { EditableBox, EditableBoxProps } from './EditableBox'
|
import { EditableBox, EditableBoxProps } from './EditableBox'
|
||||||
import { useMutation } from 'react-query'
|
import { useMutation } from 'react-query'
|
||||||
import { publishMqttMessage } from '@/api/mqtt'
|
|
||||||
import { cn } from '@/utils/cn'
|
import { cn } from '@/utils/cn'
|
||||||
|
import { publishMqttBroker } from '@/api/mqttBrokers'
|
||||||
|
|
||||||
type Props = EditableBoxProps & {
|
type Props = EditableBoxProps & {
|
||||||
data: DashboardMQTTButtonData
|
data: DashboardMQTTButtonData
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BoxMQTTButtonContent = ({ data, ...boxProps }: Props) => {
|
export const BoxMQTTButtonContent = ({ data, ...boxProps }: Props) => {
|
||||||
const pushMutation = useMutation(publishMqttMessage)
|
const pushMutation = useMutation(publishMqttBroker)
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
pushMutation.mutate({
|
const id = data.mqttBrokerId
|
||||||
...data,
|
|
||||||
})
|
if (id) {
|
||||||
|
pushMutation.mutate({
|
||||||
|
mqttBrokerId: id,
|
||||||
|
message: data.message,
|
||||||
|
topic: data.topic,
|
||||||
|
qos: data.qos,
|
||||||
|
retain: data.retain,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import {
|
||||||
|
MQTTBrokerInfo,
|
||||||
|
deleteMQTTBroker,
|
||||||
|
getMQTTBrokers,
|
||||||
|
} from '@/api/mqttBrokers'
|
||||||
|
import { DataTable } from '@/components/DataTable'
|
||||||
|
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
||||||
|
import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
||||||
|
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||||
|
import { MQTTBrokerFormModal } from './components/MQTTBrokerFormModal'
|
||||||
|
import { NoMQTTBrokers } from './components/NoMQTTBrokers'
|
||||||
|
|
||||||
|
export const MqttBrokersPage = () => {
|
||||||
|
const brokers = useQuery(['/mqtt/brokers'], getMQTTBrokers)
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const deleteMutation = useMutation(deleteMQTTBroker, {
|
||||||
|
onSuccess: () => queryClient.invalidateQueries(['/mqtt/brokers']),
|
||||||
|
})
|
||||||
|
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
const [edited, setEdited] = useState<MQTTBrokerInfo>()
|
||||||
|
|
||||||
|
const deleteConfirm = useConfirmModal({
|
||||||
|
content: (deleted: MQTTBrokerInfo) =>
|
||||||
|
`Are you sure you want to delete sensor ${deleted.name}?`,
|
||||||
|
onConfirm: (deleted) => deleteMutation.mutate(deleted.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserLayout className="sensors-page">
|
||||||
|
<div className="section-title">
|
||||||
|
<h2>Sensors</h2>
|
||||||
|
<button onClick={() => setShowNew(true)}>
|
||||||
|
<PlusIcon /> Add MQTT Broker
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box-shadow">
|
||||||
|
<DataTable
|
||||||
|
query={brokers}
|
||||||
|
emptyMessage={<NoMQTTBrokers />}
|
||||||
|
columns={[
|
||||||
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
|
{
|
||||||
|
key: 'address',
|
||||||
|
title: 'Address',
|
||||||
|
render: (c) => c.address,
|
||||||
|
scale: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
title: 'Actions',
|
||||||
|
width: '15rem',
|
||||||
|
className: 'actions',
|
||||||
|
render: (c) => (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="danger-variant"
|
||||||
|
onClick={() => deleteConfirm.show(c)}
|
||||||
|
>
|
||||||
|
<TrashIcon /> Delete
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setEdited(c)}>
|
||||||
|
<EditIcon /> Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(showNew || edited) && (
|
||||||
|
<MQTTBrokerFormModal
|
||||||
|
open
|
||||||
|
mqttBroker={edited}
|
||||||
|
onClose={() => {
|
||||||
|
setShowNew(false)
|
||||||
|
setEdited(undefined)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</UserLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import {
|
||||||
|
MQTTBrokerInfo,
|
||||||
|
createMQTTBroker,
|
||||||
|
updateMQTTBroker,
|
||||||
|
} from '@/api/mqttBrokers'
|
||||||
|
import { FormField } from '@/components/FormField'
|
||||||
|
import { Modal } from '@/components/Modal'
|
||||||
|
import { useForm } from '@/utils/hooks/useForm'
|
||||||
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
mqttBroker?: MQTTBrokerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MQTTBrokerFormModal = ({ open, onClose, mqttBroker }: Props) => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const createMutation = useMutation(createMQTTBroker)
|
||||||
|
const updateMutation = useMutation(updateMQTTBroker)
|
||||||
|
|
||||||
|
const { handleSubmit, register } = useForm({
|
||||||
|
defaultValue: () => ({
|
||||||
|
name: mqttBroker?.name ?? '',
|
||||||
|
address: mqttBroker?.address ?? '',
|
||||||
|
clientId: mqttBroker?.clientId ?? '',
|
||||||
|
username: mqttBroker?.username ?? '',
|
||||||
|
password: mqttBroker?.password ?? '',
|
||||||
|
}),
|
||||||
|
onSubmit: async (v) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: v.name,
|
||||||
|
address: v.address,
|
||||||
|
clientId: v.clientId,
|
||||||
|
username: v.username,
|
||||||
|
password: v.password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mqttBroker) {
|
||||||
|
await updateMutation.mutateAsync({
|
||||||
|
id: mqttBroker.id,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await createMutation.mutateAsync(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries(['/mqtt/brokers'])
|
||||||
|
|
||||||
|
onClose()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLoading = createMutation.isLoading || updateMutation.isLoading
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onClose={onClose} open={open}>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="input">
|
||||||
|
<label>Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
minLength={1}
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
{...register('name')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name="address"
|
||||||
|
label="MQTT Broker Address"
|
||||||
|
hint="Example: tcp://10.10.1.1:1883"
|
||||||
|
>
|
||||||
|
<input required {...register('address')} />
|
||||||
|
</FormField>
|
||||||
|
<FormField name="clientId" label="Client ID">
|
||||||
|
<input {...register('clientId')} />
|
||||||
|
</FormField>
|
||||||
|
<FormField name="username" label="Username">
|
||||||
|
<input {...register('username')} />
|
||||||
|
</FormField>
|
||||||
|
<FormField name="password" label="Password">
|
||||||
|
<input type="password" {...register('password')} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<button className="cancel" type="button" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button disabled={isLoading}>Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { PlusIcon } from '@/icons'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { MQTTBrokerFormModal } from './MQTTBrokerFormModal'
|
||||||
|
|
||||||
|
export const NoMQTTBrokers = () => {
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="empty">
|
||||||
|
<div>No MQTT brokers defined.</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setShowNew(true)}>
|
||||||
|
<PlusIcon /> Add a new MQTT broker
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showNew && (
|
||||||
|
<MQTTBrokerFormModal open onClose={() => setShowNew(false)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { deleteSensor, SensorInfo } from '@/api/sensors'
|
|
||||||
import { InputWithCopy } from '@/components/InputWithCopy'
|
|
||||||
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
|
||||||
import { CancelIcon, EditIcon } from '@/icons'
|
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
sensor: SensorInfo
|
|
||||||
onEdit: (sensor: SensorInfo) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SensorItem = ({ sensor, onEdit }: Props) => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const deleteMutation = useMutation(deleteSensor, {
|
|
||||||
onSuccess: () => queryClient.invalidateQueries(['/sensors']),
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteConfirm = useConfirmModal({
|
|
||||||
content: `Are you sure you want to delete sensor ${sensor.name}?`,
|
|
||||||
onConfirm: () => deleteMutation.mutate(sensor.id),
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="box sensor-item">
|
|
||||||
<div className="name">{sensor.name}</div>
|
|
||||||
|
|
||||||
<div className="auth">
|
|
||||||
<div className="auth-value">
|
|
||||||
<div className="label">ID</div>
|
|
||||||
<div className="value">
|
|
||||||
<InputWithCopy value={sensor.id.toString()} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="auth-value">
|
|
||||||
<div className="label">KEY</div>
|
|
||||||
<div className="value">
|
|
||||||
<InputWithCopy value={sensor.authKey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="actions">
|
|
||||||
<button onClick={() => onEdit(sensor)}>
|
|
||||||
<EditIcon /> Edit
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={deleteConfirm.show}
|
|
||||||
disabled={deleteMutation.isLoading}
|
|
||||||
>
|
|
||||||
<CancelIcon /> Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -46,10 +46,7 @@ export type DashboardDialData = {
|
||||||
|
|
||||||
export type DashboardMQTTButtonData = {
|
export type DashboardMQTTButtonData = {
|
||||||
type: 'mqttButton'
|
type: 'mqttButton'
|
||||||
server: string
|
mqttBrokerId?: number
|
||||||
clientId?: string
|
|
||||||
username?: string
|
|
||||||
password?: string
|
|
||||||
qos?: number
|
qos?: number
|
||||||
retain?: boolean
|
retain?: boolean
|
||||||
message: string
|
message: string
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,8 @@ export const useHashRouterLocation = () => {
|
||||||
|
|
||||||
const locationWithoutQueryString = location.split('?')[0]
|
const locationWithoutQueryString = location.split('?')[0]
|
||||||
|
|
||||||
return [locationWithoutQueryString, setLocation] as const
|
return [locationWithoutQueryString, setLocation] as [
|
||||||
|
string,
|
||||||
|
(to: string) => void
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS mqtt_brokers (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
username TEXT,
|
||||||
|
password TEXT,
|
||||||
|
client_id TEXT
|
||||||
|
);
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
package services
|
package integrations
|
||||||
|
|
||||||
import (
|
import (
|
||||||
MQTT "github.com/eclipse/paho.mqtt.golang"
|
MQTT "github.com/eclipse/paho.mqtt.golang"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MQTTService struct {
|
type MQTTIntegration struct{}
|
||||||
// TODO:
|
|
||||||
// ctx *Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MQTTService) Publish(server string, username string, password string, clientId string, retain bool, qos byte, topic string, message string) error {
|
func (s *MQTTIntegration) Publish(server string, username string, password string, clientId string, retain bool, qos byte, topic string, message string) error {
|
||||||
opts := MQTT.NewClientOptions()
|
opts := MQTT.NewClientOptions()
|
||||||
opts.AddBroker(server)
|
opts.AddBroker(server)
|
||||||
opts.SetUsername(username)
|
opts.SetUsername(username)
|
||||||
|
|
@ -69,7 +69,12 @@ func main() {
|
||||||
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/contact-points/test", routes.TestContactPoint(server))
|
||||||
loginProtected.POST("/api/mqtt/publish", routes.PutMQTTPublish(server))
|
loginProtected.GET("/api/mqtt/brokers", routes.GetMQTTBrokers(server))
|
||||||
|
loginProtected.POST("/api/mqtt/brokers", routes.PostMQTTBroker(server))
|
||||||
|
loginProtected.GET("/api/mqtt/brokers/:brokerId", routes.GetMQTTBroker(server))
|
||||||
|
loginProtected.PUT("/api/mqtt/brokers/:brokerId", routes.PutMQTTBroker(server))
|
||||||
|
loginProtected.DELETE("/api/mqtt/brokers/:brokerId", routes.DeleteMQTTBroker(server))
|
||||||
|
loginProtected.POST("/api/mqtt/brokers/:brokerId/publish", routes.PostMQTTBrokerPublish(server))
|
||||||
|
|
||||||
if server.Config.AuthEnabled {
|
if server.Config.AuthEnabled {
|
||||||
loginProtected.POST("/api/logout", routes.Logout(server))
|
loginProtected.POST("/api/logout", routes.Logout(server))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type MQTTBrokerItem struct {
|
||||||
|
Id int `json:"id" db:"id"`
|
||||||
|
Name string `json:"name" db:"name"`
|
||||||
|
Address string `json:"address" db:"address"`
|
||||||
|
Username *string `json:"username" db:"username"`
|
||||||
|
Password *string `json:"password" db:"password"`
|
||||||
|
ClientId *string `json:"client_id" db:"client_id"`
|
||||||
|
}
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
package routes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"basic-sensor-receiver/app"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
type putMQTTPublishBody struct {
|
|
||||||
Server string `json:"server" binding:"required"`
|
|
||||||
ClientId string `json:"clientId"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Retain *bool `json:"retain"`
|
|
||||||
Qos *float64 `json:"qos"`
|
|
||||||
Topic string `json:"topic" binding:"required"`
|
|
||||||
Message string `json:"message" binding:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func PutMQTTPublish(s *app.Server) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
body := putMQTTPublishBody{}
|
|
||||||
|
|
||||||
if err := bindJSONBodyOrAbort(c, &body); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
qos := byte(0)
|
|
||||||
retain := false
|
|
||||||
|
|
||||||
if body.Retain != nil {
|
|
||||||
retain = *body.Retain
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.Qos != nil {
|
|
||||||
qos = byte(*body.Qos)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Services.MQTT.Publish(body.Server, body.Username, body.Password, body.ClientId, retain, qos, body.Topic, body.Message); err != nil {
|
|
||||||
c.AbortWithError(500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Writer.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"basic-sensor-receiver/app"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postOrPutMQTTBrokerRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
Password *string `json:"password"`
|
||||||
|
ClientId *string `json:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type postMQTTPublishRequest struct {
|
||||||
|
Retain *bool `json:"retain"`
|
||||||
|
Qos *float64 `json:"qos"`
|
||||||
|
Topic string `json:"topic" binding:"required"`
|
||||||
|
Message string `json:"message" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMQTTBrokers(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
brokers, err := s.Services.MQTTBrokers.GetList()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, brokers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostMQTTBroker(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
body := postOrPutMQTTBrokerRequest{}
|
||||||
|
if err := bindJSONBodyOrAbort(c, &body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
broker, err := s.Services.MQTTBrokers.Create(body.Name, body.Address, body.Username, body.Password, body.ClientId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, broker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutMQTTBroker(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
brokerId, err := getIntParamOrAbort(c, "brokerId")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := postOrPutMQTTBrokerRequest{}
|
||||||
|
if err := bindJSONBodyOrAbort(c, &body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
broker, err := s.Services.MQTTBrokers.Update(brokerId, body.Name, body.Address, body.Username, body.Password, body.ClientId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, broker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMQTTBroker(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
brokerId, err := getIntParamOrAbort(c, "brokerId")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
broker, err := s.Services.MQTTBrokers.GetById(brokerId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, broker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteMQTTBroker(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
brokerId, err := getIntParamOrAbort(c, "brokerId")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Services.MQTTBrokers.Delete(brokerId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostMQTTBrokerPublish(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
brokerId, err := getIntParamOrAbort(c, "brokerId")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body := postMQTTPublishRequest{}
|
||||||
|
|
||||||
|
if err := bindJSONBodyOrAbort(c, &body); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
qos := byte(0)
|
||||||
|
retain := false
|
||||||
|
|
||||||
|
if body.Retain != nil {
|
||||||
|
retain = *body.Retain
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Qos != nil {
|
||||||
|
qos = byte(*body.Qos)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Services.MQTTBrokers.PublishTopic(brokerId, body.Topic, body.Message, qos, retain); err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import "basic-sensor-receiver/models"
|
||||||
|
|
||||||
|
type MQTTBrokersService struct {
|
||||||
|
ctx *Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MQTTBrokersService) GetList() ([]models.MQTTBrokerItem, error) {
|
||||||
|
brokers := []models.MQTTBrokerItem{}
|
||||||
|
|
||||||
|
err := s.ctx.DB.Select(&brokers, `
|
||||||
|
SELECT id, name, address, username, password, client_id
|
||||||
|
FROM mqtt_brokers
|
||||||
|
ORDER BY name ASC
|
||||||
|
`)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return brokers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MQTTBrokersService) GetById(id int64) (*models.MQTTBrokerItem, error) {
|
||||||
|
broker := models.MQTTBrokerItem{}
|
||||||
|
|
||||||
|
err := s.ctx.DB.Get(&broker,
|
||||||
|
`
|
||||||
|
SELECT id, name, address, username, password, client_id
|
||||||
|
FROM mqtt_brokers
|
||||||
|
WHERE id = $1
|
||||||
|
`,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &broker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MQTTBrokersService) Create(name string, address string, username *string, password *string, clientId *string) (*models.MQTTBrokerItem, error) {
|
||||||
|
broker := models.MQTTBrokerItem{
|
||||||
|
Name: name,
|
||||||
|
Address: address,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
ClientId: clientId,
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.ctx.DB.NamedExec(
|
||||||
|
`
|
||||||
|
INSERT INTO mqtt_brokers (name, address, username, password, client_id)
|
||||||
|
VALUES (:name, :address, :username, :password, :client_id)
|
||||||
|
`,
|
||||||
|
broker,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
broker.Id = int(id)
|
||||||
|
|
||||||
|
return &broker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MQTTBrokersService) Update(id int64, name string, address string, username *string, password *string, clientId *string) (*models.MQTTBrokerItem, error) {
|
||||||
|
broker := models.MQTTBrokerItem{
|
||||||
|
Id: int(id),
|
||||||
|
Name: name,
|
||||||
|
Address: address,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
ClientId: clientId,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.ctx.DB.NamedExec(
|
||||||
|
`
|
||||||
|
UPDATE mqtt_brokers
|
||||||
|
SET name = :name, address = :address, username = :username, password = :password, client_id = :client_id
|
||||||
|
WHERE id = :id
|
||||||
|
`,
|
||||||
|
broker,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &broker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MQTTBrokersService) Delete(id int64) error {
|
||||||
|
_, err := s.ctx.DB.Exec(
|
||||||
|
`
|
||||||
|
DELETE FROM mqtt_brokers
|
||||||
|
WHERE id = $1
|
||||||
|
`,
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MQTTBrokersService) PublishTopic(brokerId int64, topic string, message string, qos byte, retain bool) error {
|
||||||
|
broker, err := s.GetById(brokerId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
username := ""
|
||||||
|
if broker.Username != nil {
|
||||||
|
username = *broker.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
password := ""
|
||||||
|
if broker.Password != nil {
|
||||||
|
password = *broker.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
clientId := ""
|
||||||
|
if broker.ClientId != nil {
|
||||||
|
clientId = *broker.ClientId
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.ctx.Integrations.MQTT.Publish(
|
||||||
|
broker.Address,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
clientId,
|
||||||
|
retain,
|
||||||
|
qos,
|
||||||
|
topic,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -17,11 +17,12 @@ type Services struct {
|
||||||
Alerts *AlertsService
|
Alerts *AlertsService
|
||||||
AlertsEvaluator *AlertsEvaluatorService
|
AlertsEvaluator *AlertsEvaluatorService
|
||||||
ContactPoints *ContactPointsService
|
ContactPoints *ContactPointsService
|
||||||
MQTT *MQTTService
|
MQTTBrokers *MQTTBrokersService
|
||||||
}
|
}
|
||||||
|
|
||||||
type Integrations struct {
|
type Integrations struct {
|
||||||
Telegram *integrations.TelegramIntegration
|
Telegram *integrations.TelegramIntegration
|
||||||
|
MQTT *integrations.MQTTIntegration
|
||||||
}
|
}
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
|
|
@ -45,10 +46,11 @@ func InitializeServices(ctx *Context) *Services {
|
||||||
services.Alerts = &AlertsService{ctx: ctx}
|
services.Alerts = &AlertsService{ctx: ctx}
|
||||||
services.AlertsEvaluator = &AlertsEvaluatorService{ctx: ctx}
|
services.AlertsEvaluator = &AlertsEvaluatorService{ctx: ctx}
|
||||||
services.ContactPoints = &ContactPointsService{ctx: ctx}
|
services.ContactPoints = &ContactPointsService{ctx: ctx}
|
||||||
services.MQTT = &MQTTService{}
|
services.MQTTBrokers = &MQTTBrokersService{ctx: ctx}
|
||||||
|
|
||||||
ctx.Integrations = &Integrations{}
|
ctx.Integrations = &Integrations{}
|
||||||
ctx.Integrations.Telegram = &integrations.TelegramIntegration{}
|
ctx.Integrations.Telegram = &integrations.TelegramIntegration{}
|
||||||
|
ctx.Integrations.MQTT = &integrations.MQTTIntegration{}
|
||||||
|
|
||||||
return &services
|
return &services
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue