From 0a54980cf1b75687a7e1b7df7eb2cd9d979c2bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Mon, 1 Apr 2024 19:42:20 +0200 Subject: [PATCH] Add MQTT brokers section --- client/src/api/mqtt.ts | 21 --- client/src/api/mqttBrokers.ts | 57 +++++++ client/src/assets/components/_input.scss | 5 +- .../UserLayout/components/UserMenu.tsx | 1 + client/src/pages/Router.tsx | 4 + .../components/MQTTButtonSettings.tsx | 28 ++-- .../components/BoxMQTTButtonContent.tsx | 18 ++- .../src/pages/mqttBrokers/MqttBrokersPage.tsx | 88 +++++++++++ .../components/MQTTBrokerFormModal.tsx | 100 ++++++++++++ .../mqttBrokers/components/NoMQTTBrokers.tsx | 23 +++ .../pages/sensors/components/SensorItem.tsx | 56 ------- client/src/utils/dashboard/parseDashboard.ts | 5 +- client/src/utils/hooks/useHashLocation.ts | 5 +- .../migrations/1711992268487_mqtt_brokers.sql | 8 + .../mqtt_service.go => integrations/mqtt.go} | 9 +- server/main.go | 7 +- server/models/mqtt_brokers.go | 10 ++ server/routes/mqtt.go | 47 ------ server/routes/mqtt_brokers.go | 146 +++++++++++++++++ server/services/mqtt_brokers_service.go | 148 ++++++++++++++++++ server/services/services.go | 6 +- 21 files changed, 630 insertions(+), 162 deletions(-) delete mode 100644 client/src/api/mqtt.ts create mode 100644 client/src/api/mqttBrokers.ts create mode 100644 client/src/pages/mqttBrokers/MqttBrokersPage.tsx create mode 100644 client/src/pages/mqttBrokers/components/MQTTBrokerFormModal.tsx create mode 100644 client/src/pages/mqttBrokers/components/NoMQTTBrokers.tsx delete mode 100644 client/src/pages/sensors/components/SensorItem.tsx create mode 100644 server/database/migrations/1711992268487_mqtt_brokers.sql rename server/{services/mqtt_service.go => integrations/mqtt.go} (65%) create mode 100644 server/models/mqtt_brokers.go delete mode 100644 server/routes/mqtt.go create mode 100644 server/routes/mqtt_brokers.go create mode 100644 server/services/mqtt_brokers_service.go diff --git a/client/src/api/mqtt.ts b/client/src/api/mqtt.ts deleted file mode 100644 index 9b46b83..0000000 --- a/client/src/api/mqtt.ts +++ /dev/null @@ -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' - ) diff --git a/client/src/api/mqttBrokers.ts b/client/src/api/mqttBrokers.ts new file mode 100644 index 0000000..53c639b --- /dev/null +++ b/client/src/api/mqttBrokers.ts @@ -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('/api/mqtt/brokers') + +export const createMQTTBroker = (data: Omit) => + request('/api/mqtt/brokers', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(data), + }) + +export const updateMQTTBroker = ({ + id, + ...body +}: Omit) => + request(`/api/mqtt/brokers/${id}`, { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }) + +export const deleteMQTTBroker = (id: number) => + request( + `/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' + ) diff --git a/client/src/assets/components/_input.scss b/client/src/assets/components/_input.scss index 7b26986..6120650 100644 --- a/client/src/assets/components/_input.scss +++ b/client/src/assets/components/_input.scss @@ -54,17 +54,16 @@ textarea { .color-input { display: flex; - align-items: center; + align-items: stretch; .current-color { width: 2rem; - height: 1.5rem; border: 1px solid var(--input-border-color); margin-right: 0.5rem; } .color-picker { - margin-top: -280px; + margin-top: -240px; > .title { display: flex; diff --git a/client/src/layouts/UserLayout/components/UserMenu.tsx b/client/src/layouts/UserLayout/components/UserMenu.tsx index f5e448a..8864ff2 100644 --- a/client/src/layouts/UserLayout/components/UserMenu.tsx +++ b/client/src/layouts/UserLayout/components/UserMenu.tsx @@ -33,6 +33,7 @@ export const UserMenu = ({ shown, popup, onHide }: Props) => { 📈 Dashboards ⌚ Sensors 🚨 Alerts + ⚙️ MQTT Brokers {/*⚙️ Settings*/} diff --git a/client/src/pages/Router.tsx b/client/src/pages/Router.tsx index 81c3fc8..55574a9 100644 --- a/client/src/pages/Router.tsx +++ b/client/src/pages/Router.tsx @@ -5,6 +5,7 @@ import { AlertsPage } from './alerts/AlertsPage' import { NewDashboardPage } from './dashboard/NewDashboardPage' import { LoginPage } from './login/LoginPage' import { SensorsPage } from './sensors/SensorsPage' +import { MqttBrokersPage } from './mqttBrokers/MqttBrokersPage' export const Router = () => { const { loggedIn } = useAppContext() @@ -22,6 +23,9 @@ export const Router = () => { + + + )} {!loggedIn && } diff --git a/client/src/pages/dashboard/components/BoxSettings/components/MQTTButtonSettings.tsx b/client/src/pages/dashboard/components/BoxSettings/components/MQTTButtonSettings.tsx index 306ca4f..ed62e03 100644 --- a/client/src/pages/dashboard/components/BoxSettings/components/MQTTButtonSettings.tsx +++ b/client/src/pages/dashboard/components/BoxSettings/components/MQTTButtonSettings.tsx @@ -1,9 +1,11 @@ +import { getMQTTBrokers } from '@/api/mqttBrokers' import { CssColorInput } from '@/components/CssColorInput' import { FormCheckboxField } from '@/components/FormCheckboxField' import { FormField } from '@/components/FormField' import { DashboardMQTTButtonData } from '@/utils/dashboard/parseDashboard' import { useForm } from '@/utils/hooks/useForm' import { omit } from '@/utils/omit' +import { useQuery } from 'react-query' type Props = { value?: DashboardMQTTButtonData @@ -11,9 +13,10 @@ type Props = { } export const MQTTButtonSettings = ({ value, onChange }: Props) => { + const brokers = useQuery('/mqtt/brokers', getMQTTBrokers) + const { register } = useForm({ defaultValue: () => ({ - server: '', topic: '', message: '', retain: false, @@ -24,21 +27,14 @@ export const MQTTButtonSettings = ({ value, onChange }: Props) => { return ( <> - - - - - - - - - - - + + + + + + + + + + + + + + + + + +
+ + +
+ + + ) +} diff --git a/client/src/pages/mqttBrokers/components/NoMQTTBrokers.tsx b/client/src/pages/mqttBrokers/components/NoMQTTBrokers.tsx new file mode 100644 index 0000000..1c654ef --- /dev/null +++ b/client/src/pages/mqttBrokers/components/NoMQTTBrokers.tsx @@ -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 ( + <> +
+
No MQTT brokers defined.
+
+ +
+
+ {showNew && ( + setShowNew(false)} /> + )} + + ) +} diff --git a/client/src/pages/sensors/components/SensorItem.tsx b/client/src/pages/sensors/components/SensorItem.tsx deleted file mode 100644 index ace6c5a..0000000 --- a/client/src/pages/sensors/components/SensorItem.tsx +++ /dev/null @@ -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 ( -
-
{sensor.name}
- -
-
-
ID
-
- -
-
-
-
KEY
-
- -
-
-
-
- - - -
-
- ) -} diff --git a/client/src/utils/dashboard/parseDashboard.ts b/client/src/utils/dashboard/parseDashboard.ts index 9fd1cc0..ed3bf56 100644 --- a/client/src/utils/dashboard/parseDashboard.ts +++ b/client/src/utils/dashboard/parseDashboard.ts @@ -46,10 +46,7 @@ export type DashboardDialData = { export type DashboardMQTTButtonData = { type: 'mqttButton' - server: string - clientId?: string - username?: string - password?: string + mqttBrokerId?: number qos?: number retain?: boolean message: string diff --git a/client/src/utils/hooks/useHashLocation.ts b/client/src/utils/hooks/useHashLocation.ts index 8288537..aa9d0fe 100644 --- a/client/src/utils/hooks/useHashLocation.ts +++ b/client/src/utils/hooks/useHashLocation.ts @@ -29,5 +29,8 @@ export const useHashRouterLocation = () => { const locationWithoutQueryString = location.split('?')[0] - return [locationWithoutQueryString, setLocation] as const + return [locationWithoutQueryString, setLocation] as [ + string, + (to: string) => void + ] } diff --git a/server/database/migrations/1711992268487_mqtt_brokers.sql b/server/database/migrations/1711992268487_mqtt_brokers.sql new file mode 100644 index 0000000..38edc78 --- /dev/null +++ b/server/database/migrations/1711992268487_mqtt_brokers.sql @@ -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 +); \ No newline at end of file diff --git a/server/services/mqtt_service.go b/server/integrations/mqtt.go similarity index 65% rename from server/services/mqtt_service.go rename to server/integrations/mqtt.go index b285153..cce4b18 100644 --- a/server/services/mqtt_service.go +++ b/server/integrations/mqtt.go @@ -1,15 +1,12 @@ -package services +package integrations import ( MQTT "github.com/eclipse/paho.mqtt.golang" ) -type MQTTService struct { - // TODO: - // ctx *Context -} +type MQTTIntegration struct{} -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.AddBroker(server) opts.SetUsername(username) diff --git a/server/main.go b/server/main.go index 9159930..53a1389 100644 --- a/server/main.go +++ b/server/main.go @@ -69,7 +69,12 @@ func main() { 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/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 { loginProtected.POST("/api/logout", routes.Logout(server)) diff --git a/server/models/mqtt_brokers.go b/server/models/mqtt_brokers.go new file mode 100644 index 0000000..0018bed --- /dev/null +++ b/server/models/mqtt_brokers.go @@ -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"` +} diff --git a/server/routes/mqtt.go b/server/routes/mqtt.go deleted file mode 100644 index e056716..0000000 --- a/server/routes/mqtt.go +++ /dev/null @@ -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) - } -} diff --git a/server/routes/mqtt_brokers.go b/server/routes/mqtt_brokers.go new file mode 100644 index 0000000..e1279fd --- /dev/null +++ b/server/routes/mqtt_brokers.go @@ -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) + } +} diff --git a/server/services/mqtt_brokers_service.go b/server/services/mqtt_brokers_service.go new file mode 100644 index 0000000..3edefca --- /dev/null +++ b/server/services/mqtt_brokers_service.go @@ -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, + ) +} diff --git a/server/services/services.go b/server/services/services.go index 20c10aa..7249007 100644 --- a/server/services/services.go +++ b/server/services/services.go @@ -17,11 +17,12 @@ type Services struct { Alerts *AlertsService AlertsEvaluator *AlertsEvaluatorService ContactPoints *ContactPointsService - MQTT *MQTTService + MQTTBrokers *MQTTBrokersService } type Integrations struct { Telegram *integrations.TelegramIntegration + MQTT *integrations.MQTTIntegration } type Context struct { @@ -45,10 +46,11 @@ func InitializeServices(ctx *Context) *Services { services.Alerts = &AlertsService{ctx: ctx} services.AlertsEvaluator = &AlertsEvaluatorService{ctx: ctx} services.ContactPoints = &ContactPointsService{ctx: ctx} - services.MQTT = &MQTTService{} + services.MQTTBrokers = &MQTTBrokersService{ctx: ctx} ctx.Integrations = &Integrations{} ctx.Integrations.Telegram = &integrations.TelegramIntegration{} + ctx.Integrations.MQTT = &integrations.MQTTIntegration{} return &services }