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]

Reply via email to