diff --git a/.drone.yml b/.drone.yml index 8f92099..62b7ce2 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2,6 +2,10 @@ kind: pipeline type: docker name: build & publish image +trigger: + branch: + - master + steps: - name: build & publish image: plugins/docker diff --git a/client/src/assets/components/_button.scss b/client/src/assets/components/_button.scss index bbeed80..9523216 100644 --- a/client/src/assets/components/_button.scss +++ b/client/src/assets/components/_button.scss @@ -56,3 +56,21 @@ button, 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; + } +} diff --git a/client/src/assets/pages/_dashboard-page.scss b/client/src/assets/pages/_dashboard-page.scss index b99850e..534cd5f 100644 --- a/client/src/assets/pages/_dashboard-page.scss +++ b/client/src/assets/pages/_dashboard-page.scss @@ -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 { .current-value { display: flex; diff --git a/client/src/assets/themes/_basic.scss b/client/src/assets/themes/_basic.scss index 90ab581..ab6cdb1 100644 --- a/client/src/assets/themes/_basic.scss +++ b/client/src/assets/themes/_basic.scss @@ -94,6 +94,11 @@ rgba(0, 0, 0, 0) 0%, 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; --button-remove-bg-color: transparent; --button-remove-fg-color: rgb(255, 118, 118); diff --git a/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx b/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx index 0803d10..dfcc424 100644 --- a/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx +++ b/client/src/pages/dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -7,6 +7,8 @@ import { useDashboardContext } from '../../contexts/DashboardContext' import { DashboardFilters } from './components/DashboardFilters' import { DashboardHeaderFilterToggle } from './components/DashboardHeaderFilterToggle' import { DashboardSwitch } from './components/DashboardSwitch' +import { DashboardRefreshButton } from './components/DashboardRefreshButton' +import { DashboardRefreshSelect } from './components/DashboardRefreshSelect' export const DashboardHeader = () => { const { verticalMode, boxes, setBoxes, setFilter } = useDashboardContext() @@ -73,6 +75,8 @@ export const DashboardHeader = () => { setFiltersShown(false)} /> + + {filtersShown && ( @@ -88,9 +92,7 @@ export const DashboardHeader = () => { Add box - - Refresh all - + > )} diff --git a/client/src/pages/dashboard/components/DashboardHeader/components/DashboardRefreshButton.tsx b/client/src/pages/dashboard/components/DashboardHeader/components/DashboardRefreshButton.tsx new file mode 100644 index 0000000..5ef7328 --- /dev/null +++ b/client/src/pages/dashboard/components/DashboardHeader/components/DashboardRefreshButton.tsx @@ -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 ( + <> + + + Refresh all + + setShowAutoRefresh((prev) => !prev)} + > + {refreshInterval} + + + + + { + setRefreshInterval(null) + setShowAutoRefresh(false) + }} + > + Off + + {getPossibleRefreshIntervals().map((interval) => ( + { + setRefreshInterval(interval) + setShowAutoRefresh(false) + }} + > + {interval} + + ))} + + + {showAutoRefresh && ( + setShowAutoRefresh((v) => !v)} + > + )} + > + ) +} diff --git a/client/src/pages/dashboard/components/DashboardHeader/components/DashboardRefreshSelect.tsx b/client/src/pages/dashboard/components/DashboardHeader/components/DashboardRefreshSelect.tsx new file mode 100644 index 0000000..8ccd2ed --- /dev/null +++ b/client/src/pages/dashboard/components/DashboardHeader/components/DashboardRefreshSelect.tsx @@ -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) => { + const value = (e.target as HTMLSelectElement).value + + if (value === 'off' || !isValidRefreshInterval(value)) { + setRefreshInterval(null) + } else { + setRefreshInterval(value) + } + } + + return ( + + Auto refresh: + + Off + {getPossibleRefreshIntervals().map((interval) => ( + + {interval} + + ))} + + + ) +} diff --git a/client/src/pages/dashboard/contexts/DashboardContext.tsx b/client/src/pages/dashboard/contexts/DashboardContext.tsx index e18860b..98bca24 100644 --- a/client/src/pages/dashboard/contexts/DashboardContext.tsx +++ b/client/src/pages/dashboard/contexts/DashboardContext.tsx @@ -24,6 +24,11 @@ import { } from '../components/DashboardHeader/components/DashboardFilters' import { BoxDefinition } from '../types' import { useQueryString } from '@/utils/hooks/useQueryString' +import { + RefreshIntervalValue, + getRefreshIntervalInMs, + isValidRefreshInterval, +} from '../utils/intervals' type DashboardContextType = { filter: FilterValue @@ -36,6 +41,8 @@ type DashboardContextType = { isDashboardSelected: boolean isDashboardLoading: boolean dashboard: DashboardInfo | undefined + refreshInterval: RefreshIntervalValue | null + setRefreshInterval: StateUpdater } const DashboardContext = createContext(null) @@ -138,9 +145,22 @@ export const DashboardContextProvider = ({ } }) + const [refreshInterval, setRefreshInterval] = + useState(() => { + const query = getQuery() + const queryRefresh = query['refresh'] + + if (!queryRefresh || !isValidRefreshInterval(queryRefresh)) { + return null + } + + return queryRefresh + }) + useEffect(() => { setQuery({ ...getQuery(), + ...(refreshInterval !== null && { refresh: refreshInterval }), dashboard: dashboardId.toString(), interval: filter.interval, ...(filter.interval === 'custom' && { @@ -148,7 +168,29 @@ export const DashboardContextProvider = ({ 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 @@ -164,6 +206,8 @@ export const DashboardContextProvider = ({ isDashboardSelected, isDashboardLoading: dashboard.isLoading, dashboard: dashboard.data, + refreshInterval, + setRefreshInterval, }), [ filter, @@ -175,6 +219,8 @@ export const DashboardContextProvider = ({ isDashboardSelected, dashboard.isLoading, dashboard.data, + refreshInterval, + setRefreshInterval, ] ) diff --git a/client/src/pages/dashboard/utils/intervals.ts b/client/src/pages/dashboard/utils/intervals.ts new file mode 100644 index 0000000..c9882b2 --- /dev/null +++ b/client/src/pages/dashboard/utils/intervals.ts @@ -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] +}