Added sensor form

This commit is contained in:
Jan Zípek 2022-08-28 12:30:37 +02:00
parent c56a678925
commit 0e202c0850
10 changed files with 182 additions and 25 deletions

View File

@ -15,9 +15,9 @@ export const createSensor = (name: string) =>
body: JSON.stringify({ name }),
})
export const updateSensor = (id: number, name: string) =>
export const updateSensor = ({ id, ...body }: { id: number; name: string }) =>
request<SensorInfo>(`/api/sensors/${id}`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name }),
body: JSON.stringify(body),
})

View File

@ -0,0 +1,27 @@
import { cn } from '@/utils/cn'
import { ComponentChild } from 'preact'
type Props = {
open: boolean
onClose: () => void
children: ComponentChild
}
export const Modal = ({ open, onClose, children }: Props) => {
const preventPropagation = (e: Event) => {
e.stopPropagation()
}
return (
<div className={cn('settings-modal', open && 'show')} onMouseDown={onClose}>
<div
className="inner"
onMouseDown={preventPropagation}
onMouseUp={preventPropagation}
onClick={preventPropagation}
>
<div className="body">{children}</div>
</div>
</div>
)
}

View File

@ -16,10 +16,10 @@ export const DashboardHeader = ({ onNewBox, onRefresh }: Props) => {
<div className="dashboard-head">
{verticalMode && (
<>
<button onClick={onNewBox}>
<button className="icon" onClick={onNewBox}>
<PlusIcon />
</button>
<button onClick={onRefresh}>
<button className="icon" onClick={onRefresh}>
<RefreshIcon />
</button>
<div className="filter-button" onClick={() => setFiltersShown(true)}>

View File

@ -1,18 +1,23 @@
import { getSensors } from '@/api/sensors'
import { getSensors, SensorInfo } from '@/api/sensors'
import { PlusIcon } from '@/icons'
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
import { useState } from 'preact/hooks'
import { useQuery } from 'react-query'
import { SensorFormModal } from './components/SensorFormModal'
import { SensorItem } from './components/SensorItem'
export const SensorsPage = () => {
const sensors = useQuery(['/sensors'], getSensors)
const [showNew, setShowNew] = useState(false)
const [edited, setEdited] = useState<SensorInfo>()
return (
<UserLayout
header={
<div className="sensors-head">
<div>Sensors</div>
<button>
<button onClick={() => setShowNew(true)}>
<PlusIcon /> Add sensor
</button>
</div>
@ -32,6 +37,16 @@ export const SensorsPage = () => {
))}
</div>
</div>
{(showNew || edited) && (
<SensorFormModal
open
sensor={edited}
onClose={() => {
setShowNew(false)
setEdited(undefined)
}}
/>
)}
</UserLayout>
)
}

View File

@ -0,0 +1,65 @@
import { createSensor, SensorInfo, updateSensor } from '@/api/sensors'
import { Modal } from '@/components/Modal'
import { useState } from 'preact/hooks'
import { useMutation } from 'react-query'
type Props = {
open: boolean
onClose: () => void
sensor?: SensorInfo
}
export const SensorFormModal = ({ open, onClose, sensor }: Props) => {
const [formState, setFormState] = useState(() => ({
name: sensor?.name ?? '',
}))
const createMutation = useMutation(createSensor)
const updateMutation = useMutation(updateSensor)
const handleSave = async (e: Event) => {
e.preventDefault()
e.stopPropagation()
onClose()
if (sensor) {
updateMutation.mutate({ id: sensor.id, name: formState.name })
} else {
createMutation.mutate(formState.name)
}
}
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement | HTMLInputElement
setFormState({
...formState,
[target.name]: target.value,
})
}
return (
<Modal onClose={onClose} open={open}>
<form onSubmit={handleSave}>
<div className="input">
<label>Name</label>
<input
type="text"
name="name"
minLength={1}
required
onChange={handleChange}
/>
</div>
<div className="actions">
<button className="cancel" type="button">
Cancel
</button>
<button>Save</button>
</div>
</form>
</Modal>
)
}

View File

@ -1,5 +1,5 @@
GIN_MODE=debug
DATABASE_URL=./sensors.sqlite3
DATABASE_URL=./sensors.sqlite3?_busy_timeout=500
PORT=8083
BIND_IP=localhost
AUTH_USERNAME=admin

View File

@ -40,6 +40,7 @@ func main() {
loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server))
loginProtected.GET("/api/sensors", routes.GetSensors(server))
loginProtected.POST("/api/sensors", routes.PostSensors(server))
loginProtected.GET("/api/sensors/:sensor", routes.GetSensor(server))
loginProtected.PUT("/api/sensors/:sensor", routes.PutSensors(server))
loginProtected.GET("/api/sensors/:sensor/values/latest", routes.GetSensorLatestValue(server))
loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server))

View File

@ -74,3 +74,30 @@ func PutSensors(s *app.Server) gin.HandlerFunc {
c.JSON(http.StatusOK, sensor)
}
}
func GetSensor(s *app.Server) gin.HandlerFunc {
return func(c *gin.Context) {
body := putSensorsBody{}
sensorId, err := getSensorId(c)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
if err := c.BindJSON(&body); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
sensor, err := s.Services.Sensors.GetById(sensorId)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, sensor)
}
}

View File

@ -1,6 +1,9 @@
package services
import "encoding/hex"
import (
"crypto/rand"
"math/big"
)
type SensorsService struct {
ctx *Context
@ -43,12 +46,18 @@ func (s *SensorsService) GetList() ([]SensorItem, error) {
}
func (s *SensorsService) Create(name string) (*SensorItem, error) {
item := SensorItem{
Name: name,
AuthKey: hex.EncodeToString(generateRandomKey(32)),
authKey, err := generateRandomString(32)
if err != nil {
return nil, err
}
res, err := s.ctx.DB.Exec("INSERT INTO sensors (id, name, auth_key) VALUES (?, ?)", item.Name, item.AuthKey)
item := SensorItem{
Name: name,
AuthKey: authKey,
}
res, err := s.ctx.DB.Exec("INSERT INTO sensors (name, auth_key) VALUES (?, ?)", item.Name, item.AuthKey)
if err != nil {
return nil, err
@ -93,3 +102,21 @@ func (s *SensorsService) Update(id int64, name string) (*SensorItem, error) {
return item, nil
}
var randomKeySource = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func generateRandomString(length int) (string, error) {
key := make([]byte, length)
for i := range key {
value, err := rand.Int(rand.Reader, big.NewInt(int64(len(randomKeySource))))
if err != nil {
return "", err
}
key[i] = randomKeySource[value.Int64()]
}
return string(key), nil
}

View File

@ -1,9 +1,6 @@
package services
import (
"crypto/rand"
"encoding/hex"
"io"
"time"
)
@ -30,13 +27,19 @@ func (s *SessionsService) GetById(id string) (*SessionItem, error) {
}
func (s *SessionsService) Create() (*SessionItem, error) {
id, err := generateRandomString(128)
if err != nil {
return nil, err
}
item := SessionItem{
// TODO: The key is not guaranteed to be unique, how do we guarantee that?
Id: hex.EncodeToString(generateRandomKey(128)),
Id: id,
ExpiresAt: generateExpiryDate().Unix(),
}
_, err := s.ctx.DB.Exec("INSERT INTO sessions (id, expires_at) VALUES (?, ?)", item.Id, item.ExpiresAt)
_, err = s.ctx.DB.Exec("INSERT INTO sessions (id, expires_at) VALUES (?, ?)", item.Id, item.ExpiresAt)
if err != nil {
return nil, err
@ -68,11 +71,3 @@ func (s *SessionsService) Cleanup() error {
func generateExpiryDate() time.Time {
return time.Now().Add(time.Hour * 24)
}
func generateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}