Move useDateRange into data plugin (#212548)

As discussed offline, the existing `useDateRange` hook integrating the
timefilter contract with the react lifecycle is a nice abstraction that
makes sense to be part of the data plugin directly.

This PR moves it into the timefilterclass:
```
const {
  timeRange,
  absoluteTimeRange,
  setTimeRange
} = data.query.timefilter.timefilter.useTimefilter(); 
```

All consumers have been changed to use this directly.
This commit is contained in:
Joe Reuter 2025-02-28 11:04:53 +01:00 committed by GitHub
parent 300e35012d
commit 8d7f34e2b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 194 additions and 140 deletions

View file

@ -25,6 +25,7 @@ import {
} from '../../../common';
import { TimeHistoryContract } from './time_history';
import { createAutoRefreshLoop, AutoRefreshDoneFn } from './lib/auto_refresh_loop';
import { TimefilterHook, createUseTimefilterHook } from './use_timefilter';
export type { AutoRefreshDoneFn };
@ -52,6 +53,8 @@ export class Timefilter {
private readonly timeDefaults: TimeRange;
private readonly refreshIntervalDefaults: RefreshInterval;
public readonly useTimefilter: () => TimefilterHook;
// Used when an auto refresh is triggered
private readonly autoRefreshLoop = createAutoRefreshLoop();
@ -73,6 +76,8 @@ export class Timefilter {
this._minRefreshInterval = config.minRefreshIntervalDefault;
this._time = config.timeDefaults;
this.setRefreshInterval(config.refreshIntervalDefaults);
this.useTimefilter = createUseTimefilterHook(this);
}
public isTimeRangeSelectorEnabled() {

View file

@ -42,6 +42,7 @@ const createSetupContractMock = () => {
getRefreshIntervalDefaults: jest.fn(),
getTimeDefaults: jest.fn(),
getAbsoluteTime: jest.fn(),
useTimefilter: jest.fn(),
};
const historyMock: jest.Mocked<TimeHistoryContract> = {

View file

@ -0,0 +1,113 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react';
import { Subject } from 'rxjs';
import { createUseTimefilterHook } from './use_timefilter';
import { TimeRange } from '@kbn/es-query';
describe('useTimefilter hook', () => {
const timeUpdateSubject = new Subject<void>();
const initialTimeRange: TimeRange = {
from: 'now-1d',
to: 'now',
};
const initialAbsoluteTimeRange = {
from: '2020-01-01T00:00:00Z',
to: '2020-01-02T00:00:00Z',
};
// Track the current time range in the mock
let currentTimeRange = { ...initialTimeRange };
const mockTimefilter = {
getTime: jest.fn().mockImplementation(() => currentTimeRange),
getAbsoluteTime: jest.fn().mockReturnValue(initialAbsoluteTimeRange),
setTime: jest.fn().mockImplementation((timeRange) => {
currentTimeRange = timeRange;
}),
getTimeUpdate$: jest.fn().mockReturnValue(timeUpdateSubject),
};
const useTimefilter = createUseTimefilterHook(mockTimefilter as any);
beforeEach(() => {
jest.clearAllMocks();
// Reset the current time range for each test
currentTimeRange = { ...initialTimeRange };
});
test('time range and absolute time range should have the right values', () => {
const { result } = renderHook(() => useTimefilter());
expect(result.current.timeRange).toEqual(initialTimeRange);
expect(result.current.absoluteTimeRange).toEqual({
start: new Date(initialAbsoluteTimeRange.from).getTime(),
end: new Date(initialAbsoluteTimeRange.to).getTime(),
});
});
test('hook should rerender when the time update gets updated', () => {
const { result } = renderHook(() => useTimefilter());
const updatedTimeRange = {
from: '2021-01-01T00:00:00Z',
to: '2021-01-02T00:00:00Z',
};
const updatedAbsoluteTimeRange = {
from: '2021-01-01T00:00:00Z',
to: '2021-01-02T00:00:00Z',
};
// Update the current time range for the mock
currentTimeRange = updatedTimeRange;
mockTimefilter.getAbsoluteTime.mockReturnValue(updatedAbsoluteTimeRange);
act(() => {
timeUpdateSubject.next();
});
expect(result.current.timeRange).toEqual(updatedTimeRange);
expect(result.current.absoluteTimeRange).toEqual({
start: new Date(updatedAbsoluteTimeRange.from).getTime(),
end: new Date(updatedAbsoluteTimeRange.to).getTime(),
});
});
test('setTimeRange should call through correctly', () => {
const { result } = renderHook(() => useTimefilter());
const newTimeRange = {
from: '2022-01-01T00:00:00Z',
to: '2022-01-02T00:00:00Z',
};
act(() => {
result.current.setTimeRange(newTimeRange);
});
expect(mockTimefilter.setTime).toHaveBeenCalledWith(newTimeRange);
// Test with callback form
act(() => {
result.current.setTimeRange((prevRange) => ({
...prevRange,
from: '2022-02-01T00:00:00Z',
}));
});
expect(mockTimefilter.setTime).toHaveBeenCalledWith({
...newTimeRange,
from: '2022-02-01T00:00:00Z',
});
});
});

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { TimeRange } from '@kbn/es-query';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Timefilter } from './timefilter';
export interface TimefilterHook {
timeRange: TimeRange;
absoluteTimeRange: {
start: number;
end: number;
};
setTimeRange: React.Dispatch<React.SetStateAction<TimeRange>>;
}
export function createUseTimefilterHook(timefilter: Timefilter) {
return function useTimefilter(): TimefilterHook {
const [timeRange, setTimeRange] = useState(() => timefilter.getTime());
const [absoluteTimeRange, setAbsoluteTimeRange] = useState(() => timefilter.getAbsoluteTime());
useEffect(() => {
const timeUpdateSubscription = timefilter.getTimeUpdate$().subscribe({
next: () => {
setTimeRange(() => timefilter.getTime());
setAbsoluteTimeRange(() => timefilter.getAbsoluteTime());
},
});
return () => {
timeUpdateSubscription.unsubscribe();
};
}, []);
const setTimeRangeMemoized: React.Dispatch<React.SetStateAction<TimeRange>> = useCallback(
(nextOrCallback) => {
const val =
typeof nextOrCallback === 'function'
? nextOrCallback(timefilter.getTime())
: nextOrCallback;
timefilter.setTime(val);
},
[]
);
const asEpoch = useMemo(() => {
return {
start: new Date(absoluteTimeRange.from).getTime(),
end: new Date(absoluteTimeRange.to).getTime(),
};
}, [absoluteTimeRange]);
return {
timeRange,
absoluteTimeRange: asEpoch,
setTimeRange: setTimeRangeMemoized,
};
};
}

View file

@ -24,7 +24,6 @@ import { useStreamsAppFetch } from '../../../../hooks/use_streams_app_fetch';
import { useKibana } from '../../../../hooks/use_kibana';
import { DetectedField, ProcessorDefinitionWithUIAttributes } from '../types';
import { processorConverter } from '../utils';
import { useDateRange } from '../../../../hooks/use_date_range';
export type Simulation = APIReturnType<'POST /api/streams/{name}/processing/_simulate'>;
export type ProcessorMetrics =
@ -95,7 +94,7 @@ export const useProcessingSimulator = ({
const {
absoluteTimeRange: { start, end },
} = useDateRange({ data });
} = data.query.timefilter.timefilter.useTimefilter();
const draftProcessors = useMemo(
() => processors.filter((processor) => processor.status === 'draft'),

View file

@ -29,7 +29,6 @@ import {
docsFilterOptions,
} from './hooks/use_processing_simulator';
import { AssetImage } from '../../asset_image';
import { useDateRange } from '../../../hooks/use_date_range';
interface ProcessorOutcomePreviewProps {
columns: TableColumn[];
@ -53,7 +52,7 @@ export const ProcessorOutcomePreview = ({
const { dependencies } = useKibana();
const { data } = dependencies.start;
const { timeRange, setTimeRange } = useDateRange({ data });
const { timeRange, setTimeRange } = data.query.timefilter.timefilter.useTimefilter();
const tableColumns = useMemo(() => {
switch (selectedDocsFilter) {

View file

@ -33,7 +33,6 @@ import { useKibana } from '../../../hooks/use_kibana';
import { DataStreamStats } from './hooks/use_data_stream_stats';
import { formatBytes } from './helpers/format_bytes';
import { StreamsAppSearchBar } from '../../streams_app_search_bar';
import { useDateRange } from '../../../hooks/use_date_range';
import { useIngestionRate, useIngestionRatePerTier } from './hooks/use_ingestion_rate';
import { useIlmPhasesColorAndDescription } from './hooks/use_ilm_phases_color_and_description';
@ -53,7 +52,7 @@ export function IngestionRate({
start: { data },
},
} = useKibana();
const { timeRange, setTimeRange } = useDateRange({ data });
const { timeRange, setTimeRange } = data.query.timefilter.timefilter.useTimefilter();
return (
<>

View file

@ -24,7 +24,6 @@ import { StreamsAppSearchBar } from '../../streams_app_search_bar';
import { useRoutingState } from './hooks/routing_state';
import { PreviewPanelIllustration } from './preview_panel_illustration';
import { PreviewMatches } from './preview_matches';
import { useDateRange } from '../../../hooks/use_date_range';
export function PreviewPanel({
definition,
@ -41,9 +40,9 @@ export function PreviewPanel({
const {
timeRange,
absoluteTimeRange: { start, end },
setTimeRange,
} = useDateRange({ data });
absoluteTimeRange: { start, end },
} = data.query.timefilter.timefilter.useTimefilter();
const {
isLoadingDocuments,

View file

@ -18,7 +18,6 @@ import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { DashboardLocatorParams } from '@kbn/dashboard-plugin/public';
import { useKibana } from '../../hooks/use_kibana';
import { tagListToReferenceList } from './to_reference_list';
import { useDateRange } from '../../hooks/use_date_range';
export function DashboardsTable({
dashboards,
@ -42,7 +41,7 @@ export function DashboardsTable({
},
},
} = useKibana();
const { timeRange } = useDateRange({ data });
const { timeRange } = data.query.timefilter.timefilter.useTimefilter();
const dashboardLocator = share.url.locators.get<DashboardLocatorParams>(DASHBOARD_APP_LOCATOR);
const columns = useMemo((): Array<EuiBasicTableColumn<SanitizedDashboardAsset>> => {
return [

View file

@ -37,7 +37,6 @@ import { useDashboardsFetch } from '../../hooks/use_dashboards_fetch';
import { DashboardsTable } from '../stream_detail_dashboards_view/dashboard_table';
import { AssetImage } from '../asset_image';
import { useWiredStreams } from '../../hooks/use_wired_streams';
import { useDateRange } from '../../hooks/use_date_range';
const formatNumber = (val: number) => {
return Number(val).toLocaleString('en', {
@ -59,9 +58,9 @@ export function StreamDetailOverview({ definition }: { definition?: IngestStream
const {
timeRange,
absoluteTimeRange: { start, end },
setTimeRange,
} = useDateRange({ data });
absoluteTimeRange: { start, end },
} = data.query.timefilter.timefilter.useTimefilter();
const indexPatterns = useMemo(() => {
return getIndexPatterns(definition?.stream);

View file

@ -1,63 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TimeRange } from '@kbn/data-plugin/common';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { useCallback, useEffect, useMemo, useState } from 'react';
export function useDateRange({ data }: { data: DataPublicPluginStart }): {
timeRange: TimeRange;
absoluteTimeRange: {
start: number;
end: number;
};
setTimeRange: React.Dispatch<React.SetStateAction<TimeRange>>;
} {
const timefilter = data.query.timefilter.timefilter;
const [timeRange, setTimeRange] = useState(() => timefilter.getTime());
const [absoluteTimeRange, setAbsoluteTimeRange] = useState(() => timefilter.getAbsoluteTime());
useEffect(() => {
const timeUpdateSubscription = timefilter.getTimeUpdate$().subscribe({
next: () => {
setTimeRange(() => timefilter.getTime());
setAbsoluteTimeRange(() => timefilter.getAbsoluteTime());
},
});
return () => {
timeUpdateSubscription.unsubscribe();
};
}, [timefilter]);
const setTimeRangeMemoized: React.Dispatch<React.SetStateAction<TimeRange>> = useCallback(
(nextOrCallback) => {
const val =
typeof nextOrCallback === 'function'
? nextOrCallback(timefilter.getTime())
: nextOrCallback;
timefilter.setTime(val);
},
[timefilter]
);
const asEpoch = useMemo(() => {
return {
start: new Date(absoluteTimeRange.from).getTime(),
end: new Date(absoluteTimeRange.to).getTime(),
};
}, [absoluteTimeRange]);
return {
timeRange,
absoluteTimeRange: asEpoch,
setTimeRange: setTimeRangeMemoized,
};
}

View file

@ -1,63 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TimeRange } from '@kbn/data-plugin/common';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { useCallback, useEffect, useMemo, useState } from 'react';
export function useDateRange({ data }: { data: DataPublicPluginStart }): {
timeRange: TimeRange;
absoluteTimeRange: {
start: number;
end: number;
};
setTimeRange: React.Dispatch<React.SetStateAction<TimeRange>>;
} {
const timefilter = data.query.timefilter.timefilter;
const [timeRange, setTimeRange] = useState(() => timefilter.getTime());
const [absoluteTimeRange, setAbsoluteTimeRange] = useState(() => timefilter.getAbsoluteTime());
useEffect(() => {
const timeUpdateSubscription = timefilter.getTimeUpdate$().subscribe({
next: () => {
setTimeRange(() => timefilter.getTime());
setAbsoluteTimeRange(() => timefilter.getAbsoluteTime());
},
});
return () => {
timeUpdateSubscription.unsubscribe();
};
}, [timefilter]);
const setTimeRangeMemoized: React.Dispatch<React.SetStateAction<TimeRange>> = useCallback(
(nextOrCallback) => {
const val =
typeof nextOrCallback === 'function'
? nextOrCallback(timefilter.getTime())
: nextOrCallback;
timefilter.setTime(val);
},
[timefilter]
);
const asEpoch = useMemo(() => {
return {
start: new Date(absoluteTimeRange.from).getTime(),
end: new Date(absoluteTimeRange.to).getTime(),
};
}, [absoluteTimeRange]);
return {
timeRange,
absoluteTimeRange: asEpoch,
setTimeRange: setTimeRangeMemoized,
};
}