diff --git a/client/src/api/alerts.ts b/client/src/api/alerts.ts index 408e7c3..97c411a 100644 --- a/client/src/api/alerts.ts +++ b/client/src/api/alerts.ts @@ -34,4 +34,4 @@ export const updateAlert = ({ }) export const deleteAlert = (id: number) => - request(`/api/alerts/${id}`, { method: 'DELETE' }, 'void') + request(`/api/alerts/${id}`, { method: 'DELETE' }, 'void') diff --git a/client/src/api/request.ts b/client/src/api/request.ts index b1c3a75..3e50717 100644 --- a/client/src/api/request.ts +++ b/client/src/api/request.ts @@ -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 ( +export const request = async < + TResult = void, + TType extends 'json' | 'void' = 'json' +>( url: string, options?: RequestInit, - type: 'json' | 'void' = 'json' -) => { + type?: TType +): Promise> => { const response = await fetch(url, options) if (!response.ok) { @@ -25,21 +33,21 @@ export const request = async ( } if (type === 'void') { - return + return undefined as RequestResult } 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 } - return JSON.parse(text) as T + return JSON.parse(text) as RequestResult } diff --git a/client/src/assets/components/_empty.scss b/client/src/assets/components/_empty.scss new file mode 100644 index 0000000..0048bda --- /dev/null +++ b/client/src/assets/components/_empty.scss @@ -0,0 +1,8 @@ +.empty { + text-align: center; + margin: 1rem auto; + + button { + margin-top: 1rem; + } +} diff --git a/client/src/assets/components/_user-layout.scss b/client/src/assets/components/_user-layout.scss index 1b21b46..4743646 100644 --- a/client/src/assets/components/_user-layout.scss +++ b/client/src/assets/components/_user-layout.scss @@ -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 { diff --git a/client/src/assets/pages/_dashboard-page.scss b/client/src/assets/pages/_dashboard-page.scss index 364b26a..b99850e 100644 --- a/client/src/assets/pages/_dashboard-page.scss +++ b/client/src/assets/pages/_dashboard-page.scss @@ -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%; } diff --git a/client/src/assets/style.scss b/client/src/assets/style.scss index 9406f30..9e6f62e 100644 --- a/client/src/assets/style.scss +++ b/client/src/assets/style.scss @@ -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'; diff --git a/client/src/components/DataTable.tsx b/client/src/components/DataTable.tsx index 0becaf6..bddb80e 100644 --- a/client/src/components/DataTable.tsx +++ b/client/src/components/DataTable.tsx @@ -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 = { - data: TRow[] +type Props = ( + | { data: TRow[]; isLoading?: boolean; query?: never } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | { query: UseQueryResult; data?: never; isLoading?: never } +) & { columns: DataTableColumn[] hideHeader?: boolean + emptyMessage?: ComponentChildren + isLoading?: boolean } type DataTableColumn = { @@ -15,10 +21,16 @@ type DataTableColumn = { } & ({ scale: number } | { width: string }) export const DataTable = ({ - data, + data: setData, + isLoading: setIsLoading, + query, columns, hideHeader, + emptyMessage, }: Props) => { + const data = query ? query.data ?? [] : setData + const isLoading = query ? query.isLoading : setIsLoading + return (
{!hideHeader && ( @@ -54,6 +66,10 @@ export const DataTable = ({ ))}
))} + + {data.length === 0 && !isLoading && emptyMessage && ( +
{emptyMessage}
+ )} ) diff --git a/client/src/pages/alerts/components/AlertsTable/AlertsTable.tsx b/client/src/pages/alerts/components/AlertsTable/AlertsTable.tsx index 88b5231..512fad8 100644 --- a/client/src/pages/alerts/components/AlertsTable/AlertsTable.tsx +++ b/client/src/pages/alerts/components/AlertsTable/AlertsTable.tsx @@ -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 = () => {
} columns={[ { key: 'name', title: 'Name', render: (c) => c.name, scale: 1 }, { diff --git a/client/src/pages/alerts/components/ContactPointsTable/ContactPointsTable.tsx b/client/src/pages/alerts/components/ContactPointsTable/ContactPointsTable.tsx index a679ecd..2fa7c31 100644 --- a/client/src/pages/alerts/components/ContactPointsTable/ContactPointsTable.tsx +++ b/client/src/pages/alerts/components/ContactPointsTable/ContactPointsTable.tsx @@ -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 = () => {
} columns={[ { key: 'name', title: 'Name', render: (c) => c.name, scale: 1 }, { diff --git a/client/src/pages/alerts/components/NoAlerts.tsx b/client/src/pages/alerts/components/NoAlerts.tsx new file mode 100644 index 0000000..781af2e --- /dev/null +++ b/client/src/pages/alerts/components/NoAlerts.tsx @@ -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 ( + <> +
+
No alert defined.
+
+ +
+
+ {showNew && setShowNew(false)} />} + + ) +} diff --git a/client/src/pages/alerts/components/NoContactPoints.tsx b/client/src/pages/alerts/components/NoContactPoints.tsx new file mode 100644 index 0000000..97715f9 --- /dev/null +++ b/client/src/pages/alerts/components/NoContactPoints.tsx @@ -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 ( + <> +
+
No contact points defined.
+
+ +
+
+ {showNew && ( + setShowNew(false)} /> + )} + + ) +} diff --git a/client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx b/client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx index 15e5ebf..d6a0485 100644 --- a/client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx +++ b/client/src/pages/dashboard/components/DashboardGrid/DashboardGrid.tsx @@ -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 = () => {
) : ( -
Please create a new dashboard
+ ) } diff --git a/client/src/pages/dashboard/components/NoDashboard.tsx b/client/src/pages/dashboard/components/NoDashboard.tsx new file mode 100644 index 0000000..c44de81 --- /dev/null +++ b/client/src/pages/dashboard/components/NoDashboard.tsx @@ -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 ( + <> +
+
No dashboard defined.
+
+ +
+
+ {showNew && setShowNew(false)} />} + + ) +} diff --git a/client/src/pages/dashboard/contexts/DashboardContext.tsx b/client/src/pages/dashboard/contexts/DashboardContext.tsx index e9245b5..e18860b 100644 --- a/client/src/pages/dashboard/contexts/DashboardContext.tsx +++ b/client/src/pages/dashboard/contexts/DashboardContext.tsx @@ -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 }) diff --git a/client/src/pages/sensors/SensorsPage.tsx b/client/src/pages/sensors/SensorsPage.tsx index d0b440f..d650ab3 100644 --- a/client/src/pages/sensors/SensorsPage.tsx +++ b/client/src/pages/sensors/SensorsPage.tsx @@ -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 = () => {
} columns={[ { key: 'type', title: 'ID', render: (c) => c.id, width: '1rem' }, { key: 'name', title: 'Name', render: (c) => c.name, scale: 1 }, diff --git a/client/src/pages/sensors/components/NoSensors.tsx b/client/src/pages/sensors/components/NoSensors.tsx new file mode 100644 index 0000000..4f62dc8 --- /dev/null +++ b/client/src/pages/sensors/components/NoSensors.tsx @@ -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 ( + <> +
+
No sensor defined.
+
+ +
+
+ {showNew && setShowNew(false)} />} + + ) +} diff --git a/server/routes/dashboards.go b/server/routes/dashboards.go index d3702f3..6c74950 100644 --- a/server/routes/dashboards.go +++ b/server/routes/dashboards.go @@ -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) diff --git a/server/services/dashboards_service.go b/server/services/dashboards_service.go index 893c57e..4b3d7d6 100644 --- a/server/services/dashboards_service.go +++ b/server/services/dashboards_service.go @@ -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