This is an automated email from the ASF dual-hosted git repository. vogievetsky pushed a commit to branch segment_timeline2 in repository https://gitbox.apache.org/repos/asf/druid.git
commit d80b2beb9566b4ef94ed1f9ab49051cda48433b1 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Mon Nov 4 12:38:13 2024 -0800 preogress --- .../src/components/segment-timeline/bubble.tsx | 9 ++- .../segment-timeline/segment-bar-chart-render.tsx | 64 +++++++++++------- .../segment-timeline/segment-bar-chart.tsx | 77 +++++++++++----------- .../segment-timeline/segment-timeline.tsx | 9 +-- 4 files changed, 89 insertions(+), 70 deletions(-) diff --git a/web-console/src/components/segment-timeline/bubble.tsx b/web-console/src/components/segment-timeline/bubble.tsx index f3a9c287b13..efb72431379 100644 --- a/web-console/src/components/segment-timeline/bubble.tsx +++ b/web-console/src/components/segment-timeline/bubble.tsx @@ -17,22 +17,25 @@ */ import classNames from 'classnames'; +import type { ReactNode } from 'react'; import { createPortal } from 'react-dom'; import './bubble.scss'; interface BubbleProps { - openOn: { x: number; y: number; text: string }; + className?: string; + openOn: { x: number; y: number; text: ReactNode } | undefined; direction?: 'up' | 'down'; mute?: boolean; } export const Bubble = function Bubble(props: BubbleProps) { - const { openOn, direction = 'up', mute } = props; + const { className, openOn, direction = 'up', mute } = props; + if (!openOn) return null; return createPortal( <div - className={classNames('bubble', direction, { mute })} + className={classNames('bubble', className, direction, { mute })} style={{ left: openOn.x, top: openOn.y }} > {openOn.text} diff --git a/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx index edb3a90e655..0152188e8e2 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx @@ -22,7 +22,7 @@ import classNames from 'classnames'; import { max } from 'd3-array'; import { axisBottom, axisLeft } from 'd3-axis'; import { scaleLinear, scaleUtc } from 'd3-scale'; -import type React from 'react'; +import type { ReactNode } from 'react'; import { useMemo, useRef, useState } from 'react'; import { getDatasourceColor } from '../../druid-models'; @@ -50,7 +50,7 @@ import { aggregateSegmentStats } from './common'; import './segment-bar-chart-render.scss'; -const CHART_MARGIN: Margin = { top: 40, right: 0, bottom: 25, left: 70 }; +const CHART_MARGIN: Margin = { top: 10, right: 0, bottom: 25, left: 70 }; const MIN_BAR_WIDTH = 2; const POSSIBLE_GRANULARITIES = [ new Duration('PT15M'), @@ -117,7 +117,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( focusDatasource, changeFocusDatasource, } = props; - const [hoverOn, setHoverOn] = useState<IntervalBar>(); + const [hoveredIntervalBar, setHoveredIntervalBar] = useState<IntervalBar>(); const [mouseDownAt, setMouseDownAt] = useState< { time: Date; action: 'select' | 'shift' } | undefined >(); @@ -223,7 +223,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( function handleMouseDown(e: React.MouseEvent) { const svg = svgRef.current; - if (!svg) return; + if (!svg || hoveredIntervalBar) return; e.preventDefault(); const rect = svg.getBoundingClientRect(); const x = e.clientX - rect.x - CHART_MARGIN.left; @@ -327,6 +327,33 @@ export const SegmentBarChartRender = function SegmentBarChartRender( console.log('Bar chart render'); + let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined; + if (hoveredIntervalBar && svgRef.current) { + const rect = svgRef.current.getBoundingClientRect(); + hoveredOpenOn = { + x: + rect.x + + CHART_MARGIN.left + + timeScale( + new Date((hoveredIntervalBar.start.valueOf() + hoveredIntervalBar.end.valueOf()) / 2), + ), + y: rect.y + CHART_MARGIN.top - 10, + text: ( + <> + <div>Datasource: {hoveredIntervalBar.datasource}</div> + <div>{`Time: ${prettyFormatIsoDate(hoveredIntervalBar.start)}/${ + hoveredIntervalBar.originalTimeSpan + }`}</div> + <div> + {`${capitalizeFirst(shownIntervalStat)}: ${formatTick( + hoveredIntervalBar[shownIntervalStat], + )}`} + </div> + </> + ), + }; + } + return ( <div className="segment-bar-chart-render"> <svg @@ -339,7 +366,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( > <g transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`} - onMouseLeave={() => setHoverOn(undefined)} + onMouseLeave={() => setHoveredIntervalBar(undefined)} > <ChartAxis className="gridline-x" @@ -389,18 +416,18 @@ export const SegmentBarChartRender = function SegmentBarChartRender( onClick={() => changeFocusDatasource(intervalBar.datasource)} onMouseOver={() => { if (mouseDownAt) return; - setHoverOn(intervalBar); + setHoveredIntervalBar(intervalBar); }} /> ); })} - {hoverOn && ( + {hoveredIntervalBar && ( <rect className="hovered-bar" - {...segmentBarToRect(hoverOn)} + {...segmentBarToRect(hoveredIntervalBar)} onClick={() => { - setHoverOn(undefined); - changeFocusDatasource(hoverOn.datasource); + setHoveredIntervalBar(undefined); + changeFocusDatasource(hoveredIntervalBar.datasource); }} /> )} @@ -429,26 +456,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender( </g> </g> </svg> - {dragging ? ( - <div className="bar-chart-tooltip"> - <div>Start: {dragging[0].toISOString()}</div> - <div>End: {dragging[1].toISOString()}</div> - </div> - ) : hoverOn ? ( - <div className="bar-chart-tooltip"> - <div>Datasource: {hoverOn.datasource}</div> - <div>{`Time: ${prettyFormatIsoDate(hoverOn.start)}/${hoverOn.originalTimeSpan}`}</div> - <div> - {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(hoverOn[shownIntervalStat])}`} - </div> - </div> - ) : undefined} {!intervalRows.length && ( <div className="empty-placeholder"> <div className="no-data-text">There are no segments in the selected range</div> </div> )} - {bubbleOpenOn && <Bubble openOn={bubbleOpenOn} mute direction="down" />} + <Bubble openOn={hoveredOpenOn} mute direction="up" /> + <Bubble openOn={bubbleOpenOn} mute direction="down" /> </div> ); }; diff --git a/web-console/src/components/segment-timeline/segment-bar-chart.tsx b/web-console/src/components/segment-timeline/segment-bar-chart.tsx index bcbe645c58e..047fda7fdd2 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -17,9 +17,10 @@ */ import type { NonNullDateRange } from '@blueprintjs/datetime'; -import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query'; +import { C, F, L, N, sql, SqlExpression, SqlQuery } from '@druid-toolkit/query'; import { useMemo } from 'react'; +import { END_OF_TIME_DATE, START_OF_TIME_DATE } from '../../druid-models'; import type { Capabilities } from '../../helpers'; import { useQueryManager } from '../../hooks'; import { Api } from '../../singletons'; @@ -53,15 +54,25 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr changeFocusDatasource, } = props; - const intervalsQuery = useMemo(() => ({ capabilities, dateRange }), [capabilities, dateRange]); + const intervalsQuery = useMemo( + () => ({ capabilities, dateRange, focusDatasource }), + [capabilities, dateRange, focusDatasource], + ); const [intervalRowsState] = useQueryManager({ query: intervalsQuery, - processQuery: async ({ capabilities, dateRange }, cancelToken) => { + processQuery: async ({ capabilities, dateRange, focusDatasource }, cancelToken) => { if (capabilities.hasSql()) { const query = SqlQuery.from(N('sys').table('segments')) .changeWhereExpression( - sql`'${dateRange[0].toISOString()}' <= "end" AND "start" <= '${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`, + SqlExpression.and( + sql`'${dateRange[0].toISOString()}' <= "end" AND "start" <= '${dateRange[1].toISOString()}'`, + C('start').unequal(START_OF_TIME_DATE), + C('end').unequal(END_OF_TIME_DATE), + C('is_published').equal(1), + C('is_overshadowed').equal(0), + focusDatasource ? C('datasource').equal(L(focusDatasource)) : undefined, + ), ) .addSelect(C('start'), { addToGroupBy: 'end' }) .addSelect(C('end'), { addToGroupBy: 'end' }) @@ -82,41 +93,31 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr }; }); // This trimming should ideally be pushed into the SQL query but at the time of this writing queries on the sys.* tables do not allow substring } else { - const datasources = await getApiArray<string>( - `/druid/coordinator/v1/datasources`, - cancelToken, + return filterMap( + await getApiArray( + `/druid/coordinator/v1/metadata/segments?${ + focusDatasource ? `datasources=${Api.encodePath(focusDatasource)}` : '' + }`, + cancelToken, + ), + (segment: any) => { + const [startStr, endStr] = segment.interval.split('/'); + if (startStr === START_OF_TIME_DATE && endStr === END_OF_TIME_DATE) return; + const start = new Date(startStr); + const end = new Date(endStr); + if (!(dateRange[0] <= end && start <= dateRange[1])) return; + + return { + start, + end, + datasource: segment.dataSource, + originalTimeSpan: Duration.fromRange(start, end, TZ_UTC), + segments: 1, + size: segment.size, + num_rows: segment.num_rows || 0, // segment.num_rows is really null on this API :-( + }; + }, ); - - return ( - await Promise.all( - datasources.map(async datasource => { - const intervalMap = ( - await Api.instance.get( - `/druid/coordinator/v1/datasources/${Api.encodePath( - datasource, - )}/intervals?simple`, - { cancelToken }, - ) - ).data; - - return filterMap(Object.entries(intervalMap), ([interval, v]) => { - const [startStr, endStr] = interval.split('/'); - const start = new Date(startStr); - const end = new Date(endStr); - // ToDo: Filter on start end - const { count, size, rows } = v as any; - return { - start, - end, - datasource, - segments: count, - size, - rows, - }; - }); - }), - ) - ).flat(); } }, }); diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 2a683b011ea..92761368d53 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -67,6 +67,8 @@ const SHOWN_DURATION_OPTIONS: Duration[] = [ new Duration('P10Y'), ]; +const SHOW_ALL = 'Show all'; + function getDateRange(shownDuration: Duration): NonNullDateRange { const end = day.ceil(new Date(), TZ_UTC); return [shownDuration.shift(end, TZ_UTC, -1), end]; @@ -101,13 +103,12 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr }); const DatasourceSelect: React.FC = () => { - const showAll = 'Show all'; - const datasourcesWzAll = [showAll].concat(datasourcesState.data || []); + const datasourcesWzAll = [SHOW_ALL].concat(datasourcesState.data || []); return ( <Select<string> items={datasourcesWzAll} onItemSelect={(selectedItem: string) => { - setFocusDatasource(selectedItem === showAll ? undefined : selectedItem); + setFocusDatasource(selectedItem === SHOW_ALL ? undefined : selectedItem); }} itemRenderer={(val, { handleClick, handleFocus, modifiers }) => { if (!modifiers.matchesPredicate) return null; @@ -135,7 +136,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr } }} > - <Button text={focusDatasource ?? showAll} rightIcon={IconNames.CARET_DOWN} /> + <Button text={focusDatasource ?? SHOW_ALL} rightIcon={IconNames.CARET_DOWN} /> </Select> ); }; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
