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 80149630a9e671b1fd315626d204043ddac4924d Author: Vadim Ogievetsky <[email protected]> AuthorDate: Thu Nov 7 16:34:01 2024 -0800 better controlls --- .../segment-timeline/segment-bar-chart-render.scss | 16 ++ .../segment-timeline/segment-bar-chart-render.tsx | 97 +++++++++++- .../segment-timeline/segment-bar-chart.tsx | 23 ++- .../segment-timeline/segment-timeline.scss | 4 + .../segment-timeline/segment-timeline.tsx | 162 +++++++++++---------- .../src/druid-models/load-rule/load-rule.ts | 1 + .../views/datasources-view/datasources-view.tsx | 6 +- 7 files changed, 221 insertions(+), 88 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 2e4626d5f02..7667dc5b1f3 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 @@ -96,6 +96,22 @@ } } + .rule-tape { + position: absolute; + top: 5px; + height: 15px; + font-size: 10px; + + .load-rule { + position: absolute; + overflow: hidden; + border-radius: 3px; + padding-left: 2px; + top: 0; + height: 100%; + } + } + .empty-placeholder { @include pin-full; display: flex; 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 4666656e246..7bdef7e2db2 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 @@ -25,7 +25,8 @@ import { scaleLinear, scaleUtc } from 'd3-scale'; import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react'; import { useMemo, useRef, useState } from 'react'; -import { getDatasourceColor } from '../../druid-models'; +import type { Rule } from '../../druid-models'; +import { getDatasourceColor, RuleUtil } from '../../druid-models'; import { useClock, useGlobalEventListener } from '../../hooks'; import { clamp, @@ -49,7 +50,7 @@ import { aggregateSegmentStats, formatIntervalStat } from './common'; import './segment-bar-chart-render.scss'; -const CHART_MARGIN: Margin = { top: 10, right: 0, bottom: 25, left: 70 }; +const CHART_MARGIN: Margin = { top: 20, right: 0, bottom: 25, left: 70 }; const MIN_BAR_WIDTH = 4; const POSSIBLE_GRANULARITIES = [ new Duration('PT15M'), @@ -62,6 +63,60 @@ const POSSIBLE_GRANULARITIES = [ const EXTEND_X_SCALE_DOMAIN_BY = 1; +// --------------------------------------- +// Load rule stuff + +function loadRuleToColors(loadRule: Rule): string[] { + switch (loadRule.type) { + case 'loadForever': + case 'loadByInterval': + case 'loadByPeriod': + return ['#188718', '#095c09']; + + case 'dropForever': + case 'dropByInterval': + case 'dropByPeriod': + case 'dropBeforeByPeriod': + return ['#485348', '#3b4a3b']; + + case 'broadcastForever': + case 'broadcastByInterval': + case 'broadcastByPeriod': + return ['#4e2edc', '#35237c']; + + default: + return ['#000']; + } +} + +const NEGATIVE_INFINITY_DATE = new Date(Date.UTC(1000, 0, 1)); +const POSITIVE_INFINITY_DATE = new Date(Date.UTC(3000, 0, 1)); + +function loadRuleToDateRange(loadRule: Rule): NonNullDateRange { + switch (loadRule.type) { + case 'loadByInterval': + case 'dropByInterval': + case 'broadcastByInterval': + return String(loadRule.interval) + .split('/') + .map(d => new Date(d)) as NonNullDateRange; + + case 'loadByPeriod': + case 'dropByPeriod': + case 'dropBeforeByPeriod': + case 'broadcastByPeriod': + return [ + new Duration(loadRule.period || 'P1D').shift(new Date(), TZ_UTC, -1), + loadRule.includeFuture ? POSITIVE_INFINITY_DATE : new Date(), + ]; + + default: + return [NEGATIVE_INFINITY_DATE, POSITIVE_INFINITY_DATE]; + } +} + +// --------------------------------------- + function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange { return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)]; } @@ -104,12 +159,18 @@ function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalB }); } -interface SegmentBarChartRenderProps { +export interface DatasourceRules { + loadRules: Rule[]; + defaultLoadRules: Rule[]; +} + +export interface SegmentBarChartRenderProps { stage: Stage; dateRange: NonNullDateRange; changeDateRange(dateRange: NonNullDateRange): void; shownIntervalStat: IntervalStat; intervalRows: IntervalRow[]; + datasourceRules: DatasourceRules | undefined; shownDatasource: string | undefined; changeShownDatasource(datasource: string | undefined): void; } @@ -123,6 +184,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( dateRange, changeDateRange, intervalRows, + datasourceRules, shownDatasource, changeShownDatasource, } = props; @@ -380,6 +442,27 @@ export const SegmentBarChartRender = function SegmentBarChartRender( }; } + function renderLoadRule(loadRule: Rule, i: number, isDefault: boolean) { + const [start, end] = loadRuleToDateRange(loadRule); + const { x, width } = startEndToXWidth({ start, end }); + const colors = loadRuleToColors(loadRule); + const title = RuleUtil.ruleToString(loadRule) + (isDefault ? ' (cluster default)' : ''); + return ( + <div + key={i} + className="load-rule" + data-tooltip={title} + style={{ + left: x + CHART_MARGIN.left, + width, + backgroundColor: colors[i % colors.length], + }} + > + {width > 90 ? title : undefined} + </div> + ); + } + const nowX = timeScale(now); return ( <div className="segment-bar-chart-render"> @@ -442,7 +525,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender( key={i} className={classNames('bar-unit', { realtime: intervalBar.realtime })} {...segmentBarToRect(intervalBar)} - style={{ fill: getDatasourceColor(intervalBar.datasource) }} + fill={getDatasourceColor(intervalBar.datasource)} onClick={() => changeShownDatasource(intervalBar.datasource)} onMouseOver={() => { if (mouseDownAt) return; @@ -486,6 +569,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender( </g> </g> </svg> + {datasourceRules && ( + <div className="rule-tape"> + {datasourceRules.defaultLoadRules.map((rule, index) => renderLoadRule(rule, index, true))} + {datasourceRules.loadRules.map((rule, index) => renderLoadRule(rule, index, false))} + </div> + )} {!intervalRows.length && ( <div className="empty-placeholder"> <div className="no-data-text">There are no segments in the selected range</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 87fb41bc278..5029a1a2d76 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -20,7 +20,7 @@ import type { NonNullDateRange } from '@blueprintjs/datetime'; import { C, F, L, N, sql, SqlExpression, SqlQuery } from '@druid-toolkit/query'; import { useMemo } from 'react'; -import { END_OF_TIME_DATE, START_OF_TIME_DATE } from '../../druid-models'; +import { END_OF_TIME_DATE, type Rule, RuleUtil, START_OF_TIME_DATE } from '../../druid-models'; import type { Capabilities } from '../../helpers'; import { useQueryManager } from '../../hooks'; import { Api } from '../../singletons'; @@ -126,6 +126,26 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr }, }); + const [allLoadRulesState] = useQueryManager({ + query: shownDatasource ? '' : undefined, + processQuery: async (_, cancelToken) => { + return ( + await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules', { + cancelToken, + }) + ).data; + }, + }); + + const datasourceRules = useMemo(() => { + const allLoadRules = allLoadRulesState.data; + if (!allLoadRules || !shownDatasource) return; + return { + loadRules: (allLoadRules[shownDatasource] || []).toReversed(), + defaultLoadRules: (allLoadRules[RuleUtil.DEFAULT_RULES_KEY] || []).toReversed(), + }; + }, [allLoadRulesState.data, shownDatasource]); + if (intervalRowsState.loading) { return <Loader />; } @@ -148,6 +168,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr changeDateRange={changeDateRange} shownIntervalStat={shownIntervalStat} intervalRows={intervalRows} + datasourceRules={datasourceRules} shownDatasource={shownDatasource} changeShownDatasource={changeShownDatasource} /> diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss index b1844016041..4224c1cab89 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.scss +++ b/web-console/src/components/segment-timeline/segment-timeline.scss @@ -26,6 +26,10 @@ align-items: start; padding: 5px; gap: 10px; + + & > .expander { + flex: 1; + } } .loading-error { diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index fd07f9cdf15..d4294750a7b 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -184,30 +184,68 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr <div className="segment-timeline"> <div className="control-bar"> <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: ${shownDatasource ?? 'all'}`} + small + rightIcon={IconNames.CARET_DOWN} + /> + </Select> + {shownDatasource && ( + <Button icon={IconNames.CROSS} small onClick={() => setShownDatasource(undefined)} /> + )} + </ButtonGroup> + <Popover + position={Position.BOTTOM_LEFT} + content={ + <Menu> + {INTERVAL_STATS.map(stat => ( + <MenuItem + key={stat} + icon={checkedCircleIcon(stat === activeSegmentStat)} + text={getIntervalStatTitle(stat)} + onClick={() => setActiveSegmentStat(stat)} + /> + ))} + </Menu> + } + > <Button - icon={IconNames.CARET_LEFT} - data-tooltip={ - previousDateRange && `Previous time period\n${formatDateRange(previousDateRange)}` - } - small - disabled={!previousDateRange} - onClick={() => setDateRange(previousDateRange)} - /> - <Button - icon={IconNames.ZOOM_OUT} - data-tooltip={zoomedOutDateRange && `Zoom out\n${formatDateRange(zoomedOutDateRange)}`} - small - disabled={!zoomedOutDateRange} - onClick={() => setDateRange(zoomedOutDateRange)} - /> - <Button - icon={IconNames.CARET_RIGHT} - data-tooltip={nextDateRange && `Next time period\n${formatDateRange(nextDateRange)}`} + text={`Show: ${getIntervalStatTitle(activeSegmentStat)}`} small - disabled={!nextDateRange} - onClick={() => setDateRange(nextDateRange)} + rightIcon={IconNames.CARET_DOWN} /> - </ButtonGroup> + </Popover> + <div className="expander" /> + <ButtonGroup> {SHOWN_DURATION_OPTIONS.map((d, i) => { const dr = getDateRange(d); @@ -245,72 +283,38 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr > <Button icon={IconNames.CALENDAR} - text={effectiveDateRange ? formatDateRange(effectiveDateRange) : '? → ?'} + text={ + effectiveDateRange ? formatDateRange(effectiveDateRange) : '????-??-?? → ????-??-??' + } small data-tooltip={showCustomDatePicker ? undefined : `Select a custom date range`} /> </Popover> </ButtonGroup> - <Popover - position={Position.BOTTOM_LEFT} - content={ - <Menu> - {INTERVAL_STATS.map(stat => ( - <MenuItem - key={stat} - icon={checkedCircleIcon(stat === activeSegmentStat)} - text={getIntervalStatTitle(stat)} - onClick={() => setActiveSegmentStat(stat)} - /> - ))} - </Menu> - } - > + <ButtonGroup> <Button - text={`Show: ${getIntervalStatTitle(activeSegmentStat)}`} + icon={IconNames.CARET_LEFT} + data-tooltip={ + previousDateRange && `Previous time period\n${formatDateRange(previousDateRange)}` + } small - rightIcon={IconNames.CARET_DOWN} + disabled={!previousDateRange} + onClick={() => setDateRange(previousDateRange)} + /> + <Button + icon={IconNames.ZOOM_OUT} + data-tooltip={zoomedOutDateRange && `Zoom out\n${formatDateRange(zoomedOutDateRange)}`} + small + disabled={!zoomedOutDateRange} + onClick={() => setDateRange(zoomedOutDateRange)} + /> + <Button + icon={IconNames.CARET_RIGHT} + data-tooltip={nextDateRange && `Next time period\n${formatDateRange(nextDateRange)}`} + small + disabled={!nextDateRange} + onClick={() => setDateRange(nextDateRange)} /> - </Popover> - <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: ${shownDatasource ?? 'all'}`} - small - rightIcon={IconNames.CARET_DOWN} - /> - </Select> - {shownDatasource && ( - <Button icon={IconNames.CROSS} small onClick={() => setShownDatasource(undefined)} /> - )} </ButtonGroup> </div> <ResizeSensor diff --git a/web-console/src/druid-models/load-rule/load-rule.ts b/web-console/src/druid-models/load-rule/load-rule.ts index 63bd162c272..31b4d600585 100644 --- a/web-console/src/druid-models/load-rule/load-rule.ts +++ b/web-console/src/druid-models/load-rule/load-rule.ts @@ -41,6 +41,7 @@ export interface Rule { } export class RuleUtil { + static DEFAULT_RULES_KEY = '_default'; static TYPES: RuleType[] = [ 'loadForever', 'loadByInterval', diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx index e79592d3aa5..30f17061d4f 100644 --- a/web-console/src/views/datasources-view/datasources-view.tsx +++ b/web-console/src/views/datasources-view/datasources-view.tsx @@ -138,8 +138,6 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[ ], }; -const DEFAULT_RULES_KEY = '_default'; - function formatLoadDrop(segmentsToLoad: NumberLike, segmentsToDrop: NumberLike): string { const loadDrop: string[] = []; if (segmentsToLoad) { @@ -586,7 +584,7 @@ GROUP BY 1, 2`; // Rules auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => { try { - const rules: Record<string, Rule[]> = ( + const rules = ( await Api.instance.get<Record<string, Rule[]>>('/druid/coordinator/v1/rules', { cancelToken, }) @@ -597,7 +595,7 @@ GROUP BY 1, 2`; ...ds, rules: rules[ds.datasource] || [], })), - defaultRules: rules[DEFAULT_RULES_KEY], + defaultRules: rules[RuleUtil.DEFAULT_RULES_KEY], }; } catch { AppToaster.show({ --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
