diff --git a/components/log-viewer-webui/client/src/components/ResultsTimeline/index.tsx b/components/log-viewer-webui/client/src/components/ResultsTimeline/index.tsx index 7126d620c8..727787a2c5 100644 --- a/components/log-viewer-webui/client/src/components/ResultsTimeline/index.tsx +++ b/components/log-viewer-webui/client/src/components/ResultsTimeline/index.tsx @@ -13,15 +13,13 @@ import { TooltipItem, } from "chart.js"; import zoomPlugin from "chartjs-plugin-zoom"; -import dayjs from "dayjs"; +import dayjs, {Dayjs} from "dayjs"; -import {Nullable} from "../../typings/common"; import {DATETIME_FORMAT_TEMPLATE} from "../../typings/datetime"; import { convertUtcDatetimeToSameLocalDate, convertZoomTimestampToUtcDatetime, } from "./datetime"; -import {TimeRange} from "./datetime/typings"; import styles from "./index.module.css"; import { TimelineBucket, @@ -45,8 +43,8 @@ ChartJs.register( interface ResultsTimelineProps { isInputDisabled: boolean; - onTimelineZoom: (newTimeRange: TimeRange) => void; - timelineBuckets: Nullable; + onTimelineZoom: (newTimeRange: [Dayjs, Dayjs]) => void; + timelineBuckets: TimelineBucket[]; timelineConfig: TimelineConfig; } @@ -78,10 +76,6 @@ const ResultsTimeline = ({ ); }, [isInputDisabled]); - if (null === timelineBuckets) { - return
; - } - const data = { datasets: [ { @@ -179,10 +173,10 @@ const ResultsTimeline = ({ return; } const {min, max} = xAxis; - const newTimeRange = { - begin: convertZoomTimestampToUtcDatetime(min), - end: convertZoomTimestampToUtcDatetime(max), - }; + const newTimeRange: [Dayjs, Dayjs] = [ + convertZoomTimestampToUtcDatetime(min), + convertZoomTimestampToUtcDatetime(max), + ]; onTimelineZoom(newTimeRange); }, diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/SearchButton/SubmitButton.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/SearchButton/SubmitButton.tsx index 338313ee5b..00e309663b 100644 --- a/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/SearchButton/SubmitButton.tsx +++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchControls/SearchButton/SubmitButton.tsx @@ -19,28 +19,29 @@ import styles from "./index.module.css"; * @return */ const SubmitButton = () => { - const {searchUiState, timeRange, queryString} = useSearchStore(); - const isQueryStringEmpty = queryString === SEARCH_STATE_DEFAULT.queryString; + const {searchUiState, timeRange, queryString, updateTimelineConfig} = useSearchStore(); /** * Submits search query. */ const handleSubmitButtonClick = useCallback(() => { - const timelineConfig = computeTimelineConfig( - timeRange[0].valueOf(), - timeRange[1].valueOf() - ); + // Update timeline to match range picker selection. + const newTimelineConfig = computeTimelineConfig(timeRange); + updateTimelineConfig(newTimelineConfig); handleQuerySubmit({ ignoreCase: false, queryString: queryString, - timeRangeBucketSizeMillis: timelineConfig.bucketDuration.asMilliseconds(), + timeRangeBucketSizeMillis: newTimelineConfig.bucketDuration.asMilliseconds(), timestampBegin: timeRange[0].valueOf(), timestampEnd: timeRange[1].valueOf(), }); }, [queryString, + updateTimelineConfig, timeRange]); + const isQueryStringEmpty = queryString === SEARCH_STATE_DEFAULT.queryString; + return ( { const { + queryString, updateTimeRange, - timeRange: [beginTime, endTime], updateTimeRangeOption, + timelineConfig, + searchUiState, + updateTimelineConfig, } = useSearchStore(); - const timestampBeginUnixMillis = beginTime.utc().valueOf(); - const timestampEndUnixMillis = endTime.utc().valueOf(); - const timelineConfig = computeTimelineConfig(timestampBeginUnixMillis, timestampEndUnixMillis); + const aggregationResults = useAggregationResults(); + - const handleTimelineZoom = (newTimeRange: TimeRange) => { - // Expand the time range to the granularity of buckets so if the user - // pans across at least one bar in the graph, we will zoom into a region - // that still contains log events. - const expandedTimeRange = expandTimeRangeToDurationMultiple( - timelineConfig.bucketDuration, - newTimeRange, - ); + const handleTimelineZoom = (newTimeRange: [Dayjs, Dayjs]) => { + const newTimelineConfig: TimelineConfig = computeTimelineConfig(newTimeRange); - updateTimeRange([expandedTimeRange.begin, - expandedTimeRange.end]); + // Update range picker selection to match zoomed range. + updateTimeRange(newTimeRange); updateTimeRangeOption(TIME_RANGE_OPTION.CUSTOM); + updateTimelineConfig(newTimelineConfig); + + const isQueryStringEmpty = queryString === SEARCH_STATE_DEFAULT.queryString; + if (isQueryStringEmpty) { + return; + } - // eslint-disable-next-line no-warning-comments - // TODO: submit query based on timelineConfig. + handleQuerySubmit({ + ignoreCase: false, + queryString: queryString, + timeRangeBucketSizeMillis: newTimelineConfig.bucketDuration.asMilliseconds(), + timestampBegin: newTimeRange[0].valueOf(), + timestampEnd: newTimeRange[1].valueOf(), + }); }; - // eslint-disable-next-line no-warning-comments - // TODO: fix `isInputDisabled` . return ( ); diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/useAggregationResults.ts b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/useAggregationResults.ts new file mode 100644 index 0000000000..ca88e6e223 --- /dev/null +++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/useAggregationResults.ts @@ -0,0 +1,32 @@ +import MongoCollectionSocket from "../../../../api/socket/MongoCollectionSocket"; +import {useCursor} from "../../../../api/socket/useCursor"; +import {TimelineBucket} from "../../../../components/ResultsTimeline/typings"; +import useSearchStore, {SEARCH_STATE_DEFAULT} from "../../SearchState/index"; + + +/** + * Custom hook to get aggregation results for the current aggregationJobId. + * + * @return + */ +const useAggregationResults = () => { + const {aggregationJobId} = useSearchStore(); + + const aggregationResultsCursor = useCursor( + () => { + // If there is no active aggregation job, there are no results to fetch. The cursor will + // return null. + if (aggregationJobId === SEARCH_STATE_DEFAULT.aggregationJobId) { + return null; + } + + const collection = new MongoCollectionSocket(aggregationJobId.toString()); + return collection.find({}, {}); + }, + [aggregationJobId] + ); + + return aggregationResultsCursor; +}; + +export {useAggregationResults}; diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/utils.ts b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/utils.ts index 445e5e5a1b..1f60c0bd34 100644 --- a/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/utils.ts +++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchResults/SearchResultsTimeline/utils.ts @@ -1,5 +1,6 @@ import dayjs from "dayjs"; import DayjsDuration, {DurationUnitType} from "dayjs/plugin/duration"; +import DayjsUtc from "dayjs/plugin/utc"; import {TimeRange} from "../../../../components/ResultsTimeline/datetime/typings"; import { @@ -8,6 +9,7 @@ import { } from "../../../../components/ResultsTimeline/typings"; +dayjs.extend(DayjsUtc); dayjs.extend(DayjsDuration); /** @@ -36,14 +38,14 @@ const expandTimeRangeToDurationMultiple = (duration: DayjsDuration.Duration, { * Computes the timestamp range and bucket duration necessary to render the bars in the timeline * chart. * - * @param timestampBeginUnixMillis - * @param timestampEndUnixMillis + * @param timeRange * @return */ const computeTimelineConfig = ( - timestampBeginUnixMillis: number, - timestampEndUnixMillis: number + timeRange: [dayjs.Dayjs, dayjs.Dayjs], ): TimelineConfig => { + const timestampBeginUnixMillis = dayjs.utc(timeRange[0]).valueOf(); + const timestampEndUnixMillis = dayjs.utc(timeRange[1]).valueOf(); const timeRangeMillis = timestampEndUnixMillis - timestampBeginUnixMillis; const exactTimelineBucketMillis = timeRangeMillis / MAX_DATA_POINTS_PER_TIMELINE; diff --git a/components/log-viewer-webui/client/src/pages/SearchPage/SearchState/index.tsx b/components/log-viewer-webui/client/src/pages/SearchPage/SearchState/index.tsx index 693c4d05d5..6ba56e891f 100644 --- a/components/log-viewer-webui/client/src/pages/SearchPage/SearchState/index.tsx +++ b/components/log-viewer-webui/client/src/pages/SearchPage/SearchState/index.tsx @@ -1,11 +1,13 @@ import dayjs from "dayjs"; import {create} from "zustand"; +import {TimelineConfig} from "../../../components/ResultsTimeline/typings"; import { DEFAULT_TIME_RANGE, TIME_RANGE_OPTION, TIME_RANGE_OPTION_DAYJS_MAP, } from "../SearchControls/TimeRangeInput/utils"; +import {computeTimelineConfig} from "../SearchResults/SearchResultsTimeline/utils"; import {SEARCH_UI_STATE} from "./typings"; @@ -21,6 +23,7 @@ const SEARCH_STATE_DEFAULT = Object.freeze({ searchUiState: SEARCH_UI_STATE.DEFAULT, timeRange: TIME_RANGE_OPTION_DAYJS_MAP[DEFAULT_TIME_RANGE], timeRangeOption: DEFAULT_TIME_RANGE, + timelineConfig: computeTimelineConfig(TIME_RANGE_OPTION_DAYJS_MAP[DEFAULT_TIME_RANGE]), }); interface SearchState { @@ -59,6 +62,13 @@ interface SearchState { */ timeRangeOption: TIME_RANGE_OPTION; + /** + * Time range and bucket duration for the timeline. The timeline config should + * only be updated when queries are submitted and not when the range picker + * selection is changed. + */ + timelineConfig: TimelineConfig; + updateAggregationJobId: (id: string | null) => void; updateQueryIsCaseSensitive: (newValue: boolean) => void; updateQueryString: (query: string) => void; @@ -66,6 +76,7 @@ interface SearchState { updateSearchUiState: (state: SEARCH_UI_STATE) => void; updateTimeRange: (range: [dayjs.Dayjs, dayjs.Dayjs]) => void; updateTimeRangeOption: (option: TIME_RANGE_OPTION) => void; + updateTimelineConfig: (config: TimelineConfig) => void; } const useSearchStore = create((set) => ({ @@ -91,6 +102,9 @@ const useSearchStore = create((set) => ({ updateTimeRangeOption: (option: TIME_RANGE_OPTION) => { set({timeRangeOption: option}); }, + updateTimelineConfig: (config) => { + set({timelineConfig: config}); + }, }));