From 1d461479795f916caaba10b9088ba35bddce3840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Tue, 23 Aug 2022 23:35:36 +0200 Subject: [PATCH] Started working on editable dashboards --- client/src/api/dashboards.ts | 41 ++++++ client/src/assets/style.css | 56 ++++++++ client/src/pages/Router.tsx | 4 +- .../src/pages/dashboard/NewDashboardPage.tsx | 59 ++++++++ .../dashboard/components/EditableBox.tsx | 136 ++++++++++++++++++ client/src/pages/dashboard/constants.ts | 1 + .../src/pages/dashboard/hooks/useDragging.ts | 68 +++++++++ client/src/pages/dashboard/hooks/useResize.ts | 105 ++++++++++++++ client/src/pages/dashboard/types.ts | 3 + .../pages/dashboard/utils/normalizeBoxes.tsx | 34 +++++ client/src/utils/createDashboardContent.ts | 6 + client/src/utils/getElementPosition.ts | 56 ++++++++ client/src/utils/hooks/useEvent.ts | 27 ++++ client/src/utils/hooks/useWindowEvent.ts | 7 + client/src/utils/parseDashboard.ts | 18 +++ .../migrations/1661289034667_dashboards.sql | 5 + server/main.go | 10 +- server/routes/dashboards.go | 86 +++++++++++ server/routes/sensor_config.go | 2 +- server/routes/sensor_values.go | 4 +- server/services/dashboards_service.go | 83 +++++++++++ server/services/sensor_values_service.go | 2 +- server/services/sensors_service.go | 2 +- server/services/services.go | 2 + 24 files changed, 807 insertions(+), 10 deletions(-) create mode 100644 client/src/api/dashboards.ts create mode 100644 client/src/pages/dashboard/NewDashboardPage.tsx create mode 100644 client/src/pages/dashboard/components/EditableBox.tsx create mode 100644 client/src/pages/dashboard/constants.ts create mode 100644 client/src/pages/dashboard/hooks/useDragging.ts create mode 100644 client/src/pages/dashboard/hooks/useResize.ts create mode 100644 client/src/pages/dashboard/types.ts create mode 100644 client/src/pages/dashboard/utils/normalizeBoxes.tsx create mode 100644 client/src/utils/createDashboardContent.ts create mode 100644 client/src/utils/getElementPosition.ts create mode 100644 client/src/utils/hooks/useEvent.ts create mode 100644 client/src/utils/hooks/useWindowEvent.ts create mode 100644 client/src/utils/parseDashboard.ts create mode 100644 server/database/migrations/1661289034667_dashboards.sql create mode 100644 server/routes/dashboards.go create mode 100644 server/services/dashboards_service.go diff --git a/client/src/api/dashboards.ts b/client/src/api/dashboards.ts new file mode 100644 index 0000000..1e02267 --- /dev/null +++ b/client/src/api/dashboards.ts @@ -0,0 +1,41 @@ +import { DashboardContent } from '@/utils/parseDashboard' +import { request } from './request' + +export type DashboardInfo = { + id: string + contents: string +} + +export const getDashboards = () => request('/api/dashboards') + +export const getDashboard = (id: string) => + request(`/api/dashboards/${encodeURI(id)}`) + +export const createDashboard = (body: { + id: string + contents: DashboardContent +}) => + request(`/api/dashboards`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + ...body, + contents: JSON.stringify(body.contents), + }), + }) + +export const updateDashboard = ({ + id, + ...body +}: { + id: string + contents: DashboardContent +}) => + request(`/api/dashboards/${encodeURI(id)}`, { + method: 'PUT', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + ...body, + contents: JSON.stringify(body.contents), + }), + }) diff --git a/client/src/assets/style.css b/client/src/assets/style.css index 14c099e..9bcfd45 100644 --- a/client/src/assets/style.css +++ b/client/src/assets/style.css @@ -194,3 +194,59 @@ form.horizontal .input label { .checkbox-label input[type=checkbox] { margin-top: 6px; } + +.grid-sensors { + position: relative; + margin: 0.25rem; +} + +.grid-sensors .grid-box { + position: absolute; + transition: all 0.2s; + padding: 0.25rem; + box-sizing: border-box; +} + +.grid-sensors .grid-box.dragging { + transition: none; +} + +.grid-sensors .grid-box .box { + position: relative; +} + +.grid-sensors .grid-box .box .resize-h { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 10px; + cursor: e-resize; +} + +.grid-sensors .grid-box .box .resize-v { + position: absolute; + right: 0; + left: 0; + bottom: 0; + height: 10px; + cursor: s-resize; +} + +.grid-sensors .grid-box .box .resize { + position: absolute; + right: 0;; + bottom: 0; + width: 10px; + height: 10px; + cursor: se-resize; +} + +.grid-sensors .box-preview { + position: absolute; + background-color: #3988FF; + opacity: 0.5; + transition: all 0.2s; + z-index: 1; + border-radius: 0.5rem; +} \ No newline at end of file diff --git a/client/src/pages/Router.tsx b/client/src/pages/Router.tsx index 9a47109..0e08c55 100644 --- a/client/src/pages/Router.tsx +++ b/client/src/pages/Router.tsx @@ -1,5 +1,5 @@ import { useAppContext } from '@/contexts/AppContext' -import { DashboardPage } from './dashboard/DashboardPage' +import { NewDashboardPage } from './dashboard/NewDashboardPage' import { LoginPage } from './login/LoginPage' export const Router = () => { @@ -7,7 +7,7 @@ export const Router = () => { return ( <> - {loggedIn && } + {loggedIn && } {!loggedIn && } ) diff --git a/client/src/pages/dashboard/NewDashboardPage.tsx b/client/src/pages/dashboard/NewDashboardPage.tsx new file mode 100644 index 0000000..e229bb8 --- /dev/null +++ b/client/src/pages/dashboard/NewDashboardPage.tsx @@ -0,0 +1,59 @@ +import { createDashboard, getDashboards } from '@/api/dashboards' +import { createDashboardContent } from '@/utils/createDashboardContent' +import { parseDashboard } from '@/utils/parseDashboard' +import { useEffect, useMemo, useState } from 'preact/hooks' +import { useQuery } from 'react-query' +import { EditableBox } from './components/EditableBox' +import { normalizeBoxes } from './utils/normalizeBoxes' + +export const NewDashboardPage = () => { + const dashboards = useQuery(['/dashboards'], getDashboards) + const dashboard = dashboards.data?.find((i) => i.id === 'default') + + const dashboardContent = useMemo( + () => (dashboard ? parseDashboard(dashboard.contents) : undefined), + [dashboard] + ) + + // Terrible code - ensure there's default dashboard + useEffect(() => { + if (dashboards.data && !dashboard) { + createDashboard({ + id: 'default', + contents: createDashboardContent(), + }).then(() => { + dashboards.refetch() + }) + } + }, [dashboards.data, dashboard]) + + const [boxes, setBoxes] = useState(dashboardContent?.boxes ?? []) + + return ( +
+ {boxes.map((b) => ( + { + setBoxes((previous) => { + b.x = p.x + b.y = p.y + + return normalizeBoxes([...previous]) + }) + }} + onResize={(s) => { + setBoxes((previous) => { + b.w = s.w + b.h = s.h + + return normalizeBoxes([...previous]) + }) + }} + /> + ))} +
+ ) +} diff --git a/client/src/pages/dashboard/components/EditableBox.tsx b/client/src/pages/dashboard/components/EditableBox.tsx new file mode 100644 index 0000000..dae98ca --- /dev/null +++ b/client/src/pages/dashboard/components/EditableBox.tsx @@ -0,0 +1,136 @@ +import { getElementPosition } from '@/utils/getElementPosition' +import { useWindowEvent } from '@/utils/hooks/useWindowEvent' +import { useRef } from 'preact/hooks' +import { GRID_WIDTH } from '../constants' +import { ResizingMode, useResize } from '../hooks/useResize' +import { useDragging } from '../hooks/useDragging' +import { BoxDefinition } from '../types' + +type Props = { + box: BoxDefinition + boxes: BoxDefinition[] + onPosition: (p: { x: number; y: number }) => void + onResize: (p: { w: number; h: number }) => void +} + +export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => { + const boxRef = useRef(null) + + const outerWidth = boxRef.current?.parentElement?.offsetWidth ?? 100 + const cellWidth = outerWidth / GRID_WIDTH + + const { dragging, setDragging, draggingPosition } = useDragging({ + cellWidth, + box, + boxes, + }) + + const { resizing, setResizing, resizingSize } = useResize({ + box, + boxes, + cellWidth, + }) + + const handleMouseDown = (e: MouseEvent) => { + e.preventDefault() + + if (!dragging) { + const pos = getElementPosition(e.target as HTMLDivElement) + + setDragging({ + active: true, + x: pos.left, + y: pos.top, + offsetX: e.clientX - pos.left, + offsetY: e.clientY - pos.top, + }) + } + } + + const handleResizeDown = (target: ResizingMode) => (e: MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + if (!resizing) { + setResizing({ + mode: target, + w: (box.w / GRID_WIDTH) * outerWidth, + h: box.h, + offsetX: e.clientX, + offsetY: e.clientY, + }) + } + } + + useWindowEvent('mouseup', () => { + if (dragging) { + onPosition(draggingPosition) + setDragging({ ...dragging, active: false }) + } + + if (resizing.mode !== ResizingMode.NONE) { + onResize({ w: resizingSize.w, h: resizingSize.h }) + setResizing({ ...resizing, mode: ResizingMode.NONE }) + } + }) + + return ( + <> +
+
+
+
+
+
+
+ + {resizing.mode !== ResizingMode.NONE && ( +
+ )} + + {dragging.active && ( +
+ )} + + ) +} diff --git a/client/src/pages/dashboard/constants.ts b/client/src/pages/dashboard/constants.ts new file mode 100644 index 0000000..96a1753 --- /dev/null +++ b/client/src/pages/dashboard/constants.ts @@ -0,0 +1 @@ +export const GRID_WIDTH = 20 diff --git a/client/src/pages/dashboard/hooks/useDragging.ts b/client/src/pages/dashboard/hooks/useDragging.ts new file mode 100644 index 0000000..be5be58 --- /dev/null +++ b/client/src/pages/dashboard/hooks/useDragging.ts @@ -0,0 +1,68 @@ +import { useWindowEvent } from '@/utils/hooks/useWindowEvent' +import { useState } from 'preact/hooks' +import { BoxDefinition } from '../types' +import { GRID_WIDTH } from '../constants' + +type Props = { + cellWidth: number + boxes: BoxDefinition[] + box: BoxDefinition +} + +// TODO: This is not optimized at all +export const useDragging = ({ cellWidth, boxes, box }: Props) => { + const [state, setState] = useState({ + active: false, + x: 0, + y: 0, + offsetX: 0, + offsetY: 0, + }) + + const actualX = Math.max( + 0, + Math.min(GRID_WIDTH - box.w, Math.round(state.x / cellWidth)) + ) + + const dragY = Math.round(state.y / 16) * 16 + + const gridHeights = Array(GRID_WIDTH) + .fill(null) + .map((_, index) => { + return boxes + .filter( + (b) => + b.id !== box.id && + b.x <= index && + b.x + b.w > index && + ((b.y < dragY + box.h && dragY < b.y + b.h) || b.y < dragY) + ) + .reduce( + (acc, item) => (item.y + item.h > acc ? item.y + item.h : acc), + 0 + ) + }) + + useWindowEvent('mousemove', (e) => { + if (state.active) { + setState({ + ...state, + x: e.clientX - state.offsetX, + y: e.clientY - state.offsetY, + }) + } + }) + + return { + dragging: state, + setDragging: setState, + draggingPosition: { + x: actualX, + y: Math.max( + ...Array(box.w) + .fill(null) + .map((_, x) => gridHeights[actualX + x]) + ), + }, + } +} diff --git a/client/src/pages/dashboard/hooks/useResize.ts b/client/src/pages/dashboard/hooks/useResize.ts new file mode 100644 index 0000000..fabb57c --- /dev/null +++ b/client/src/pages/dashboard/hooks/useResize.ts @@ -0,0 +1,105 @@ +import { useWindowEvent } from '@/utils/hooks/useWindowEvent' +import { useState } from 'preact/hooks' +import { BoxDefinition } from '../types' +import { GRID_WIDTH } from '../constants' + +export enum ResizingMode { + NONE, + WIDTH, + HEIGHT, + ALL, +} + +type Props = { + cellWidth: number + boxes: BoxDefinition[] + box: BoxDefinition +} + +// TODO: This is not optimized at all +export const useResize = ({ cellWidth, box, boxes }: Props) => { + const [state, setState] = useState({ + mode: ResizingMode.NONE, + w: 0, + h: 0, + offsetX: 0, + offsetY: 0, + }) + + const isResizing = state.mode !== ResizingMode.NONE + + const maxHeights = isResizing + ? Array(GRID_WIDTH) + .fill(null) + .map((_, index) => { + return boxes + .filter( + (b) => + b.id !== box.id && + b.x <= index && + b.x + b.w > index && + b.y > box.y + ) + .reduce( + (acc, item) => (item.y - box.y > acc ? item.y - box.y : acc), + 0 + ) + }) + : [] + + const actualHeight = isResizing + ? Math.min( + ...Array(box.w) + .fill(null) + .map((_, x) => maxHeights[box.x + x]) + .filter((x) => x > 0), + Math.round(state.h / 16) * 16 + ) + : 0 + + const maxWidth = isResizing + ? boxes + .filter( + (b) => + b.id !== box.id && + b.x > box.x && + b.y < box.y + box.h && + box.y < b.y + b.h + ) + .map((b) => b.x - box.x) + .reduce((acc, item) => (item < acc ? item : acc), GRID_WIDTH) + : 0 + + const actualWidth = Math.min(maxWidth, Math.round(state.w / cellWidth)) + + useWindowEvent('mousemove', (e) => { + if (isResizing) { + const newState = { + ...state, + } + + if ( + state.mode === ResizingMode.ALL || + state.mode === ResizingMode.HEIGHT + ) { + newState.h = box.h + e.clientY - state.offsetY + } + + if ( + state.mode === ResizingMode.ALL || + state.mode === ResizingMode.WIDTH + ) { + newState.w = + (box.w / GRID_WIDTH) * outerWidth + e.clientX - state.offsetX + } + + setState(newState) + } + }) + + return { + setResizing: setState, + resizingSize: { w: actualWidth, h: actualHeight }, + resizing: state, + } +} diff --git a/client/src/pages/dashboard/types.ts b/client/src/pages/dashboard/types.ts new file mode 100644 index 0000000..a2c3b23 --- /dev/null +++ b/client/src/pages/dashboard/types.ts @@ -0,0 +1,3 @@ +import { DashboardContentBox } from '@/utils/parseDashboard' + +export type BoxDefinition = DashboardContentBox diff --git a/client/src/pages/dashboard/utils/normalizeBoxes.tsx b/client/src/pages/dashboard/utils/normalizeBoxes.tsx new file mode 100644 index 0000000..f198556 --- /dev/null +++ b/client/src/pages/dashboard/utils/normalizeBoxes.tsx @@ -0,0 +1,34 @@ +import { BoxDefinition } from '../types' + +export function normalizeBoxes(boxes: BoxDefinition[]) { + // TODO: This is not optimized at all + for (let i = 0; i < boxes.length; i++) { + const box = boxes[i] + + if (box.y > 0) { + const above = boxes + .filter( + (b) => + b.id !== box.id && + b.x < box.x + box.w && + box.x < b.x + b.w && + b.y < box.y + ) + .sort((a, b) => b.y - a.y) + + if (above.length === 0) { + box.y = 0 + i = -1 + } else { + const newY = above[0].h + above[0].y + + if (box.y !== newY) { + box.y = newY + i = -1 + } + } + } + } + + return boxes +} diff --git a/client/src/utils/createDashboardContent.ts b/client/src/utils/createDashboardContent.ts new file mode 100644 index 0000000..629f807 --- /dev/null +++ b/client/src/utils/createDashboardContent.ts @@ -0,0 +1,6 @@ +import { DashboardContent } from './parseDashboard' + +export const createDashboardContent = (): DashboardContent => ({ + version: '1.0', + boxes: [], +}) diff --git a/client/src/utils/getElementPosition.ts b/client/src/utils/getElementPosition.ts new file mode 100644 index 0000000..7a0c044 --- /dev/null +++ b/client/src/utils/getElementPosition.ts @@ -0,0 +1,56 @@ +export interface ElementPositionResult { + left: number + top: number + width: number + height: number +} + +export function getElementPosition( + el: HTMLElement, + global = true +): ElementPositionResult { + if (!el) { + return { + left: 0, + top: 0, + width: 0, + height: 0, + } + } + + const bb = el.getBoundingClientRect() + + if (global) { + return { + left: bb.left, + top: bb.top, + width: bb.width, + height: bb.height, + } + } + + let offsetLeft = el.offsetLeft + let offsetTop = el.offsetTop + let current: HTMLElement | null = el.offsetParent as HTMLElement + + while (current) { + offsetLeft += current.offsetLeft + offsetTop += current.offsetTop + current = current.offsetParent as HTMLElement + + if (!global && current) { + const position = getComputedStyle(current).getPropertyValue('position') + + if (position === 'relative' || position === 'absolute') { + break + } + } + } + + return { + left: offsetLeft, + top: offsetTop, + width: bb.width, + height: bb.height, + } +} diff --git a/client/src/utils/hooks/useEvent.ts b/client/src/utils/hooks/useEvent.ts new file mode 100644 index 0000000..1ffc052 --- /dev/null +++ b/client/src/utils/hooks/useEvent.ts @@ -0,0 +1,27 @@ +import { useCallback, useEffect, useRef } from 'preact/hooks' + +export const useEvent = ( + target: EventTarget, + event: string, + callback: (e: E) => void, + cancelBubble = false +) => { + // Hold reference to callback + const callbackRef = useRef(callback) + callbackRef.current = callback + + // Since we use ref, .current will always be correct callback + const listener = useCallback((e: Event) => { + if (callbackRef.current) { + callbackRef.current(e as E) + } + }, []) + + useEffect(() => { + // Add our listener on mount + target.addEventListener(event, listener, cancelBubble) + + // Remove it on dismount + return () => target.removeEventListener(event, listener) + }, [target, event, cancelBubble, listener]) +} diff --git a/client/src/utils/hooks/useWindowEvent.ts b/client/src/utils/hooks/useWindowEvent.ts new file mode 100644 index 0000000..c594026 --- /dev/null +++ b/client/src/utils/hooks/useWindowEvent.ts @@ -0,0 +1,7 @@ +import { useEvent } from './useEvent' + +export const useWindowEvent = ( + event: K, + callback: (e: WindowEventMap[K]) => void, + cancelBubble = false +) => useEvent(window, event, callback, cancelBubble) diff --git a/client/src/utils/parseDashboard.ts b/client/src/utils/parseDashboard.ts new file mode 100644 index 0000000..ba65255 --- /dev/null +++ b/client/src/utils/parseDashboard.ts @@ -0,0 +1,18 @@ +export type DashboardContent = { + version: string + boxes: DashboardContentBox[] +} + +export type DashboardContentBox = { + id: string + x: number + y: number + w: number + h: number + sensor: string + title?: string +} + +export const parseDashboard = (input: string) => { + return JSON.parse(input) as DashboardContent +} diff --git a/server/database/migrations/1661289034667_dashboards.sql b/server/database/migrations/1661289034667_dashboards.sql new file mode 100644 index 0000000..313c025 --- /dev/null +++ b/server/database/migrations/1661289034667_dashboards.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS dashboards ( + id TEXT NOT NULL, + contents TEXT NOT NULL, + PRIMARY KEY (id) +); diff --git a/server/main.go b/server/main.go index 9e44638..83d67e6 100644 --- a/server/main.go +++ b/server/main.go @@ -39,14 +39,18 @@ func main() { // Routes that are only accessible after logging in loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server)) loginProtected.GET("/api/sensors", routes.GetSensors(server)) - loginProtected.GET("/api/sensors/:sensor/values", routes.HandleGetSensorValues(server)) + loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server)) loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server)) - loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.HandlePutSensorConfig(server)) + loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server)) + loginProtected.GET("/api/dashboards", routes.GetDashboards(server)) + loginProtected.POST("/api/dashboards", routes.PostDashboard(server)) + loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server)) + loginProtected.PUT("/api/dashboards/:id", routes.PutDashboard(server)) loginProtected.POST("/api/logout", routes.Logout(server)) // Routes accessible using auth key keyProtected := router.Group("/", middleware.KeyAuthMiddleware(server)) - keyProtected.POST("/api/sensors/:sensor/values", routes.HandlePostSensorValues(server)) + keyProtected.POST("/api/sensors/:sensor/values", routes.PostSensorValues(server)) // Starts session cleanup goroutine server.StartCleaner() diff --git a/server/routes/dashboards.go b/server/routes/dashboards.go new file mode 100644 index 0000000..da648fe --- /dev/null +++ b/server/routes/dashboards.go @@ -0,0 +1,86 @@ +package routes + +import ( + "basic-sensor-receiver/app" + "net/http" + + "github.com/gin-gonic/gin" +) + +type postDashboardBody struct { + Id string `json:"id"` + Contents string `json:"contents"` +} + +type putDashboardBody struct { + Contents string `json:"contents"` +} + +func GetDashboards(s *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + items, err := s.Services.Dashboards.GetList() + + if err != nil { + c.AbortWithError(500, err) + return + } + + c.JSON(http.StatusOK, items) + } +} + +func GetDashboardById(s *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + + item, err := s.Services.Dashboards.GetById(id) + + if err != nil { + c.AbortWithError(500, err) + return + } + + c.JSON(http.StatusOK, item) + } +} + +func PostDashboard(s *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + body := postDashboardBody{} + + if err := c.BindJSON(&body); err != nil { + c.AbortWithError(400, err) + return + } + + item, err := s.Services.Dashboards.Create(body.Id, body.Contents) + + if err != nil { + c.AbortWithError(500, err) + return + } + + c.JSON(http.StatusOK, item) + } +} + +func PutDashboard(s *app.Server) gin.HandlerFunc { + return func(c *gin.Context) { + id := c.Param("id") + body := putDashboardBody{} + + if err := c.BindJSON(&body); err != nil { + c.AbortWithError(400, err) + return + } + + item, err := s.Services.Dashboards.Update(id, body.Contents) + + if err != nil { + c.AbortWithError(500, err) + return + } + + c.JSON(http.StatusOK, item) + } +} diff --git a/server/routes/sensor_config.go b/server/routes/sensor_config.go index 193df41..0f7c6ba 100644 --- a/server/routes/sensor_config.go +++ b/server/routes/sensor_config.go @@ -11,7 +11,7 @@ type sensorConfigValue struct { Value string `json:"value"` } -func HandlePutSensorConfig(s *app.Server) gin.HandlerFunc { +func PutSensorConfig(s *app.Server) gin.HandlerFunc { return func(c *gin.Context) { var configValue sensorConfigValue sensor := c.Param("sensor") diff --git a/server/routes/sensor_values.go b/server/routes/sensor_values.go index df6ef8f..a883475 100644 --- a/server/routes/sensor_values.go +++ b/server/routes/sensor_values.go @@ -16,7 +16,7 @@ type getSensorQuery struct { To int64 `form:"to"` } -func HandlePostSensorValues(s *app.Server) gin.HandlerFunc { +func PostSensorValues(s *app.Server) gin.HandlerFunc { return func(c *gin.Context) { var newValue postSensorValueBody sensor := c.Param("sensor") @@ -35,7 +35,7 @@ func HandlePostSensorValues(s *app.Server) gin.HandlerFunc { } } -func HandleGetSensorValues(s *app.Server) gin.HandlerFunc { +func GetSensorValues(s *app.Server) gin.HandlerFunc { return func(c *gin.Context) { var query getSensorQuery diff --git a/server/services/dashboards_service.go b/server/services/dashboards_service.go new file mode 100644 index 0000000..a7daff5 --- /dev/null +++ b/server/services/dashboards_service.go @@ -0,0 +1,83 @@ +package services + +type DashboardsService struct { + ctx *Context +} + +type DashboardItem struct { + Id string `json:"id"` + Contents string `json:"contents"` +} + +func (s *DashboardsService) GetList() ([]DashboardItem, error) { + items := make([]DashboardItem, 0) + + rows, err := s.ctx.DB.Query("SELECT id, contents FROM dashboards") + + if err != nil { + return nil, err + } + + defer rows.Close() + + for rows.Next() { + item := DashboardItem{} + + err := rows.Scan(&item.Id, &item.Contents) + if err != nil { + return nil, err + } + + items = append(items, item) + } + + err = rows.Err() + if err != nil { + return nil, err + } + + return items, nil +} + +func (s *DashboardsService) Create(id string, contents string) (*DashboardItem, error) { + item := DashboardItem{ + Id: id, + Contents: contents, + } + + _, err := s.ctx.DB.Exec("INSERT INTO dashboards (id, contents) VALUES (?, ?)", item.Id, item.Contents) + + if err != nil { + return nil, err + } + + return &item, nil +} + +func (s *DashboardsService) Update(id string, contents string) (*DashboardItem, error) { + item := DashboardItem{ + Id: id, + Contents: contents, + } + + _, err := s.ctx.DB.Exec("UPDATE dashboards SET contents = ? WHERE id = ?", contents, id) + + if err != nil { + return nil, err + } + + return &item, nil +} + +func (s *DashboardsService) GetById(id string) (*DashboardItem, error) { + item := DashboardItem{} + + row := s.ctx.DB.QueryRow("SELECT id, contents FROM dashboards WHERE id = ?", id) + + err := row.Scan(&item.Id, &item.Contents) + if err != nil { + return nil, err + } + + return &item, nil +} diff --git a/server/services/sensor_values_service.go b/server/services/sensor_values_service.go index 1155815..c5255fb 100644 --- a/server/services/sensor_values_service.go +++ b/server/services/sensor_values_service.go @@ -24,7 +24,7 @@ func (s *SensorValuesService) Push(sensor string, value float64) (int64, error) func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]sensorValue, error) { var value float64 var timestamp int64 - var values []sensorValue + values := make([]sensorValue, 0) rows, err := s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensor, from, to) diff --git a/server/services/sensors_service.go b/server/services/sensors_service.go index fba72e2..c1008dc 100644 --- a/server/services/sensors_service.go +++ b/server/services/sensors_service.go @@ -11,7 +11,7 @@ type sensorItem struct { } func (s *SensorsService) GetList() ([]sensorItem, error) { - var sensors []sensorItem + sensors := make([]sensorItem, 0) var sensor string var lastUpdate string diff --git a/server/services/services.go b/server/services/services.go index 56f8953..ab9e13e 100644 --- a/server/services/services.go +++ b/server/services/services.go @@ -11,6 +11,7 @@ type Services struct { Sensors *SensorsService Sessions *SessionsService Auth *AuthService + Dashboards *DashboardsService } type Context struct { @@ -29,6 +30,7 @@ func InitializeServices(ctx *Context) *Services { services.Sensors = &SensorsService{ctx: ctx} services.Sessions = &SessionsService{ctx: ctx} services.Auth = &AuthService{ctx: ctx} + services.Dashboards = &DashboardsService{ctx: ctx} return &services }