You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by vo...@apache.org on 2024/11/18 23:28:40 UTC
(druid) 05/42: step
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 <va...@ogievetsky.com>
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: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org