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 fb7371370a3987695141608167b35266946aa777 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Tue Oct 22 14:34:57 2024 -0700 init refactor of segment timeline --- web-console/lib/keywords.ts | 8 ++ .../src/components/segment-timeline/bar-group.tsx | 19 ++-- .../components/segment-timeline/bar-unit.spec.tsx | 33 ------- .../src/components/segment-timeline/chart-axis.tsx | 7 +- .../segment-timeline/{bar-unit.tsx => common.ts} | 45 ++++----- .../segment-timeline/segment-timeline.scss | 8 +- .../segment-timeline/segment-timeline.tsx | 83 ++++++---------- .../segment-timeline/stacked-bar-chart.tsx | 104 ++++++++------------- .../views/datasources-view/datasources-view.tsx | 2 +- 9 files changed, 119 insertions(+), 190 deletions(-) diff --git a/web-console/lib/keywords.ts b/web-console/lib/keywords.ts index 06d7ccdcc94..5985ae0a6aa 100644 --- a/web-console/lib/keywords.ts +++ b/web-console/lib/keywords.ts @@ -100,15 +100,23 @@ export const SQL_EXPRESSION_PARTS = [ 'TRAILING', 'EPOCH', 'SECOND', + 'SECONDS', 'MINUTE', + 'MINUTES', 'HOUR', + 'HOURS', 'DAY', + 'DAYS', 'DOW', 'DOY', 'WEEK', + 'WEEKS', 'MONTH', + 'MONTHS', 'QUARTER', + 'QUARTERS', 'YEAR', + 'YEARS', 'TIMESTAMP', 'INTERVAL', 'CSV', diff --git a/web-console/src/components/segment-timeline/bar-group.tsx b/web-console/src/components/segment-timeline/bar-group.tsx index d0cf867e2b2..3c61859115a 100644 --- a/web-console/src/components/segment-timeline/bar-group.tsx +++ b/web-console/src/components/segment-timeline/bar-group.tsx @@ -19,26 +19,18 @@ import type { AxisScale } from 'd3-axis'; import React from 'react'; -import { BarUnit } from './bar-unit'; -import type { BarUnitData, HoveredBarInfo } from './stacked-bar-chart'; +import type { BarUnitData, HoveredBarInfo } from './common'; interface BarGroupProps { dataToRender: BarUnitData[]; changeActiveDatasource: (dataSource: string) => void; - formatTick: (e: number) => string; xScale: AxisScale<Date>; yScale: AxisScale<number>; barWidth: number; - onHoverBar?: (e: any) => void; - offHoverBar?: () => void; - hoverOn?: HoveredBarInfo | null; + onHoverBar: (e: HoveredBarInfo) => void; } export class BarGroup extends React.Component<BarGroupProps> { - shouldComponentUpdate(nextProps: BarGroupProps): boolean { - return nextProps.hoverOn === this.props.hoverOn; - } - render() { const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, barWidth } = this.props; @@ -47,6 +39,8 @@ export class BarGroup extends React.Component<BarGroupProps> { return dataToRender.map((entry: BarUnitData, i: number) => { const y0 = yScale(entry.y0 || 0) || 0; const x = xScale(new Date(entry.x + 'T00:00:00Z')); + if (typeof x === 'undefined') return; + const y = yScale((entry.y0 || 0) + entry.y) || 0; const height = Math.max(y0 - y, 0); const barInfo: HoveredBarInfo = { @@ -59,15 +53,16 @@ export class BarGroup extends React.Component<BarGroupProps> { dailySize: entry.dailySize, }; return ( - <BarUnit + <rect key={i} + className="bar-unit" x={x} y={y} width={barWidth} height={height} style={{ fill: entry.color }} onClick={() => changeActiveDatasource(entry.datasource)} - onHover={() => onHoverBar && onHoverBar(barInfo)} + onMouseOver={() => onHoverBar(barInfo)} /> ); }); diff --git a/web-console/src/components/segment-timeline/bar-unit.spec.tsx b/web-console/src/components/segment-timeline/bar-unit.spec.tsx deleted file mode 100644 index d5926dcf69f..00000000000 --- a/web-console/src/components/segment-timeline/bar-unit.spec.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { render } from '@testing-library/react'; - -import { BarUnit } from './bar-unit'; - -describe('BarUnit', () => { - it('matches snapshot', () => { - const barGroup = ( - <svg> - <BarUnit x={10} y={10} width={10} height={10} /> - </svg> - ); - const { container } = render(barGroup); - expect(container.firstChild).toMatchSnapshot(); - }); -}); diff --git a/web-console/src/components/segment-timeline/chart-axis.tsx b/web-console/src/components/segment-timeline/chart-axis.tsx index bc333d33b77..18dc7d3e076 100644 --- a/web-console/src/components/segment-timeline/chart-axis.tsx +++ b/web-console/src/components/segment-timeline/chart-axis.tsx @@ -16,22 +16,23 @@ * limitations under the License. */ +import type { Axis } from 'd3-axis'; import { select } from 'd3-selection'; import React from 'react'; interface ChartAxisProps { transform?: string; - scale: any; + axis: Axis<any>; className?: string; } export const ChartAxis = React.memo(function ChartAxis(props: ChartAxisProps) { - const { transform, scale, className } = props; + const { transform, axis, className } = props; return ( <g className={`chart-axis ${className}`} transform={transform} - ref={node => select(node).call(scale)} + ref={node => select(node).call(axis as any)} /> ); }); diff --git a/web-console/src/components/segment-timeline/bar-unit.tsx b/web-console/src/components/segment-timeline/common.ts similarity index 63% rename from web-console/src/components/segment-timeline/bar-unit.tsx rename to web-console/src/components/segment-timeline/common.ts index 8591f68cc2e..9add957e2c1 100644 --- a/web-console/src/components/segment-timeline/bar-unit.tsx +++ b/web-console/src/components/segment-timeline/common.ts @@ -16,30 +16,31 @@ * limitations under the License. */ -interface BarChartUnitProps { - x: number | undefined; +export interface BarUnitData { + x: number; y: number; + y0?: number; width: number; - height: number; - style?: any; - onClick?: () => void; - onHover?: () => void; - offHover?: () => void; + datasource: string; + color: string; + dailySize: number; } -export function BarUnit(props: BarChartUnitProps) { - const { x, y, width, height, style, onClick, onHover, offHover } = props; - return ( - <rect - className="bar-unit" - x={x} - y={y} - width={width} - height={height} - style={style} - onClick={onClick} - onMouseOver={onHover} - onMouseLeave={offHover} - /> - ); +export interface Margin { + top: number; + right: number; + bottom: number; + left: number; } + +export interface HoveredBarInfo { + xCoordinate: number; + yCoordinate: number; + height: number; + datasource: string; + xValue: number; + yValue: number; + dailySize: number; +} + +export type SegmentStat = 'sizeData' | 'countData'; diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss index aa437052d61..24ee569ad3a 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.scss +++ b/web-console/src/components/segment-timeline/segment-timeline.scss @@ -16,9 +16,12 @@ * limitations under the License. */ +@import '../../variables'; + .segment-timeline { display: grid; - grid-template-columns: 1fr 220px; + grid-template-columns: 1fr 240px; + gap: 8px; .loader { width: 85%; @@ -39,6 +42,7 @@ } .side-control { - padding-top: 20px; + @include card-like; + padding: 10px; } } diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 8aee0c66d47..f59a59e2399 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -31,8 +31,6 @@ import type { Capabilities } from '../../helpers'; import { Api } from '../../singletons'; import { ceilToUtcDay, - formatBytes, - formatInteger, isNonNullRange, localToUtcDateRange, queryDruidSql, @@ -42,7 +40,7 @@ import { } from '../../utils'; import { Loader } from '../loader/loader'; -import type { BarUnitData } from './stacked-bar-chart'; +import type { BarUnitData, SegmentStat } from './common'; import { StackedBarChart } from './stacked-bar-chart'; import './segment-timeline.scss'; @@ -51,8 +49,6 @@ interface SegmentTimelineProps { capabilities: Capabilities; } -type ActiveDataType = 'sizeData' | 'countData'; - interface SegmentTimelineState { chartHeight: number; chartWidth: number; @@ -60,8 +56,8 @@ interface SegmentTimelineState { datasources: string[]; stackedData?: Record<string, BarUnitData[]>; singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>; - activeDatasource: string | null; - activeDataType: ActiveDataType; + activeDatasource?: string; + activeSegmentStat: SegmentStat; dataToRender: BarUnitData[]; loading: boolean; error?: Error; @@ -246,7 +242,7 @@ ORDER BY "start" DESC`; any >; - private readonly chartMargin = { top: 40, right: 15, bottom: 20, left: 60 }; + private readonly chartMargin = { top: 40, right: 0, bottom: 20, left: 60 }; constructor(props: SegmentTimelineProps) { super(props); @@ -260,8 +256,7 @@ ORDER BY "start" DESC`; stackedData: {}, singleDatasourceData: {}, dataToRender: [], - activeDatasource: null, - activeDataType: 'sizeData', + activeSegmentStat: 'sizeData', loading: true, xScale: null, yScale: null, @@ -355,10 +350,10 @@ ORDER BY "start" DESC`; } componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void { - const { activeDatasource, activeDataType, singleDatasourceData, stackedData } = this.state; + const { activeDatasource, activeSegmentStat, singleDatasourceData, stackedData } = this.state; if ( prevState.data !== this.state.data || - prevState.activeDataType !== this.state.activeDataType || + prevState.activeSegmentStat !== this.state.activeSegmentStat || prevState.activeDatasource !== this.state.activeDatasource || prevState.chartWidth !== this.state.chartWidth || prevState.chartHeight !== this.state.chartHeight @@ -366,10 +361,10 @@ ORDER BY "start" DESC`; const scales: BarChartScales | undefined = this.calculateScales(); const dataToRender: BarUnitData[] | undefined = activeDatasource ? singleDatasourceData - ? singleDatasourceData[activeDataType][activeDatasource] + ? singleDatasourceData[activeSegmentStat][activeDatasource] : undefined : stackedData - ? stackedData[activeDataType] + ? stackedData[activeSegmentStat] : undefined; if (scales && dataToRender) { @@ -387,31 +382,26 @@ ORDER BY "start" DESC`; chartWidth, chartHeight, data, - activeDataType, + activeSegmentStat, activeDatasource, singleDatasourceData, dateRange, } = this.state; if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) return; - const activeData = data[activeDataType]; + const activeData = data[activeSegmentStat]; - let yDomain: number[] = [ - 0, + let yMax = activeData.length === 0 - ? 0 - : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total, - ]; + ? 100 + : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total; if ( - activeDatasource !== null && - singleDatasourceData![activeDataType][activeDatasource] !== undefined + activeDatasource && + singleDatasourceData![activeSegmentStat][activeDatasource] !== undefined ) { - yDomain = [ - 0, - singleDatasourceData![activeDataType][activeDatasource].reduce((max: any, d: any) => - max.y > d.y ? max : d, - ).y, - ]; + yMax = singleDatasourceData![activeSegmentStat][activeDatasource].reduce((max: any, d: any) => + max.y > d.y ? max : d, + ).y; } const xScale: AxisScale<Date> = scaleUtc() @@ -420,7 +410,7 @@ ORDER BY "start" DESC`; const yScale: AxisScale<number> = scaleLinear() .rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0]) - .domain(yDomain); + .domain([0, yMax]); return { xScale, @@ -428,16 +418,6 @@ ORDER BY "start" DESC`; }; } - private readonly formatTick = (n: number) => { - if (isNaN(n)) return ''; - const { activeDataType } = this.state; - if (activeDataType === 'countData') { - return formatInteger(n); - } else { - return formatBytes(n); - } - }; - private readonly handleResize = (entries: ResizeObserverEntry[]) => { const chartRect = entries[0].contentRect; this.setState({ @@ -452,7 +432,7 @@ ORDER BY "start" DESC`; chartHeight, loading, dataToRender, - activeDataType, + activeSegmentStat, error, xScale, yScale, @@ -485,7 +465,7 @@ ORDER BY "start" DESC`; ); } - if (data![activeDataType].length === 0) { + if (data![activeSegmentStat].length === 0) { return ( <div> <span className="no-data-text">There are no segments for the selected interval</span> @@ -494,8 +474,8 @@ ORDER BY "start" DESC`; } if ( - activeDatasource !== null && - data![activeDataType].every((d: any) => d[activeDatasource] === undefined) + activeDatasource && + data![activeSegmentStat].every((d: any) => d[activeDatasource] === undefined) ) { return ( <div> @@ -519,13 +499,12 @@ ORDER BY "start" DESC`; svgHeight={chartHeight} svgWidth={chartWidth} margin={this.chartMargin} - changeActiveDatasource={(datasource: string | null) => + changeActiveDatasource={(datasource: string | undefined) => this.setState(prevState => ({ - activeDatasource: prevState.activeDatasource ? null : datasource, + activeDatasource: prevState.activeDatasource ? undefined : datasource, })) } - activeDataType={activeDataType} - formatTick={(n: number) => this.formatTick(n)} + shownSegmentStat={activeSegmentStat} xScale={xScale} yScale={yScale} barWidth={barWidth} @@ -536,7 +515,7 @@ ORDER BY "start" DESC`; render() { const { capabilities } = this.props; - const { datasources, activeDataType, activeDatasource, dateRange, selectedDateRange } = + const { datasources, activeSegmentStat, activeDatasource, dateRange, selectedDateRange } = this.state; const filterDatasource: ItemPredicate<string> = (query, val, _index, exactMatch) => { @@ -574,7 +553,7 @@ ORDER BY "start" DESC`; const showAll = 'Show all'; const handleItemSelected = (selectedItem: string) => { this.setState({ - activeDatasource: selectedItem === showAll ? null : selectedItem, + activeDatasource: selectedItem === showAll ? undefined : selectedItem, }); }; const datasourcesWzAll = [showAll].concat(datasources); @@ -602,9 +581,9 @@ ORDER BY "start" DESC`; <div className="side-control"> <FormGroup label="Show"> <SegmentedControl - value={activeDataType} + value={activeSegmentStat} onValueChange={activeDataType => - this.setState({ activeDataType: activeDataType as ActiveDataType }) + this.setState({ activeSegmentStat: activeDataType as SegmentStat }) } options={[ { diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx index 8018aaee5f6..571c50f65a5 100644 --- a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx @@ -20,47 +20,21 @@ import type { AxisScale } from 'd3-axis'; import { axisBottom, axisLeft } from 'd3-axis'; import React, { useState } from 'react'; +import { formatBytes, formatInteger } from '../../utils'; + import { BarGroup } from './bar-group'; import { ChartAxis } from './chart-axis'; +import type { BarUnitData, HoveredBarInfo, Margin, SegmentStat } from './common'; import './stacked-bar-chart.scss'; -export interface BarUnitData { - x: number; - y: number; - y0?: number; - width: number; - datasource: string; - color: string; - dailySize?: number; -} - -export interface BarChartMargin { - top: number; - right: number; - bottom: number; - left: number; -} - -export interface HoveredBarInfo { - xCoordinate?: number; - yCoordinate?: number; - height?: number; - width?: number; - datasource?: string; - xValue?: number; - yValue?: number; - dailySize?: number; -} - interface StackedBarChartProps { svgWidth: number; svgHeight: number; - margin: BarChartMargin; - activeDataType?: string; + margin: Margin; + shownSegmentStat: SegmentStat; dataToRender: BarUnitData[]; - changeActiveDatasource: (e: string | null) => void; - formatTick: (e: number) => string; + changeActiveDatasource: (e: string | undefined) => void; xScale: AxisScale<Date>; yScale: AxisScale<number>; barWidth: number; @@ -71,11 +45,10 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart( ref, ) { const { - activeDataType, + shownSegmentStat, svgWidth, svgHeight, margin, - formatTick, xScale, yScale, dataToRender, @@ -84,11 +57,36 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart( } = props; const [hoverOn, setHoverOn] = useState<HoveredBarInfo>(); + const formatTick = (n: number) => { + if (isNaN(n)) return ''; + if (shownSegmentStat === 'countData') { + return formatInteger(n); + } else { + return formatBytes(n); + } + }; + const width = svgWidth - margin.left - margin.right; const height = svgHeight - margin.top - margin.bottom; - function renderBarChart() { - return ( + return ( + <div className="stacked-bar-chart" ref={ref as any}> + {hoverOn && ( + <div className="bar-chart-tooltip"> + <div>Datasource: {hoverOn.datasource}</div> + <div>Time: {hoverOn.xValue}</div> + <div> + {`${ + shownSegmentStat === 'countData' ? 'Daily total count:' : 'Daily total size:' + } ${formatTick(hoverOn.dailySize)}`} + </div> + <div> + {`${shownSegmentStat === 'countData' ? 'Count:' : 'Size:'} ${formatTick( + hoverOn.yValue, + )}`} + </div> + </div> + )} <svg width={svgWidth} height={svgHeight} @@ -102,7 +100,7 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart( <ChartAxis className="gridline-x" transform="translate(0, 0)" - scale={axisLeft(yScale) + axis={axisLeft(yScale) .ticks(5) .tickSize(-width) .tickFormat(() => '') @@ -111,30 +109,28 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart( <BarGroup dataToRender={dataToRender} changeActiveDatasource={changeActiveDatasource} - formatTick={formatTick} xScale={xScale} yScale={yScale} onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)} - hoverOn={hoverOn} barWidth={barWidth} /> <ChartAxis className="axis-x" transform={`translate(0, ${height})`} - scale={axisBottom(xScale)} + axis={axisBottom(xScale)} /> <ChartAxis className="axis-y" - scale={axisLeft(yScale) + axis={axisLeft(yScale) .ticks(5) - .tickFormat((e: number) => formatTick(e))} + .tickFormat(e => formatTick(e))} /> {hoverOn && ( <g className="hovered-bar" onClick={() => { setHoverOn(undefined); - changeActiveDatasource(hoverOn.datasource ?? null); + changeActiveDatasource(hoverOn.datasource); }} > <rect @@ -147,28 +143,6 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart( )} </g> </svg> - ); - } - - return ( - <div className="stacked-bar-chart" ref={ref as any}> - {hoverOn && ( - <div className="bar-chart-tooltip"> - <div>Datasource: {hoverOn.datasource}</div> - <div>Time: {hoverOn.xValue}</div> - <div> - {`${ - activeDataType === 'countData' ? 'Daily total count:' : 'Daily total size:' - } ${formatTick(hoverOn.dailySize!)}`} - </div> - <div> - {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${formatTick( - hoverOn.yValue!, - )}`} - </div> - </div> - )} - {renderBarChart()} </div> ); }); diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index 3fb6a5be40d..cafc69cade5 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -418,7 +418,7 @@ GROUP BY 1, 2`; LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION, ['Segment size', 'Segment granularity'], ), - showSegmentTimeline: false, + showSegmentTimeline: true, actions: [], }; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
