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) =>
|
||||
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 {
|
||||
constructor(public response: Response, message: string) {
|
||||
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,
|
||||
options?: RequestInit,
|
||||
type: 'json' | 'void' = 'json'
|
||||
) => {
|
||||
type?: TType
|
||||
): Promise<RequestResult<TResult, TType>> => {
|
||||
const response = await fetch(url, options)
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -25,21 +33,21 @@ export const request = async <T = void>(
|
|||
}
|
||||
|
||||
if (type === 'void') {
|
||||
return
|
||||
return undefined as RequestResult<TResult, TType>
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
|
||||
if (!text) {
|
||||
if (type === 'json') {
|
||||
if (!type || type === 'json') {
|
||||
throw new ResponseEmptyError(
|
||||
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;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
color: var(--box-fg-color);
|
||||
width: 15rem;
|
||||
transition: left 0.1s, margin-left 0.1s;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
|
||||
.inner {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
/*
|
||||
.dashboard-page {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.empty {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
.dashboard-head {
|
||||
display: flex;
|
||||
|
|
@ -29,7 +26,7 @@
|
|||
min-width: 10rem;
|
||||
}
|
||||
|
||||
button {
|
||||
> button {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 125%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ a {
|
|||
@import 'components/box';
|
||||
@import 'components/data-table';
|
||||
@import 'components/section-title';
|
||||
@import 'components/empty';
|
||||
|
||||
@import 'pages/login-page';
|
||||
@import 'pages/sensors-page';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { cn } from '@/utils/cn'
|
||||
import { ComponentChild } from 'preact'
|
||||
import { ComponentChild, ComponentChildren } from 'preact'
|
||||
import { UseQueryResult } from 'react-query'
|
||||
|
||||
type Props<TRow> = {
|
||||
data: TRow[]
|
||||
type Props<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>[]
|
||||
hideHeader?: boolean
|
||||
emptyMessage?: ComponentChildren
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
type DataTableColumn<TRow> = {
|
||||
|
|
@ -15,10 +21,16 @@ type DataTableColumn<TRow> = {
|
|||
} & ({ scale: number } | { width: string })
|
||||
|
||||
export const DataTable = <TRow,>({
|
||||
data,
|
||||
data: setData,
|
||||
isLoading: setIsLoading,
|
||||
query,
|
||||
columns,
|
||||
hideHeader,
|
||||
emptyMessage,
|
||||
}: Props<TRow>) => {
|
||||
const data = query ? query.data ?? [] : setData
|
||||
const isLoading = query ? query.isLoading : setIsLoading
|
||||
|
||||
return (
|
||||
<div className="data-table">
|
||||
{!hideHeader && (
|
||||
|
|
@ -54,6 +66,10 @@ export const DataTable = <TRow,>({
|
|||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{data.length === 0 && !isLoading && emptyMessage && (
|
||||
<div className="empty-data">{emptyMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import { EditIcon, PlusIcon, TrashIcon } from '@/icons'
|
|||
import { useState } from 'preact/hooks'
|
||||
import { useMutation, useQuery } from 'react-query'
|
||||
import { AlertFormModal } from './components/AlertFormModal'
|
||||
import { NoAlerts } from '../NoAlerts'
|
||||
|
||||
export const AlertsTable = () => {
|
||||
const alerts = useQuery('/alerts', () => getAlerts(), {
|
||||
const alerts = useQuery('/alerts', getAlerts, {
|
||||
refetchInterval: 500,
|
||||
})
|
||||
|
||||
|
|
@ -35,7 +36,8 @@ export const AlertsTable = () => {
|
|||
|
||||
<div className="box-shadow">
|
||||
<DataTable
|
||||
data={alerts.data ?? []}
|
||||
query={alerts}
|
||||
emptyMessage={<NoAlerts />}
|
||||
columns={[
|
||||
{ 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 { useState } from 'preact/hooks'
|
||||
import { useMutation, useQuery } from 'react-query'
|
||||
import { NoContactPoints } from '../NoContactPoints'
|
||||
import { ContactPointFormModal } from './components/ContactPointFormModal'
|
||||
|
||||
export const ContactPointsTable = () => {
|
||||
|
|
@ -35,7 +36,8 @@ export const ContactPointsTable = () => {
|
|||
|
||||
<div className="box-shadow">
|
||||
<DataTable
|
||||
data={contactPoints.data ?? []}
|
||||
query={contactPoints}
|
||||
emptyMessage={<NoContactPoints />}
|
||||
columns={[
|
||||
{ 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 { normalizeBoxes } from '../../utils/normalizeBoxes'
|
||||
import { NoDashboard } from '../NoDashboard'
|
||||
import { GeneralBox } from './components/GeneralBox'
|
||||
|
||||
export const DashboardGrid = () => {
|
||||
|
|
@ -45,6 +46,6 @@ export const DashboardGrid = () => {
|
|||
</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 query = getQuery()
|
||||
const queryDashboardId = +(query['dashboard'] ?? '')
|
||||
|
||||
if (query.dashboard === undefined) {
|
||||
return -1
|
||||
}
|
||||
|
||||
const queryDashboardId = parseInt(query.dashboard)
|
||||
|
||||
return !isNaN(queryDashboardId) ? queryDashboardId : -1
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'
|
|||
import { SensorFormModal } from './components/SensorFormModal'
|
||||
import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
|
||||
import { SensorDetailModal } from './components/SensorDetailModal'
|
||||
import { NoSensors } from './components/NoSensors'
|
||||
|
||||
export const SensorsPage = () => {
|
||||
const sensors = useQuery(['/sensors'], getSensors)
|
||||
|
|
@ -37,7 +38,8 @@ export const SensorsPage = () => {
|
|||
|
||||
<div className="box-shadow">
|
||||
<DataTable
|
||||
data={sensors.data ?? []}
|
||||
query={sensors}
|
||||
emptyMessage={<NoSensors />}
|
||||
columns={[
|
||||
{ key: 'type', title: 'ID', render: (c) => c.id, width: '1rem' },
|
||||
{ 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 {
|
||||
Id int64 `json:"id" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Contents string `json:"contents" binding:"required"`
|
||||
}
|
||||
|
|
@ -57,7 +56,7 @@ func PostDashboard(s *app.Server) gin.HandlerFunc {
|
|||
body := postDashboardBody{}
|
||||
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 {
|
||||
c.AbortWithError(500, err)
|
||||
|
|
|
|||
|
|
@ -46,14 +46,13 @@ func (s *DashboardsService) GetList() ([]DashboardItem, error) {
|
|||
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{
|
||||
Id: id,
|
||||
Name: name,
|
||||
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 {
|
||||
return nil, err
|
||||
|
|
|
|||
Loading…
Reference in New Issue