Improved dashboard filter functionality

This commit is contained in:
Jan Zípek 2022-09-04 10:53:47 +02:00
parent 9206f7f2e1
commit 2df3e2f716
Signed by: kamen
GPG Key ID: A17882625B33AC31
11 changed files with 205 additions and 57 deletions

View File

@ -15,6 +15,11 @@ input {
outline: none; outline: none;
} }
&:disabled {
background-color: var(--input-disabled-bg-color);
color: var(--input-disabled-fg-color);
}
&.small { &.small {
padding: 0.1rem 0.25rem; padding: 0.1rem 0.25rem;
} }
@ -28,3 +33,28 @@ input {
.checkbox-label input[type="checkbox"] { .checkbox-label input[type="checkbox"] {
margin-top: 6px; margin-top: 6px;
} }
.input.buttons {
.button-picker {
> button {
margin: 0.1rem;
}
}
}
.input.date-time {
> div {
display: flex;
align-items: center;
}
input[type="date"] {
margin-right: 0.25rem;
flex: 1;
}
input[type="time"] {
flex-grow: 0;
flex-shrink: 0;
}
}

View File

@ -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="M19 9l-7 7-7-7"></path></svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -23,6 +23,10 @@
display: flex; display: flex;
align-items: center; align-items: center;
> select {
min-width: 10rem;
}
button { button {
margin-left: 0.25rem; margin-left: 0.25rem;
font-size: 125%; font-size: 125%;
@ -51,7 +55,7 @@
top: 0; top: 0;
bottom: 0; bottom: 0;
z-index: 3; z-index: 3;
width: 15rem; width: 16rem;
display: flex; display: flex;
max-width: 100%; max-width: 100%;
overflow: auto; overflow: auto;
@ -60,6 +64,17 @@
background-color: var(--box-bg-color); background-color: var(--box-bg-color);
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
flex: 1; flex: 1;
.dashboard-switch {
padding-bottom: 1rem;
margin: 1rem 0rem;
border-bottom: 1px solid var(--box-border-color);
> select {
flex: 1;
min-width: 0;
}
}
} }
.shadow { .shadow {
@ -74,3 +89,32 @@
font-size: 150%; font-size: 150%;
} }
} }
.filter-toggle {
.current-value {
display: flex;
align-items: center;
cursor: pointer;
> .svg-icon {
margin-left: 0.5rem;
}
}
.filter-popup {
display: none;
position: absolute;
z-index: 3;
top: 35px;
right: 0;
width: 20rem;
> .box {
padding: 1rem;
}
&.show {
display: block;
}
}
}

View File

@ -36,6 +36,8 @@
--input-focus-border-color: #3988ff; --input-focus-border-color: #3988ff;
--input-bg-color: #fff; --input-bg-color: #fff;
--input-fg-color: #000; --input-fg-color: #000;
--input-disabled-fg-color: #000;
--input-disabled-bg-color: #fff;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -62,6 +64,8 @@
--input-focus-border-color: #666; --input-focus-border-color: #666;
--input-bg-color: #222; --input-bg-color: #222;
--input-fg-color: #ccc; --input-fg-color: #ccc;
--input-disabled-fg-color: #bbb;
--input-disabled-bg-color: #2a2a2a;
} }
.sensor-item .auth .auth-value .label { .sensor-item .auth .auth-value .label {

View File

@ -1,11 +1,12 @@
import { splitDateTime } from '@/utils/splitDateTime' import { splitDateTime } from '@/utils/splitDateTime'
import { HTMLAttributes } from 'preact/compat'
type Props = { type Props = {
value: Date value: Date
onChange: (value: Date) => void onChange: (value: Date) => void
} } & Omit<HTMLAttributes<HTMLInputElement>, 'value' | 'onChange'>
export const DateTimeInput = ({ value, onChange }: Props) => { export const DateTimeInput = ({ value, onChange, ...inputProps }: Props) => {
const splitValue = splitDateTime(value) const splitValue = splitDateTime(value)
const handleDateChange = (e: Event) => { const handleDateChange = (e: Event) => {
@ -30,8 +31,18 @@ export const DateTimeInput = ({ value, onChange }: Props) => {
return ( return (
<> <>
<input type="date" value={splitValue[0]} onChange={handleDateChange} /> <input
<input type="time" value={splitValue[1]} onChange={handleTimeChange} /> {...inputProps}
type="date"
value={splitValue[0]}
onChange={handleDateChange}
/>
<input
{...inputProps}
type="time"
value={splitValue[1]}
onChange={handleTimeChange}
/>
</> </>
) )
} }

View File

@ -9,3 +9,4 @@ export { ReactComponent as EyeOffIcon } from '@/assets/icons/eye-off.svg'
export { ReactComponent as EditIcon } from '@/assets/icons/edit.svg' export { ReactComponent as EditIcon } from '@/assets/icons/edit.svg'
export { ReactComponent as ClipboardCopyIcon } from '@/assets/icons/clipboard-copy.svg' export { ReactComponent as ClipboardCopyIcon } from '@/assets/icons/clipboard-copy.svg'
export { ReactComponent as ClipboardCheckIcon } from '@/assets/icons/clipboard-check.svg' export { ReactComponent as ClipboardCheckIcon } from '@/assets/icons/clipboard-check.svg'
export { ReactComponent as ChevronDown } from '@/assets/icons/chevron-down.svg'

View File

@ -5,6 +5,7 @@ import { useState } from 'preact/hooks'
import { GRID_H_SNAP, GRID_WIDTH } from '../../constants' import { GRID_H_SNAP, GRID_WIDTH } from '../../constants'
import { useDashboardContext } from '../../contexts/DashboardContext' import { useDashboardContext } from '../../contexts/DashboardContext'
import { DashboardFilters } from './components/DashboardFilters' import { DashboardFilters } from './components/DashboardFilters'
import { DashboardHeaderFilterToggle } from './components/DashboardHeaderFilterToggle'
import { DashboardSwitch } from './components/DashboardSwitch' import { DashboardSwitch } from './components/DashboardSwitch'
export const DashboardHeader = () => { export const DashboardHeader = () => {
@ -86,7 +87,7 @@ export const DashboardHeader = () => {
<DashboardSwitch /> <DashboardSwitch />
<button onClick={handleNewBox}>Add box</button> <button onClick={handleNewBox}>Add box</button>
<div className="spacer" /> <div className="spacer" />
<DashboardFilters /> <DashboardHeaderFilterToggle />
<div className="spacer" /> <div className="spacer" />
<button onClick={handleRefresh}> <button onClick={handleRefresh}>
<RefreshIcon /> Refresh all <RefreshIcon /> Refresh all

View File

@ -1,4 +1,5 @@
import { DateTimeInput } from '@/components/DateTimeInput' import { DateTimeInput } from '@/components/DateTimeInput'
import { useFilterIntervals } from '@/pages/dashboard/hooks/useFilterIntervals'
import { intervalToRange } from '@/utils/intervalToRange' import { intervalToRange } from '@/utils/intervalToRange'
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
import { useDashboardContext } from '../../../contexts/DashboardContext' import { useDashboardContext } from '../../../contexts/DashboardContext'
@ -17,23 +18,30 @@ export type FilterValue = {
customTo: Date customTo: Date
} }
export const DashboardFilters = () => { type Props = {
const { filter: preset, setFilter, verticalMode } = useDashboardContext() onClose?: () => void
}
export const DashboardFilters = ({ onClose }: Props) => {
const { filter: preset, setFilter } = useDashboardContext()
const [value, setValue] = useState(preset) const [value, setValue] = useState(preset)
const isCustomSelected = value.interval === 'custom' const intervals = useFilterIntervals()
const handleSubmit = (e: Event) => { const handleSubmit = (e: Event) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setFilter(value) setFilter({
...preset,
...value,
})
onClose?.()
} }
const handleIntervalChange = (e: Event) => { const setInterval = (interval: FilterInterval) => {
const target = e.target as HTMLSelectElement
const interval = target.value as FilterInterval
const range = intervalToRange(interval, value.customFrom, value.customTo) const range = intervalToRange(interval, value.customFrom, value.customTo)
setValue({ setValue({
@ -42,57 +50,55 @@ export const DashboardFilters = () => {
customFrom: range[0], customFrom: range[0],
customTo: range[1], customTo: range[1],
}) })
onClose?.()
} }
useEffect(() => { useEffect(() => {
setFilter(value) setValue(preset)
}, [value]) }, [preset])
return ( return (
<div className="filter-form"> <div className="filter-form">
<form <form onSubmit={handleSubmit}>
className={!verticalMode ? 'horizontal' : undefined} <div className="input buttons">
onSubmit={handleSubmit} <label>Dynamic</label>
> <div className="button-picker">
<div className="input"> {intervals.map((i) => (
<label>Interval</label> <button
<select key={i.value}
name="interval" onClick={() => setInterval(i.value as FilterInterval)}
onChange={handleIntervalChange} >
value={value.interval} {i.label}
className="small" </button>
> ))}
<option value="hour">Hour</option> </div>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
<option value="custom">Custom</option>
</select>
</div> </div>
{isCustomSelected && ( <div className="input date-time">
<> <label>From</label>
<div className="input date-time"> <div>
<label>From</label> <DateTimeInput
<div> value={value.customFrom}
<DateTimeInput onChange={(v) =>
value={value.customFrom} setValue({ ...value, customFrom: v, interval: 'custom' })
onChange={(v) => setValue({ ...value, customFrom: v })} }
/> />
</div> </div>
</div> </div>
<div className="input date-time"> <div className="input date-time">
<label>To</label> <label>To</label>
<div> <div>
<DateTimeInput <DateTimeInput
value={value.customTo} value={value.customTo}
onChange={(v) => setValue({ ...value, customTo: v })} onChange={(v) =>
/> setValue({ ...value, customTo: v, interval: 'custom' })
</div> }
</div> />
</> </div>
)} </div>
<button>Apply</button>
</form> </form>
</div> </div>
) )

View File

@ -0,0 +1,37 @@
import { ChevronDown } from '@/icons'
import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext'
import { useFilterIntervals } from '@/pages/dashboard/hooks/useFilterIntervals'
import { cn } from '@/utils/cn'
import { useState } from 'preact/hooks'
import { DashboardFilters } from './DashboardFilters'
const formatDate = (d: Date) =>
Intl.DateTimeFormat([], { dateStyle: 'short', timeStyle: 'short' }).format(d)
export const DashboardHeaderFilterToggle = () => {
const { filter } = useDashboardContext()
const intervals = useFilterIntervals()
const [show, setShow] = useState(false)
const selected =
intervals.find((i) => i.value === filter.interval)?.label ??
`${formatDate(filter.customFrom)} - ${formatDate(filter.customTo)}`
return (
<div className="filter-toggle">
<div className="current-value" onClick={() => setShow((v) => !v)}>
<span>{selected}</span>
<ChevronDown />
</div>
<div className={cn('filter-popup', show && 'show')}>
<div className="box">
<DashboardFilters onClose={() => setShow(false)} />
</div>
</div>
{show && (
<div className="menu-overlay" onClick={() => setShow((v) => !v)}></div>
)}
</div>
)
}

View File

@ -0,0 +1,13 @@
import { useMemo } from 'preact/hooks'
export const useFilterIntervals = () =>
useMemo(
() => [
{ value: 'hour', label: 'Last hour' },
{ value: 'day', label: 'Last 24 hours' },
{ value: 'week', label: 'Last 7 days' },
{ value: 'month', label: 'Last 30 days' },
{ value: 'year', label: 'Last 365 days' },
],
[]
)

View File

@ -63,7 +63,7 @@
/* Experimental Options */ /* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"lib": [ "DOM", "es2019" ] "lib": [ "DOM", "es2019", "ES2020.Intl" ]
}, },
"include": [ "include": [
"src/**/*" "src/**/*"