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 bc202c9e4923050b60f19a5ff58a5038a51389b9 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Mon Nov 18 12:49:16 2024 -0800 fixes --- .../components/segment-timeline/portal-bubble.scss | 27 ++++++++- .../components/segment-timeline/portal-bubble.tsx | 27 +++++++-- .../segment-timeline/segment-bar-chart-render.scss | 10 +++- .../segment-timeline/segment-bar-chart-render.tsx | 69 ++++++++++++++++------ .../segment-timeline/segment-bar-chart.tsx | 31 +++------- .../segment-timeline/segment-timeline.tsx | 10 ++-- 6 files changed, 119 insertions(+), 55 deletions(-) diff --git a/web-console/src/components/segment-timeline/portal-bubble.scss b/web-console/src/components/segment-timeline/portal-bubble.scss index 38663be81eb..8fc2451bdb5 100644 --- a/web-console/src/components/segment-timeline/portal-bubble.scss +++ b/web-console/src/components/segment-timeline/portal-bubble.scss @@ -21,8 +21,6 @@ .portal-bubble { position: absolute; @include card-like; - padding: 5px; - white-space: nowrap; .#{$bp-ns}-dark & { background: $dark-gray1; @@ -39,4 +37,29 @@ &.mute { pointer-events: none; } + + & > .bubble-title-bar { + position: relative; + padding: 5px 5px 0 5px; + font-weight: bold; + + &.with-close { + padding-right: 26px; + + .close-button { + position: absolute; + top: 0; + right: 0; + } + } + } + + & > .bubble-content { + padding: 5px; + white-space: nowrap; + } + + .bubble-title-bar + .bubble-content { + padding-top: 0; + } } diff --git a/web-console/src/components/segment-timeline/portal-bubble.tsx b/web-console/src/components/segment-timeline/portal-bubble.tsx index e35c0246919..f12f6150404 100644 --- a/web-console/src/components/segment-timeline/portal-bubble.tsx +++ b/web-console/src/components/segment-timeline/portal-bubble.tsx @@ -16,6 +16,8 @@ * limitations under the License. */ +import { Button } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; import classNames from 'classnames'; import type { ReactNode } from 'react'; import { useRef } from 'react'; @@ -27,13 +29,14 @@ import './portal-bubble.scss'; interface PortalBubbleProps { className?: string; - openOn: { x: number; y: number; text: ReactNode } | undefined; + openOn: { x: number; y: number; title?: string; text: ReactNode } | undefined; direction?: 'up' | 'down'; + onClose?(): void; mute?: boolean; } export const PortalBubble = function PortalBubble(props: PortalBubbleProps) { - const { className, openOn, direction = 'up', mute } = props; + const { className, openOn, direction = 'up', onClose, mute } = props; const ref = useRef<HTMLDivElement | null>(null); if (!openOn) return null; @@ -44,11 +47,27 @@ export const PortalBubble = function PortalBubble(props: PortalBubbleProps) { return createPortal( <div - className={classNames('portal-bubble', className, direction, { mute })} + className={classNames('portal-bubble', className, direction, { + mute: mute && !onClose, + })} ref={ref} style={{ left: x, top: openOn.y }} > - {openOn.text} + {(openOn.title || onClose) && ( + <div className={classNames('bubble-title-bar', { 'with-close': Boolean(onClose) })}> + {openOn.title} + {onClose && ( + <Button + className="close-button" + icon={IconNames.CROSS} + small + minimal + onClick={onClose} + /> + )} + </div> + )} + <div className="bubble-content">{openOn.text}</div> </div>, document.body, ); 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 53d5f93d941..ec9f2d77446 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 @@ -89,7 +89,7 @@ } .now-line { - stroke: orange; + stroke: $orange4; stroke-dasharray: 2, 2; opacity: 0.7; } @@ -153,3 +153,11 @@ pointer-events: none; } } + +.segment-bar-chart-bubble { + .button-bar { + padding-top: 5px; + display: flex; + gap: 5px; + } +} 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 ac4ce6d8ed7..df4842988c3 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,8 +16,9 @@ * limitations under the License. */ -import { Button } from '@blueprintjs/core'; +import { Button, Intent } from '@blueprintjs/core'; import type { NonNullDateRange } from '@blueprintjs/datetime'; +import { IconNames } from '@blueprintjs/icons'; import IntervalTree from '@flatten-js/interval-tree'; import classNames from 'classnames'; import { max, sort, sum } from 'd3-array'; @@ -205,12 +206,13 @@ export interface DatasourceRules { } export interface SegmentBarChartRenderProps { + intervalRows: IntervalRow[]; + datasourceRules: DatasourceRules | undefined; + stage: Stage; dateRange: NonNullDateRange; changeDateRange(dateRange: NonNullDateRange): void; shownIntervalStat: IntervalStat; - intervalRows: IntervalRow[]; - datasourceRules: DatasourceRules | undefined; shownDatasource: string | undefined; changeShownDatasource(datasource: string | undefined): void; } @@ -497,25 +499,27 @@ export const SegmentBarChartRender = function SegmentBarChartRender( console.log('Bar chart render'); - let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined; + let hoveredOpenOn: { x: number; y: number; title?: string; text: ReactNode } | undefined; if (svgRef.current) { const rect = svgRef.current.getBoundingClientRect(); if (bubbleInfo) { const hoveredIntervalBars = bubbleInfo.intervalBars; + let title: string | undefined; let text: ReactNode; if (hoveredIntervalBars.length === 0) { - text = bubbleInfo.timeLabel; + title = bubbleInfo.timeLabel; + text = ''; } else if (hoveredIntervalBars.length === 1) { const hoveredIntervalBar = hoveredIntervalBars[0]; + title = `${formatStartDuration( + hoveredIntervalBar.start, + hoveredIntervalBar.originalTimeSpan, + )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`; text = ( <> - <div>{`${formatStartDuration( - hoveredIntervalBar.start, - hoveredIntervalBar.originalTimeSpan, - )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div> - <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div> + {!shownDatasource && <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>} <div>{`Size: ${ hoveredIntervalBar.realtime ? 'estimated for realtime' @@ -528,10 +532,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender( } else { const datasources = uniq(hoveredIntervalBars.map(b => b.datasource)); const agg = aggregateSegmentStats(hoveredIntervalBars); + title = bubbleInfo.timeLabel; text = ( <> - <div>{bubbleInfo.timeLabel}</div> - <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div> + {!shownDatasource && ( + <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> @@ -545,31 +551,50 @@ export const SegmentBarChartRender = function SegmentBarChartRender( CHART_MARGIN.left + timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)), y: rect.y + CHART_MARGIN.top - 10, + title, text, }; } else if (selection) { + const selectedBars = intervalTree.search([ + selection.start.valueOf() + 1, + selection.end.valueOf() - 1, + ]) as IntervalBar[]; + const datasources = uniq(selectedBars.map(b => b.datasource)); + const agg = aggregateSegmentStats(selectedBars); hoveredOpenOn = { x: rect.x + CHART_MARGIN.left + timeScale(new Date((selection.start.valueOf() + selection.end.valueOf()) / 2)), y: rect.y + CHART_MARGIN.top - 10, + title: `${formatIsoDateOnly(selection.start)} → ${formatIsoDateOnly(selection.end)}`, text: ( <> - <div>{`${formatIsoDateOnly(selection.start)} → ${formatIsoDateOnly( - selection.end, - )}`}</div> + {!shownDatasource && ( + <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> {selection.done && ( - <div> + <div className="button-bar"> <Button + icon={IconNames.ZOOM_IN} text="Zoom in" + intent={Intent.PRIMARY} + small onClick={() => { if (!selection) return; setSelection(undefined); changeDateRange([selection.start, selection.end]); }} /> - <Button text="Cancel" onClick={() => setSelection(undefined)} /> + <Button + text="Open in segments view" + small + rightIcon={IconNames.ARROW_TOP_RIGHT} + onClick={() => null} + /> </div> )} </> @@ -660,7 +685,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( /> ); })} - {bubbleInfo && + {bubbleInfo?.intervalBars.length === 1 && bubbleInfo.intervalBars.map((intervalBar, i) => ( <rect key={i} className="hovered-bar" {...segmentBarToRect(intervalBar)} /> ))} @@ -699,7 +724,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender( <div className="no-data-text">There are no segments in the selected range</div> </div> )} - <PortalBubble openOn={hoveredOpenOn} mute={!selection?.done} direction="up" /> + <PortalBubble + className="segment-bar-chart-bubble" + openOn={hoveredOpenOn} + onClose={selection?.done ? () => setSelection(undefined) : undefined} + mute + 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 5a8c162f0c2..20b1dec7045 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -16,7 +16,6 @@ * limitations under the License. */ -import type { NonNullDateRange } from '@blueprintjs/datetime'; import { C, F, L, N, sql, SqlExpression, SqlQuery } from '@druid-toolkit/query'; import { useMemo } from 'react'; @@ -25,34 +24,21 @@ import type { Capabilities } from '../../helpers'; import { useQueryManager } from '../../hooks'; import { Api } from '../../singletons'; import { Duration, filterMap, getApiArray, queryDruidSql, TZ_UTC } from '../../utils'; -import type { Stage } from '../../utils/stage'; import { Loader } from '../loader/loader'; -import type { IntervalRow, IntervalStat } from './common'; +import type { IntervalRow } from './common'; +import type { SegmentBarChartRenderProps } from './segment-bar-chart-render'; import { SegmentBarChartRender } from './segment-bar-chart-render'; import './segment-bar-chart.scss'; -interface SegmentBarChartProps { +interface SegmentBarChartProps + extends Omit<SegmentBarChartRenderProps, 'intervalRows' | 'datasourceRules'> { capabilities: Capabilities; - stage: Stage; - dateRange: NonNullDateRange; - changeDateRange(newDateRange: NonNullDateRange): void; - shownIntervalStat: IntervalStat; - shownDatasource: string | undefined; - changeShownDatasource(datasource: string | undefined): void; } export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) { - const { - capabilities, - dateRange, - changeDateRange, - stage, - shownIntervalStat, - shownDatasource, - changeShownDatasource, - } = props; + const { capabilities, dateRange, shownDatasource, ...otherProps } = props; const intervalsQuery = useMemo( () => ({ capabilities, dateRange, shownDatasource: shownDatasource }), @@ -158,14 +144,11 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr <> {intervalRows && ( <SegmentBarChartRender - stage={stage} - dateRange={dateRange} - changeDateRange={changeDateRange} - shownIntervalStat={shownIntervalStat} intervalRows={intervalRows} datasourceRules={datasourceRules} + dateRange={dateRange} shownDatasource={shownDatasource} - changeShownDatasource={changeShownDatasource} + {...otherProps} /> )} {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 ab06abf52c3..72be20a3d83 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -56,11 +56,6 @@ import { SegmentBarChart } from './segment-bar-chart'; import './segment-timeline.scss'; -interface SegmentTimelineProps { - capabilities: Capabilities; - datasource: string | undefined; -} - const DEFAULT_SHOWN_DURATION = new Duration('P1Y'); const SHOWN_DURATION_OPTIONS: Duration[] = [ new Duration('P1D'), @@ -85,6 +80,11 @@ function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean return dr1[0].valueOf() === dr2[0].valueOf() && dr2[1].valueOf() === dr2[1].valueOf(); } +interface SegmentTimelineProps { + capabilities: Capabilities; + datasource: string | undefined; +} + export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) { const { capabilities, datasource } = props; const [stage, setStage] = useState<Stage | undefined>(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
