Started working on editable dashboards
This commit is contained in:
parent
6f85f4a45b
commit
1d46147979
|
|
@ -0,0 +1,41 @@
|
|||
import { DashboardContent } from '@/utils/parseDashboard'
|
||||
import { request } from './request'
|
||||
|
||||
export type DashboardInfo = {
|
||||
id: string
|
||||
contents: string
|
||||
}
|
||||
|
||||
export const getDashboards = () => request<DashboardInfo[]>('/api/dashboards')
|
||||
|
||||
export const getDashboard = (id: string) =>
|
||||
request<DashboardInfo>(`/api/dashboards/${encodeURI(id)}`)
|
||||
|
||||
export const createDashboard = (body: {
|
||||
id: string
|
||||
contents: DashboardContent
|
||||
}) =>
|
||||
request<DashboardInfo>(`/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<DashboardInfo>(`/api/dashboards/${encodeURI(id)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...body,
|
||||
contents: JSON.stringify(body.contents),
|
||||
}),
|
||||
})
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 && <DashboardPage />}
|
||||
{loggedIn && <NewDashboardPage />}
|
||||
{!loggedIn && <LoginPage />}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="grid-sensors">
|
||||
{boxes.map((b) => (
|
||||
<EditableBox
|
||||
box={b}
|
||||
key={b.id}
|
||||
boxes={boxes}
|
||||
onPosition={(p) => {
|
||||
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])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<HTMLDivElement>(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 (
|
||||
<>
|
||||
<div
|
||||
ref={boxRef}
|
||||
className={`grid-box${dragging ? ' dragging' : ''}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
left: (box.x / GRID_WIDTH) * 100 + '%',
|
||||
top: box.y + 'px',
|
||||
width: (box.w / GRID_WIDTH) * 100 + '%',
|
||||
height: box.h + 'px',
|
||||
...(dragging.active && {
|
||||
left: dragging.x,
|
||||
top: dragging.y,
|
||||
zIndex: 2,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div className="box" style={{ height: '100%' }}>
|
||||
<div
|
||||
className="resize-h"
|
||||
onMouseDown={handleResizeDown(ResizingMode.WIDTH)}
|
||||
/>
|
||||
<div
|
||||
className="resize-v"
|
||||
onMouseDown={handleResizeDown(ResizingMode.HEIGHT)}
|
||||
/>
|
||||
<div
|
||||
className="resize"
|
||||
onMouseDown={handleResizeDown(ResizingMode.ALL)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{resizing.mode !== ResizingMode.NONE && (
|
||||
<div
|
||||
className={'box-preview'}
|
||||
style={{
|
||||
left: (box.x / GRID_WIDTH) * 100 + '%',
|
||||
top: box.y + 'px',
|
||||
width: (resizingSize.w / GRID_WIDTH) * 100 + '%',
|
||||
height: resizingSize.h + 'px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dragging.active && (
|
||||
<div
|
||||
className={'box-preview'}
|
||||
style={{
|
||||
left: (draggingPosition.x / GRID_WIDTH) * 100 + '%',
|
||||
top: draggingPosition.y,
|
||||
width: (box.w / GRID_WIDTH) * 100 + '%',
|
||||
height: box.h + 'px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const GRID_WIDTH = 20
|
||||
|
|
@ -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])
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { DashboardContentBox } from '@/utils/parseDashboard'
|
||||
|
||||
export type BoxDefinition = DashboardContentBox
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { DashboardContent } from './parseDashboard'
|
||||
|
||||
export const createDashboardContent = (): DashboardContent => ({
|
||||
version: '1.0',
|
||||
boxes: [],
|
||||
})
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||
|
||||
export const useEvent = <E extends Event>(
|
||||
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])
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { useEvent } from './useEvent'
|
||||
|
||||
export const useWindowEvent = <K extends keyof WindowEventMap>(
|
||||
event: K,
|
||||
callback: (e: WindowEventMap[K]) => void,
|
||||
cancelBubble = false
|
||||
) => useEvent(window, event, callback, cancelBubble)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE IF NOT EXISTS dashboards (
|
||||
id TEXT NOT NULL,
|
||||
contents TEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue