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 dc1a507081bfb98c7fd78bbb0c0697ccf96d4902 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Tue Nov 5 13:05:24 2024 -0800 fixes --- .../src/components/segment-timeline/bubble.scss | 5 ++ .../src/components/segment-timeline/common.ts | 1 + .../segment-timeline/segment-bar-chart-render.scss | 15 ++++ .../segment-timeline/segment-bar-chart-render.tsx | 84 ++++++++++++------- .../segment-timeline/segment-bar-chart.tsx | 42 ++++++---- .../segment-timeline/segment-timeline.tsx | 96 ++++++++++++---------- 6 files changed, 151 insertions(+), 92 deletions(-) diff --git a/web-console/src/components/segment-timeline/bubble.scss b/web-console/src/components/segment-timeline/bubble.scss index 5dcf9986551..5961ae4fbe4 100644 --- a/web-console/src/components/segment-timeline/bubble.scss +++ b/web-console/src/components/segment-timeline/bubble.scss @@ -22,6 +22,11 @@ position: absolute; @include card-like; padding: 5px; + white-space: nowrap; + + .#{$bp-ns}-dark & { + background: $dark-gray1; + } &.up { transform: translate(-50%, -100%); diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts index c808161ce17..b4cea01062b 100644 --- a/web-console/src/components/segment-timeline/common.ts +++ b/web-console/src/components/segment-timeline/common.ts @@ -69,6 +69,7 @@ export interface IntervalRow extends Record<IntervalStat, number> { start: Date; end: Date; datasource: string; + realtime: boolean; originalTimeSpan: Duration; } 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 e6badd45a7c..2e4626d5f02 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 @@ -22,6 +22,15 @@ position: relative; overflow: hidden; + @keyframes pulseOpacity { + 0% { + opacity: 0.8; + } + 100% { + opacity: 0.95; + } + } + svg { position: absolute; @@ -79,6 +88,12 @@ stroke-dasharray: 2, 2; opacity: 0.7; } + + .bar-unit { + &.realtime { + animation: pulseOpacity 3s alternate infinite; + } + } } .empty-placeholder { 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 8b29ed428dd..fc5cfd9ccd6 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 @@ -28,11 +28,9 @@ import { useMemo, useRef, useState } from 'react'; import { getDatasourceColor } from '../../druid-models'; import { useClock, useGlobalEventListener } from '../../hooks'; import { - capitalizeFirst, clamp, day, Duration, - filterMap, formatBytes, formatNumber, groupBy, @@ -47,7 +45,7 @@ import type { Margin, Stage } from '../../utils/stage'; import { Bubble } from './bubble'; import { ChartAxis } from './chart-axis'; import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common'; -import { aggregateSegmentStats, formatIntervalStat, INTERVAL_STATS } from './common'; +import { aggregateSegmentStats, formatIntervalStat } from './common'; import './segment-bar-chart-render.scss'; @@ -62,7 +60,7 @@ const POSSIBLE_GRANULARITIES = [ new Duration('P1Y'), ]; -const EXTEND_X_SCALE_DOMAIN_BY = 4; +const EXTEND_X_SCALE_DOMAIN_BY = 1; function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange { return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)]; @@ -84,7 +82,10 @@ function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalB b.originalTimeSpan.getCanonicalLength() - a.originalTimeSpan.getCanonicalLength(); if (timeSpanDiff) return timeSpanDiff; - return datasourceToTotalSize[b.datasource] - datasourceToTotalSize[a.datasource]; + const totalSizeDiff = datasourceToTotalSize[b.datasource] - datasourceToTotalSize[a.datasource]; + if (totalSizeDiff) return totalSizeDiff; + + return Number(a.realtime) - Number(b.realtime); }); const intervalTree = new IntervalTree(); @@ -109,8 +110,8 @@ interface SegmentBarChartRenderProps { changeDateRange(dateRange: NonNullDateRange): void; shownIntervalStat: IntervalStat; intervalRows: IntervalRow[]; - focusDatasource: string | undefined; - changeFocusDatasource(datasource: string | undefined): void; + shownDatasource: string | undefined; + changeShownDatasource(datasource: string | undefined): void; } export const SegmentBarChartRender = function SegmentBarChartRender( @@ -122,8 +123,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender( dateRange, changeDateRange, intervalRows, - focusDatasource, - changeFocusDatasource, + shownDatasource, + changeShownDatasource, } = props; const [hoveredIntervalBar, setHoveredIntervalBar] = useState<IntervalBar>(); const [mouseDownAt, setMouseDownAt] = useState< @@ -146,21 +147,40 @@ export const SegmentBarChartRender = function SegmentBarChartRender( }, [dateRange, stage.width]); const intervalBars = useMemo(() => { + const shownIntervalRows = shownDatasource + ? intervalRows.filter(({ datasource }) => datasource === shownDatasource) + : intervalRows; + + const averageRowSizeByDatasource = groupByAsMap( + shownIntervalRows.filter(intervalRow => intervalRow.size > 0 && intervalRow.rows > 0), + intervalRow => intervalRow.datasource, + intervalRows => sum(intervalRows, d => d.size) / sum(intervalRows, d => d.rows), + ); + const trimDuration = new Duration(trimGranularity); - const trimmedIntervalRows = filterMap(intervalRows, intervalRow => { - if (focusDatasource && intervalRow.datasource !== focusDatasource) return; - const start = trimDuration.floor(intervalRow.start, TZ_UTC); - const end = trimDuration.ceil(intervalRow.end, TZ_UTC); - const shownDays = (end.valueOf() - start.valueOf()) / day.canonicalLength; + const trimmedIntervalRows = shownIntervalRows.map(intervalRow => { + const { start, end, segments, size, rows } = intervalRow; + const startTrimmed = trimDuration.floor(start, TZ_UTC); + let endTrimmed = trimDuration.ceil(end, TZ_UTC); + + // Special handling to catch WEEK intervals when trimming to month. + if (trimGranularity === 'P1M' && intervalRow.originalTimeSpan.toString() === 'P7D') { + endTrimmed = trimDuration.shift(startTrimmed, TZ_UTC); + } + + const shownDays = (endTrimmed.valueOf() - startTrimmed.valueOf()) / day.canonicalLength; + const shownSize = + size === 0 ? rows * averageRowSizeByDatasource[intervalRow.datasource] : size; return { ...intervalRow, - start, - end, + start: startTrimmed, + end: endTrimmed, shownDays, + size: shownSize, normalized: { - segments: intervalRow.segments / shownDays, - size: intervalRow.size / shownDays, - rows: intervalRow.rows / shownDays, + size: shownSize / shownDays, + rows: rows / shownDays, + segments: segments / shownDays, }, }; }); @@ -173,6 +193,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( trimmedIntervalRow.end.toISOString(), trimmedIntervalRow.originalTimeSpan, trimmedIntervalRow.datasource, + trimmedIntervalRow.realtime, ].join('/'), (trimmedIntervalRows): TrimmedIntervalRow => { const firstIntervalRow = trimmedIntervalRows[0]; @@ -185,7 +206,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( ); return stackIntervalRows(fullyGroupedSegmentRows); - }, [intervalRows, trimGranularity, focusDatasource]); + }, [intervalRows, trimGranularity, shownDatasource]); const innerStage = stage.applyMargin(CHART_MARGIN); @@ -325,7 +346,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( return { ...startEndToXWidth(intervalBar), y: y, - height: Math.abs(y0 - y), + height: y0 - y, }; } @@ -345,21 +366,22 @@ export const SegmentBarChartRender = function SegmentBarChartRender( text: ( <> <div>Datasource: {hoveredIntervalBar.datasource}</div> - <div>{`Time: ${prettyFormatIsoDate(hoveredIntervalBar.start)}/${ + <div>{`${prettyFormatIsoDate(hoveredIntervalBar.start)}/${ hoveredIntervalBar.originalTimeSpan + }${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div> + <div>{`Size: ${ + hoveredIntervalBar.realtime + ? 'estimated for realtime' + : formatIntervalStat('size', hoveredIntervalBar.size) }`}</div> - {INTERVAL_STATS.map(stat => ( - <div key={stat}> - {`${capitalizeFirst(stat)}: ${formatIntervalStat(stat, hoveredIntervalBar[stat])}`} - </div> - ))} + <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div> + <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div> </> ), }; } const nowX = timeScale(now); - return ( <div className="segment-bar-chart-render"> <svg @@ -419,10 +441,10 @@ export const SegmentBarChartRender = function SegmentBarChartRender( return ( <rect key={i} - className="bar-unit" + className={classNames('bar-unit', { realtime: intervalBar.realtime })} {...segmentBarToRect(intervalBar)} style={{ fill: getDatasourceColor(intervalBar.datasource) }} - onClick={() => changeFocusDatasource(intervalBar.datasource)} + onClick={() => changeShownDatasource(intervalBar.datasource)} onMouseOver={() => { if (mouseDownAt) return; setHoveredIntervalBar(intervalBar); @@ -436,7 +458,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( {...segmentBarToRect(hoveredIntervalBar)} onClick={() => { setHoveredIntervalBar(undefined); - changeFocusDatasource(hoveredIntervalBar.datasource); + changeShownDatasource(hoveredIntervalBar.datasource); }} /> )} 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 ba73c8422c6..b745014d0a2 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -28,7 +28,7 @@ import { Duration, filterMap, getApiArray, queryDruidSql, TZ_UTC } from '../../u import type { Stage } from '../../utils/stage'; import { Loader } from '../loader/loader'; -import type { IntervalStat } from './common'; +import type { IntervalRow, IntervalStat } from './common'; import { SegmentBarChartRender } from './segment-bar-chart-render'; import './segment-bar-chart.scss'; @@ -39,8 +39,8 @@ interface SegmentBarChartProps { dateRange: NonNullDateRange; changeDateRange(newDateRange: NonNullDateRange): void; shownIntervalStat: IntervalStat; - focusDatasource: string | undefined; - changeFocusDatasource: (datasource: string | undefined) => void; + shownDatasource: string | undefined; + changeShownDatasource(datasource: string | undefined): void; } export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) { @@ -50,18 +50,18 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr changeDateRange, stage, shownIntervalStat, - focusDatasource, - changeFocusDatasource, + shownDatasource, + changeShownDatasource, } = props; const intervalsQuery = useMemo( - () => ({ capabilities, dateRange, focusDatasource }), - [capabilities, dateRange, focusDatasource], + () => ({ capabilities, dateRange, shownDatasource: shownDatasource }), + [capabilities, dateRange, shownDatasource], ); const [intervalRowsState] = useQueryManager({ query: intervalsQuery, - processQuery: async ({ capabilities, dateRange, focusDatasource }, cancelToken) => { + processQuery: async ({ capabilities, dateRange, shownDatasource }, cancelToken) => { if (capabilities.hasSql()) { const query = SqlQuery.from(N('sys').table('segments')) .changeWhereExpression( @@ -69,19 +69,23 @@ 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_published').equal(1), C('is_overshadowed').equal(0), - focusDatasource ? C('datasource').equal(L(focusDatasource)) : undefined, + shownDatasource ? C('datasource').equal(L(shownDatasource)) : undefined, ), ) .addSelect(C('start'), { addToGroupBy: 'end' }) .addSelect(C('end'), { addToGroupBy: 'end' }) .addSelect(C('datasource'), { addToGroupBy: 'end' }) + .addSelect(C('is_realtime').as('realtime'), { addToGroupBy: 'end' }) .addSelect(F.count().as('segments')) .addSelect(F.sum(C('size')).as('size')) - .addSelect(F.sum(C('num_rows')).as('rows')); + .addSelect(F.sum(C('num_rows')).as('rows')) + .toString(); - return (await queryDruidSql({ query: query.toString() }, cancelToken)).map(sr => { + console.log(query); + + return (await queryDruidSql({ query }, cancelToken)).map(sr => { const start = new Date(sr.start); const end = new Date(sr.end); @@ -89,14 +93,15 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr ...sr, start, end, + realtime: Boolean(sr.realtime), originalTimeSpan: Duration.fromRange(start, end, TZ_UTC), - }; + } as IntervalRow; }); // 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 { return filterMap( await getApiArray( `/druid/coordinator/v1/metadata/segments?${ - focusDatasource ? `datasources=${Api.encodePath(focusDatasource)}` : '' + shownDatasource ? `datasources=${Api.encodePath(shownDatasource)}` : '' }`, cancelToken, ), @@ -111,11 +116,12 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr start, end, datasource: segment.dataSource, + realtime: false, // ToDo: fill me 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 :-( - }; + rows: segment.num_rows || 0, // segment.num_rows is really null on this API :-( + } as IntervalRow; }, ); } @@ -144,8 +150,8 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr changeDateRange={changeDateRange} shownIntervalStat={shownIntervalStat} intervalRows={intervalRows} - focusDatasource={focusDatasource} - changeFocusDatasource={changeFocusDatasource} + shownDatasource={shownDatasource} + changeShownDatasource={changeShownDatasource} /> ); }; diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 81b26c04d8b..dd79f07f0dd 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -16,7 +16,15 @@ * limitations under the License. */ -import { Button, ButtonGroup, Menu, MenuItem, Popover, ResizeSensor } from '@blueprintjs/core'; +import { + Button, + ButtonGroup, + Menu, + MenuItem, + Popover, + Position, + ResizeSensor, +} from '@blueprintjs/core'; import type { NonNullDateRange } from '@blueprintjs/datetime'; import { DateRangePicker3 } from '@blueprintjs/datetime2'; import { IconNames } from '@blueprintjs/icons'; @@ -60,8 +68,6 @@ 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]; @@ -71,7 +77,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr const { capabilities } = props; const [stage, setStage] = useState<Stage | undefined>(); const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size'); - const [focusDatasource, setFocusDatasource] = useState<string | undefined>(); + const [shownDatasource, setShownDatasource] = useState<string | undefined>(); const [dateRange, setDateRange] = useState<NonNullDateRange>( getDateRange(DEFAULT_SHOWN_DURATION), ); @@ -163,6 +169,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr </Popover> </ButtonGroup> <Popover + position={Position.BOTTOM_LEFT} content={ <Menu> {INTERVAL_STATS.map(stat => ( @@ -182,43 +189,46 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr rightIcon={IconNames.CARET_DOWN} /> </Popover> - <Select<string> - items={[SHOW_ALL].concat(datasourcesState.data || [])} - onItemSelect={(selectedItem: string) => { - setFocusDatasource(selectedItem === SHOW_ALL ? undefined : selectedItem); - }} - itemRenderer={(val, { handleClick, handleFocus, modifiers }) => { - if (!modifiers.matchesPredicate) return null; - return ( - <MenuItem - key={val} - disabled={modifiers.disabled} - active={modifiers.active} - onClick={handleClick} - onFocus={handleFocus} - roleStructure="listoption" - text={val} - /> - ); - }} - noResults={<MenuItem disabled text="No results" roleStructure="listoption" />} - itemPredicate={(query, val, _index, exactMatch) => { - const normalizedTitle = val.toLowerCase(); - const normalizedQuery = query.toLowerCase(); + <ButtonGroup> + <Select<string> + items={datasourcesState.data || []} + onItemSelect={setShownDatasource} + itemRenderer={(val, { handleClick, handleFocus, modifiers }) => { + if (!modifiers.matchesPredicate) return null; + return ( + <MenuItem + key={val} + disabled={modifiers.disabled} + active={modifiers.active} + onClick={handleClick} + onFocus={handleFocus} + roleStructure="listoption" + text={val} + /> + ); + }} + noResults={<MenuItem disabled text="No results" roleStructure="listoption" />} + itemPredicate={(query, val, _index, exactMatch) => { + const normalizedTitle = val.toLowerCase(); + const normalizedQuery = query.toLowerCase(); - if (exactMatch) { - return normalizedTitle === normalizedQuery; - } else { - return normalizedTitle.includes(normalizedQuery); - } - }} - > - <Button - text={`Datasource: ${focusDatasource ?? 'all'}`} - small - rightIcon={IconNames.CARET_DOWN} - /> - </Select> + if (exactMatch) { + return normalizedTitle === normalizedQuery; + } else { + return normalizedTitle.includes(normalizedQuery); + } + }} + > + <Button + text={`Datasource: ${shownDatasource ?? 'all'}`} + small + rightIcon={IconNames.CARET_DOWN} + /> + </Select> + {shownDatasource && ( + <Button icon={IconNames.CROSS} small onClick={() => setShownDatasource(undefined)} /> + )} + </ButtonGroup> </div> <ResizeSensor onResize={(entries: ResizeObserverEntry[]) => { @@ -234,9 +244,9 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr dateRange={dateRange} changeDateRange={setDateRange} shownIntervalStat={activeSegmentStat} - focusDatasource={focusDatasource} - changeFocusDatasource={(datasource: string | undefined) => - setFocusDatasource(focusDatasource ? undefined : datasource) + shownDatasource={shownDatasource} + changeShownDatasource={(datasource: string | undefined) => + setShownDatasource(shownDatasource ? undefined : datasource) } /> )} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
