Display empty messages when there's nothing
This commit is contained in:
parent
71b1b3ad0b
commit
0fc4eb2f8c
|
|
@ -34,4 +34,4 @@ export const updateAlert = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const deleteAlert = (id: number) =>
|
export const deleteAlert = (id: number) =>
|
||||||
request<AlertInfo>(`/api/alerts/${id}`, { method: 'DELETE' }, 'void')
|
request<AlertInfo, 'void'>(`/api/alerts/${id}`, { method: 'DELETE' }, 'void')
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
type RequestResult<
|
||||||
|
T = void,
|
||||||
|
TType extends 'json' | 'void' = 'json'
|
||||||
|
> = TType extends 'void' ? undefined : TType extends 'json' ? T : void
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
constructor(public response: Response, message: string) {
|
constructor(public response: Response, message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
|
|
@ -10,11 +15,14 @@ export class ResponseEmptyError extends RequestError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const request = async <T = void>(
|
export const request = async <
|
||||||
|
TResult = void,
|
||||||
|
TType extends 'json' | 'void' = 'json'
|
||||||
|
>(
|
||||||
url: string,
|
url: string,
|
||||||
options?: RequestInit,
|
options?: RequestInit,
|
||||||
type: 'json' | 'void' = 'json'
|
type?: TType
|
||||||
) => {
|
): Promise<RequestResult<TResult, TType>> => {
|
||||||
const response = await fetch(url, options)
|
const response = await fetch(url, options)
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -25,21 +33,21 @@ export const request = async <T = void>(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'void') {
|
if (type === 'void') {
|
||||||
return
|
return undefined as RequestResult<TResult, TType>
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
if (type === 'json') {
|
if (!type || type === 'json') {
|
||||||
throw new ResponseEmptyError(
|
throw new ResponseEmptyError(
|
||||||
response,
|
response,
|
||||||
'Expected json, got empty response'
|
'Expected json, got empty response'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return undefined as RequestResult<TResult, TType>
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.parse(text) as T
|
return JSON.parse(text) as RequestResult<TResult, TType>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem auto;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,12 +17,12 @@ main.layout {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
color: var(--box-fg-color);
|
color: var(--box-fg-color);
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
transition: left 0.1s, margin-left 0.1s;
|
transition: left 0.1s, margin-left 0.1s;
|
||||||
z-index: 2;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
/*
|
|
||||||
.dashboard-page {
|
.dashboard-page {
|
||||||
height: 100%;
|
.empty {
|
||||||
overflow: auto;
|
margin-top: 5rem;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
.dashboard-head {
|
.dashboard-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -29,7 +26,7 @@
|
||||||
min-width: 10rem;
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
> button {
|
||||||
margin-left: 0.25rem;
|
margin-left: 0.25rem;
|
||||||
font-size: 125%;
|
font-size: 125%;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ a {
|
||||||
@import 'components/box';
|
@import 'components/box';
|
||||||
@import 'components/data-table';
|
@import 'components/data-table';
|
||||||
@import 'components/section-title';
|
@import 'components/section-title';
|
||||||
|
@import 'components/empty';
|
||||||
|
|
||||||
@import 'pages/login-page';
|
@import 'pages/login-page';
|
||||||
@import 'pages/sensors-page';
|
@import 'pages/sensors-page';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import { cn } from '@/utils/cn'
|
import { cn } from '@/utils/cn'
|
||||||
import { ComponentChild } from 'preact'
|
import { ComponentChild, ComponentChildren } from 'preact'
|
||||||
|
import { UseQueryResult } from 'react-query'
|
||||||
|
|
||||||
type Props<TRow> = {
|
type Props<TRow> = (
|
||||||
data: TRow[]
|
| { data: TRow[]; isLoading?: boolean; query?: never }
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
| { query: UseQueryResult<TRow[], any>; data?: never; isLoading?: never }
|
||||||
|
) & {
|
||||||
columns: DataTableColumn<TRow>[]
|
columns: DataTableColumn<TRow>[]
|
||||||
hideHeader?: boolean
|
hideHeader?: boolean
|
||||||
|
emptyMessage?: ComponentChildren
|
||||||
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataTableColumn<TRow> = {
|
type DataTableColumn<TRow> = {
|
||||||
|
|
@ -15,10 +21,16 @@ type DataTableColumn<TRow> = {
|
||||||
} & ({ scale: number } | { width: string })
|
} & ({ scale: number } | { width: string })
|
||||||
|
|
||||||
export const DataTable = <TRow,>({
|
export const DataTable = <TRow,>({
|
||||||
data,
|
data: setData,
|
||||||
|
isLoading: setIsLoading,
|
||||||
|
query,
|
||||||
columns,
|
columns,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
|
emptyMessage,
|
||||||
}: Props<TRow>) => {
|
}: Props<TRow>) => {
|
||||||
|
const data = query ? query.data ?? [] : setData
|
||||||
|
const isLoading = query ? query.isLoading : setIsLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="data-table">
|
<div className="data-table">
|
||||||
{!hideHeader && (
|
{!hideHeader && (
|
||||||
|
|
@ -54,6 +66,10 @@ export const DataTable = <TRow,>({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{data.length === 0 && !isLoading && emptyMessage && (
|
||||||
|
<div className="empty-data">{emptyMessage}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { useMutation, useQuery } from 'react-query'
|
import { useMutation, useQuery } from 'react-query'
|
||||||
import { AlertFormModal } from './components/AlertFormModal'
|
import { AlertFormModal } from './components/AlertFormModal'
|
||||||
|
import { NoAlerts } from '../NoAlerts'
|
||||||
|
|
||||||
export const AlertsTable = () => {
|
export const AlertsTable = () => {
|
||||||
const alerts = useQuery('/alerts', () => getAlerts(), {
|
const alerts = useQuery('/alerts', getAlerts, {
|
||||||
refetchInterval: 500,
|
refetchInterval: 500,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -35,7 +36,8 @@ export const AlertsTable = () => {
|
||||||
|
|
||||||
<div className="box-shadow">
|
<div className="box-shadow">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={alerts.data ?? []}
|
query={alerts}
|
||||||
|
emptyMessage={<NoAlerts />}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { DataTable } from '@/components/DataTable'
|
||||||
import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import { useMutation, useQuery } from 'react-query'
|
import { useMutation, useQuery } from 'react-query'
|
||||||
|
import { NoContactPoints } from '../NoContactPoints'
|
||||||
import { ContactPointFormModal } from './components/ContactPointFormModal'
|
import { ContactPointFormModal } from './components/ContactPointFormModal'
|
||||||
|
|
||||||
export const ContactPointsTable = () => {
|
export const ContactPointsTable = () => {
|
||||||
|
|
@ -35,7 +36,8 @@ export const ContactPointsTable = () => {
|
||||||
|
|
||||||
<div className="box-shadow">
|
<div className="box-shadow">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={contactPoints.data ?? []}
|
query={contactPoints}
|
||||||
|
emptyMessage={<NoContactPoints />}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { PlusIcon } from '@/icons'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { AlertFormModal } from './AlertsTable/components/AlertFormModal'
|
||||||
|
|
||||||
|
export const NoAlerts = () => {
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="empty">
|
||||||
|
<div>No alert defined.</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setShowNew(true)}>
|
||||||
|
<PlusIcon /> Add a new alert
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showNew && <AlertFormModal open onClose={() => setShowNew(false)} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { PlusIcon } from '@/icons'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { ContactPointFormModal } from './ContactPointsTable/components/ContactPointFormModal'
|
||||||
|
|
||||||
|
export const NoContactPoints = () => {
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="empty">
|
||||||
|
<div>No contact points defined.</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setShowNew(true)}>
|
||||||
|
<PlusIcon /> Add a new contact point
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showNew && (
|
||||||
|
<ContactPointFormModal open onClose={() => setShowNew(false)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useDashboardContext } from '../../contexts/DashboardContext'
|
import { useDashboardContext } from '../../contexts/DashboardContext'
|
||||||
import { normalizeBoxes } from '../../utils/normalizeBoxes'
|
import { normalizeBoxes } from '../../utils/normalizeBoxes'
|
||||||
|
import { NoDashboard } from '../NoDashboard'
|
||||||
import { GeneralBox } from './components/GeneralBox'
|
import { GeneralBox } from './components/GeneralBox'
|
||||||
|
|
||||||
export const DashboardGrid = () => {
|
export const DashboardGrid = () => {
|
||||||
|
|
@ -45,6 +46,6 @@ export const DashboardGrid = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>Please create a new dashboard</div>
|
<NoDashboard />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { PlusIcon } from '@/icons'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { DashboardSettings } from './DashboardHeader/components/DashboardSettings'
|
||||||
|
|
||||||
|
export const NoDashboard = () => {
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="empty">
|
||||||
|
<div>No dashboard defined.</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setShowNew(true)}>
|
||||||
|
<PlusIcon /> Create a new dashboard
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showNew && <DashboardSettings onClose={() => setShowNew(false)} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -51,7 +51,12 @@ export const DashboardContextProvider = ({
|
||||||
|
|
||||||
const [dashboardId, setDashboardId] = useState(() => {
|
const [dashboardId, setDashboardId] = useState(() => {
|
||||||
const query = getQuery()
|
const query = getQuery()
|
||||||
const queryDashboardId = +(query['dashboard'] ?? '')
|
|
||||||
|
if (query.dashboard === undefined) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryDashboardId = parseInt(query.dashboard)
|
||||||
|
|
||||||
return !isNaN(queryDashboardId) ? queryDashboardId : -1
|
return !isNaN(queryDashboardId) ? queryDashboardId : -1
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'
|
||||||
import { SensorFormModal } from './components/SensorFormModal'
|
import { SensorFormModal } from './components/SensorFormModal'
|
||||||
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
||||||
import { SensorDetailModal } from './components/SensorDetailModal'
|
import { SensorDetailModal } from './components/SensorDetailModal'
|
||||||
|
import { NoSensors } from './components/NoSensors'
|
||||||
|
|
||||||
export const SensorsPage = () => {
|
export const SensorsPage = () => {
|
||||||
const sensors = useQuery(['/sensors'], getSensors)
|
const sensors = useQuery(['/sensors'], getSensors)
|
||||||
|
|
@ -37,7 +38,8 @@ export const SensorsPage = () => {
|
||||||
|
|
||||||
<div className="box-shadow">
|
<div className="box-shadow">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={sensors.data ?? []}
|
query={sensors}
|
||||||
|
emptyMessage={<NoSensors />}
|
||||||
columns={[
|
columns={[
|
||||||
{ key: 'type', title: 'ID', render: (c) => c.id, width: '1rem' },
|
{ key: 'type', title: 'ID', render: (c) => c.id, width: '1rem' },
|
||||||
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
{ key: 'name', title: 'Name', render: (c) => c.name, scale: 1 },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { PlusIcon } from '@/icons'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
import { SensorFormModal } from './SensorFormModal'
|
||||||
|
|
||||||
|
export const NoSensors = () => {
|
||||||
|
const [showNew, setShowNew] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="empty">
|
||||||
|
<div>No sensor defined.</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setShowNew(true)}>
|
||||||
|
<PlusIcon /> Add a new sensor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showNew && <SensorFormModal open onClose={() => setShowNew(false)} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type postDashboardBody struct {
|
type postDashboardBody struct {
|
||||||
Id int64 `json:"id" binding:"required"`
|
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Contents string `json:"contents" binding:"required"`
|
Contents string `json:"contents" binding:"required"`
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +56,7 @@ func PostDashboard(s *app.Server) gin.HandlerFunc {
|
||||||
body := postDashboardBody{}
|
body := postDashboardBody{}
|
||||||
bindJSONBodyOrAbort(c, &body)
|
bindJSONBodyOrAbort(c, &body)
|
||||||
|
|
||||||
item, err := s.Services.Dashboards.Create(body.Id, body.Name, body.Contents)
|
item, err := s.Services.Dashboards.Create(body.Name, body.Contents)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.AbortWithError(500, err)
|
c.AbortWithError(500, err)
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,13 @@ func (s *DashboardsService) GetList() ([]DashboardItem, error) {
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DashboardsService) Create(id int64, name string, contents string) (*DashboardItem, error) {
|
func (s *DashboardsService) Create(name string, contents string) (*DashboardItem, error) {
|
||||||
item := DashboardItem{
|
item := DashboardItem{
|
||||||
Id: id,
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Contents: contents,
|
Contents: contents,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.ctx.DB.Exec("INSERT INTO dashboards (id, name, contents) VALUES (?, ?, ?)", item.Id, item.Name, item.Contents)
|
_, err := s.ctx.DB.Exec("INSERT INTO dashboards (name, contents) VALUES (?, ?)", item.Name, item.Contents)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue