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]

Reply via email to