Improved dashboard filter functionality
This commit is contained in:
parent
9206f7f2e1
commit
2df3e2f716
|
|
@ -15,6 +15,11 @@ input {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--input-disabled-bg-color);
|
||||
color: var(--input-disabled-fg-color);
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: 0.1rem 0.25rem;
|
||||
}
|
||||
|
|
@ -28,3 +33,28 @@ input {
|
|||
.checkbox-label input[type="checkbox"] {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -23,6 +23,10 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> select {
|
||||
min-width: 10rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 125%;
|
||||
|
|
@ -51,7 +55,7 @@
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
width: 15rem;
|
||||
width: 16rem;
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
|
|
@ -60,6 +64,17 @@
|
|||
background-color: var(--box-bg-color);
|
||||
padding: 0.25rem 0.5rem;
|
||||
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 {
|
||||
|
|
@ -74,3 +89,32 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
--input-focus-border-color: #3988ff;
|
||||
--input-bg-color: #fff;
|
||||
--input-fg-color: #000;
|
||||
--input-disabled-fg-color: #000;
|
||||
--input-disabled-bg-color: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
|
@ -62,6 +64,8 @@
|
|||
--input-focus-border-color: #666;
|
||||
--input-bg-color: #222;
|
||||
--input-fg-color: #ccc;
|
||||
--input-disabled-fg-color: #bbb;
|
||||
--input-disabled-bg-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.sensor-item .auth .auth-value .label {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { splitDateTime } from '@/utils/splitDateTime'
|
||||
import { HTMLAttributes } from 'preact/compat'
|
||||
|
||||
type Props = {
|
||||
value: Date
|
||||
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 handleDateChange = (e: Event) => {
|
||||
|
|
@ -30,8 +31,18 @@ export const DateTimeInput = ({ value, onChange }: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<input type="date" value={splitValue[0]} onChange={handleDateChange} />
|
||||
<input type="time" value={splitValue[1]} onChange={handleTimeChange} />
|
||||
<input
|
||||
{...inputProps}
|
||||
type="date"
|
||||
value={splitValue[0]}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
<input
|
||||
{...inputProps}
|
||||
type="time"
|
||||
value={splitValue[1]}
|
||||
onChange={handleTimeChange}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ClipboardCopyIcon } from '@/assets/icons/clipboard-copy.svg'
|
||||
export { ReactComponent as ClipboardCheckIcon } from '@/assets/icons/clipboard-check.svg'
|
||||
export { ReactComponent as ChevronDown } from '@/assets/icons/chevron-down.svg'
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useState } from 'preact/hooks'
|
|||
import { GRID_H_SNAP, GRID_WIDTH } from '../../constants'
|
||||
import { useDashboardContext } from '../../contexts/DashboardContext'
|
||||
import { DashboardFilters } from './components/DashboardFilters'
|
||||
import { DashboardHeaderFilterToggle } from './components/DashboardHeaderFilterToggle'
|
||||
import { DashboardSwitch } from './components/DashboardSwitch'
|
||||
|
||||
export const DashboardHeader = () => {
|
||||
|
|
@ -86,7 +87,7 @@ export const DashboardHeader = () => {
|
|||
<DashboardSwitch />
|
||||
<button onClick={handleNewBox}>Add box</button>
|
||||
<div className="spacer" />
|
||||
<DashboardFilters />
|
||||
<DashboardHeaderFilterToggle />
|
||||
<div className="spacer" />
|
||||
<button onClick={handleRefresh}>
|
||||
<RefreshIcon /> Refresh all
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { DateTimeInput } from '@/components/DateTimeInput'
|
||||
import { useFilterIntervals } from '@/pages/dashboard/hooks/useFilterIntervals'
|
||||
import { intervalToRange } from '@/utils/intervalToRange'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { useDashboardContext } from '../../../contexts/DashboardContext'
|
||||
|
|
@ -17,23 +18,30 @@ export type FilterValue = {
|
|||
customTo: Date
|
||||
}
|
||||
|
||||
export const DashboardFilters = () => {
|
||||
const { filter: preset, setFilter, verticalMode } = useDashboardContext()
|
||||
type Props = {
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export const DashboardFilters = ({ onClose }: Props) => {
|
||||
const { filter: preset, setFilter } = useDashboardContext()
|
||||
|
||||
const [value, setValue] = useState(preset)
|
||||
|
||||
const isCustomSelected = value.interval === 'custom'
|
||||
const intervals = useFilterIntervals()
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
setFilter(value)
|
||||
setFilter({
|
||||
...preset,
|
||||
...value,
|
||||
})
|
||||
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
const handleIntervalChange = (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement
|
||||
const interval = target.value as FilterInterval
|
||||
const setInterval = (interval: FilterInterval) => {
|
||||
const range = intervalToRange(interval, value.customFrom, value.customTo)
|
||||
|
||||
setValue({
|
||||
|
|
@ -42,43 +50,39 @@ export const DashboardFilters = () => {
|
|||
customFrom: range[0],
|
||||
customTo: range[1],
|
||||
})
|
||||
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setFilter(value)
|
||||
}, [value])
|
||||
setValue(preset)
|
||||
}, [preset])
|
||||
|
||||
return (
|
||||
<div className="filter-form">
|
||||
<form
|
||||
className={!verticalMode ? 'horizontal' : undefined}
|
||||
onSubmit={handleSubmit}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="input buttons">
|
||||
<label>Dynamic</label>
|
||||
<div className="button-picker">
|
||||
{intervals.map((i) => (
|
||||
<button
|
||||
key={i.value}
|
||||
onClick={() => setInterval(i.value as FilterInterval)}
|
||||
>
|
||||
<div className="input">
|
||||
<label>Interval</label>
|
||||
<select
|
||||
name="interval"
|
||||
onChange={handleIntervalChange}
|
||||
value={value.interval}
|
||||
className="small"
|
||||
>
|
||||
<option value="hour">Hour</option>
|
||||
<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>
|
||||
{i.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCustomSelected && (
|
||||
<>
|
||||
<div className="input date-time">
|
||||
<label>From</label>
|
||||
<div>
|
||||
<DateTimeInput
|
||||
value={value.customFrom}
|
||||
onChange={(v) => setValue({ ...value, customFrom: v })}
|
||||
onChange={(v) =>
|
||||
setValue({ ...value, customFrom: v, interval: 'custom' })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -87,12 +91,14 @@ export const DashboardFilters = () => {
|
|||
<div>
|
||||
<DateTimeInput
|
||||
value={value.customTo}
|
||||
onChange={(v) => setValue({ ...value, customTo: v })}
|
||||
onChange={(v) =>
|
||||
setValue({ ...value, customTo: v, interval: 'custom' })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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' },
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"lib": [ "DOM", "es2019" ]
|
||||
"lib": [ "DOM", "es2019", "ES2020.Intl" ]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
|
|
|
|||
Loading…
Reference in New Issue