Working on sensors page
This commit is contained in:
parent
85201e254e
commit
c56a678925
|
|
@ -11,7 +11,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"plotly.js": "^2.14.0",
|
"plotly.js": "^2.14.0",
|
||||||
"preact": "^10.10.6",
|
"preact": "^10.10.6",
|
||||||
"react-query": "^3.39.2"
|
"react-query": "^3.39.2",
|
||||||
|
"wouter": "^2.8.0-alpha.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/preset-vite": "^2.3.0",
|
"@preact/preset-vite": "^2.3.0",
|
||||||
|
|
@ -6423,6 +6424,14 @@
|
||||||
"object-assign": "^4.1.0"
|
"object-assign": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wouter": {
|
||||||
|
"version": "2.8.0-alpha.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wouter/-/wouter-2.8.0-alpha.2.tgz",
|
||||||
|
"integrity": "sha512-aPsL5m5rW9RiceClOmGj6t5gn9Ut2TJVr98UDi1u9MIRNYiYVflg6vFIjdDYJ4IAyH0JdnkSgGwfo0LQS3k2zg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|
@ -11300,6 +11309,12 @@
|
||||||
"object-assign": "^4.1.0"
|
"object-assign": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wouter": {
|
||||||
|
"version": "2.8.0-alpha.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wouter/-/wouter-2.8.0-alpha.2.tgz",
|
||||||
|
"integrity": "sha512-aPsL5m5rW9RiceClOmGj6t5gn9Ut2TJVr98UDi1u9MIRNYiYVflg6vFIjdDYJ4IAyH0JdnkSgGwfo0LQS3k2zg==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"plotly.js": "^2.14.0",
|
"plotly.js": "^2.14.0",
|
||||||
"preact": "^10.10.6",
|
"preact": "^10.10.6",
|
||||||
"react-query": "^3.39.2"
|
"react-query": "^3.39.2",
|
||||||
|
"wouter": "^2.8.0-alpha.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,23 @@
|
||||||
import { request } from './request'
|
import { request } from './request'
|
||||||
|
|
||||||
export type SensorInfo = {
|
export type SensorInfo = {
|
||||||
sensor: string
|
id: number
|
||||||
config: Record<string, string>
|
name: string
|
||||||
|
authKey: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSensors = () => request<SensorInfo[]>('/api/sensors')
|
export const getSensors = () => request<SensorInfo[]>('/api/sensors')
|
||||||
|
|
||||||
|
export const createSensor = (name: string) =>
|
||||||
|
request<SensorInfo>('/api/sensors', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const updateSensor = (id: number, name: string) =>
|
||||||
|
request<SensorInfo>(`/api/sensors/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
button {
|
||||||
|
background: var(--button-bg-color);
|
||||||
|
color: var(--button-fg-color);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
padding: 0.25rem 0.8rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button > .svg-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1px;
|
||||||
|
margin-right: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--button-hover-bg-color);
|
||||||
|
color: var(--button-hover-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.icon {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.icon > .svg-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
.sensors-page {
|
||||||
|
.sensors-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.content {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
> .box {
|
||||||
|
max-width: 50rem;
|
||||||
|
padding: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensors-list {
|
||||||
|
.sensor-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
|
||||||
|
&.head {
|
||||||
|
opacity: 0.75;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .id {
|
||||||
|
flex: 0.3;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .name {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .key {
|
||||||
|
display: flex;
|
||||||
|
flex: 1.5;
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .actions {
|
||||||
|
flex: 2;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path></svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 431 B |
|
|
@ -1,4 +1,5 @@
|
||||||
:root {
|
:root {
|
||||||
|
--main-font: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
--main-bg-color: #eee;
|
--main-bg-color: #eee;
|
||||||
--main-fg-color: #000;
|
--main-fg-color: #000;
|
||||||
--button-bg-color: #3988ff;
|
--button-bg-color: #3988ff;
|
||||||
|
|
@ -66,30 +67,16 @@ html {
|
||||||
body {
|
body {
|
||||||
background: var(--main-bg-color);
|
background: var(--main-bg-color);
|
||||||
color: var(--main-fg-color);
|
color: var(--main-fg-color);
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
font-family: var(--main-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
font-family: var(--main-font);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
@import 'components/button';
|
||||||
background: var(--button-bg-color);
|
|
||||||
color: var(--button-fg-color);
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--button-hover-bg-color);
|
|
||||||
color: var(--button-hover-fg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
|
|
@ -197,6 +184,7 @@ header.header > .inner > .menu-button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 125%;
|
font-size: 125%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
margin-right: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
section.content {
|
section.content {
|
||||||
|
|
@ -249,7 +237,7 @@ section.content {
|
||||||
.settings-modal .inner {
|
.settings-modal .inner {
|
||||||
background-color: var(--box-bg-color);
|
background-color: var(--box-bg-color);
|
||||||
color: var(--box-fg-color);
|
color: var(--box-fg-color);
|
||||||
margin-top: 5%;
|
margin-top: 5vh;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
|
|
@ -490,6 +478,11 @@ form.horizontal .input label {
|
||||||
stroke: currentColor;
|
stroke: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
|
|
@ -501,3 +494,4 @@ form.horizontal .input label {
|
||||||
|
|
||||||
@import "components/dashboard-header";
|
@import "components/dashboard-header";
|
||||||
@import "components/filters-panel";
|
@import "components/filters-panel";
|
||||||
|
@import "components/sensors-page";
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,6 @@ export { ReactComponent as MenuIcon } from '@/assets/icons/menu.svg'
|
||||||
export { ReactComponent as CancelIcon } from '@/assets/icons/cancel.svg'
|
export { ReactComponent as CancelIcon } from '@/assets/icons/cancel.svg'
|
||||||
export { ReactComponent as FiltersIcon } from '@/assets/icons/filters.svg'
|
export { ReactComponent as FiltersIcon } from '@/assets/icons/filters.svg'
|
||||||
export { ReactComponent as PlusIcon } from '@/assets/icons/plus.svg'
|
export { ReactComponent as PlusIcon } from '@/assets/icons/plus.svg'
|
||||||
|
export { ReactComponent as EyeIcon } from '@/assets/icons/eye.svg'
|
||||||
|
export { ReactComponent as EyeOffIcon } from '@/assets/icons/eye-off.svg'
|
||||||
|
export { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { CancelIcon } from '@/icons'
|
import { CancelIcon } from '@/icons'
|
||||||
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shown: boolean
|
shown: boolean
|
||||||
|
|
@ -16,7 +17,7 @@ export const UserMenu = ({ shown, onHide }: Props) => {
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#">Dashboards</a>
|
<a href="#">Dashboards</a>
|
||||||
<a href="#">Sensors</a>
|
<Link href="/sensors">Sensors</Link>
|
||||||
<a href="#">Settings</a>
|
<a href="#">Settings</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,25 @@
|
||||||
import { useAppContext } from '@/contexts/AppContext'
|
import { useAppContext } from '@/contexts/AppContext'
|
||||||
|
import { useHashLocation } from '@/utils/hooks/useHashLocation'
|
||||||
|
import { Route, Router as Wouter } from 'wouter'
|
||||||
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'
|
||||||
|
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
const { loggedIn } = useAppContext()
|
const { loggedIn } = useAppContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{loggedIn && <NewDashboardPage />}
|
{loggedIn && (
|
||||||
|
<Wouter hook={useHashLocation}>
|
||||||
|
<Route path="/">
|
||||||
|
<NewDashboardPage />
|
||||||
|
</Route>
|
||||||
|
<Route path="/sensors">
|
||||||
|
<SensorsPage />
|
||||||
|
</Route>
|
||||||
|
</Wouter>
|
||||||
|
)}
|
||||||
{!loggedIn && <LoginPage />}
|
{!loggedIn && <LoginPage />}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{sensors.data?.map((s) => (
|
{sensors.data?.map((s) => (
|
||||||
<option key={s.sensor} value={s.sensor}>
|
<option key={s.id} value={s.id}>
|
||||||
{s.config?.name ?? s.sensor}
|
{s.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ export const EditableBox = ({
|
||||||
<div className="box" style={{ height: '100%' }}>
|
<div className="box" style={{ height: '100%' }}>
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="drag-handle" onMouseDown={handleMouseDown}>
|
<div className="drag-handle" onMouseDown={handleMouseDown}>
|
||||||
<div className="name">{box.title ?? box.sensor ?? ''}</div>
|
<div className="name">{box.title || box.sensor || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="action" onClick={() => refreshRef.current?.()}>
|
<div className="action" onClick={() => refreshRef.current?.()}>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => {
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(value).map(([name, value]) =>
|
Object.entries(value).map(([name, value]) =>
|
||||||
setSensorConfig({ sensor: sensor.sensor, name, value })
|
setSensorConfig({ sensor: sensor.id, name, value })
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -64,7 +64,7 @@ export const SensorSettings = ({ sensor, onClose, onUpdate }: Props) => {
|
||||||
<form onSubmit={handleSave}>
|
<form onSubmit={handleSave}>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Sensor</label>
|
<label>Sensor</label>
|
||||||
<input value={sensor.sensor} disabled />
|
<input value={sensor.id} disabled />
|
||||||
</div>
|
</div>
|
||||||
<div className="input">
|
<div className="input">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { getSensors } from '@/api/sensors'
|
||||||
|
import { PlusIcon } from '@/icons'
|
||||||
|
import { UserLayout } from '@/layouts/UserLayout/UserLayout'
|
||||||
|
import { useQuery } from 'react-query'
|
||||||
|
import { SensorItem } from './components/SensorItem'
|
||||||
|
|
||||||
|
export const SensorsPage = () => {
|
||||||
|
const sensors = useQuery(['/sensors'], getSensors)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserLayout
|
||||||
|
header={
|
||||||
|
<div className="sensors-head">
|
||||||
|
<div>Sensors</div>
|
||||||
|
<button>
|
||||||
|
<PlusIcon /> Add sensor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className="sensors-page"
|
||||||
|
>
|
||||||
|
<div className="box">
|
||||||
|
<div className="sensors-list">
|
||||||
|
<div className="sensor-item head">
|
||||||
|
<div className="id">ID</div>
|
||||||
|
<div className="name">Name</div>
|
||||||
|
<div className="key">Key</div>
|
||||||
|
<div className="actions"></div>
|
||||||
|
</div>
|
||||||
|
{sensors.data?.map((i) => (
|
||||||
|
<SensorItem key={i.id} sensor={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UserLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { SensorInfo } from '@/api/sensors'
|
||||||
|
import { CancelIcon, EditIcon, EyeIcon, RefreshIcon } from '@/icons'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sensor: SensorInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SensorItem = ({ sensor }: Props) => {
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sensor-item">
|
||||||
|
<div className="id">{sensor.id}</div>
|
||||||
|
<div className="name">{sensor.name}</div>
|
||||||
|
<div className="key">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={sensor.authKey}
|
||||||
|
disabled
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<button className="icon" onClick={() => setShowPassword((v) => !v)}>
|
||||||
|
<EyeIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<button>
|
||||||
|
<EditIcon /> Edit
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
<RefreshIcon /> Refresh key
|
||||||
|
</button>
|
||||||
|
<button>
|
||||||
|
<CancelIcon /> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/** @source wouter example */
|
||||||
|
import { useState, useEffect } from 'preact/hooks'
|
||||||
|
|
||||||
|
// (excluding the leading '#' symbol)
|
||||||
|
const currentLocation = () => {
|
||||||
|
return window.location.hash.replace(/^#/, '') || '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigate = (to: string) => (window.location.hash = to)
|
||||||
|
|
||||||
|
export const useHashLocation = () => {
|
||||||
|
const [loc, setLoc] = useState(currentLocation())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// this function is called whenever the hash changes
|
||||||
|
const handler = () => setLoc(currentLocation())
|
||||||
|
|
||||||
|
// subscribe to hash changes
|
||||||
|
window.addEventListener('hashchange', handler)
|
||||||
|
|
||||||
|
return () => window.removeEventListener('hashchange', handler)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return [loc, navigate]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS sensors (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
auth_key TEXT NOT NULL,
|
||||||
|
/* Temporary column used for migration */
|
||||||
|
ident TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/* Add rows for sensors */
|
||||||
|
INSERT INTO sensors (ident, name, auth_key)
|
||||||
|
SELECT
|
||||||
|
sensor,
|
||||||
|
IFNULL(
|
||||||
|
(SELECT c.value FROM sensor_config c WHERE c.sensor = sensor),
|
||||||
|
sensor
|
||||||
|
) as "name",
|
||||||
|
hex(randomblob(32)) as "auth_key"
|
||||||
|
FROM sensor_values
|
||||||
|
GROUP BY sensor;
|
||||||
|
|
||||||
|
/* We need to add FK key and the only way is to create new table */
|
||||||
|
/* So we rename old table, create a new one and migrate the data there */
|
||||||
|
ALTER TABLE sensor_values RENAME TO sensor_values_old;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sensor_values (
|
||||||
|
timestamp INTEGER NOT NULL,
|
||||||
|
sensor_id INTEGER NOT NULL,
|
||||||
|
value REAL NOT NULL,
|
||||||
|
FOREIGN KEY (sensor_id) REFERENCES sensors(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sensor_values (timestamp, sensor_id, value)
|
||||||
|
SELECT timestamp, (SELECT s.id FROM sensors s WHERE s.ident = sensor), value
|
||||||
|
FROM sensor_values_old;
|
||||||
|
|
||||||
|
DROP TABLE sensor_values_old;
|
||||||
|
|
||||||
|
/* this column was temporary */
|
||||||
|
ALTER TABLE sensors DROP COLUMN ident;
|
||||||
|
|
@ -39,10 +39,12 @@ func main() {
|
||||||
// Routes that are only accessible after logging in
|
// Routes that are only accessible after logging in
|
||||||
loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server))
|
loginProtected := router.Group("/", middleware.LoginAuthMiddleware(server))
|
||||||
loginProtected.GET("/api/sensors", routes.GetSensors(server))
|
loginProtected.GET("/api/sensors", routes.GetSensors(server))
|
||||||
|
loginProtected.POST("/api/sensors", routes.PostSensors(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/latest", routes.GetSensorLatestValue(server))
|
||||||
loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server))
|
loginProtected.GET("/api/sensors/:sensor/values", routes.GetSensorValues(server))
|
||||||
loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server))
|
//loginProtected.GET("/api/sensors/:sensor/config", routes.GetSensorConfig(server))
|
||||||
loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server))
|
//loginProtected.PUT("/api/sensors/:sensor/config/:key", routes.PutSensorConfig(server))
|
||||||
loginProtected.GET("/api/dashboards", routes.GetDashboards(server))
|
loginProtected.GET("/api/dashboards", routes.GetDashboards(server))
|
||||||
loginProtected.POST("/api/dashboards", routes.PostDashboard(server))
|
loginProtected.POST("/api/dashboards", routes.PostDashboard(server))
|
||||||
loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server))
|
loginProtected.GET("/api/dashboards/:id", routes.GetDashboardById(server))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"basic-sensor-receiver/app"
|
"basic-sensor-receiver/app"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -22,10 +23,23 @@ func LoginAuthMiddleware(server *app.Server) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
func KeyAuthMiddleware(server *app.Server) gin.HandlerFunc {
|
func KeyAuthMiddleware(server *app.Server) gin.HandlerFunc {
|
||||||
keyWithBearer := "Bearer " + server.Config.AuthKey
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if c.GetHeader("authorization") != keyWithBearer {
|
sensorParam := c.Param("sensor")
|
||||||
|
sensorId, err := strconv.ParseInt(sensorParam, 10, 64)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor, err := server.Services.Sensors.GetById(sensorId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GetHeader("authorization") != "Bearer "+sensor.AuthKey {
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package routes
|
||||||
import (
|
import (
|
||||||
"basic-sensor-receiver/app"
|
"basic-sensor-receiver/app"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -23,14 +24,20 @@ type getLatestSensorValueQuery struct {
|
||||||
func PostSensorValues(s *app.Server) gin.HandlerFunc {
|
func PostSensorValues(s *app.Server) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var newValue postSensorValueBody
|
var newValue postSensorValueBody
|
||||||
sensor := c.Param("sensor")
|
|
||||||
|
|
||||||
if err := c.BindJSON(&newValue); err != nil {
|
if err := c.BindJSON(&newValue); err != nil {
|
||||||
c.AbortWithError(400, err)
|
c.AbortWithError(400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.Services.SensorValues.Push(sensor, newValue.Value); err != nil {
|
sensorId, err := getSensorId(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.Services.SensorValues.Push(sensorId, newValue.Value); err != nil {
|
||||||
c.AbortWithError(400, err)
|
c.AbortWithError(400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -43,14 +50,19 @@ func GetSensorValues(s *app.Server) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var query getSensorValuesQuery
|
var query getSensorValuesQuery
|
||||||
|
|
||||||
sensor := c.Param("sensor")
|
sensorId, err := getSensorId(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.BindQuery(&query); err != nil {
|
if err := c.BindQuery(&query); err != nil {
|
||||||
c.AbortWithError(500, err)
|
c.AbortWithError(500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
values, err := s.Services.SensorValues.GetList(sensor, query.From, query.To)
|
values, err := s.Services.SensorValues.GetList(sensorId, query.From, query.To)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(500, err)
|
c.AbortWithError(500, err)
|
||||||
|
|
@ -64,14 +76,19 @@ func GetSensorLatestValue(s *app.Server) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var query getLatestSensorValueQuery
|
var query getLatestSensorValueQuery
|
||||||
|
|
||||||
sensor := c.Param("sensor")
|
sensorId, err := getSensorId(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.BindQuery(&query); err != nil {
|
if err := c.BindQuery(&query); err != nil {
|
||||||
c.AbortWithError(500, err)
|
c.AbortWithError(500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value, err := s.Services.SensorValues.GetLatest(sensor, query.To)
|
value, err := s.Services.SensorValues.GetLatest(sensorId, query.To)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(500, err)
|
c.AbortWithError(500, err)
|
||||||
|
|
@ -81,3 +98,9 @@ func GetSensorLatestValue(s *app.Server) gin.HandlerFunc {
|
||||||
c.JSON(http.StatusOK, value)
|
c.JSON(http.StatusOK, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSensorId(c *gin.Context) (int64, error) {
|
||||||
|
sensor := c.Param("sensor")
|
||||||
|
|
||||||
|
return strconv.ParseInt(sensor, 10, 64)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,70 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type postSensorsBody struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type putSensorsBody struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
func GetSensors(s *app.Server) gin.HandlerFunc {
|
func GetSensors(s *app.Server) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
sensors, err := s.Services.Sensors.GetList()
|
sensors, err := s.Services.Sensors.GetList()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(500, err)
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, sensors)
|
c.JSON(http.StatusOK, sensors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PostSensors(s *app.Server) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
body := postSensorsBody{}
|
||||||
|
|
||||||
|
if err := c.BindJSON(&body); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor, err := s.Services.Sensors.Create(body.Name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, sensor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutSensors(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.Update(sensorId, body.Name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithError(http.StatusInternalServerError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, sensor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ type sensorValue struct {
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SensorValuesService) Push(sensor string, value float64) (int64, error) {
|
func (s *SensorValuesService) Push(sensorId int64, value float64) (int64, error) {
|
||||||
res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor, value) VALUES (?, ?, ?)", time.Now().Unix(), sensor, value)
|
res, err := s.ctx.DB.Exec("INSERT INTO sensor_values (timestamp, sensor_id, value) VALUES (?, ?, ?)", time.Now().Unix(), sensorId, value)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|
@ -21,12 +21,12 @@ func (s *SensorValuesService) Push(sensor string, value float64) (int64, error)
|
||||||
return res.LastInsertId()
|
return res.LastInsertId()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]sensorValue, error) {
|
func (s *SensorValuesService) GetList(sensorId int64, from int64, to int64) ([]sensorValue, error) {
|
||||||
var value float64
|
var value float64
|
||||||
var timestamp int64
|
var timestamp int64
|
||||||
values := make([]sensorValue, 0)
|
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)
|
rows, err := s.ctx.DB.Query("SELECT timestamp, value FROM sensor_values WHERE sensor_id = ? AND timestamp > ? AND timestamp < ? ORDER BY timestamp ASC", sensorId, from, to)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -51,10 +51,10 @@ func (s *SensorValuesService) GetList(sensor string, from int64, to int64) ([]se
|
||||||
return values, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SensorValuesService) GetLatest(sensor string, to int64) (*sensorValue, error) {
|
func (s *SensorValuesService) GetLatest(sensorId int64, to int64) (*sensorValue, error) {
|
||||||
var value = sensorValue{}
|
var value = sensorValue{}
|
||||||
|
|
||||||
row := s.ctx.DB.QueryRow("SELECT timestamp, value FROM sensor_values WHERE sensor = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT 1", sensor, to)
|
row := s.ctx.DB.QueryRow("SELECT timestamp, value FROM sensor_values WHERE sensor_id = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT 1", sensorId, to)
|
||||||
|
|
||||||
err := row.Scan(&value.Timestamp, &value.Value)
|
err := row.Scan(&value.Timestamp, &value.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
|
import "encoding/hex"
|
||||||
|
|
||||||
type SensorsService struct {
|
type SensorsService struct {
|
||||||
ctx *Context
|
ctx *Context
|
||||||
}
|
}
|
||||||
|
|
||||||
type sensorItem struct {
|
type SensorItem struct {
|
||||||
Sensor string `json:"sensor"`
|
Id int64 `json:"id"`
|
||||||
LastUpdate string `json:"lastUpdate"`
|
Name string `json:"name"`
|
||||||
Config map[string]string `json:"config"`
|
AuthKey string `json:"authKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SensorsService) GetList() ([]sensorItem, error) {
|
func (s *SensorsService) GetList() ([]SensorItem, error) {
|
||||||
sensors := make([]sensorItem, 0)
|
sensors := make([]SensorItem, 0)
|
||||||
var sensor string
|
|
||||||
var lastUpdate string
|
|
||||||
|
|
||||||
rows, err := s.ctx.DB.Query("SELECT sensor, MAX(timestamp) last_update FROM sensor_values GROUP BY sensor")
|
rows, err := s.ctx.DB.Query("SELECT id, name, auth_key FROM sensors")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -24,18 +24,14 @@ func (s *SensorsService) GetList() ([]sensorItem, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
err := rows.Scan(&sensor, &lastUpdate)
|
item := SensorItem{}
|
||||||
|
|
||||||
|
err := rows.Scan(&item.Id, &item.Name, &item.AuthKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := s.ctx.Services.SensorConfig.GetValues(sensor)
|
sensors = append(sensors, item)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sensors = append(sensors, sensorItem{Sensor: sensor, LastUpdate: lastUpdate, Config: config})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = rows.Err()
|
err = rows.Err()
|
||||||
|
|
@ -45,3 +41,55 @@ func (s *SensorsService) GetList() ([]sensorItem, error) {
|
||||||
|
|
||||||
return sensors, nil
|
return sensors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SensorsService) Create(name string) (*SensorItem, error) {
|
||||||
|
item := SensorItem{
|
||||||
|
Name: name,
|
||||||
|
AuthKey: hex.EncodeToString(generateRandomKey(32)),
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.ctx.DB.Exec("INSERT INTO sensors (id, name, auth_key) VALUES (?, ?)", item.Name, item.AuthKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Id, err = res.LastInsertId()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SensorsService) GetById(id int64) (*SensorItem, error) {
|
||||||
|
item := SensorItem{}
|
||||||
|
|
||||||
|
row := s.ctx.DB.QueryRow("SELECT id, name, auth_key FROM sensors WHERE id = ?", id)
|
||||||
|
|
||||||
|
err := row.Scan(&item.Id, &item.Name, &item.AuthKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SensorsService) Update(id int64, name string) (*SensorItem, error) {
|
||||||
|
item, err := s.GetById(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Name = name
|
||||||
|
|
||||||
|
_, err = s.ctx.DB.Exec("UPDATE sensors SET name = ? WHERE id = ?", item.Name, item.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue