Merge branch 'feature/auto-refresh' of kamen/sensors-dashboard into master
This commit is contained in:
commit
3094d13dc3
|
|
@ -2,6 +2,10 @@ kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: build & publish image
|
name: build & publish image
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- master
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build & publish
|
- name: build & publish
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,21 @@ button,
|
||||||
color: var(--button-remove-fg-color);
|
color: var(--button-remove-fg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
button:first-child {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:last-child {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,57 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-refresh {
|
||||||
|
button.auto-refresh-toggle {
|
||||||
|
padding: 0rem 0.5rem;
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-refresh-popup {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 35px;
|
||||||
|
right: 10px;
|
||||||
|
width: 5rem;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-interval-option {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--input-bg-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-refresh-select {
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-top: 1px solid var(--box-border-color);
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.filter-toggle {
|
.filter-toggle {
|
||||||
.current-value {
|
.current-value {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,11 @@
|
||||||
rgba(0, 0, 0, 0) 0%,
|
rgba(0, 0, 0, 0) 0%,
|
||||||
rgba(0, 0, 0, 0.2) 100%
|
rgba(0, 0, 0, 0.2) 100%
|
||||||
);
|
);
|
||||||
|
--filters-menu-shadow: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(0, 0, 0, 0) 0%,
|
||||||
|
rgba(0, 0, 0, 0.2) 100%
|
||||||
|
);
|
||||||
--input-hint-color: #aaa;
|
--input-hint-color: #aaa;
|
||||||
--button-remove-bg-color: transparent;
|
--button-remove-bg-color: transparent;
|
||||||
--button-remove-fg-color: rgb(255, 118, 118);
|
--button-remove-fg-color: rgb(255, 118, 118);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { useDashboardContext } from '../../contexts/DashboardContext'
|
||||||
import { DashboardFilters } from './components/DashboardFilters'
|
import { DashboardFilters } from './components/DashboardFilters'
|
||||||
import { DashboardHeaderFilterToggle } from './components/DashboardHeaderFilterToggle'
|
import { DashboardHeaderFilterToggle } from './components/DashboardHeaderFilterToggle'
|
||||||
import { DashboardSwitch } from './components/DashboardSwitch'
|
import { DashboardSwitch } from './components/DashboardSwitch'
|
||||||
|
import { DashboardRefreshButton } from './components/DashboardRefreshButton'
|
||||||
|
import { DashboardRefreshSelect } from './components/DashboardRefreshSelect'
|
||||||
|
|
||||||
export const DashboardHeader = () => {
|
export const DashboardHeader = () => {
|
||||||
const { verticalMode, boxes, setBoxes, setFilter } = useDashboardContext()
|
const { verticalMode, boxes, setBoxes, setFilter } = useDashboardContext()
|
||||||
|
|
@ -73,6 +75,8 @@ export const DashboardHeader = () => {
|
||||||
<DashboardSwitch />
|
<DashboardSwitch />
|
||||||
|
|
||||||
<DashboardFilters onClose={() => setFiltersShown(false)} />
|
<DashboardFilters onClose={() => setFiltersShown(false)} />
|
||||||
|
|
||||||
|
<DashboardRefreshSelect />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{filtersShown && (
|
{filtersShown && (
|
||||||
|
|
@ -88,9 +92,7 @@ export const DashboardHeader = () => {
|
||||||
<DashboardSwitch />
|
<DashboardSwitch />
|
||||||
<button onClick={handleNewBox}>Add box</button>
|
<button onClick={handleNewBox}>Add box</button>
|
||||||
<DashboardHeaderFilterToggle />
|
<DashboardHeaderFilterToggle />
|
||||||
<button onClick={handleRefresh}>
|
<DashboardRefreshButton onRefresh={handleRefresh} />
|
||||||
<RefreshIcon /> Refresh all
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { ChevronDownIcon, RefreshIcon } from '@/icons'
|
||||||
|
import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext'
|
||||||
|
import { getPossibleRefreshIntervals } from '@/pages/dashboard/utils/intervals'
|
||||||
|
import { useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onRefresh: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardRefreshButton = ({ onRefresh }: Props) => {
|
||||||
|
const { refreshInterval, setRefreshInterval } = useDashboardContext()
|
||||||
|
const [showAutoRefresh, setShowAutoRefresh] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="button-group dashboard-refresh">
|
||||||
|
<button onClick={onRefresh}>
|
||||||
|
<RefreshIcon /> Refresh all
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="auto-refresh-toggle"
|
||||||
|
onClick={() => setShowAutoRefresh((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{refreshInterval} <ChevronDownIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
showAutoRefresh ? 'auto-refresh-popup show' : 'auto-refresh-popup'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="box">
|
||||||
|
<div
|
||||||
|
className="refresh-interval-option"
|
||||||
|
onClick={() => {
|
||||||
|
setRefreshInterval(null)
|
||||||
|
setShowAutoRefresh(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Off
|
||||||
|
</div>
|
||||||
|
{getPossibleRefreshIntervals().map((interval) => (
|
||||||
|
<div
|
||||||
|
className="refresh-interval-option"
|
||||||
|
key={interval}
|
||||||
|
onClick={() => {
|
||||||
|
setRefreshInterval(interval)
|
||||||
|
setShowAutoRefresh(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{interval}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{showAutoRefresh && (
|
||||||
|
<div
|
||||||
|
className="menu-overlay"
|
||||||
|
onClick={() => setShowAutoRefresh((v) => !v)}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext'
|
||||||
|
import {
|
||||||
|
getPossibleRefreshIntervals,
|
||||||
|
isValidRefreshInterval,
|
||||||
|
} from '@/pages/dashboard/utils/intervals'
|
||||||
|
import { ChangeEvent } from 'preact/compat'
|
||||||
|
|
||||||
|
export const DashboardRefreshSelect = () => {
|
||||||
|
const { refreshInterval, setRefreshInterval } = useDashboardContext()
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const value = (e.target as HTMLSelectElement).value
|
||||||
|
|
||||||
|
if (value === 'off' || !isValidRefreshInterval(value)) {
|
||||||
|
setRefreshInterval(null)
|
||||||
|
} else {
|
||||||
|
setRefreshInterval(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'dashboard-refresh-select'}>
|
||||||
|
<label>Auto refresh:</label>
|
||||||
|
<select value={refreshInterval ?? 'off'} onChange={handleChange}>
|
||||||
|
<option value="off">Off</option>
|
||||||
|
{getPossibleRefreshIntervals().map((interval) => (
|
||||||
|
<option value={interval} key={interval}>
|
||||||
|
{interval}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,11 @@ import {
|
||||||
} from '../components/DashboardHeader/components/DashboardFilters'
|
} from '../components/DashboardHeader/components/DashboardFilters'
|
||||||
import { BoxDefinition } from '../types'
|
import { BoxDefinition } from '../types'
|
||||||
import { useQueryString } from '@/utils/hooks/useQueryString'
|
import { useQueryString } from '@/utils/hooks/useQueryString'
|
||||||
|
import {
|
||||||
|
RefreshIntervalValue,
|
||||||
|
getRefreshIntervalInMs,
|
||||||
|
isValidRefreshInterval,
|
||||||
|
} from '../utils/intervals'
|
||||||
|
|
||||||
type DashboardContextType = {
|
type DashboardContextType = {
|
||||||
filter: FilterValue
|
filter: FilterValue
|
||||||
|
|
@ -36,6 +41,8 @@ type DashboardContextType = {
|
||||||
isDashboardSelected: boolean
|
isDashboardSelected: boolean
|
||||||
isDashboardLoading: boolean
|
isDashboardLoading: boolean
|
||||||
dashboard: DashboardInfo | undefined
|
dashboard: DashboardInfo | undefined
|
||||||
|
refreshInterval: RefreshIntervalValue | null
|
||||||
|
setRefreshInterval: StateUpdater<RefreshIntervalValue | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DashboardContext = createContext<DashboardContextType | null>(null)
|
const DashboardContext = createContext<DashboardContextType | null>(null)
|
||||||
|
|
@ -138,9 +145,22 @@ export const DashboardContextProvider = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [refreshInterval, setRefreshInterval] =
|
||||||
|
useState<RefreshIntervalValue | null>(() => {
|
||||||
|
const query = getQuery()
|
||||||
|
const queryRefresh = query['refresh']
|
||||||
|
|
||||||
|
if (!queryRefresh || !isValidRefreshInterval(queryRefresh)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryRefresh
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuery({
|
setQuery({
|
||||||
...getQuery(),
|
...getQuery(),
|
||||||
|
...(refreshInterval !== null && { refresh: refreshInterval }),
|
||||||
dashboard: dashboardId.toString(),
|
dashboard: dashboardId.toString(),
|
||||||
interval: filter.interval,
|
interval: filter.interval,
|
||||||
...(filter.interval === 'custom' && {
|
...(filter.interval === 'custom' && {
|
||||||
|
|
@ -148,7 +168,29 @@ export const DashboardContextProvider = ({
|
||||||
to: filter.customTo.toISOString(),
|
to: filter.customTo.toISOString(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}, [filter, dashboardId])
|
}, [filter, dashboardId, refreshInterval])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refreshInterval === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalInMs = getRefreshIntervalInMs(refreshInterval)
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setFilter((v) => {
|
||||||
|
const range = intervalToRange(v.interval, v.customFrom, v.customTo)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...v,
|
||||||
|
customFrom: range[0],
|
||||||
|
customTo: range[1],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, intervalInMs)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [refreshInterval])
|
||||||
|
|
||||||
const verticalMode = viewport.width < 800
|
const verticalMode = viewport.width < 800
|
||||||
|
|
||||||
|
|
@ -164,6 +206,8 @@ export const DashboardContextProvider = ({
|
||||||
isDashboardSelected,
|
isDashboardSelected,
|
||||||
isDashboardLoading: dashboard.isLoading,
|
isDashboardLoading: dashboard.isLoading,
|
||||||
dashboard: dashboard.data,
|
dashboard: dashboard.data,
|
||||||
|
refreshInterval,
|
||||||
|
setRefreshInterval,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
filter,
|
filter,
|
||||||
|
|
@ -175,6 +219,8 @@ export const DashboardContextProvider = ({
|
||||||
isDashboardSelected,
|
isDashboardSelected,
|
||||||
dashboard.isLoading,
|
dashboard.isLoading,
|
||||||
dashboard.data,
|
dashboard.data,
|
||||||
|
refreshInterval,
|
||||||
|
setRefreshInterval,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
export type RefreshIntervalValue = keyof typeof INTERVALS_TO_MS
|
||||||
|
|
||||||
|
const INTERVALS = ['5s', '10s', '30s', '1m', '5m', '10m', '30m', '1h'] as const
|
||||||
|
|
||||||
|
const INTERVALS_TO_MS = {
|
||||||
|
'5s': 5000,
|
||||||
|
'10s': 10000,
|
||||||
|
'30s': 30000,
|
||||||
|
'1m': 60000,
|
||||||
|
'5m': 300000,
|
||||||
|
'10m': 600000,
|
||||||
|
'30m': 1800000,
|
||||||
|
'1h': 3600000,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPossibleRefreshIntervals = () => INTERVALS
|
||||||
|
|
||||||
|
export const isValidRefreshInterval = (
|
||||||
|
interval: string
|
||||||
|
): interval is RefreshIntervalValue =>
|
||||||
|
INTERVALS.includes(interval as RefreshIntervalValue)
|
||||||
|
|
||||||
|
export const getRefreshIntervalInMs = (interval: RefreshIntervalValue) => {
|
||||||
|
return INTERVALS_TO_MS[interval]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue