Display empty messages when there's nothing

This commit is contained in:
Jan Zípek 2024-04-01 13:10:08 +02:00
parent 71b1b3ad0b
commit 0fc4eb2f8c
Signed by: kamen
GPG Key ID: A17882625B33AC31
18 changed files with 157 additions and 31 deletions

View File

@ -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')

View File

@ -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>
}

View File

@ -0,0 +1,8 @@
.empty {
text-align: center;
margin: 1rem auto;
button {
margin-top: 1rem;
}
}

View File

@ -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 {

View File

@ -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%;
}

View File

@ -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';

View File

@ -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>
)

View File

@ -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 },
{

View File

@ -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 },
{

View File

@ -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)} />}
</>
)
}

View File

@ -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)} />
)}
</>
)
}

View File

@ -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 />
)
}

View File

@ -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)} />}
</>
)
}

View File

@ -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
})

View File

@ -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 },

View File

@ -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)} />}
</>
)
}

View File

@ -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)

View File

@ -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