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 dca621c02e798ffaaa1f524fbf59e7000dd740c7 Author: Vadim Ogievetsky <[email protected]> AuthorDate: Sun Oct 27 14:57:54 2024 -0700 step --- web-console/package-lock.json | 12 ++ web-console/package.json | 1 + .../src/components/segment-timeline/chart-axis.tsx | 5 +- .../src/components/segment-timeline/common.ts | 29 +++- .../segment-timeline/segment-bar-chart-render.scss | 6 + .../segment-timeline/segment-bar-chart-render.tsx | 167 +++++++++++++++------ .../segment-timeline/segment-bar-chart.tsx | 122 +++++++++++---- .../segment-timeline/segment-timeline.tsx | 7 +- web-console/src/utils/date.ts | 82 ++++++++++ web-console/src/utils/general.tsx | 9 ++ .../time-menu-items/time-menu-items.tsx | 64 ++------ 11 files changed, 364 insertions(+), 140 deletions(-) diff --git a/web-console/package-lock.json b/web-console/package-lock.json index 95768117648..4abbd266e0c 100644 --- a/web-console/package-lock.json +++ b/web-console/package-lock.json @@ -15,6 +15,7 @@ "@blueprintjs/icons": "^5.13.0", "@blueprintjs/select": "^5.2.5", "@druid-toolkit/query": "^0.22.23", + "@flatten-js/interval-tree": "^1.1.3", "@fontsource/open-sans": "^5.0.30", "ace-builds": "~1.5.3", "axios": "^1.7.7", @@ -2385,6 +2386,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@flatten-js/interval-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz", + "integrity": "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A==", + "license": "MIT" + }, "node_modules/@fontsource/open-sans": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.1.0.tgz", @@ -19782,6 +19789,11 @@ "levn": "^0.4.1" } }, + "@flatten-js/interval-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@flatten-js/interval-tree/-/interval-tree-1.1.3.tgz", + "integrity": "sha512-xhFWUBoHJFF77cJO1D6REjdgJEMRf2Y2Z+eKEPav8evGKcLSnj1ud5pLXQSbGuxF3VSvT1rWhMfVpXEKJLTL+A==" + }, "@fontsource/open-sans": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.1.0.tgz", diff --git a/web-console/package.json b/web-console/package.json index 1e76234841a..2580151bac1 100644 --- a/web-console/package.json +++ b/web-console/package.json @@ -56,6 +56,7 @@ "@blueprintjs/icons": "^5.13.0", "@blueprintjs/select": "^5.2.5", "@druid-toolkit/query": "^0.22.23", + "@flatten-js/interval-tree": "^1.1.3", "@fontsource/open-sans": "^5.0.30", "ace-builds": "~1.5.3", "axios": "^1.7.7", diff --git a/web-console/src/components/segment-timeline/chart-axis.tsx b/web-console/src/components/segment-timeline/chart-axis.tsx index 18dc7d3e076..0e48e10fde8 100644 --- a/web-console/src/components/segment-timeline/chart-axis.tsx +++ b/web-console/src/components/segment-timeline/chart-axis.tsx @@ -18,7 +18,6 @@ import type { Axis } from 'd3-axis'; import { select } from 'd3-selection'; -import React from 'react'; interface ChartAxisProps { transform?: string; @@ -26,7 +25,7 @@ interface ChartAxisProps { className?: string; } -export const ChartAxis = React.memo(function ChartAxis(props: ChartAxisProps) { +export const ChartAxis = function ChartAxis(props: ChartAxisProps) { const { transform, axis, className } = props; return ( <g @@ -35,4 +34,4 @@ export const ChartAxis = React.memo(function ChartAxis(props: ChartAxisProps) { ref={node => select(node).call(axis as any)} /> ); -}); +}; diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts index 10a29751721..a1907846f29 100644 --- a/web-console/src/components/segment-timeline/common.ts +++ b/web-console/src/components/segment-timeline/common.ts @@ -16,19 +16,36 @@ * limitations under the License. */ +import { sum } from 'd3-array'; + export type SegmentStat = 'count' | 'size' | 'rows'; +export function aggregateSegmentStats( + xs: readonly Record<SegmentStat, number>[], +): Record<SegmentStat, number> { + return { + count: sum(xs, s => s.count), + size: sum(xs, s => s.size), + rows: sum(xs, s => s.rows), + }; +} + export interface SegmentRow extends Record<SegmentStat, number> { - start: string; - end: string; + start: Date; + end: Date; + durationSeconds: number; datasource?: string; } export interface SegmentBar extends SegmentRow { - startDate: Date; - endDate: Date; + offset: Record<SegmentStat, number>; } -export interface StackedSegmentBar extends SegmentBar { - offset: Record<SegmentStat, number>; +export function normalizedSegmentRow(sr: SegmentRow): SegmentRow { + return { + ...sr, + count: sr.count / sr.durationSeconds, + size: sr.size / sr.durationSeconds, + rows: sr.rows / sr.durationSeconds, + }; } 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 629499e13db..9cd7742e851 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 @@ -44,6 +44,12 @@ stroke-width: 1.5px; } + .selection { + fill: transparent; + stroke: #ffffff; + stroke-width: 1px; + } + .gridline-x { line { stroke-dasharray: 5, 5; 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 41823b50b0a..4c17c5d44b1 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 @@ -18,12 +18,13 @@ import type { NonNullDateRange } from '@blueprintjs/datetime'; import { max } from 'd3-array'; -import type { AxisScale } from 'd3-axis'; import { axisBottom, axisLeft } from 'd3-axis'; import { scaleLinear, scaleUtc } from 'd3-scale'; -import { useState } from 'react'; +import type React from 'react'; +import { useRef, useState } from 'react'; -import { formatBytes, formatInteger } from '../../utils'; +import { useGlobalEventListener } from '../../hooks'; +import { ceilDay, floorDay, formatBytes, formatInteger } from '../../utils'; import type { Margin, Stage } from '../../utils/stage'; import { ChartAxis } from './chart-axis'; @@ -31,11 +32,31 @@ import type { SegmentBar, SegmentStat } from './common'; import './segment-bar-chart-render.scss'; -const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 60 }; +const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 10 }; + +const COLORS = [ + '#b33040', + '#d25c4d', + '#f2b447', + '#d9d574', + '#4FAA7E', + '#57ceff', + '#789113', + '#098777', + '#b33040', + '#d2757b', + '#f29063', + '#d9a241', + '#80aa61', + '#c4ff9e', + '#915412', + '#87606c', +]; interface SegmentBarChartRenderProps { stage: Stage; dateRange: NonNullDateRange; + changeDateRange(newDateRange: NonNullDateRange): void; shownSegmentStat: SegmentStat; segmentBars: SegmentBar[]; changeActiveDatasource(datasource: string | undefined): void; @@ -44,15 +65,25 @@ interface SegmentBarChartRenderProps { export const SegmentBarChartRender = function SegmentBarChartRender( props: SegmentBarChartRenderProps, ) { - const { stage, shownSegmentStat, dateRange, segmentBars, changeActiveDatasource } = props; + const { + stage, + shownSegmentStat, + dateRange, + changeDateRange, + segmentBars, + changeActiveDatasource, + } = props; const [hoverOn, setHoverOn] = useState<SegmentBar>(); + const [mouseDownAt, setMouseDownAt] = useState<Date | undefined>(); + const [dragging, setDragging] = useState<NonNullDateRange | undefined>(); + const svgRef = useRef<SVGSVGElement | null>(null); const innerStage = stage.applyMargin(CHART_MARGIN); - const timeScale: AxisScale<Date> = scaleUtc().domain(dateRange).range([0, innerStage.width]); + const timeScale = scaleUtc().domain(dateRange).range([0, innerStage.width]); - const maxStat = max(segmentBars, d => d[shownSegmentStat]); - const statScale: AxisScale<number> = scaleLinear() + const maxStat = max(segmentBars, d => d[shownSegmentStat] + d.offset[shownSegmentStat]); + const statScale = scaleLinear() .rangeRound([innerStage.height, 0]) .domain([0, maxStat ?? 1]); @@ -65,11 +96,43 @@ export const SegmentBarChartRender = function SegmentBarChartRender( } }; + function handleMouseDown(e: React.MouseEvent) { + const svg = svgRef.current; + if (!svg) return; + const rect = svg.getBoundingClientRect(); + const x = e.clientX - rect.x - CHART_MARGIN.left; + setMouseDownAt(timeScale.invert(x)); + } + + useGlobalEventListener('mousemove', (e: MouseEvent) => { + if (!mouseDownAt) return; + const svg = svgRef.current; + if (!svg) return; + const rect = svg.getBoundingClientRect(); + const x = e.clientX - rect.x - CHART_MARGIN.left; + const b = timeScale.invert(x); + if (mouseDownAt < b) { + setDragging([floorDay(mouseDownAt), ceilDay(b)]); + } else { + setDragging([floorDay(b), ceilDay(mouseDownAt)]); + } + }); + + useGlobalEventListener('mouseup', () => { + if (mouseDownAt) { + setMouseDownAt(undefined); + } + if (dragging) { + setDragging(undefined); + changeDateRange(dragging); + } + }); + function segmentBarToRect(segmentBar: SegmentBar) { - const y0 = statScale(0)!; // segmentBar.y0 || - const xStart = timeScale(segmentBar.startDate)!; - const xEnd = timeScale(segmentBar.endDate)!; - const y = statScale(segmentBar[shownSegmentStat]) || 0; + const xStart = timeScale(segmentBar.start); + const xEnd = timeScale(segmentBar.end); + const y0 = statScale(segmentBar.offset[shownSegmentStat]); + const y = statScale(segmentBar[shownSegmentStat] + segmentBar.offset[shownSegmentStat]); return { x: xStart, @@ -81,22 +144,29 @@ export const SegmentBarChartRender = function SegmentBarChartRender( return ( <div className="segment-bar-chart-render"> - {hoverOn && ( + {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: {hoverOn.start}</div> + <div>Time: {hoverOn.start.toISOString()}</div> <div> {`${shownSegmentStat === 'count' ? 'Count' : 'Size'}: ${formatTick( - hoverOn[shownSegmentStat], + hoverOn[shownSegmentStat] * hoverOn.durationSeconds, )}`} </div> </div> - )} + ) : undefined} <svg + ref={svgRef} width={stage.width} height={stage.height} viewBox={`0 0 ${stage.width} ${stage.height}`} preserveAspectRatio="xMinYMin meet" + onMouseDown={handleMouseDown} > <g transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`} @@ -116,38 +186,43 @@ export const SegmentBarChartRender = function SegmentBarChartRender( transform={`translate(0,${innerStage.height})`} axis={axisBottom(timeScale)} /> - <ChartAxis - className="axis-y" - axis={axisLeft(statScale) - .ticks(5) - .tickFormat(e => formatTick(e))} - /> - {segmentBars.map((segmentBar, i) => { - return ( + <g className="bar-group"> + {segmentBars.map((segmentBar, i) => { + return ( + <rect + key={i} + className="bar-unit" + {...segmentBarToRect(segmentBar)} + style={{ fill: COLORS[i % COLORS.length] }} + onClick={ + segmentBar.datasource + ? () => changeActiveDatasource(segmentBar.datasource) + : undefined + } + onMouseOver={() => setHoverOn(segmentBar)} + /> + ); + })} + {hoverOn && ( + <rect + className="hovered-bar" + {...segmentBarToRect(hoverOn)} + onClick={() => { + setHoverOn(undefined); + changeActiveDatasource(hoverOn.datasource); + }} + /> + )} + {(dragging || mouseDownAt) && ( <rect - key={i} - className="bar-unit" - {...segmentBarToRect(segmentBar)} - style={{ fill: i % 2 ? 'red' : 'blue' }} - onClick={ - segmentBar.datasource - ? () => changeActiveDatasource(segmentBar.datasource) - : undefined - } - onMouseOver={() => setHoverOn(segmentBar)} + className="selection" + x={timeScale(dragging?.[0] || mouseDownAt!)} + y={0} + height={innerStage.height} + width={dragging ? timeScale(dragging[1]) - timeScale(dragging[0]) : 1} /> - ); - })} - {hoverOn && ( - <rect - className="hovered-bar" - {...segmentBarToRect(hoverOn)} - onClick={() => { - setHoverOn(undefined); - changeActiveDatasource(hoverOn.datasource); - }} - /> - )} + )} + </g> </g> </svg> </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 a6468ac3d4c..32d6f053b18 100644 --- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx +++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx @@ -18,47 +18,101 @@ import type { NonNullDateRange } from '@blueprintjs/datetime'; import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query'; -import { sum } from 'd3-array'; +import IntervalTree from '@flatten-js/interval-tree'; import { useMemo } from 'react'; import type { Capabilities } from '../../helpers'; import { useQueryManager } from '../../hooks'; import { Api } from '../../singletons'; -import { filterMap, groupBy, queryDruidSql } from '../../utils'; +import { + ceilDay, + ceilHour, + ceilMonth, + ceilYear, + filterMap, + floorDay, + floorHour, + floorMonth, + floorYear, + groupBy, + queryDruidSql, +} from '../../utils'; import type { Stage } from '../../utils/stage'; import { Loader } from '../loader/loader'; import type { SegmentBar, SegmentRow, SegmentStat } from './common'; +import { aggregateSegmentStats, normalizedSegmentRow } from './common'; import { SegmentBarChartRender } from './segment-bar-chart-render'; import './segment-bar-chart.scss'; type TrimDuration = 'PT1H' | 'P1D' | 'P1M' | 'P1Y'; -function trimUtcDate(date: string, duration: TrimDuration): string { - // date like 2024-09-26T00:00:00.000Z +function floorToDuration(date: Date, duration: TrimDuration): Date { switch (duration) { case 'PT1H': - return date.substring(0, 13) + ':00:00Z'; + return floorHour(date); case 'P1D': - return date.substring(0, 10) + 'T00:00:00Z'; + return floorDay(date); case 'P1M': - return date.substring(0, 7) + '-01T00:00:00Z'; + return floorMonth(date); case 'P1Y': - return date.substring(0, 4) + '-01-01T00:00:00Z'; + return floorYear(date); default: throw new Error(`Unexpected duration: ${duration}`); } } +function ceilToDuration(date: Date, duration: TrimDuration): Date { + switch (duration) { + case 'PT1H': + return ceilHour(date); + + case 'P1D': + return ceilDay(date); + + case 'P1M': + return ceilMonth(date); + + case 'P1Y': + return ceilYear(date); + + default: + throw new Error(`Unexpected duration: ${duration}`); + } +} + +function stackSegmentRows(segmentRows: SegmentRow[]): SegmentBar[] { + const sorted = segmentRows.sort((a, b) => { + const diff = b.durationSeconds - a.durationSeconds; + if (diff) return diff; + if (!a.datasource || !b.datasource) return 0; + return b.datasource.localeCompare(a.datasource); + }); + + const intervalTree = new IntervalTree(); + return sorted.map(segmentRow => { + segmentRow = normalizedSegmentRow(segmentRow); + const startMs = segmentRow.start.valueOf(); + const endMs = segmentRow.end.valueOf(); + const segmentRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as SegmentRow[]; + intervalTree.insert([startMs, endMs], segmentRow); + return { + ...segmentRow, + offset: aggregateSegmentStats(segmentRowsBelow), + }; + }); +} + interface SegmentBarChartProps { capabilities: Capabilities; stage: Stage; dateRange: NonNullDateRange; + changeDateRange(newDateRange: NonNullDateRange): void; breakByDataSource: boolean; shownSegmentStat: SegmentStat; changeActiveDatasource: (datasource: string | undefined) => void; @@ -68,6 +122,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr const { capabilities, dateRange, + changeDateRange, breakByDataSource, stage, shownSegmentStat, @@ -82,6 +137,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr const [segmentRowsState] = useQueryManager({ query: intervalsQuery, processQuery: async ({ capabilities, dateRange, breakByDataSource }, cancelToken) => { + const trimDuration: TrimDuration = 'PT1H'; let segmentRows: SegmentRow[]; if (capabilities.hasSql()) { const query = SqlQuery.from(N('sys').table('segments')) @@ -95,7 +151,16 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr .addSelect(F.sum(C('size')).as('size')) .addSelect(F.sum(C('num_rows')).as('rows')); - segmentRows = await queryDruidSql({ query: query.toString() }); + segmentRows = (await queryDruidSql({ query: query.toString() })).map(sr => { + const start = floorToDuration(new Date(sr.start), trimDuration); + const end = ceilToDuration(new Date(sr.end), trimDuration); + return { + ...sr, + start, + end, + durationSeconds: (end.valueOf() - start.valueOf()) / 1000, + }; + }); // 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 { const datasources: string[] = ( await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken }) @@ -114,11 +179,14 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr return filterMap(Object.entries(intervalMap), ([interval, v]) => { // ToDo: Filter on start end - const [start, end] = interval.split('/'); + const [startStr, endStr] = interval.split('/'); + const start = floorToDuration(new Date(startStr), trimDuration); + const end = ceilToDuration(new Date(endStr), trimDuration); const { count, size, rows } = v as any; return { start, end, + durationSeconds: (end.valueOf() - start.valueOf()) / 1000, datasource: breakByDataSource ? datasource : undefined, count, size, @@ -130,31 +198,23 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr ).flat(); } - const trimDuration: TrimDuration = 'P1D'; - return groupBy( + const fullyGroupedSegmentRows = groupBy( segmentRows, segmentRow => - // 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 - `${trimUtcDate(segmentRow.start, trimDuration)}/${trimUtcDate( - segmentRow.end, - trimDuration, - )}/${segmentRow.datasource || ''}`, - (segmentRows): SegmentBar => { - const firstRow = segmentRows[0]; - const start = trimUtcDate(firstRow.start, trimDuration); - const end = trimUtcDate(firstRow.end, trimDuration); + [ + segmentRow.start.toISOString(), + segmentRow.end.toISOString(), + segmentRow.datasource || '', + ].join('/'), + (segmentRows): SegmentRow => { return { - ...firstRow, - start, - startDate: new Date(start), - end, - endDate: new Date(end), - count: sum(segmentRows, s => s.count), - size: sum(segmentRows, s => s.size), - rows: sum(segmentRows, s => s.rows), + ...segmentRows[0], + ...aggregateSegmentStats(segmentRows), }; }, ); + + return stackSegmentRows(fullyGroupedSegmentRows); }, }); @@ -181,13 +241,13 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr ); } - console.log(segmentRows); return ( <SegmentBarChartRender stage={stage} dateRange={dateRange} + changeDateRange={changeDateRange} shownSegmentStat={shownSegmentStat} - segmentBars={segmentRows} + segmentBars={segmentRows as any} changeActiveDatasource={changeActiveDatasource} /> ); diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index df4e1c12960..10118c30f9d 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -126,8 +126,9 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr capabilities={capabilities} stage={stage} dateRange={dateRange} + changeDateRange={setDateRange} shownSegmentStat={activeSegmentStat} - breakByDataSource={false} + breakByDataSource changeActiveDatasource={(datasource: string | undefined) => setActiveDatasource(activeDatasource ? undefined : datasource) } @@ -146,6 +147,10 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr label: 'Size', value: 'size', }, + { + label: 'Rows', + value: 'rows', + }, { label: 'Count', value: 'count', diff --git a/web-console/src/utils/date.ts b/web-console/src/utils/date.ts index 1d5aa4f61e9..897618c030b 100644 --- a/web-console/src/utils/date.ts +++ b/web-console/src/utils/date.ts @@ -105,3 +105,85 @@ export function ceilToUtcDay(date: Date): Date { date.setUTCDate(date.getUTCDate() + 1); return date; } + +// ------------------------------------ + +export function floorHour(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCMinutes(0, 0, 0); + return dt; +} + +export function nextHour(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCHours(dt.getUTCHours() + 1); + return dt; +} + +export function ceilHour(dt: Date): Date { + const floor = floorHour(dt); + if (floor.valueOf() === dt.valueOf()) return dt; + return nextHour(floor); +} + +// ------------------------------------ + +export function floorDay(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCHours(0, 0, 0, 0); + return dt; +} + +export function nextDay(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCDate(dt.getUTCDate() + 1); + return dt; +} + +export function ceilDay(dt: Date): Date { + const floor = floorDay(dt); + if (floor.valueOf() === dt.valueOf()) return dt; + return nextDay(floor); +} + +// ------------------------------------ + +export function floorMonth(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCHours(0, 0, 0, 0); + dt.setUTCDate(1); + return dt; +} + +export function nextMonth(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCMonth(dt.getUTCMonth() + 1); + return dt; +} + +export function ceilMonth(dt: Date): Date { + const floor = floorMonth(dt); + if (floor.valueOf() === dt.valueOf()) return dt; + return nextMonth(floor); +} + +// ------------------------------------ + +export function floorYear(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCHours(0, 0, 0, 0); + dt.setUTCMonth(0, 1); + return dt; +} + +export function nextYear(dt: Date): Date { + dt = new Date(dt.valueOf()); + dt.setUTCFullYear(dt.getUTCFullYear() + 1); + return dt; +} + +export function ceilYear(dt: Date): Date { + const floor = floorYear(dt); + if (floor.valueOf() === dt.valueOf()) return dt; + return nextYear(floor); +} diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index b742013b2e8..18a1ac65eca 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -656,3 +656,12 @@ export function offsetToRowColumn(str: string, offset: number): RowColumn | unde return; } + +export function findParentSVG(element: Element): SVGElement | undefined { + let currentElement: Element | null = element; + while (currentElement) { + if (currentElement.tagName === 'svg') return currentElement as SVGElement; + currentElement = currentElement.parentElement; + } + return; +} diff --git a/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx b/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx index cbe569ac6de..11b3a67a3cb 100644 --- a/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx +++ b/web-console/src/views/workbench-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx @@ -23,7 +23,17 @@ import { C, F, SqlExpression } from '@druid-toolkit/query'; import type { JSX } from 'react'; import React from 'react'; -import { prettyPrintSql } from '../../../../../utils'; +import { + floorDay, + floorHour, + floorMonth, + floorYear, + nextDay, + nextHour, + nextMonth, + nextYear, + prettyPrintSql, +} from '../../../../../utils'; const LATEST_HOUR: SqlExpression = SqlExpression.parse( `? >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR`, @@ -52,58 +62,6 @@ function fillWithColumnStartEnd(columnName: string, start: Date, end: Date): Sql return BETWEEN.fillPlaceholders([start, column, column, end]); } -// ------------------------------------ - -function floorHour(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCMinutes(0, 0, 0); - return dt; -} - -function nextHour(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCHours(dt.getUTCHours() + 1); - return dt; -} - -function floorDay(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCHours(0, 0, 0, 0); - return dt; -} - -function nextDay(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCDate(dt.getUTCDate() + 1); - return dt; -} - -function floorMonth(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCHours(0, 0, 0, 0); - dt.setUTCDate(1); - return dt; -} - -function nextMonth(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCMonth(dt.getUTCMonth() + 1); - return dt; -} - -function floorYear(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCHours(0, 0, 0, 0); - dt.setUTCMonth(0, 1); - return dt; -} - -function nextYear(dt: Date): Date { - dt = new Date(dt.valueOf()); - dt.setUTCFullYear(dt.getUTCFullYear() + 1); - return dt; -} - export interface TimeMenuItemsProps { table: string; schema: string; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
