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 ad0517a3cb3b01f2febc338882cea3cad85c897d Author: Vadim Ogievetsky <[email protected]> AuthorDate: Fri Nov 15 22:20:31 2024 -0800 progress --- .../src/components/segment-timeline/common.ts | 4 + .../segment-timeline/segment-bar-chart-render.scss | 5 + .../segment-timeline/segment-bar-chart-render.tsx | 193 +++++++++++++-------- .../segment-timeline/segment-bar-chart.tsx | 34 ++-- .../segment-timeline/segment-timeline.tsx | 4 +- 5 files changed, 145 insertions(+), 95 deletions(-) diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts index b4cea01062b..48aa5ce48d4 100644 --- a/web-console/src/components/segment-timeline/common.ts +++ b/web-console/src/components/segment-timeline/common.ts @@ -81,3 +81,7 @@ export interface TrimmedIntervalRow extends IntervalRow { export interface IntervalBar extends TrimmedIntervalRow { offset: Record<IntervalStat, number>; } + +export function formatIsoDateOnly(date: Date): string { + return date.toISOString().slice(0, 10); +} diff --git a/web-console/src/components/segment-timeline/segment-bar-chart-render.scss b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss index 576cf54e146..53d5f93d941 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart-render.scss +++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss @@ -53,6 +53,11 @@ fill: transparent; stroke: #ffffff; stroke-width: 1px; + opacity: 0.8; + + &.done { + opacity: 1; + } } .shifter { 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 bc73d3db7a4..ac4ce6d8ed7 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 @@ -16,6 +16,7 @@ * limitations under the License. */ +import { Button } from '@blueprintjs/core'; import type { NonNullDateRange } from '@blueprintjs/datetime'; import IntervalTree from '@flatten-js/interval-tree'; import classNames from 'classnames'; @@ -46,7 +47,7 @@ import type { Margin, Stage } from '../../utils/stage'; import { ChartAxis } from './chart-axis'; import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common'; -import { aggregateSegmentStats, formatIntervalStat } from './common'; +import { aggregateSegmentStats, formatIntervalStat, formatIsoDateOnly } from './common'; import { PortalBubble } from './portal-bubble'; import './segment-bar-chart-render.scss'; @@ -192,6 +193,12 @@ interface BubbleInfo { intervalBars: IntervalBar[]; } +interface SelectionRange { + start: Date; + end: Date; + done?: boolean; +} + export interface DatasourceRules { loadRules: Rule[]; defaultLoadRules: Rule[]; @@ -224,7 +231,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( const [mouseDownAt, setMouseDownAt] = useState< { time: Date; action: 'select' | 'shift' } | undefined >(); - const [dragging, setDragging] = useState<NonNullDateRange | undefined>(); + const [selection, setSelection] = useState<SelectionRange | undefined>(); const [shiftOffset, setShiftOffset] = useState<number | undefined>(); const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>(); const now = useClock(minute.canonicalLength); @@ -239,10 +246,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender( }, [dateRange, stage.width]); const { intervalBars, intervalTree } = useMemo(() => { - const shownIntervalRows = shownDatasource - ? intervalRows.filter(({ datasource }) => datasource === shownDatasource) - : intervalRows; - + const shownIntervalRows = intervalRows.filter( + ({ start, end, datasource }) => + start <= dateRange[1] && + dateRange[0] < end && + (!shownDatasource || datasource === shownDatasource), + ); const averageRowSizeByDatasource = groupByAsMap( shownIntervalRows.filter(intervalRow => intervalRow.size > 0 && intervalRow.rows > 0), intervalRow => intervalRow.datasource, @@ -298,7 +307,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( ); return stackIntervalRows(fullyGroupedSegmentRows); - }, [intervalRows, trimGranularity, shownDatasource]); + }, [intervalRows, trimGranularity, dateRange, shownDatasource]); const innerStage = stage.applyMargin(CHART_MARGIN); @@ -334,16 +343,21 @@ export const SegmentBarChartRender = function SegmentBarChartRender( const svg = svgRef.current; if (!svg) return; e.preventDefault(); - const rect = svg.getBoundingClientRect(); - const x = e.clientX - rect.x - CHART_MARGIN.left; - const y = e.clientY - rect.y - CHART_MARGIN.top; - const time = baseTimeScale.invert(x); - const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select'; - setBubbleInfo(undefined); - setMouseDownAt({ - time, - action, - }); + + if (selection) { + setSelection(undefined); + } else { + const rect = svg.getBoundingClientRect(); + const x = e.clientX - rect.x - CHART_MARGIN.left; + const y = e.clientY - rect.y - CHART_MARGIN.top; + const time = baseTimeScale.invert(x); + const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select'; + setBubbleInfo(undefined); + setMouseDownAt({ + time, + action, + }); + } } useGlobalEventListener('mousemove', (e: MouseEvent) => { @@ -361,12 +375,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender( setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf()); } else { if (mouseDownAt.time < b) { - setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, TZ_UTC)]); + setSelection({ start: day.floor(mouseDownAt.time, TZ_UTC), end: day.ceil(b, TZ_UTC) }); } else { - setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, TZ_UTC)]); + setSelection({ start: day.floor(b, TZ_UTC), end: day.ceil(mouseDownAt.time, TZ_UTC) }); } } - } else { + } else if (!selection) { if ( 0 <= x && x <= innerStage.width && @@ -421,16 +435,15 @@ export const SegmentBarChartRender = function SegmentBarChartRender( const x = e.clientX - rect.x - CHART_MARGIN.left; const y = e.clientY - rect.y - CHART_MARGIN.top; - if (shiftOffset || dragging) { - setDragging(undefined); + if (shiftOffset || selection) { setShiftOffset(undefined); if (mouseDownAt.action === 'shift' || e.shiftKey) { if (shiftOffset) { changeDateRange(offsetDateRange(dateRange, shiftOffset)); } } else { - if (dragging) { - changeDateRange(dragging); + if (selection) { + setSelection({ ...selection, done: true }); } } } else if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) { @@ -455,7 +468,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( useGlobalEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Escape' && mouseDownAt) { setMouseDownAt(undefined); - setDragging(undefined); + setSelection(undefined); } }); @@ -485,53 +498,84 @@ export const SegmentBarChartRender = function SegmentBarChartRender( console.log('Bar chart render'); let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined; - if (bubbleInfo && svgRef.current) { - const hoveredIntervalBars = bubbleInfo.intervalBars; - + if (svgRef.current) { const rect = svgRef.current.getBoundingClientRect(); - let text: ReactNode; - if (hoveredIntervalBars.length === 0) { - text = bubbleInfo.timeLabel; - } else if (hoveredIntervalBars.length === 1) { - const hoveredIntervalBar = hoveredIntervalBars[0]; - text = ( - <> - <div>{`${formatStartDuration( - hoveredIntervalBar.start, - hoveredIntervalBar.originalTimeSpan, - )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div> - <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div> - <div>{`Size: ${ - hoveredIntervalBar.realtime - ? 'estimated for realtime' - : formatIntervalStat('size', hoveredIntervalBar.size) - }`}</div> - <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div> - <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div> - </> - ); - } else { - const datasources = uniq(hoveredIntervalBars.map(b => b.datasource)); - const agg = aggregateSegmentStats(hoveredIntervalBars); - text = ( - <> - <div>{bubbleInfo.timeLabel}</div> - <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div> - <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div> - <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div> - <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div> - </> - ); - } - hoveredOpenOn = { - x: - rect.x + - CHART_MARGIN.left + - timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)), - y: rect.y + CHART_MARGIN.top - 10, - text, - }; + if (bubbleInfo) { + const hoveredIntervalBars = bubbleInfo.intervalBars; + + let text: ReactNode; + if (hoveredIntervalBars.length === 0) { + text = bubbleInfo.timeLabel; + } else if (hoveredIntervalBars.length === 1) { + const hoveredIntervalBar = hoveredIntervalBars[0]; + text = ( + <> + <div>{`${formatStartDuration( + hoveredIntervalBar.start, + hoveredIntervalBar.originalTimeSpan, + )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div> + <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div> + <div>{`Size: ${ + hoveredIntervalBar.realtime + ? 'estimated for realtime' + : formatIntervalStat('size', hoveredIntervalBar.size) + }`}</div> + <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div> + <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div> + </> + ); + } else { + const datasources = uniq(hoveredIntervalBars.map(b => b.datasource)); + const agg = aggregateSegmentStats(hoveredIntervalBars); + text = ( + <> + <div>{bubbleInfo.timeLabel}</div> + <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div> + <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div> + <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div> + <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div> + </> + ); + } + + hoveredOpenOn = { + x: + rect.x + + CHART_MARGIN.left + + timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)), + y: rect.y + CHART_MARGIN.top - 10, + text, + }; + } else if (selection) { + hoveredOpenOn = { + x: + rect.x + + CHART_MARGIN.left + + timeScale(new Date((selection.start.valueOf() + selection.end.valueOf()) / 2)), + y: rect.y + CHART_MARGIN.top - 10, + text: ( + <> + <div>{`${formatIsoDateOnly(selection.start)} → ${formatIsoDateOnly( + selection.end, + )}`}</div> + {selection.done && ( + <div> + <Button + text="Zoom in" + onClick={() => { + if (!selection) return; + setSelection(undefined); + changeDateRange([selection.start, selection.end]); + }} + /> + <Button text="Cancel" onClick={() => setSelection(undefined)} /> + </div> + )} + </> + ), + }; + } } function renderLoadRule(loadRule: Rule, i: number, isDefault: boolean) { @@ -620,13 +664,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender( bubbleInfo.intervalBars.map((intervalBar, i) => ( <rect key={i} className="hovered-bar" {...segmentBarToRect(intervalBar)} /> ))} - {dragging && ( + {selection && ( <rect - className="selection" - x={timeScale(dragging[0])} + className={classNames('selection', { done: selection.done })} + {...startEndToXWidth(selection)} y={0} height={innerStage.height} - width={timeScale(dragging[1]) - timeScale(dragging[0])} /> )} {!!shiftOffset && ( @@ -656,7 +699,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( <div className="no-data-text">There are no segments in the selected range</div> </div> )} - <PortalBubble openOn={hoveredOpenOn} mute direction="up" /> + <PortalBubble openOn={hoveredOpenOn} mute={!selection?.done} direction="up" /> </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 5029a1a2d76..5a8c162f0c2 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -69,7 +69,6 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr sql`"start" <= '${dateRange[1].toISOString()}' AND '${dateRange[0].toISOString()}' < "end"`, C('start').unequal(START_OF_TIME_DATE), C('end').unequal(END_OF_TIME_DATE), - // C('is_published').equal(1), C('is_overshadowed').equal(0), shownDatasource ? C('datasource').equal(L(shownDatasource)) : undefined, ), @@ -146,10 +145,6 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr }; }, [allLoadRulesState.data, shownDatasource]); - if (intervalRowsState.loading) { - return <Loader />; - } - if (intervalRowsState.error) { return ( <div className="empty-placeholder"> @@ -158,19 +153,22 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr ); } - const intervalRows = intervalRowsState.data; - if (!intervalRows) return null; - + const intervalRows = intervalRowsState.getSomeData(); return ( - <SegmentBarChartRender - stage={stage} - dateRange={dateRange} - changeDateRange={changeDateRange} - shownIntervalStat={shownIntervalStat} - intervalRows={intervalRows} - datasourceRules={datasourceRules} - shownDatasource={shownDatasource} - changeShownDatasource={changeShownDatasource} - /> + <> + {intervalRows && ( + <SegmentBarChartRender + stage={stage} + dateRange={dateRange} + changeDateRange={changeDateRange} + shownIntervalStat={shownIntervalStat} + intervalRows={intervalRows} + datasourceRules={datasourceRules} + shownDatasource={shownDatasource} + changeShownDatasource={changeShownDatasource} + /> + )} + {intervalRowsState.loading && <Loader />} + </> ); }; diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index ae8cde2a9a7..0d8ee73caca 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -51,7 +51,7 @@ import { Stage } from '../../utils/stage'; import { Loader } from '../loader/loader'; import type { IntervalStat } from './common'; -import { getIntervalStatTitle, INTERVAL_STATS } from './common'; +import { formatIsoDateOnly, getIntervalStatTitle, INTERVAL_STATS } from './common'; import { SegmentBarChart } from './segment-bar-chart'; import './segment-timeline.scss'; @@ -78,7 +78,7 @@ function getDateRange(shownDuration: Duration): NonNullDateRange { } function formatDateRange(dateRange: NonNullDateRange): string { - return `${dateRange[0].toISOString().slice(0, 10)} → ${dateRange[1].toISOString().slice(0, 10)}`; + return `${formatIsoDateOnly(dateRange[0])} → ${formatIsoDateOnly(dateRange[1])}`; } function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
