Skip to content

Commit

Permalink
Don't render time bins until there is a proper initial size.
Browse files Browse the repository at this point in the history
  • Loading branch information
justinkambic committed Jun 6, 2024
1 parent 232c61d commit af444ef
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* 2.0.
*/

import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';

import { EuiPanel, useEuiTheme, EuiResizeObserver, EuiSpacer } from '@elastic/eui';
import { EuiPanel, useEuiTheme, EuiResizeObserver, EuiSpacer, EuiProgress } from '@elastic/eui';
import { Chart, Settings, Heatmap, ScaleType, Tooltip, LEGACY_LIGHT_THEME } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { MonitorStatusHeader } from './monitor_status_header';
Expand All @@ -31,8 +31,9 @@ export const MonitorStatusPanel = ({
onBrushed,
}: MonitorStatusPanelProps) => {
const { euiTheme, colorMode } = useEuiTheme();
const initialSizeRef = useRef<HTMLDivElement | null>(null);
const { loading, timeBins, handleResize, getTimeBinByXValue, xDomain, minsPerBin } =
useMonitorStatusData({ from, to });
useMonitorStatusData({ from, to, initSize: initialSizeRef.current?.clientWidth });

const heatmap = useMemo(() => {
return getMonitorStatusChartTheme(euiTheme, brushable);
Expand All @@ -51,61 +52,66 @@ export const MonitorStatusPanel = ({

<EuiSpacer size="m" />

<EuiResizeObserver onResize={handleResize}>
{(resizeRef) => (
<div ref={resizeRef}>
<Chart
size={{
height: 60,
}}
>
<Tooltip
customTooltip={({ values }) => (
<MonitorStatusCellTooltip
timeBin={getTimeBinByXValue(values?.[0]?.datum?.x)}
isLoading={loading}
<div ref={initialSizeRef}>
<EuiResizeObserver onResize={(e) => handleResize(e)}>
{(resizeRef) => (
<div ref={resizeRef}>
{minsPerBin && (
<Chart
size={{
height: 60,
}}
>
<Tooltip
customTooltip={({ values }) => (
<MonitorStatusCellTooltip
timeBin={getTimeBinByXValue(values?.[0]?.datum?.x)}
isLoading={loading}
/>
)}
/>
)}
/>
<Settings
showLegend={false}
xDomain={xDomain}
theme={{ heatmap }}
// TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md
baseTheme={LEGACY_LIGHT_THEME}
onBrushEnd={(brushArea) => {
onBrushed?.(getBrushData(brushArea));
}}
locale={i18n.getLocale()}
/>
<Heatmap
id="monitor-details-monitor-status-chart"
colorScale={{
type: 'bands',
bands: getColorBands(euiTheme, colorMode),
}}
data={timeBins}
xAccessor={({ end }) => end}
yAccessor={() => 'T'}
valueAccessor={(timeBin) => timeBin.value}
valueFormatter={(d) => d.toFixed(2)}
xAxisLabelFormatter={getXAxisLabelFormatter(minsPerBin)}
timeZone="UTC"
xScale={{
type: ScaleType.Time,
interval: {
type: 'calendar',
unit: 'm',
value: minsPerBin,
},
}}
/>
</Chart>
</div>
)}
</EuiResizeObserver>
<Settings
showLegend={false}
xDomain={xDomain}
theme={{ heatmap }}
// TODO connect to charts.theme service see src/plugins/charts/public/services/theme/README.md
baseTheme={LEGACY_LIGHT_THEME}
onBrushEnd={(brushArea) => {
onBrushed?.(getBrushData(brushArea));
}}
locale={i18n.getLocale()}
/>
<Heatmap
id="monitor-details-monitor-status-chart"
colorScale={{
type: 'bands',
bands: getColorBands(euiTheme, colorMode),
}}
data={timeBins}
xAccessor={({ end }) => end}
yAccessor={() => 'T'}
valueAccessor={(timeBin) => timeBin.value}
valueFormatter={(d) => d.toFixed(2)}
xAxisLabelFormatter={getXAxisLabelFormatter(minsPerBin)}
timeZone="UTC"
xScale={{
type: ScaleType.Time,
interval: {
type: 'calendar',
unit: 'm',
value: minsPerBin,
},
}}
/>
</Chart>
)}
</div>
)}
</EuiResizeObserver>
</div>

<MonitorStatusLegend brushable={brushable} />
{loading && <EuiProgress size="xs" color="accent" />}
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
*/

import { useCallback, useEffect, useMemo, useState } from 'react';
import { throttle } from 'lodash';
import { useSelector, useDispatch } from 'react-redux';
import { useDebounce } from 'react-use';
import { useLocation } from 'react-router-dom';

import { scheduleToMinutes } from '../../../../../../common/lib/schedule_to_time';
import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context';

import { useSelectedMonitor } from '../hooks/use_selected_monitor';
Expand All @@ -23,36 +23,43 @@ import {
MonitorStatusTimeBin,
} from './monitor_status_data';
import { useSelectedLocation } from '../hooks/use_selected_location';
import { getMonitorStatusHeatmapAction, selectHeatmap } from '../../../state/status_heatmap';
import {
clearMonitorStatusHeatmapAction,
quietGetMonitorStatusHeatmapAction,
selectHeatmap,
} from '../../../state/status_heatmap';

type Props = Pick<MonitorStatusPanelProps, 'from' | 'to'> & { initSize?: number };

export const useMonitorStatusData = ({
from,
to,
}: Pick<MonitorStatusPanelProps, 'from' | 'to'>) => {
export const useMonitorStatusData = ({ from, to, initSize }: Props) => {
const { lastRefresh } = useSyntheticsRefreshContext();
const { monitor } = useSelectedMonitor();
const location = useSelectedLocation();
const monitorInterval = Math.max(3, monitor?.schedule ? scheduleToMinutes(monitor?.schedule) : 3);
const pageLocation = useLocation();

const fromMillis = dateToMilli(from);
const toMillis = dateToMilli(to);
const totalMinutes = Math.ceil(toMillis - fromMillis) / (1000 * 60);

const [binsAvailableByWidth, setBinsAvailableByWidth] = useState<number | null>(null);
const minsPerBin = Math.floor(
Math.max(
monitorInterval,
binsAvailableByWidth !== null ? totalMinutes / binsAvailableByWidth : 0
)
);
const [debouncedBinsCount, setDebouncedCount] = useState<number | null>(null);

const minsPerBin =
debouncedBinsCount !== null ? Math.floor(totalMinutes / debouncedBinsCount) : null;

const dispatch = useDispatch();
const { heatmap: dateHistogram, loading } = useSelector(selectHeatmap);

useEffect(() => {
if (monitor?.id && location?.label) {
if (binsAvailableByWidth === null && initSize) {
setBinsAvailableByWidth(Math.floor(initSize / CHART_CELL_WIDTH));
}
}, [binsAvailableByWidth, initSize]);

useEffect(() => {
if (monitor?.id && location?.label && debouncedBinsCount !== null && minsPerBin !== null) {
dispatch(
getMonitorStatusHeatmapAction.get({
quietGetMonitorStatusHeatmapAction.get({
monitorId: monitor.id,
location: location.label,
from,
Expand All @@ -61,23 +68,51 @@ export const useMonitorStatusData = ({
})
);
}
}, [dispatch, from, to, minsPerBin, location?.label, monitor?.id, lastRefresh]);
}, [
dispatch,
from,
to,
minsPerBin,
location?.label,
monitor?.id,
lastRefresh,
debouncedBinsCount,
]);

useEffect(() => {
dispatch(clearMonitorStatusHeatmapAction());
}, [dispatch, pageLocation.pathname]);

// Disabling deps warning as we wanna throttle the callback
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleResize = useCallback(
throttle((e: { width: number; height: number }) => {
setBinsAvailableByWidth(Math.floor(e.width / CHART_CELL_WIDTH));
}, 500),
(e: { width: number; height: number }) =>
setBinsAvailableByWidth(Math.floor(e.width / CHART_CELL_WIDTH)),
[]
);

useDebounce(
async () => {
setDebouncedCount(binsAvailableByWidth);
},
500,
[binsAvailableByWidth]
);

const { timeBins, timeBinMap, xDomain } = useMemo((): {
timeBins: MonitorStatusTimeBin[];
timeBinMap: Map<number, MonitorStatusTimeBin>;
xDomain: { min: number; max: number };
} => {
const timeBuckets = createTimeBuckets(minsPerBin, fromMillis, toMillis);
if (minsPerBin === null) {
return {
timeBins: [],
timeBinMap: new Map(),
xDomain: {
min: fromMillis,
max: toMillis,
},
};
}
const timeBuckets = createTimeBuckets(minsPerBin ?? 50, fromMillis, toMillis);
const bins = createStatusTimeBins(timeBuckets, dateHistogram);
return {
timeBins: bins,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { fetchServiceLocationsEffect } from './service_locations';
import { browserJourneyEffects, fetchJourneyStepsEffect } from './browser_journey';
import { fetchPingStatusesEffect } from './ping_status';
import { fetchOverviewStatusEffect } from './overview_status';
import { fetchMonitorStatusHeatmap } from './status_heatmap';
import { fetchMonitorStatusHeatmap, quietFetchMonitorStatusHeatmap } from './status_heatmap';

export const rootEffect = function* root(): Generator {
yield all([
Expand Down Expand Up @@ -73,5 +73,6 @@ export const rootEffect = function* root(): Generator {
fork(getDefaultAlertingEffect),
fork(enableDefaultAlertingSilentlyEffect),
fork(fetchMonitorStatusHeatmap),
fork(quietFetchMonitorStatusHeatmap),
]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { createAction } from '@reduxjs/toolkit';
import { MonitorStatusHeatmapBucket } from '../../../../../common/runtime_types';
import { createAsyncAction } from '../utils/actions';

Expand All @@ -14,3 +15,10 @@ export const getMonitorStatusHeatmapAction = createAsyncAction<
MonitorStatusHeatmapActionArgs,
MonitorStatusHeatmapBucket[]
>('MONITOR STATUS HEATMAP');

export const clearMonitorStatusHeatmapAction = createAction<void>('CLEAR MONITOR STATUS HEATMAP');

export const quietGetMonitorStatusHeatmapAction = createAsyncAction<
MonitorStatusHeatmapActionArgs,
MonitorStatusHeatmapBucket[]
>('QUIET GET MONITOR STATUS HEATMAP');
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { takeLatest } from 'redux-saga/effects';
import { fetchEffectFactory } from '../utils/fetch_effect';
import { fetchMonitorStatusHeatmap as api } from './api';

import { getMonitorStatusHeatmapAction } from './actions';
import { getMonitorStatusHeatmapAction, quietGetMonitorStatusHeatmapAction } from './actions';

export function* fetchMonitorStatusHeatmap() {
yield takeLatest(
Expand All @@ -21,3 +21,14 @@ export function* fetchMonitorStatusHeatmap() {
) as ReturnType<typeof fetchEffectFactory>
);
}

export function* quietFetchMonitorStatusHeatmap() {
yield takeLatest(
quietGetMonitorStatusHeatmapAction.get,
fetchEffectFactory(
api,
getMonitorStatusHeatmapAction.success,
getMonitorStatusHeatmapAction.fail
) as ReturnType<typeof fetchEffectFactory>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import { MonitorStatusHeatmapBucket } from '../../../../../common/runtime_types'

import { IHttpSerializedFetchError } from '../utils/http_error';

import { getMonitorStatusHeatmapAction } from './actions';
import {
clearMonitorStatusHeatmapAction,
getMonitorStatusHeatmapAction,
quietGetMonitorStatusHeatmapAction,
} from './actions';

export interface MonitorStatusHeatmap {
heatmap: MonitorStatusHeatmapBucket[];
Expand All @@ -27,6 +31,13 @@ const initialState: MonitorStatusHeatmap = {

export const monitorStatusHeatmapReducer = createReducer(initialState, (builder) => {
builder
.addCase(quietGetMonitorStatusHeatmapAction.success, (state, action) => {
state.heatmap = action.payload;
state.loading = false;
})
.addCase(quietGetMonitorStatusHeatmapAction.get, (state) => {
state.loading = true;
})
.addCase(getMonitorStatusHeatmapAction.get, (state) => {
state.loading = true;
state.heatmap = [];
Expand All @@ -38,6 +49,9 @@ export const monitorStatusHeatmapReducer = createReducer(initialState, (builder)
.addCase(getMonitorStatusHeatmapAction.fail, (state, action) => {
state.error = action.payload;
state.loading = false;
})
.addCase(clearMonitorStatusHeatmapAction, (state) => {
state.heatmap = [];
});
});

Expand Down

0 comments on commit af444ef

Please sign in to comment.