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 6a1821a571fd37f11a20333be26ace6f2e5ab704 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Wed Oct 30 20:03:28 2024 -0700 no highlight --- .../segment-timeline/segment-bar-chart-render.scss | 34 +++-- .../segment-timeline/segment-bar-chart-render.tsx | 50 ++++--- .../segment-timeline/segment-bar-chart.tsx | 2 +- .../segment-timeline/segment-timeline.scss | 36 ++---- .../segment-timeline/segment-timeline.tsx | 144 +++++++++++++-------- 5 files changed, 158 insertions(+), 108 deletions(-) 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 a30ee800f0a..90a6bcbe8d6 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 @@ -16,21 +16,12 @@ * limitations under the License. */ +@import '../../variables'; + .segment-bar-chart-render { position: relative; overflow: hidden; - .bar-chart-tooltip { - position: absolute; - left: 20px; - right: 0; - - div { - display: inline-block; - width: 230px; - } - } - svg { position: absolute; @@ -78,4 +69,25 @@ } } } + + .bar-chart-tooltip { + position: absolute; + left: 20px; + right: 0; + + div { + display: inline-block; + width: 230px; + } + } + + .empty-placeholder { + @include pin-full; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + user-select: none; + pointer-events: none; + } } 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 8753f8bca64..dd813261125 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 @@ -31,7 +31,6 @@ import { clamp, day, Duration, - formatByteRate, formatBytes, formatInteger, formatNumber, @@ -48,7 +47,7 @@ import { aggregateSegmentStats, normalizeIntervalRow } from './common'; import './segment-bar-chart-render.scss'; -const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 80 }; +const CHART_MARGIN: Margin = { top: 40, right: 10, bottom: 25, left: 80 }; const MIN_BAR_WIDTH = 2; const POSSIBLE_GRANULARITIES = [ new Duration('PT15M'), @@ -210,6 +209,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( }; const formatTickRate = (n: number) => { + n = (n * new Duration(trimGranularity).getCanonicalLength()) / 1000; switch (shownIntervalStat) { case 'segments': return formatNumber(n) + ' seg/s'; @@ -218,13 +218,14 @@ export const SegmentBarChartRender = function SegmentBarChartRender( return formatNumber(n) + ' row/s'; case 'size': - return formatByteRate(n); + return formatBytes(n); } }; function handleMouseDown(e: React.MouseEvent) { 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; @@ -236,6 +237,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender( useGlobalEventListener('mousemove', (e: MouseEvent) => { if (!mouseDownAt) return; + e.preventDefault(); + const svg = svgRef.current; if (!svg) return; const rect = svg.getBoundingClientRect(); @@ -254,6 +257,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender( useGlobalEventListener('mouseup', (e: MouseEvent) => { if (!mouseDownAt) return; + e.preventDefault(); + setMouseDownAt(undefined); if (!shiftOffset && !dragging) return; @@ -286,22 +291,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender( return ( <div className="segment-bar-chart-render"> - {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] * hoverOn.shownSeconds, - )}`} - </div> - </div> - ) : undefined} <svg ref={svgRef} width={stage.width} @@ -318,7 +307,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( className="gridline-x" transform="translate(0,0)" axis={axisLeft(statScale) - .ticks(5) + .ticks(3) .tickSize(-innerStage.width) .tickFormat(() => '') .tickSizeOuter(0)} @@ -396,6 +385,27 @@ 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] * hoverOn.shownSeconds, + )}`} + </div> + </div> + ) : undefined} + {!intervalRows.length && ( + <div className="empty-placeholder"> + <div className="no-data-text">There are no segments in the selected date range</div> + </div> + )} </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 d88a023f883..73c69bb8973 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -125,7 +125,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr if (intervalRowsState.error) { return ( <div className="empty-placeholder"> - <span className="no-data-text">{`Error when loading data: ${intervalRowsState.getErrorMessage()}`}</span> + <span className="error-text">{`Error when loading data: ${intervalRowsState.getErrorMessage()}`}</span> </div> ); } diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss index 583ce290ac9..8295fa3cdfd 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.scss +++ b/web-console/src/components/segment-timeline/segment-timeline.scss @@ -19,6 +19,14 @@ @import '../../variables'; .segment-timeline { + .control-bar { + @include card-like; + height: 40px; + display: flex; + align-items: start; + gap: 10px; + } + .loading-error { position: fixed; top: 50%; @@ -26,34 +34,16 @@ transform: translate(-50%, -50%); } - .side-control { - @include card-like; - height: 100%; - padding: 10px; - } - .chart-container { position: absolute; + top: 40px; width: 100%; - height: 100%; + bottom: 0; overflow: hidden; - } - - .segment-bar-chart, - .segment-bar-chart-render { - position: absolute; - width: 100%; - height: 100%; - } - - .empty-placeholder { - height: 100%; - .no-data-text { - position: absolute; - left: 30vw; - top: 15vh; - font-size: 20px; + .segment-bar-chart, + .segment-bar-chart-render { + @include pin-full; } } } diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 5344916c95e..83208304d69 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -16,26 +16,33 @@ * limitations under the License. */ -import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from '@blueprintjs/core'; +import { + Button, + ButtonGroup, + FormGroup, + MenuItem, + Popover, + ResizeSensor, + SegmentedControl, +} from '@blueprintjs/core'; import type { NonNullDateRange } from '@blueprintjs/datetime'; -import { DateRangeInput3 } from '@blueprintjs/datetime2'; +import { DateRangePicker3 } from '@blueprintjs/datetime2'; import { IconNames } from '@blueprintjs/icons'; import { Select } from '@blueprintjs/select'; -import enUS from 'date-fns/locale/en-US'; import type React from 'react'; import { useState } from 'react'; import type { Capabilities } from '../../helpers'; import { day, + Duration, isNonNullRange, localToUtcDateRange, - month, + prettyFormatIsoDate, TZ_UTC, utcToLocalDateRange, } from '../../utils'; import { Stage } from '../../utils/stage'; -import { SplitterLayout } from '../splitter-layout/splitter-layout'; import type { IntervalStat } from './common'; import { SegmentBarChart } from './segment-bar-chart'; @@ -46,11 +53,20 @@ interface SegmentTimelineProps { capabilities: Capabilities; } -const DEFAULT_TIME_SPAN_MONTHS = 3; +const DEFAULT_SHOWN_DURATION = new Duration('P3M'); +const SHOWN_DURATION_OPTIONS: Duration[] = [ + new Duration('P1D'), + new Duration('P1W'), + new Duration('P1M'), + new Duration('P3M'), + new Duration('P1Y'), + new Duration('P5Y'), + new Duration('P10Y'), +]; -function getDefaultDateRange(): NonNullDateRange { +function getDateRange(shownDuration: Duration): NonNullDateRange { const end = day.ceil(new Date(), TZ_UTC); - return [month.shift(end, TZ_UTC, -DEFAULT_TIME_SPAN_MONTHS), end]; + return [shownDuration.shift(end, TZ_UTC, -1), end]; } export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) { @@ -58,7 +74,10 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr const [stage, setStage] = useState<Stage | undefined>(); const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size'); const [activeDatasource, setActiveDatasource] = useState<string | undefined>(); - const [dateRange, setDateRange] = useState<NonNullDateRange>(getDefaultDateRange); + const [dateRange, setDateRange] = useState<NonNullDateRange>( + getDateRange(DEFAULT_SHOWN_DURATION), + ); + const [showCustomDatePicker, setShowCustomDatePicker] = useState(false); const datasources: string[] = ['wiki', 'kttm']; // ToDo @@ -96,7 +115,6 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr return normalizedTitle.includes(normalizedQuery); } }} - fill > <Button text={activeDatasource === null ? showAll : activeDatasource} @@ -108,39 +126,50 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr }; return ( - <SplitterLayout - className="segment-timeline" - primaryMinSize={400} - secondaryInitialSize={220} - secondaryMaxSize={400} - > - <ResizeSensor - onResize={(entries: ResizeObserverEntry[]) => { - const rect = entries[0].contentRect; - setStage(new Stage(rect.width, rect.height)); - }} - > - <div className="chart-container"> - {stage && ( - <SegmentBarChart - capabilities={capabilities} - stage={stage} - dateRange={dateRange} - changeDateRange={setDateRange} - shownIntervalStat={activeSegmentStat} - changeActiveDatasource={(datasource: string | undefined) => - setActiveDatasource(activeDatasource ? undefined : datasource) - } + <div className="segment-timeline"> + <div className="control-bar"> + <ButtonGroup> + {SHOWN_DURATION_OPTIONS.map((d, i) => ( + <Button + key={i} + text={d.toString().replace('P', '')} + data-tooltip={`Show last ${d.getDescription()}`} + small + onClick={() => setDateRange(getDateRange(d))} /> - )} - </div> - </ResizeSensor> - <div className="side-control"> - <FormGroup label="Show"> + ))} + <Popover + isOpen={showCustomDatePicker} + onInteraction={setShowCustomDatePicker} + content={ + <DateRangePicker3 + defaultValue={utcToLocalDateRange(dateRange)} + onChange={newDateRange => { + const newUtcDateRange = localToUtcDateRange(newDateRange); + if (!isNonNullRange(newUtcDateRange)) return; + setDateRange(newUtcDateRange); + setShowCustomDatePicker(false); + }} + contiguousCalendarMonths={false} + reverseMonthAndYearMenus + timePickerProps={undefined} + shortcuts={false} + /> + } + > + <Button + icon={IconNames.CALENDAR} + data-tooltip={`Select a custom date range\nCurrent range: ${prettyFormatIsoDate( + dateRange[0], + )} - ${prettyFormatIsoDate(dateRange[1])}`} + /> + </Popover> + </ButtonGroup> + <FormGroup label="Show" inline> <SegmentedControl value={activeSegmentStat} onValueChange={s => setActiveSegmentStat(s as IntervalStat)} - fill + small options={[ { label: 'Size', @@ -157,22 +186,31 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr ]} /> </FormGroup> - <FormGroup label="Interval"> - <DateRangeInput3 - value={utcToLocalDateRange(dateRange)} - onChange={newDateRange => { - const newUtcDateRange = localToUtcDateRange(newDateRange); - if (!isNonNullRange(newUtcDateRange)) return; - setDateRange(newUtcDateRange); - }} - fill - locale={enUS} - /> - </FormGroup> - <FormGroup label="Datasource"> + <FormGroup label="Datasource" inline> <DatasourceSelect /> </FormGroup> </div> - </SplitterLayout> + <ResizeSensor + onResize={(entries: ResizeObserverEntry[]) => { + const rect = entries[0].contentRect; + setStage(new Stage(rect.width, rect.height)); + }} + > + <div className="chart-container"> + {stage && ( + <SegmentBarChart + capabilities={capabilities} + stage={stage} + dateRange={dateRange} + changeDateRange={setDateRange} + shownIntervalStat={activeSegmentStat} + changeActiveDatasource={(datasource: string | undefined) => + setActiveDatasource(activeDatasource ? undefined : datasource) + } + /> + )} + </div> + </ResizeSensor> + </div> ); }; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
