Improved dashboard filter functionality
This commit is contained in:
parent
9206f7f2e1
commit
2df3e2f716
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
/* 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/**/*"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue