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 5855a5bb7ff47858e97483fa054c834dae539482 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Mon Nov 18 15:28:05 2024 -0800 fix render spamming --- .../segment-timeline/segment-bar-chart-render.tsx | 58 +++++++++++++++++----- .../segment-timeline/segment-bar-chart.tsx | 2 +- .../segment-timeline/segment-timeline.tsx | 6 ++- .../table-filterable-cell.tsx | 2 +- web-console/src/console-application.tsx | 15 ++++-- .../src/react-table/react-table-utils.spec.ts | 4 ++ web-console/src/react-table/react-table-utils.ts | 43 ++++++++++++---- web-console/src/utils/general.tsx | 4 ++ .../views/datasources-view/datasources-view.tsx | 16 ++++-- .../src/views/segments-view/segments-view.tsx | 27 ++++++++-- 10 files changed, 142 insertions(+), 35 deletions(-) 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 df4842988c3..ea6310cc843 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,6 +31,7 @@ import type { Rule } from '../../druid-models'; import { getDatasourceColor, RuleUtil } from '../../druid-models'; import { useClock, useGlobalEventListener } from '../../hooks'; import { + arraysEqualByElement, clamp, day, Duration, @@ -215,6 +216,7 @@ export interface SegmentBarChartRenderProps { shownIntervalStat: IntervalStat; shownDatasource: string | undefined; changeShownDatasource(datasource: string | undefined): void; + getActionButton?(start: Date, end: Date, datasource?: string): ReactNode; } export const SegmentBarChartRender = function SegmentBarChartRender( @@ -229,13 +231,42 @@ export const SegmentBarChartRender = function SegmentBarChartRender( datasourceRules, shownDatasource, changeShownDatasource, + getActionButton, } = props; const [mouseDownAt, setMouseDownAt] = useState< { time: Date; action: 'select' | 'shift' } | undefined >(); const [selection, setSelection] = useState<SelectionRange | undefined>(); - const [shiftOffset, setShiftOffset] = useState<number | undefined>(); + + function setSelectionIfNeeded(newSelection: SelectionRange) { + if ( + selection && + selection.start.valueOf() === newSelection.start.valueOf() && + selection.end.valueOf() === newSelection.end.valueOf() && + selection.done === newSelection.done + ) { + return; + } + setSelection(newSelection); + } + const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>(); + + function setBubbleInfoIfNeeded(newBubbleInfo: BubbleInfo) { + if ( + bubbleInfo && + bubbleInfo.start.valueOf() === newBubbleInfo.start.valueOf() && + bubbleInfo.end.valueOf() === newBubbleInfo.end.valueOf() && + bubbleInfo.timeLabel === newBubbleInfo.timeLabel && + arraysEqualByElement(bubbleInfo.intervalBars, newBubbleInfo.intervalBars) + ) { + return; + } + setBubbleInfo(newBubbleInfo); + } + + const [shiftOffset, setShiftOffset] = useState<number | undefined>(); + const now = useClock(minute.canonicalLength); const svgRef = useRef<SVGSVGElement | null>(null); @@ -377,9 +408,15 @@ export const SegmentBarChartRender = function SegmentBarChartRender( setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf()); } else { if (mouseDownAt.time < b) { - setSelection({ start: day.floor(mouseDownAt.time, TZ_UTC), end: day.ceil(b, TZ_UTC) }); + setSelectionIfNeeded({ + start: day.floor(mouseDownAt.time, TZ_UTC), + end: day.ceil(b, TZ_UTC), + }); } else { - setSelection({ start: day.floor(b, TZ_UTC), end: day.ceil(mouseDownAt.time, TZ_UTC) }); + setSelectionIfNeeded({ + start: day.floor(b, TZ_UTC), + end: day.ceil(mouseDownAt.time, TZ_UTC), + }); } } } else if (!selection) { @@ -414,7 +451,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( intervalBars = hoverBar ? [hoverBar] : bars; } } - setBubbleInfo({ + setBubbleInfoIfNeeded({ start, end, timeLabel: start.toISOString().slice(0, shifter === day ? 10 : 7), @@ -497,8 +534,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender( }; } - console.log('Bar chart render'); - let hoveredOpenOn: { x: number; y: number; title?: string; text: ReactNode } | undefined; if (svgRef.current) { const rect = svgRef.current.getBoundingClientRect(); @@ -589,12 +624,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender( changeDateRange([selection.start, selection.end]); }} /> - <Button - text="Open in segments view" - small - rightIcon={IconNames.ARROW_TOP_RIGHT} - onClick={() => null} - /> + {getActionButton?.( + selection.start, + selection.end, + datasources.length === 1 ? datasources[0] : undefined, + )} </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 20b1dec7045..9be6aab54fb 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -32,7 +32,7 @@ import { SegmentBarChartRender } from './segment-bar-chart-render'; import './segment-bar-chart.scss'; -interface SegmentBarChartProps +export interface SegmentBarChartProps extends Omit<SegmentBarChartRenderProps, 'intervalRows' | 'datasourceRules'> { capabilities: Capabilities; } diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 72be20a3d83..18bda4f838b 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -52,6 +52,7 @@ import { Loader } from '../loader/loader'; import type { IntervalStat } from './common'; import { formatIsoDateOnly, getIntervalStatTitle, INTERVAL_STATS } from './common'; +import type { SegmentBarChartProps } from './segment-bar-chart'; import { SegmentBarChart } from './segment-bar-chart'; import './segment-timeline.scss'; @@ -80,13 +81,13 @@ function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean return dr1[0].valueOf() === dr2[0].valueOf() && dr2[1].valueOf() === dr2[1].valueOf(); } -interface SegmentTimelineProps { +interface SegmentTimelineProps extends Pick<SegmentBarChartProps, 'getActionButton'> { capabilities: Capabilities; datasource: string | undefined; } export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) { - const { capabilities, datasource } = props; + const { capabilities, datasource, ...otherProps } = props; const [stage, setStage] = useState<Stage | undefined>(); const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size'); const [shownDatasource, setShownDatasource] = useState<string | undefined>(datasource); @@ -351,6 +352,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr shownIntervalStat={activeSegmentStat} shownDatasource={shownDatasource} changeShownDatasource={setShownDatasource} + {...otherProps} /> )} {initDatasourceDateRangeState.isLoading() && <Loader />} diff --git a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx index 631fa224aaf..c03f0038cc7 100644 --- a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx +++ b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx @@ -27,7 +27,7 @@ import { Deferred } from '../deferred/deferred'; import './table-filterable-cell.scss'; -const FILTER_MODES: FilterMode[] = ['=', '!=', '<=', '>=']; +const FILTER_MODES: FilterMode[] = ['=', '!=', '<', '>=']; const FILTER_MODES_NO_COMPARISONS: FilterMode[] = ['=', '!=']; export interface TableFilterableCellProps { diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx index 95336713dbb..acb7d9c81a3 100644 --- a/web-console/src/console-application.tsx +++ b/web-console/src/console-application.tsx @@ -182,12 +182,21 @@ export class ConsoleApplication extends React.PureComponent< changeTabWithFilter('datasources', [{ id: 'datasource', value: `=${datasource}` }]); }; - private readonly goToSegments = (datasource: string, onlyUnavailable = false) => { + private readonly goToSegments = ({ + start, + end, + datasource, + }: { + start?: Date; + end?: Date; + datasource?: string; + }) => { changeTabWithFilter( 'segments', compact([ - { id: 'datasource', value: `=${datasource}` }, - onlyUnavailable ? { id: 'is_available', value: '=false' } : undefined, + start && { id: 'start', value: `>=${start.toISOString()}` }, + end && { id: 'end', value: `<${end.toISOString()}` }, + datasource && { id: 'datasource', value: `=${datasource}` }, ]), ); }; diff --git a/web-console/src/react-table/react-table-utils.spec.ts b/web-console/src/react-table/react-table-utils.spec.ts index 0a1bbf3f9ad..1dd245b49f7 100644 --- a/web-console/src/react-table/react-table-utils.spec.ts +++ b/web-console/src/react-table/react-table-utils.spec.ts @@ -75,5 +75,9 @@ describe('react-table-utils', () => { { id: 'x', value: '~y' }, { id: 'z', value: '=w&' }, ]); + expect(stringToTableFilters('x<3&y<=3')).toEqual([ + { id: 'x', value: '<3' }, + { id: 'y', value: '<=3' }, + ]); }); }); diff --git a/web-console/src/react-table/react-table-utils.ts b/web-console/src/react-table/react-table-utils.ts index dc3debdf32c..ec1272b2282 100644 --- a/web-console/src/react-table/react-table-utils.ts +++ b/web-console/src/react-table/react-table-utils.ts @@ -31,9 +31,9 @@ export const STANDARD_TABLE_PAGE_SIZE_OPTIONS = [50, 100, 200]; export const SMALL_TABLE_PAGE_SIZE = 25; export const SMALL_TABLE_PAGE_SIZE_OPTIONS = [25, 50, 100]; -export type FilterMode = '~' | '=' | '!=' | '<=' | '>='; +export type FilterMode = '~' | '=' | '!=' | '<' | '<=' | '>' | '>='; -export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<=', '>=']; +export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<', '<=', '>', '>=']; export const FILTER_MODES_NO_COMPARISON: FilterMode[] = ['~', '=', '!=']; export function filterModeToIcon(mode: FilterMode): IconName { @@ -44,8 +44,12 @@ export function filterModeToIcon(mode: FilterMode): IconName { return IconNames.EQUALS; case '!=': return IconNames.NOT_EQUAL_TO; + case '<': + return IconNames.LESS_THAN; case '<=': return IconNames.LESS_THAN_OR_EQUAL_TO; + case '>': + return IconNames.GREATER_THAN; case '>=': return IconNames.GREATER_THAN_OR_EQUAL_TO; default: @@ -61,8 +65,12 @@ export function filterModeToTitle(mode: FilterMode): string { return 'Equals'; case '!=': return 'Not equals'; + case '<': + return 'Less than'; case '<=': return 'Less than or equal'; + case '>': + return 'Greater than'; case '>=': return 'Greater than or equal'; default: @@ -88,7 +96,7 @@ export function parseFilterModeAndNeedle( filter: Filter, loose = false, ): FilterModeAndNeedle | undefined { - const m = /^(~|=|!=|<=|>=)?(.*)$/.exec(String(filter.value)); + const m = /^(~|=|!=|<(?!=)|<=|>(?!=)|>=)?(.*)$/.exec(String(filter.value)); if (!m) return; if (!loose && !m[2]) return; const mode = (m[1] as FilterMode) || '~'; @@ -111,21 +119,28 @@ export function booleanCustomTableFilter(filter: Filter, value: unknown): boolea const modeAndNeedle = parseFilterModeAndNeedle(filter); if (!modeAndNeedle) return true; const { mode, needle } = modeAndNeedle; + const strValue = String(value); switch (mode) { case '=': - return String(value) === needle; + return strValue === needle; case '!=': - return String(value) !== needle; + return strValue !== needle; + + case '<': + return strValue < needle; case '<=': - return String(value) <= needle; + return strValue <= needle; + + case '>': + return strValue > needle; case '>=': - return String(value) >= needle; + return strValue >= needle; default: - return caseInsensitiveContains(String(value), needle); + return caseInsensitiveContains(strValue, needle); } } @@ -141,9 +156,15 @@ export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression | undef case '!=': return column.unequal(needle); + case '<': + return column.lessThan(needle); + case '<=': return column.lessThanOrEqual(needle); + case '>': + return column.greaterThan(needle); + case '>=': return column.greaterThanOrEqual(needle); @@ -164,9 +185,11 @@ export function tableFiltersToString(tableFilters: Filter[]): string { export function stringToTableFilters(str: string | undefined): Filter[] { if (!str) return []; - // '~' | '=' | '!=' | '<=' | '>='; + // '~' | '=' | '!=' | '<' | '<=' | '>' | '>='; return filterMap(str.split('&'), clause => { - const m = /^(\w+)((?:~|=|!=|<=|>=).*)$/.exec(clause.replace(/%2[56]/g, decodeURIComponent)); + const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec( + clause.replace(/%2[56]/g, decodeURIComponent), + ); if (!m) return; return { id: m[1], value: m[2] }; }); diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 9e2e5e7f525..89a05467649 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -59,6 +59,10 @@ export function isSimpleArray(a: any): a is (string | number | boolean)[] { ); } +export function arraysEqualByElement<T>(xs: T[], ys: T[]): boolean { + return xs.length === ys.length && xs.every((x, i) => x === ys[i]); +} + export function wait(ms: number): Promise<void> { return new Promise(resolve => { setTimeout(resolve, ms); diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index 9891368e585..064f95e673e 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -16,7 +16,7 @@ * limitations under the License. */ -import { FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core'; +import { Button, FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { SqlQuery, T } from '@druid-toolkit/query'; import { sum } from 'd3-array'; @@ -305,7 +305,7 @@ export interface DatasourcesViewProps { onFiltersChange(filters: Filter[]): void; goToQuery(queryWithContext: QueryWithContext): void; goToTasks(datasource?: string): void; - goToSegments(datasource: string, onlyUnavailable?: boolean): void; + goToSegments(options: { start?: Date; end?: Date; datasource?: string }): void; capabilities: Capabilities; } @@ -1692,7 +1692,7 @@ GROUP BY 1, 2`; } render() { - const { capabilities } = this.props; + const { capabilities, goToSegments } = this.props; const { showUnused, visibleColumns, @@ -1752,6 +1752,16 @@ GROUP BY 1, 2`; <SegmentTimeline capabilities={capabilities} datasource={showSegmentTimeline.datasource} + getActionButton={(start, end, datasource) => { + return ( + <Button + text="Open in segments view" + small + rightIcon={IconNames.ARROW_TOP_RIGHT} + onClick={() => goToSegments({ start, end, datasource })} + /> + ); + }} /> )} {this.renderDatasourcesTable()} diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx index 3233c587a4e..56659415a25 100644 --- a/web-console/src/views/segments-view/segments-view.tsx +++ b/web-console/src/views/segments-view/segments-view.tsx @@ -989,6 +989,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment } render() { + const { capabilities, onFiltersChange } = this.props; const { segmentTableActionDialogId, datasourceTableActionDialogId, @@ -996,9 +997,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment visibleColumns, showSegmentTimeline, showFullShardSpec, + groupByInterval, } = this.state; - const { capabilities } = this.props; - const { groupByInterval } = this.state; return ( <div className="segments-view app-view"> @@ -1060,7 +1060,28 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment secondaryMinSize={10} > {showSegmentTimeline && ( - <SegmentTimeline capabilities={capabilities} datasource={undefined} /> + <SegmentTimeline + capabilities={capabilities} + datasource={undefined} + getActionButton={(start, end, datasource) => { + return ( + <Button + text="Apply fitler to table" + small + rightIcon={IconNames.ARROW_DOWN} + onClick={() => + onFiltersChange( + compact([ + start && { id: 'start', value: `>=${start.toISOString()}` }, + end && { id: 'end', value: `<${end.toISOString()}` }, + datasource && { id: 'datasource', value: `=${datasource}` }, + ]), + ) + } + /> + ); + }} + /> )} {this.renderSegmentsTable()} </SplitterLayout> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
