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:35 UTC

(druid) branch segment_timeline2 updated (224ca761175 -> 5855a5bb7ff)

This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a change to branch segment_timeline2
in repository https://gitbox.apache.org/repos/asf/druid.git


 discard 224ca761175 improve segment view
 discard 6b9cc0d59a2 fix
 discard 0314aa5697a step
 discard 2987bea12d6 check in
 discard 52b02d9fc0e small fixes
 discard d3a53716750 more cleanup
 discard 22043fc67cf init refactor of segment timeline
    omit cb82983a4f9 fix snapshots
    omit 1625a3e3c99 more splitter layout
    omit 8b9e045408e better key
    omit 0ef44f8d573 update default size
    omit 1ce04978056 better positioning
    omit ee01cc89cca added splitterSize option
    omit 273e2b60b2b better defaults
    omit 61528e273c1 update licenses
    omit 0b9109ee9f4 better v splitter
    omit 2bf1b7d0dcc init lookups big
    omit 20d42f68605 some query tab fixes
     add 9dfb3787116 Remove unused coordinator dynamic configs mergeSegmentsLimit, mergeBytesLimit (#17384)
     add 1157ecdec37 abstract common base of SQL micro-benchmarks to reduce boilerplate and standardize parameters (#17383)
     add 60daddedf8b SeekableStreamSupervisor: Use workerExec as the client connectExec. (#17394)
     add 43b325b6aaf Add missing `@Nullable` annotations to SqlQuery (#17398)
     add 1e96c85b383 WindowOperatorQueryFrameProcessor: Avoid writing multiple frames to output channel in runIncrementally() (#17373)
     add 7e8671caa99 GroupByQueryConfig: Skip unnecessary toString. (#17396)
     add c4b513e5990 SeekableStreamSupervisor: Don't await task futures in workerExec. (#17403)
     add fe0f4150c9a MSQ ingestion: Improve error message on encountering non-long timestamp column (#17411)
     add b59317e42b5 Fix typo in security.md (#17413)
     add 7306d280cc0 Migrate jaxb bind dependency to jakarta (#17370)
     add 65acc86756b Capture more dumps on failure. (#17412)
     add 73675d06712 clean up some thread pools in tests (#17421)
     add 446a8f466f2 Update errorprone, mockito, jacoco, checkerframework. (#17414)
     add 6a9c050095d DruidOverlord: Move becomeLeader/stopBeingLeader earlier. (#17415)
     add b7c661b8013  Make tempStorageDirectory configuration optional and rely on task dir instead (#17015)
     add 1be2b852e9d [Kafka Ingestion Tutorial] Update docs for Schema Config (#17409)
     add 10208baab24 use big endian for compressed complex column values to fit object strategy expectations (#17422)
     add 66eb365e4d5 Revert "always set taskLocation (#17350)" (#17417)
     add 63c91ad813e Fix backward compatibility issues in WindowOperatorQueryFrameProcessorFactory and WindowOperatorQueryFrameProcessor (#17433)
     add 21e7e5cddd2 Add benchmark suite for MSQ window functions (#17377)
     add d5bb7de5cf4 Fix Map Lookup Introspection Endpoints and update doc for Globally Cached Lookups (#17436)
     add 4b7902e74af Web console: Improve workbench view with resizable side panels (#17387)
     add 5fcf4205e4b Handle empty values for task and datasource conditions in pod template selector (#17400)
     new fb7371370a3 init refactor of segment timeline
     new cf8f1d8f0b1 more cleanup
     new 88f993c4d18 small fixes
     new 289d95c7d09 check in
     new dca621c02e7 step
     new b0c756fea07 fix
     new c89e0588747 improve segment view
     new 5c430de66ce progress
     new ded6fc2a0d6 checkpoint
     new 8ff3f44ec39 better timezone menu
     new 17051be2648 add file
     new f08a337e445 auto trimming
     new 6a1821a571f no highlight
     new 1b68c63d47c goodies
     new 15885be68a3 cleaner normalization
     new 2243da03823 simple bubble
     new f320470b244 fix supervisor view
     new 7e8003112cc fixes
     new f33a31aff4f fixes
     new 883e0f9a7c2 segment wheel fixes
     new 952b0fb2608 api
     new d80b2beb956 preogress
     new 91af28e7299 more fixes
     new 109022c4c4e normalize to days
     new cac4f78be40 good fixes
     new b865422cc54 now line and fixes
     new dc1a507081b fixes
     new 2036585da68 init datasource
     new 02b963d8188 fixes
     new 9c5875555e8 fix zooming
     new 80149630a9e better controlls
     new 06562491fda misc fixes
     new 6278d3d2ff3 fix
     new 020d7ac5477 fixes
     new b6b52962499 add unnest
     new 797fabae237 increment store only on appropriate engine
     new 4a1286048a6 portal bubble
     new 7fe805aeb0b goodies
     new ad0517a3cb3 progress
     new 7513ec5c4f0 fix todo
     new bc202c9e492 fixes
     new 5855a5bb7ff fix render spamming

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (224ca761175)
            \
             N -- N -- N   refs/heads/segment_timeline2 (5855a5bb7ff)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 42 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .github/workflows/reusable-unit-tests.yml          |  27 +-
 LABELS                                             |   5 -
 benchmarks/pom.xml                                 |  17 +
 .../lookup/SqlReverseLookupBenchmark.java          |   8 +-
 .../query/MSQWindowFunctionsBenchmark.java         | 224 +++++
 .../druid/benchmark/query/SqlBaseBenchmark.java    | 559 ++++++++++++
 .../benchmark/query/SqlBasePlanBenchmark.java      |  52 ++
 .../benchmark/query/SqlBaseQueryBenchmark.java     |  50 ++
 .../apache/druid/benchmark/query/SqlBenchmark.java | 513 ++---------
 .../benchmark/query/SqlBenchmarkDatasets.java      | 423 +++++++++
 .../query/SqlComplexMetricsColumnsBenchmark.java   |  72 ++
 .../benchmark/query/SqlExpressionBenchmark.java    | 346 ++-----
 .../druid/benchmark/query/SqlGroupByBenchmark.java | 310 +------
 .../benchmark/query/SqlNestedDataBenchmark.java    | 405 ++-------
 .../druid/benchmark/query/SqlPlanBenchmark.java    |  95 ++
 .../benchmark/query/SqlProjectionsBenchmark.java   |  80 ++
 .../query/SqlWindowFunctionsBenchmark.java         | 408 +--------
 docs/api-reference/dynamic-configuration-api.md    |   8 +-
 docs/assets/tutorial-kafka-data-loader-05.png      | Bin 206028 -> 506701 bytes
 docs/assets/tutorial-kafka-data-loader-05b.png     | Bin 202167 -> 0 bytes
 docs/configuration/index.md                        |   2 -
 docs/multi-stage-query/reference.md                |  14 +-
 docs/multi-stage-query/security.md                 |   4 +-
 docs/querying/lookups-cached-global.md             |   2 +-
 docs/tutorials/tutorial-kafka.md                   |  11 +-
 .../k8s/overlord/KubernetesPeonLifecycle.java      |  51 +-
 .../druid/k8s/overlord/execution/Selector.java     |   8 +-
 .../k8s/overlord/KubernetesPeonLifecycleTest.java  | 118 ++-
 .../druid/k8s/overlord/execution/SelectorTest.java |  57 ++
 .../RabbitStreamIndexTaskClientFactory.java        |   6 +-
 .../storage/azure/output/AzureOutputConfig.java    |  17 +-
 .../output/AzureStorageConnectorProvider.java      |   8 +-
 .../azure/output/AzureOutputConfigTest.java        |   3 +-
 .../storage/azure/output/AzureOutputSerdeTest.java |  12 -
 .../output/AzureStorageConnectorProviderTest.java  |  41 +-
 .../hll/HllSketchAggregatorFactoryTest.java        |   7 +-
 .../google/output/GoogleExportStorageProvider.java |  13 +-
 .../storage/google/output/GoogleOutputConfig.java  |   9 +-
 .../output/GoogleStorageConnectorProvider.java     |   7 +-
 .../output/GoogleExportStorageProviderTest.java    |   6 +-
 .../output/GoogleStorageConnectorProviderTest.java |  10 +-
 .../kafka/KafkaIndexTaskClientFactory.java         |   6 +-
 .../kafkainput/KafkaStringHeaderFormatTest.java    |  13 +-
 .../kafka/supervisor/KafkaSupervisorTest.java      | 999 +++++++++++++++------
 .../kinesis/KinesisIndexTaskClientFactory.java     |   6 +-
 .../kinesis/supervisor/KinesisSupervisorTest.java  |  24 +-
 extensions-core/multi-stage-query/pom.xml          |  11 +
 .../apache/druid/msq/exec/ControllerContext.java   |  10 +
 .../org/apache/druid/msq/exec/ControllerImpl.java  |  28 +-
 .../druid/msq/exec/ExportMetadataManager.java      |   7 +-
 .../java/org/apache/druid/msq/exec/MSQTasks.java   |   8 +-
 .../druid/msq/guice/MSQDurableStorageModule.java   |  12 +-
 .../msq/indexing/IndexerControllerContext.java     |  16 +-
 .../druid/msq/indexing/IndexerWorkerContext.java   |   9 +-
 .../indexing/cleaner/DurableStorageCleaner.java    |   5 +-
 .../WindowOperatorQueryFrameProcessor.java         |  41 +-
 .../WindowOperatorQueryFrameProcessorFactory.java  |  34 +-
 .../druid/msq/querykit/WindowOperatorQueryKit.java |  50 +-
 .../ExportResultsFrameProcessorFactory.java        |   2 +-
 .../msq/sql/resources/SqlStatementResource.java    |   5 +-
 .../org/apache/druid/msq/exec/MSQTasksTest.java    |  39 +
 .../msq/indexing/DurableStorageCleanerTest.java    |   5 +-
 .../msq/indexing/IndexerWorkerContextTest.java     |   6 +
 ...ndowOperatorQueryFrameProcessorFactoryTest.java |   8 +-
 .../WindowOperatorQueryFrameProcessorTest.java     |  84 ++
 .../resources/SqlMSQStatementResourcePostTest.java |   5 +-
 .../sql/resources/SqlStatementResourceTest.java    |   5 +-
 .../druid/msq/test/CalciteMSQTestsHelper.java      |   3 +
 .../druid/msq/test/ExtractResultsFactory.java      |   2 -
 .../druid/msq/test/MSQTestControllerContext.java   |   9 +
 .../msq/test/StandardMSQComponentSupplier.java     |   2 +-
 extensions-core/orc-extensions/pom.xml             |   8 +-
 .../druid/storage/s3/output/S3ExportConfig.java    |   3 +-
 .../storage/s3/output/S3ExportStorageProvider.java |  14 +-
 .../druid/storage/s3/output/S3OutputConfig.java    |  10 +-
 .../s3/output/S3StorageConnectorProvider.java      |   8 +-
 .../storage/s3/S3StorageConnectorProviderTest.java |  16 +-
 .../druid/storage/s3/output/S3OutputSerdeTest.java |  14 -
 indexing-hadoop/pom.xml                            |   5 +
 .../indexer/DataSegmentAndIndexZipFilePath.java    |  17 +-
 .../DataSegmentAndIndexZipFilePathTest.java        |  13 +-
 .../druid/indexing/overlord/DruidOverlord.java     |  25 +-
 .../apache/druid/indexing/overlord/TaskMaster.java |  74 +-
 .../SeekableStreamIndexTaskClientFactory.java      |  27 +-
 .../supervisor/SeekableStreamSupervisor.java       | 197 ++--
 .../compact/OverlordCompactionSchedulerTest.java   |  14 +-
 .../SeekableStreamSupervisorStateTest.java         |   2 +-
 licenses.yaml                                      | 121 +--
 pom.xml                                            |  16 +-
 processing/pom.xml                                 |   4 +-
 .../druid/query/groupby/GroupByQueryConfig.java    |   2 +-
 .../druid/query/operator/ScanOperatorFactory.java  |  14 +-
 .../druid/segment/data/CompressedBlockReader.java  |  23 +-
 .../druid/segment/data/CompressedLongsReader.java  |   7 +-
 .../CompressedVariableSizedBlobColumnSupplier.java |  24 +-
 .../segment/generator/GeneratorSchemaInfo.java     |  16 +
 .../segment/nested/NestedDataColumnSupplier.java   |   1 +
 .../segment/nested/NestedDataColumnSupplierV4.java |   1 +
 .../segment/serde/CompressedComplexColumn.java     |   8 +-
 .../serde/CompressedComplexColumnSupplier.java     |  18 +-
 .../druid/storage/ExportStorageProvider.java       |   7 +-
 .../druid/storage/StorageConnectorProvider.java    |  15 +-
 .../local/LocalFileExportStorageProvider.java      |   2 +-
 .../local/LocalFileStorageConnectorProvider.java   |   2 +-
 .../apache/druid/common/guava/GuavaUtilsTest.java  |  77 +-
 .../java/util/emitter/core/HttpEmitterTest.java    |  36 +-
 .../core/HttpPostEmitterLoggerStressTest.java      |  96 +-
 .../emitter/core/HttpPostEmitterStressTest.java    | 273 +++---
 .../util/emitter/core/HttpPostEmitterTest.java     |  32 +-
 .../metrics/AllocationMetricCollectorTest.java     |   1 -
 .../util/metrics/BasicMonitorSchedulerTest.java    |   7 +
 .../java/util/metrics/JvmPidDiscovererTest.java    |   8 +-
 .../epinephelinae/ConcurrentGrouperTest.java       | 210 +++--
 .../query/operator/ScanOperatorFactoryTest.java    |  20 +-
 .../java/org/apache/druid/segment/TestHelper.java  |  15 +
 .../apache/druid/segment/VirtualColumnsTest.java   |  31 +-
 .../data/CompressedVariableSizeBlobColumnTest.java |  64 ++
 .../druid/segment/generator/SegmentGenerator.java  | 175 +++-
 .../nested/NestedDataColumnSupplierTest.java       |  45 +-
 .../nested/NestedDataColumnSupplierV4Test.java     |  45 +-
 .../nested/ScalarDoubleColumnSupplierTest.java     |  45 +-
 .../nested/ScalarLongColumnSupplierTest.java       |  45 +-
 .../nested/ScalarStringColumnSupplierTest.java     |  45 +-
 .../segment/nested/VariantColumnSupplierTest.java  |  43 +-
 .../druid/storage/StorageConnectorModuleTest.java  |   5 +-
 .../local/LocalFileStorageConnectorTest.java       |  20 +-
 .../query/lookup/MapLookupExtractorFactory.java    |   4 +-
 .../coordinator/CoordinatorDynamicConfig.java      |  38 +-
 .../druid/catalog/model/TableMetadataTest.java     |   1 -
 .../lookup/LookupIntrospectionResourceTest.java    |   4 +-
 .../server/http/CoordinatorDynamicConfigTest.java  |  88 +-
 services/pom.xml                                   |  12 +-
 .../java/org/apache/druid/sql/http/SqlQuery.java   |   7 +-
 .../druid/sql/avatica/DruidAvaticaHandlerTest.java |   1 -
 .../druid/sql/calcite/BaseCalciteQueryTest.java    |   7 +-
 .../apache/druid/sql/calcite/QueryTestRunner.java  |   7 +-
 .../druid/sql/calcite/SqlTestFrameworkConfig.java  |  11 +-
 .../druid/sql/calcite/util/CalciteTests.java       |   1 +
 .../druid/sql/calcite/util/TestDataBuilder.java    |  59 ++
 web-console/jest.common.config.js                  |   2 +
 web-console/package-lock.json                      | 119 +--
 web-console/package.json                           |   2 +-
 web-console/script/create-sql-docs.mjs             |   6 +
 web-console/script/licenses                        |   1 +
 web-console/src/components/auto-form/auto-form.tsx |   1 -
 .../src/components/rule-editor/rule-editor.scss    |   4 +
 .../src/components/rule-editor/rule-editor.tsx     |   2 +-
 .../__snapshots__/bar-unit.spec.tsx.snap           |  13 -
 .../__snapshots__/segment-timeline.spec.tsx.snap   | 293 +++---
 .../src/components/segment-timeline/common.ts      |  68 +-
 .../{segment-timeline.scss => portal-bubble.scss}  |  60 +-
 .../components/segment-timeline/portal-bubble.tsx  |  74 ++
 .../segment-timeline/segment-bar-chart-render.scss | 100 ++-
 .../segment-timeline/segment-bar-chart-render.tsx  | 750 +++++++++++++---
 .../segment-timeline/segment-bar-chart.tsx         | 271 ++----
 .../segment-timeline/segment-timeline.scss         |  41 +-
 .../segment-timeline/segment-timeline.spec.tsx     |   4 +-
 .../segment-timeline/segment-timeline.tsx          | 422 ++++++---
 .../supervisor-history-panel.tsx                   |  13 +-
 .../table-clickable-cell/table-clickable-cell.tsx  |   5 +-
 .../table-filterable-cell.tsx                      |   2 +-
 web-console/src/console-application.tsx            |  15 +-
 .../compaction-history-dialog.tsx                  |   7 +-
 .../coordinator-dynamic-config-dialog.tsx          |   7 +-
 .../overlord-dynamic-config-dialog.tsx             |   7 +-
 .../dialogs/retention-dialog/retention-dialog.tsx  |  15 +-
 .../src/dialogs/status-dialog/status-dialog.tsx    |   3 +-
 .../supervisor-reset-offsets-dialog.tsx            |  11 +-
 .../coordinator-dynamic-config.mock.ts             |   2 -
 .../coordinator-dynamic-config.tsx                 |  14 -
 .../datasource/datasource.ts}                      |  36 +-
 web-console/src/druid-models/index.ts              |   2 +
 .../src/druid-models/load-rule/load-rule.ts        |   1 +
 .../segment/segment.ts}                            |  53 +-
 web-console/src/entry.scss                         |   1 +
 web-console/src/hooks/use-clock.ts                 |  18 +-
 .../src/react-table/react-table-utils.spec.ts      |   4 +
 web-console/src/react-table/react-table-utils.ts   |  50 +-
 .../date-floor-shift-ceil-utc.spec.ts              | 169 ++++
 .../date-floor-shift-ceil.spec.ts                  | 181 ++++
 .../date-floor-shift-ceil/date-floor-shift-ceil.ts | 296 ++++++
 web-console/src/utils/date.spec.ts                 |   9 -
 web-console/src/utils/date.ts                      |  89 +-
 web-console/src/utils/druid-query.ts               |   6 +
 web-console/src/utils/duration/duration.spec.ts    | 505 +++++++++++
 web-console/src/utils/duration/duration.ts         | 388 ++++++++
 web-console/src/utils/general.spec.ts              |   7 -
 web-console/src/utils/general.tsx                  |  16 +-
 web-console/src/utils/index.tsx                    |   2 +
 .../src/utils/query-manager/query-manager.ts       |   2 +-
 web-console/src/utils/table-helpers.ts             |  11 +
 web-console/src/variables.scss                     |   8 +
 .../__snapshots__/datasources-view.spec.tsx.snap   |  15 +-
 .../views/datasources-view/datasources-view.scss   |  15 +-
 .../views/datasources-view/datasources-view.tsx    | 112 ++-
 .../components/source-pane/source-pane.tsx         |   4 +-
 .../modules/multi-axis-chart-module.tsx            |  12 +-
 .../explore-view/modules/time-chart-module.tsx     |  17 +-
 .../src/views/explore-view/utils/duration.spec.ts  |  39 -
 .../src/views/explore-view/utils/duration.ts       |  46 -
 .../explore-view/utils/filter-pattern-helpers.ts   |   8 +-
 .../explore-view/utils/get-auto-granularity.ts     |  85 +-
 web-console/src/views/explore-view/utils/index.ts  |   2 -
 .../explore-view/utils/snap-to-granularity.ts      |  57 --
 .../src/views/explore-view/utils/table-query.ts    |   7 +-
 .../datasources-card/datasources-card.tsx          |  10 +-
 .../home-view/segments-card/segments-card.tsx      |   7 +-
 .../home-view/services-card/services-card.tsx      |  13 +-
 .../supervisors-card/supervisors-card.tsx          |  15 +-
 .../src/views/home-view/tasks-card/tasks-card.tsx  |  17 +-
 .../src/views/load-data-view/load-data-view.tsx    |   4 +-
 .../src/views/lookups-view/lookups-view.tsx        |  10 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |  50 +-
 .../src/views/segments-view/segments-view.scss     |   2 +
 .../src/views/segments-view/segments-view.tsx      | 343 +++----
 .../src/views/services-view/services-view.tsx      |  44 +-
 .../views/supervisors-view/supervisors-view.tsx    | 115 ++-
 web-console/src/views/tasks-view/tasks-view.tsx    |  33 +-
 .../time-menu-items/time-menu-items.tsx            |  28 +-
 .../current-dart-panel/current-dart-panel.tsx      |  12 +-
 .../execution-stages-pane.tsx                      |   2 +-
 .../views/workbench-view/query-tab/query-tab.tsx   |  13 +-
 .../recent-query-task-panel.tsx                    |  18 +-
 .../views/workbench-view/run-panel/run-panel.tsx   |  71 +-
 .../timezone-menu-items.spec.tsx.snap              | 563 ++++++++++++
 .../timezone-menu-items.spec.tsx}                  |  24 +-
 .../timezone-menu-items/timezone-menu-items.tsx    | 135 +++
 .../src/views/workbench-view/work-state-store.ts   |  22 +-
 228 files changed, 8926 insertions(+), 5000 deletions(-)
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/MSQWindowFunctionsBenchmark.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBaseBenchmark.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBasePlanBenchmark.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBaseQueryBenchmark.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBenchmarkDatasets.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlComplexMetricsColumnsBenchmark.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlPlanBenchmark.java
 create mode 100644 benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlProjectionsBenchmark.java
 delete mode 100644 docs/assets/tutorial-kafka-data-loader-05b.png
 delete mode 100644 web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
 copy web-console/src/components/segment-timeline/{segment-timeline.scss => portal-bubble.scss} (58%)
 create mode 100644 web-console/src/components/segment-timeline/portal-bubble.tsx
 copy web-console/src/{components/rule-editor/rule-editor.scss => druid-models/datasource/datasource.ts} (64%)
 copy web-console/src/{components/segment-timeline/segment-timeline.scss => druid-models/segment/segment.ts} (50%)
 create mode 100755 web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil-utc.spec.ts
 create mode 100755 web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.spec.ts
 create mode 100755 web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.ts
 create mode 100755 web-console/src/utils/duration/duration.spec.ts
 create mode 100755 web-console/src/utils/duration/duration.ts
 delete mode 100644 web-console/src/views/explore-view/utils/duration.spec.ts
 delete mode 100644 web-console/src/views/explore-view/utils/duration.ts
 delete mode 100644 web-console/src/views/explore-view/utils/snap-to-granularity.ts
 create mode 100644 web-console/src/views/workbench-view/timezone-menu-items/__snapshots__/timezone-menu-items.spec.tsx.snap
 copy web-console/src/{components/segment-timeline/segment-timeline.spec.tsx => views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx} (61%)
 create mode 100644 web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 33/42: fix

Posted by vo...@apache.org.
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 6278d3d2ff3dcddd94e0c393dff09616547014e2
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 8 13:09:20 2024 -0800

    fix
---
 .../src/components/segment-timeline/segment-bar-chart-render.scss  | 3 ++-
 .../src/components/segment-timeline/segment-bar-chart-render.tsx   | 7 ++++++-
 2 files changed, 8 insertions(+), 2 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 75da5e4e666..de13ad4c123 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
@@ -105,8 +105,9 @@
     .load-rule {
       position: absolute;
       overflow: hidden;
-      border-radius: 3px;
       padding-left: 2px;
+      border-left: 1px solid $dark-gray2;
+      border-right: 1px solid $dark-gray2;
       top: 0;
       height: 100%;
       text-overflow: ellipsis;
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 7ba310048c0..a397c619101 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
@@ -103,13 +103,18 @@ function loadRuleToDateRange(loadRule: Rule): 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(),
       ];
 
+    case 'dropBeforeByPeriod':
+      return [
+        NEGATIVE_INFINITY_DATE,
+        new Duration(loadRule.period || 'P1D').shift(new Date(), TZ_UTC, -1),
+      ];
+
     default:
       return [NEGATIVE_INFINITY_DATE, POSITIVE_INFINITY_DATE];
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 26/42: now line and fixes

Posted by vo...@apache.org.
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 b865422cc54d1e1eeb8ab2284f095930c2ffa962
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 4 20:56:51 2024 -0800

    now line and fixes
---
 .../segment-timeline/segment-bar-chart-render.scss | 13 ++++-------
 .../segment-timeline/segment-bar-chart-render.tsx  | 27 +++++++++++++++++-----
 .../segment-timeline/segment-bar-chart.tsx         |  4 ++--
 web-console/src/hooks/use-clock.ts                 | 18 ++++++---------
 4 files changed, 34 insertions(+), 28 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 c036c313cf3..e6badd45a7c 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
@@ -73,16 +73,11 @@
         opacity: 0.5;
       }
     }
-  }
-
-  .bar-chart-tooltip {
-    position: absolute;
-    left: 20px;
-    right: 0;
 
-    div {
-      display: inline-block;
-      width: 230px;
+    .now-line {
+      stroke: orange;
+      stroke-dasharray: 2, 2;
+      opacity: 0.7;
     }
   }
 
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 adfb5657309..8b29ed428dd 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
@@ -19,14 +19,14 @@
 import type { NonNullDateRange } from '@blueprintjs/datetime';
 import IntervalTree from '@flatten-js/interval-tree';
 import classNames from 'classnames';
-import { max } from 'd3-array';
+import { max, sort, sum } from 'd3-array';
 import { axisBottom, axisLeft } from 'd3-axis';
 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 { useGlobalEventListener } from '../../hooks';
+import { useClock, useGlobalEventListener } from '../../hooks';
 import {
   capitalizeFirst,
   clamp,
@@ -36,6 +36,8 @@ import {
   formatBytes,
   formatNumber,
   groupBy,
+  groupByAsMap,
+  minute,
   month,
   prettyFormatIsoDate,
   TZ_UTC,
@@ -50,7 +52,7 @@ import { aggregateSegmentStats, formatIntervalStat, INTERVAL_STATS } from './com
 import './segment-bar-chart-render.scss';
 
 const CHART_MARGIN: Margin = { top: 10, right: 0, bottom: 25, left: 70 };
-const MIN_BAR_WIDTH = 3;
+const MIN_BAR_WIDTH = 4;
 const POSSIBLE_GRANULARITIES = [
   new Duration('PT15M'),
   new Duration('PT1H'),
@@ -60,14 +62,21 @@ const POSSIBLE_GRANULARITIES = [
   new Duration('P1Y'),
 ];
 
-const EXTEND_X_SCALE_DOMAIN_BY = 10;
+const EXTEND_X_SCALE_DOMAIN_BY = 4;
 
 function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
   return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
 }
 
 function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalBar[] {
-  const sortedIntervalRows = trimmedIntervalRows.sort((a, b) => {
+  // Total size of the datasource will be user as an ordering tiebreaker
+  const datasourceToTotalSize = groupByAsMap(
+    trimmedIntervalRows,
+    intervalRow => intervalRow.datasource,
+    intervalRows => sum(intervalRows, intervalRow => intervalRow.size),
+  );
+
+  const sortedIntervalRows = sort(trimmedIntervalRows, (a, b) => {
     const shownDaysDiff = b.shownDays - a.shownDays;
     if (shownDaysDiff) return shownDaysDiff;
 
@@ -75,7 +84,7 @@ function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalB
       b.originalTimeSpan.getCanonicalLength() - a.originalTimeSpan.getCanonicalLength();
     if (timeSpanDiff) return timeSpanDiff;
 
-    return b.datasource.localeCompare(a.datasource);
+    return datasourceToTotalSize[b.datasource] - datasourceToTotalSize[a.datasource];
   });
 
   const intervalTree = new IntervalTree();
@@ -125,6 +134,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const [bubbleOpenOn, setBubbleOpenOn] = useState<
     { start: Date; end: Date; x: number; y: number; text: string } | undefined
   >();
+  const now = useClock(minute.canonicalLength);
   const svgRef = useRef<SVGSVGElement | null>(null);
 
   const trimGranularity = useMemo(() => {
@@ -348,6 +358,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     };
   }
 
+  const nowX = timeScale(now);
+
   return (
     <div className="segment-bar-chart-render">
       <svg
@@ -400,6 +412,9 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                 height={innerStage.height}
               />
             )}
+            {0 < nowX && nowX < innerStage.width && (
+              <line className="now-line" x1={nowX} x2={nowX} y1={0} y2={innerStage.height + 8} />
+            )}
             {intervalBars.map((intervalBar, i) => {
               return (
                 <rect
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 047fda7fdd2..ba73c8422c6 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -66,7 +66,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
         const query = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
             SqlExpression.and(
-              sql`'${dateRange[0].toISOString()}' <= "end" AND "start" <= '${dateRange[1].toISOString()}'`,
+              sql`"start" <= '${dateRange[1].toISOString()}' AND '${dateRange[0].toISOString()}' < "end"`,
               C('start').unequal(START_OF_TIME_DATE),
               C('end').unequal(END_OF_TIME_DATE),
               C('is_published').equal(1),
@@ -105,7 +105,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
             if (startStr === START_OF_TIME_DATE && endStr === END_OF_TIME_DATE) return;
             const start = new Date(startStr);
             const end = new Date(endStr);
-            if (!(dateRange[0] <= end && start <= dateRange[1])) return;
+            if (!(start <= dateRange[1] && dateRange[0] < end)) return;
 
             return {
               start,
diff --git a/web-console/src/hooks/use-clock.ts b/web-console/src/hooks/use-clock.ts
index 5d126442195..a909a2c1f74 100644
--- a/web-console/src/hooks/use-clock.ts
+++ b/web-console/src/hooks/use-clock.ts
@@ -16,7 +16,9 @@
  * limitations under the License.
  */
 
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
+
+import { useInterval } from './use-interval';
 
 function getNowToSecond(): Date {
   const now = new Date();
@@ -24,18 +26,12 @@ function getNowToSecond(): Date {
   return now;
 }
 
-export function useClock() {
+export function useClock(updateInterval = 1000) {
   const [now, setNow] = useState<Date>(getNowToSecond);
 
-  useEffect(() => {
-    const checkInterval = setInterval(() => {
-      setNow(getNowToSecond());
-    }, 1000);
-
-    return () => {
-      clearInterval(checkInterval);
-    };
-  }, []);
+  useInterval(() => {
+    setNow(getNowToSecond());
+  }, updateInterval);
 
   return now;
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 35/42: add unnest

Posted by vo...@apache.org.
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 b6b5296249962eb5ca8a61143f577b89f5f33bb5
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Nov 12 12:35:24 2024 -0800

    add unnest
---
 web-console/script/create-sql-docs.mjs | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/web-console/script/create-sql-docs.mjs b/web-console/script/create-sql-docs.mjs
index 62e1f44d017..b14055341a0 100755
--- a/web-console/script/create-sql-docs.mjs
+++ b/web-console/script/create-sql-docs.mjs
@@ -39,6 +39,12 @@ const initialFunctionDocs = {
       ),
     ],
   ],
+  UNNEST: [
+    [
+      'arrayExpression',
+      convertMarkdownToHtml("Unnests ARRAY typed values. The source for UNNEST can be an array type column, or an input that's been transformed into an array, such as with helper functions like `MV_TO_ARRAY` or `ARRAY`.")
+    ]
+  ]
 };
 
 function hasHtmlTags(str) {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 17/42: fix supervisor view

Posted by vo...@apache.org.
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 f320470b244a5e4008c9c7cbd4db8bf58d608c0e
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 1 00:10:18 2024 -0700

    fix supervisor view
---
 .../segment-timeline/segment-bar-chart-render.tsx  | 34 +----------
 .../datasource/datasource.ts}                      | 57 +++++++++----------
 web-console/src/druid-models/index.ts              |  1 +
 web-console/src/react-table/react-table-utils.ts   |  7 ++-
 .../views/datasources-view/datasources-view.scss   |  7 +++
 .../views/datasources-view/datasources-view.tsx    | 16 ++++--
 .../src/views/segments-view/segments-view.tsx      | 42 ++++++++++----
 .../views/supervisors-view/supervisors-view.tsx    | 65 ++++++++++++++--------
 8 files changed, 128 insertions(+), 101 deletions(-)

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 44407e22233..026b6e7d0d8 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,6 +25,7 @@ import { scaleLinear, scaleUtc } from 'd3-scale';
 import type React from 'react';
 import { useMemo, useRef, useState } from 'react';
 
+import { getDatasourceColor } from '../../druid-models';
 import { useGlobalEventListener } from '../../hooks';
 import {
   capitalizeFirst,
@@ -36,7 +37,6 @@ import {
   formatInteger,
   formatNumber,
   groupBy,
-  hashJoaat,
   prettyFormatIsoDate,
   TZ_UTC,
 } from '../../utils';
@@ -63,30 +63,6 @@ const POSSIBLE_GRANULARITIES = [
 const EXTEND_DATE_RANGE_BY = 0;
 const EXTEND_X_SCALE_DOMAIN_BY = 10;
 
-const COLORS = [
-  '#b33040',
-  '#d25c4d',
-  '#f2b447',
-  '#d9d574',
-  '#4FAA7E',
-  '#57ceff',
-  '#789113',
-  '#098777',
-  '#b33040',
-  '#d2757b',
-  '#f29063',
-  '#d9a241',
-  '#80aa61',
-  '#c4ff9e',
-  '#915412',
-  '#87606c',
-];
-
-const COLORIZER = ({ datasource }: IntervalBar) => {
-  const hash = hashJoaat(datasource);
-  return COLORS[hash % COLORS.length];
-};
-
 function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
   return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
 }
@@ -390,12 +366,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                   key={i}
                   className="bar-unit"
                   {...segmentBarToRect(intervalBar)}
-                  style={{ fill: COLORIZER(intervalBar) }}
-                  onClick={
-                    intervalBar.datasource
-                      ? () => changeFocusDatasource(intervalBar.datasource)
-                      : undefined
-                  }
+                  style={{ fill: getDatasourceColor(intervalBar.datasource) }}
+                  onClick={() => changeFocusDatasource(intervalBar.datasource)}
                   onMouseOver={() => {
                     if (mouseDownAt) return;
                     setHoverOn(intervalBar);
diff --git a/web-console/src/views/datasources-view/datasources-view.scss b/web-console/src/druid-models/datasource/datasource.ts
similarity index 59%
copy from web-console/src/views/datasources-view/datasources-view.scss
copy to web-console/src/druid-models/datasource/datasource.ts
index 1f9d9df328e..313d711b35c 100644
--- a/web-console/src/views/datasources-view/datasources-view.scss
+++ b/web-console/src/druid-models/datasource/datasource.ts
@@ -16,37 +16,32 @@
  * limitations under the License.
  */
 
-@import '../../variables';
+import { hashJoaat } from '../../utils';
 
-.datasources-view {
-  height: 100%;
-  width: 100%;
-  overflow: auto;
+const COLORS = [
+  '#1f77b4',
+  '#aec7e8',
+  '#ff7f0e',
+  '#ffbb78',
+  '#2ca02c',
+  '#98df8a',
+  '#d62728',
+  '#ff9896',
+  '#9467bd',
+  '#c5b0d5',
+  '#8c564b',
+  '#c49c94',
+  '#e377c2',
+  '#f7b6d2',
+  '#7f7f7f',
+  '#c7c7c7',
+  '#bcbd22',
+  '#dbdb8d',
+  '#17becf',
+  '#9edae5',
+];
 
-  .splitter-layout.timeline-datasources-splitter {
-    position: absolute;
-    top: $view-control-bar-height + $standard-padding;
-    bottom: 0;
-    width: 100%;
-
-    & > .layout-splitter:hover {
-      background: black;
-      opacity: 0.1;
-      border-radius: 2px;
-    }
-  }
-
-  .segment-timeline {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-  }
-
-  .ReactTable {
-    @include pin-full;
-
-    .clickable-cell {
-      cursor: pointer;
-    }
-  }
+export function getDatasourceColor(datasource: string) {
+  const hash = hashJoaat(datasource);
+  return COLORS[hash % COLORS.length];
 }
diff --git a/web-console/src/druid-models/index.ts b/web-console/src/druid-models/index.ts
index 3e5c7062232..e31eedeea73 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -21,6 +21,7 @@ export * from './compaction-config/compaction-config';
 export * from './compaction-status/compaction-status';
 export * from './coordinator-dynamic-config/coordinator-dynamic-config';
 export * from './dart/dart-query-entry';
+export * from './datasource/datasource';
 export * from './dimension-spec/dimension-spec';
 export * from './druid-engine/druid-engine';
 export * from './execution/execution';
diff --git a/web-console/src/react-table/react-table-utils.ts b/web-console/src/react-table/react-table-utils.ts
index ce9497a1209..dc3debdf32c 100644
--- a/web-console/src/react-table/react-table-utils.ts
+++ b/web-console/src/react-table/react-table-utils.ts
@@ -18,8 +18,7 @@
 
 import type { IconName } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import type { SqlExpression } from '@druid-toolkit/query';
-import { C, F } from '@druid-toolkit/query';
+import { C, F, SqlExpression } from '@druid-toolkit/query';
 import type { Filter } from 'react-table';
 
 import { addOrUpdate, caseInsensitiveContains, filterMap } from '../utils';
@@ -153,6 +152,10 @@ export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression | undef
   }
 }
 
+export function sqlQueryCustomTableFilters(filters: Filter[]): SqlExpression {
+  return SqlExpression.and(...filterMap(filters, sqlQueryCustomTableFilter));
+}
+
 export function tableFiltersToString(tableFilters: Filter[]): string {
   return tableFilters
     .map(({ id, value }) => `${id}${value.replace(/[&%]/g, encodeURIComponent)}`)
diff --git a/web-console/src/views/datasources-view/datasources-view.scss b/web-console/src/views/datasources-view/datasources-view.scss
index 1f9d9df328e..91a95bc32a0 100644
--- a/web-console/src/views/datasources-view/datasources-view.scss
+++ b/web-console/src/views/datasources-view/datasources-view.scss
@@ -45,6 +45,13 @@
   .ReactTable {
     @include pin-full;
 
+    .datasource-color-tab {
+      display: inline-block;
+      width: 10px;
+      height: 20px;
+      border-radius: 3px;
+    }
+
     .clickable-cell {
       cursor: pointer;
     }
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index 4477540af66..f58caee1ae8 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -56,6 +56,7 @@ import type {
 import {
   END_OF_TIME_DATE,
   formatCompactionInfo,
+  getDatasourceColor,
   RuleUtil,
   START_OF_TIME_DATE,
   zeroCompactionStatus,
@@ -1145,7 +1146,8 @@ GROUP BY 1, 2`;
 
   private renderDatasourcesTable() {
     const { goToSegments, goToTasks, capabilities, filters, onFiltersChange } = this.props;
-    const { datasourcesAndDefaultRulesState, showUnused, visibleColumns } = this.state;
+    const { datasourcesAndDefaultRulesState, showUnused, visibleColumns, showSegmentTimeline } =
+      this.state;
 
     let { datasources, defaultRules } = datasourcesAndDefaultRulesState.data || { datasources: [] };
 
@@ -1199,12 +1201,18 @@ GROUP BY 1, 2`;
             show: visibleColumns.shown('Datasource name'),
             accessor: 'datasource',
             width: 150,
-            Cell: row => (
+            Cell: ({ value, original }) => (
               <TableClickableCell
-                onClick={() => this.onDetail(row.original)}
+                onClick={() => this.onDetail(original)}
                 hoverIcon={IconNames.SEARCH_TEMPLATE}
               >
-                {row.value}
+                {showSegmentTimeline ? (
+                  <>
+                    <span style={{ color: getDatasourceColor(value) }}>&#9632;</span> {value}
+                  </>
+                ) : (
+                  value
+                )}
               </TableClickableCell>
             ),
           },
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 18dcc4c4cc3..0f7751bf76c 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -20,6 +20,7 @@ import { Button, ButtonGroup, Intent, Label, MenuItem, Switch, Tag } from '@blue
 import { IconNames } from '@blueprintjs/icons';
 import { C, L, SqlComparison, SqlExpression } from '@druid-toolkit/query';
 import * as JSONBig from 'json-bigint-native';
+import type { ReactNode } from 'react';
 import React from 'react';
 import type { Filter, SortingRule } from 'react-table';
 import ReactTable from 'react-table';
@@ -44,7 +45,7 @@ import { AsyncActionDialog } from '../../dialogs';
 import { SegmentTableActionDialog } from '../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
 import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
 import type { QueryWithContext } from '../../druid-models';
-import { computeSegmentTimeSpan } from '../../druid-models';
+import { computeSegmentTimeSpan, getDatasourceColor } from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import {
   booleanCustomTableFilter,
@@ -275,8 +276,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         if (capabilities.hasSql()) {
           const whereExpression = segmentFiltersToExpression(filtered);
 
-          let queryParts: string[];
-
           let filterClause = '';
           if (whereExpression.toString() !== 'TRUE') {
             filterClause = whereExpression.toString();
@@ -296,6 +295,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           const base = SegmentsView.baseQuery(visibleColumns);
           const orderByClause = sortedToOrderByClause(effectiveSorted);
 
+          let queryParts: string[];
           if (groupByInterval) {
             const innerQuery = compact([
               `SELECT "start", "end"`,
@@ -476,7 +476,11 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
     });
   }
 
-  private renderFilterableCell(field: string, enableComparisons = false) {
+  private renderFilterableCell(
+    field: string,
+    enableComparisons = false,
+    valueFn: (value: string) => ReactNode = String,
+  ) {
     const { filters, onFiltersChange } = this.props;
 
     // eslint-disable-next-line react/display-name
@@ -488,14 +492,22 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         onFiltersChange={onFiltersChange}
         enableComparisons={enableComparisons}
       >
-        {row.value}
+        {valueFn(row.value)}
       </TableFilterableCell>
     );
   }
 
   renderSegmentsTable() {
     const { capabilities, filters, onFiltersChange } = this.props;
-    const { segmentsState, visibleColumns, groupByInterval, page, pageSize, sorted } = this.state;
+    const {
+      segmentsState,
+      visibleColumns,
+      groupByInterval,
+      page,
+      pageSize,
+      sorted,
+      showSegmentTimeline,
+    } = this.state;
 
     const segments = segmentsState.data || [];
 
@@ -530,15 +542,15 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         onFilteredChange={onFiltersChange}
         sorted={sorted}
         onSortedChange={sorted => this.setState({ sorted })}
-        showPageJump={false}
-        ofText=""
-        pivotBy={groupByInterval ? ['interval'] : []}
         page={page}
         onPageChange={page => this.setState({ page })}
         pageSize={pageSize}
         onPageSizeChange={pageSize => this.setState({ pageSize })}
         pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
         showPagination
+        showPageJump={false}
+        ofText=""
+        pivotBy={groupByInterval ? ['interval'] : []}
         columns={[
           {
             Header: 'Segment ID',
@@ -561,7 +573,17 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             show: visibleColumns.shown('Datasource'),
             accessor: 'datasource',
             width: 140,
-            Cell: this.renderFilterableCell('datasource'),
+            Cell: this.renderFilterableCell(
+              'datasource',
+              false,
+              showSegmentTimeline
+                ? value => (
+                    <>
+                      <span style={{ color: getDatasourceColor(value) }}>&#9632;</span> {value}
+                    </>
+                  )
+                : String,
+            ),
           },
           {
             Header: 'Interval',
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx
index 2474782e0ae..8c557fe3c54 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -18,11 +18,10 @@
 
 import { Icon, Intent, Menu, MenuItem, Popover, Position, Tag } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { SqlExpression } from '@druid-toolkit/query';
 import * as JSONBig from 'json-bigint-native';
 import type { JSX } from 'react';
 import React from 'react';
-import type { Filter } from 'react-table';
+import type { Filter, SortingRule } from 'react-table';
 import ReactTable from 'react-table';
 
 import type { TableColumnSelectorColumn } from '../../components';
@@ -58,7 +57,7 @@ import type { Capabilities } from '../../helpers';
 import {
   SMALL_TABLE_PAGE_SIZE,
   SMALL_TABLE_PAGE_SIZE_OPTIONS,
-  sqlQueryCustomTableFilter,
+  sqlQueryCustomTableFilters,
 } from '../../react-table';
 import { Api, AppToaster } from '../../singletons';
 import type { AuxiliaryQueryFn, TableState } from '../../utils';
@@ -67,7 +66,6 @@ import {
   changeByIndex,
   checkedCircleIcon,
   deepGet,
-  filterMap,
   formatByteRate,
   formatBytes,
   formatInteger,
@@ -162,7 +160,11 @@ export interface SupervisorsViewState {
 
   supervisorTableActionDialogId?: string;
   supervisorTableActionDialogActions: BasicAction[];
+
   visibleColumns: LocalStorageBackedVisibility;
+  page: number;
+  pageSize: number;
+  sorted: SortingRule[];
 }
 
 function detailedStateToColor(detailedState: string): string {
@@ -223,6 +225,9 @@ export class SupervisorsView extends React.PureComponent<
       visibleColumns: new LocalStorageBackedVisibility(
         LocalStorageKeys.SUPERVISOR_TABLE_COLUMN_SELECTION,
       ),
+      page: 0,
+      pageSize: SMALL_TABLE_PAGE_SIZE,
+      sorted: [],
     };
 
     this.supervisorQueryManager = new QueryManager({
@@ -233,10 +238,11 @@ export class SupervisorsView extends React.PureComponent<
       ) => {
         let supervisors: SupervisorQueryResultRow[];
         if (capabilities.hasSql()) {
+          const whereExpression = sqlQueryCustomTableFilters(filtered);
+
           let filterClause = '';
-          const whereParts = filterMap(filtered, sqlQueryCustomTableFilter);
-          if (whereParts.length) {
-            filterClause = SqlExpression.and(...whereParts).toString();
+          if (whereExpression.toString() !== 'TRUE') {
+            filterClause = whereExpression.toString();
           }
 
           const sqlQuery = assemble(
@@ -355,22 +361,33 @@ export class SupervisorsView extends React.PureComponent<
     });
   }
 
-  private lastTableState: TableState | undefined;
-
   componentWillUnmount(): void {
     this.supervisorQueryManager.terminate();
   }
 
-  private readonly fetchData = (tableState?: TableState) => {
-    const { capabilities } = this.props;
-    const { visibleColumns } = this.state;
-    if (tableState) this.lastTableState = tableState;
-    if (!this.lastTableState) return;
-    const { page, pageSize, filtered, sorted } = this.lastTableState;
+  componentDidUpdate(
+    prevProps: Readonly<SupervisorsViewProps>,
+    prevState: Readonly<SupervisorsViewState>,
+  ) {
+    const { filters } = this.props;
+    const { page, pageSize, sorted } = this.state;
+    if (
+      !sqlQueryCustomTableFilters(filters).equals(sqlQueryCustomTableFilters(prevProps.filters)) ||
+      page !== prevState.page ||
+      pageSize !== prevState.pageSize ||
+      sortedToOrderByClause(sorted) !== sortedToOrderByClause(prevState.sorted)
+    ) {
+      this.fetchData();
+    }
+  }
+
+  private readonly fetchData = () => {
+    const { capabilities, filters } = this.props;
+    const { visibleColumns, page, pageSize, sorted } = this.state;
     this.supervisorQueryManager.runQuery({
       page,
       pageSize,
-      filtered,
+      filtered: filters,
       sorted,
       visibleColumns,
       capabilities,
@@ -663,7 +680,7 @@ export class SupervisorsView extends React.PureComponent<
 
   private renderSupervisorTable() {
     const { goToTasks, filters, onFiltersChange } = this.props;
-    const { supervisorsState, statsKey, visibleColumns } = this.state;
+    const { supervisorsState, statsKey, visibleColumns, page, pageSize, sorted } = this.state;
 
     const supervisors = supervisorsState.data || [];
     return (
@@ -678,14 +695,16 @@ export class SupervisorsView extends React.PureComponent<
         filterable
         filtered={filters}
         onFilteredChange={onFiltersChange}
-        onFetchData={tableState => {
-          this.fetchData(tableState);
-        }}
-        showPageJump={false}
-        ofText=""
-        defaultPageSize={SMALL_TABLE_PAGE_SIZE}
+        sorted={sorted}
+        onSortedChange={sorted => this.setState({ sorted })}
+        page={page}
+        onPageChange={page => this.setState({ page })}
+        pageSize={pageSize}
+        onPageSizeChange={pageSize => this.setState({ pageSize })}
         pageSizeOptions={SMALL_TABLE_PAGE_SIZE_OPTIONS}
         showPagination={supervisors.length > SMALL_TABLE_PAGE_SIZE}
+        showPageJump={false}
+        ofText=""
         columns={[
           {
             Header: twoLines('Supervisor ID', <i>(datasource)</i>),


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 21/42: api

Posted by vo...@apache.org.
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 952b0fb2608fabcaf96fc6ab38d3984aa92f6e43
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 4 09:42:08 2024 -0800

    api
---
 .../segment-timeline/segment-bar-chart.tsx         |   9 +-
 .../segment-timeline/segment-timeline.tsx          |   4 +-
 .../supervisor-history-panel.tsx                   |  13 ++-
 .../compaction-history-dialog.tsx                  |   7 +-
 .../coordinator-dynamic-config-dialog.tsx          |   7 +-
 .../overlord-dynamic-config-dialog.tsx             |   7 +-
 .../dialogs/retention-dialog/retention-dialog.tsx  |  15 +--
 .../src/dialogs/status-dialog/status-dialog.tsx    |   3 +-
 .../supervisor-reset-offsets-dialog.tsx            |  11 +-
 web-console/src/utils/druid-query.ts               |   6 +
 web-console/src/utils/table-helpers.ts             |  11 ++
 .../views/datasources-view/datasources-view.tsx    |  18 +--
 .../datasources-card/datasources-card.tsx          |  10 +-
 .../home-view/segments-card/segments-card.tsx      |   7 +-
 .../home-view/services-card/services-card.tsx      |  13 +--
 .../supervisors-card/supervisors-card.tsx          |  15 ++-
 .../src/views/home-view/tasks-card/tasks-card.tsx  |  17 ++-
 .../src/views/load-data-view/load-data-view.tsx    |   4 +-
 .../src/views/lookups-view/lookups-view.tsx        |   9 +-
 .../src/views/segments-view/segments-view.tsx      | 124 +++++++++------------
 .../src/views/services-view/services-view.tsx      |  44 ++++----
 .../views/supervisors-view/supervisors-view.tsx    |  37 +++---
 web-console/src/views/tasks-view/tasks-view.tsx    |  32 +++---
 23 files changed, 201 insertions(+), 222 deletions(-)

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 17f2866abee..bcbe645c58e 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -23,7 +23,7 @@ import { useMemo } from 'react';
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { Api } from '../../singletons';
-import { Duration, filterMap, queryDruidSql, TZ_UTC } from '../../utils';
+import { Duration, filterMap, getApiArray, queryDruidSql, TZ_UTC } from '../../utils';
 import type { Stage } from '../../utils/stage';
 import { Loader } from '../loader/loader';
 
@@ -82,9 +82,10 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
           };
         }); // 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 })
-        ).data;
+        const datasources = await getApiArray<string>(
+          `/druid/coordinator/v1/datasources`,
+          cancelToken,
+        );
 
         return (
           await Promise.all(
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 5fbc2587ea2..2a683b011ea 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -34,10 +34,10 @@ import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
-import { Api } from '../../singletons';
 import {
   day,
   Duration,
+  getApiArray,
   isNonNullRange,
   localToUtcDateRange,
   prettyFormatIsoDate,
@@ -95,7 +95,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
 
         return tables.map(d => d.TABLE_NAME);
       } else {
-        return (await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken })).data;
+        return await getApiArray(`/druid/coordinator/v1/datasources`, cancelToken);
       }
     },
   });
diff --git a/web-console/src/components/supervisor-history-panel/supervisor-history-panel.tsx b/web-console/src/components/supervisor-history-panel/supervisor-history-panel.tsx
index 74cb55682f3..a7fa38fd445 100644
--- a/web-console/src/components/supervisor-history-panel/supervisor-history-panel.tsx
+++ b/web-console/src/components/supervisor-history-panel/supervisor-history-panel.tsx
@@ -25,7 +25,7 @@ import type { IngestionSpec } from '../../druid-models';
 import { cleanSpec } from '../../druid-models';
 import { useQueryManager } from '../../hooks';
 import { Api } from '../../singletons';
-import { deepSet } from '../../utils';
+import { deepSet, getApiArray } from '../../utils';
 import { Loader } from '../loader/loader';
 import { ShowValue } from '../show-value/show-value';
 
@@ -49,11 +49,12 @@ export const SupervisorHistoryPanel = React.memo(function SupervisorHistoryPanel
   const [historyState] = useQueryManager<string, SupervisorHistoryEntry[]>({
     initQuery: supervisorId,
     processQuery: async (supervisorId, cancelToken) => {
-      const resp = await Api.instance.get(
-        `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/history`,
-        { cancelToken },
-      );
-      return resp.data.map((vs: SupervisorHistoryEntry) => deepSet(vs, 'spec', cleanSpec(vs.spec)));
+      return (
+        await getApiArray<SupervisorHistoryEntry>(
+          `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/history`,
+          cancelToken,
+        )
+      ).map(vs => deepSet(vs, 'spec', cleanSpec(vs.spec)));
     },
   });
 
diff --git a/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx b/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx
index 9e19e043c71..8ee0ffdbfbc 100644
--- a/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx
+++ b/web-console/src/dialogs/compaction-history-dialog/compaction-history-dialog.tsx
@@ -24,7 +24,7 @@ import { Loader, ShowValue } from '../../components';
 import type { CompactionConfig } from '../../druid-models';
 import { useQueryManager } from '../../hooks';
 import { Api } from '../../singletons';
-import { formatInteger, formatPercent } from '../../utils';
+import { formatInteger, formatPercent, getApiArray } from '../../utils';
 import { DiffDialog } from '../diff-dialog/diff-dialog';
 
 import './compaction-history-dialog.scss';
@@ -65,11 +65,10 @@ export const CompactionHistoryDialog = React.memo(function CompactionHistoryDial
     initQuery: datasource,
     processQuery: async (datasource, cancelToken) => {
       try {
-        const resp = await Api.instance.get(
+        return await getApiArray<CompactionHistoryEntry>(
           `/druid/coordinator/v1/config/compaction/${Api.encodePath(datasource)}/history?count=20`,
-          { cancelToken },
+          cancelToken,
         );
-        return resp.data;
       } catch (e) {
         if (e.response?.status === 404) return [];
         throw e;
diff --git a/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx
index ec964f5507e..ab4fed8ae1a 100644
--- a/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx
+++ b/web-console/src/dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog.tsx
@@ -27,7 +27,7 @@ import { COORDINATOR_DYNAMIC_CONFIG_FIELDS } from '../../druid-models';
 import { useQueryManager } from '../../hooks';
 import { getLink } from '../../links';
 import { Api, AppToaster } from '../../singletons';
-import { getDruidErrorMessage } from '../../utils';
+import { getApiArray, getDruidErrorMessage } from '../../utils';
 import { SnitchDialog } from '..';
 
 import './coordinator-dynamic-config-dialog.scss';
@@ -47,10 +47,7 @@ export const CoordinatorDynamicConfigDialog = React.memo(function CoordinatorDyn
   const [historyRecordsState] = useQueryManager<null, any[]>({
     initQuery: null,
     processQuery: async (_, cancelToken) => {
-      const historyResp = await Api.instance.get(`/druid/coordinator/v1/config/history?count=100`, {
-        cancelToken,
-      });
-      return historyResp.data;
+      return await getApiArray(`/druid/coordinator/v1/config/history?count=100`, cancelToken);
     },
   });
 
diff --git a/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx b/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx
index ba30118b0a3..5b1233c6384 100644
--- a/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx
+++ b/web-console/src/dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog.tsx
@@ -27,7 +27,7 @@ import { OVERLORD_DYNAMIC_CONFIG_FIELDS } from '../../druid-models';
 import { useQueryManager } from '../../hooks';
 import { getLink } from '../../links';
 import { Api, AppToaster } from '../../singletons';
-import { getDruidErrorMessage } from '../../utils';
+import { getApiArray, getDruidErrorMessage } from '../../utils';
 import { SnitchDialog } from '..';
 
 import './overlord-dynamic-config-dialog.scss';
@@ -47,10 +47,7 @@ export const OverlordDynamicConfigDialog = React.memo(function OverlordDynamicCo
   const [historyRecordsState] = useQueryManager<null, any[]>({
     initQuery: null,
     processQuery: async (_, cancelToken) => {
-      const historyResp = await Api.instance.get(`/druid/indexer/v1/worker/history?count=100`, {
-        cancelToken,
-      });
-      return historyResp.data;
+      return await getApiArray(`/druid/indexer/v1/worker/history?count=100`, cancelToken);
     },
   });
 
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
index 5ee4d51a3a5..e656a4cbdff 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
@@ -27,7 +27,7 @@ import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { getLink } from '../../links';
 import { Api } from '../../singletons';
-import { filterMap, queryDruidSql, swapElements } from '../../utils';
+import { filterMap, getApiArray, queryDruidSql, swapElements } from '../../utils';
 import { SnitchDialog } from '..';
 
 import './retention-dialog.scss';
@@ -67,11 +67,9 @@ ORDER BY 1`,
 
         return sqlResp.map(d => d.tier);
       } else if (capabilities.hasCoordinatorAccess()) {
-        const allServiceResp = await Api.instance.get('/druid/coordinator/v1/servers?simple', {
-          cancelToken,
-        });
-        return filterMap(allServiceResp.data, (s: any) =>
-          s.type === 'historical' ? s.tier : undefined,
+        return filterMap(
+          await getApiArray('/druid/coordinator/v1/servers?simple', cancelToken),
+          (s: any) => (s.type === 'historical' ? s.tier : undefined),
         );
       } else {
         throw new Error(`must have sql or coordinator access`);
@@ -84,11 +82,10 @@ ORDER BY 1`,
   const [historyQueryState] = useQueryManager<string, any[]>({
     initQuery: props.datasource,
     processQuery: async (datasource, cancelToken) => {
-      const historyResp = await Api.instance.get(
+      return await getApiArray(
         `/druid/coordinator/v1/rules/${Api.encodePath(datasource)}/history?count=200`,
-        { cancelToken },
+        cancelToken,
       );
-      return historyResp.data;
     },
   });
 
diff --git a/web-console/src/dialogs/status-dialog/status-dialog.tsx b/web-console/src/dialogs/status-dialog/status-dialog.tsx
index 311f3e05664..672fc40910f 100644
--- a/web-console/src/dialogs/status-dialog/status-dialog.tsx
+++ b/web-console/src/dialogs/status-dialog/status-dialog.tsx
@@ -50,8 +50,7 @@ export const StatusDialog = React.memo(function StatusDialog(props: StatusDialog
   const [responseState] = useQueryManager<null, StatusResponse>({
     initQuery: null,
     processQuery: async (_, cancelToken) => {
-      const resp = await Api.instance.get(`/status`, { cancelToken });
-      return resp.data;
+      return (await Api.instance.get(`/status`, { cancelToken })).data;
     },
   });
 
diff --git a/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx b/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx
index 009d8326060..d10fc00eb5c 100644
--- a/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx
+++ b/web-console/src/dialogs/supervisor-reset-offsets-dialog/supervisor-reset-offsets-dialog.tsx
@@ -106,11 +106,12 @@ export const SupervisorResetOffsetsDialog = React.memo(function SupervisorResetO
   const [statusResp] = useQueryManager<string, SupervisorStatus>({
     initQuery: supervisorId,
     processQuery: async (supervisorId, cancelToken) => {
-      const statusResp = await Api.instance.get(
-        `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/status`,
-        { cancelToken },
-      );
-      return statusResp.data;
+      return (
+        await Api.instance.get(
+          `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/status`,
+          { cancelToken },
+        )
+      ).data;
     },
   });
 
diff --git a/web-console/src/utils/druid-query.ts b/web-console/src/utils/druid-query.ts
index 8102db89ca3..9e33b01a4e3 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -358,6 +358,12 @@ export async function queryDruidSqlDart<T = any>(
   return sqlResultResp.data;
 }
 
+export async function getApiArray<T = any>(url: string, cancelToken?: CancelToken): Promise<T[]> {
+  const result = (await Api.instance.get(url, { cancelToken })).data;
+  if (!Array.isArray(result)) throw new Error('unexpected result');
+  return result;
+}
+
 export interface QueryExplanation {
   query: any;
   signature: { name: string; type: string }[];
diff --git a/web-console/src/utils/table-helpers.ts b/web-console/src/utils/table-helpers.ts
index 117b34f2d1a..b2073944e46 100644
--- a/web-console/src/utils/table-helpers.ts
+++ b/web-console/src/utils/table-helpers.ts
@@ -18,6 +18,7 @@
 
 import type { QueryResult, SqlExpression } from '@druid-toolkit/query';
 import { C } from '@druid-toolkit/query';
+import { ascending, descending, sort } from 'd3-array';
 import type { Filter, SortingRule } from 'react-table';
 
 import { filterMap, formatNumber, isNumberLike, oneOf } from './general';
@@ -78,3 +79,13 @@ export function sortedToOrderByClause(sorted: SortingRule[]): string | undefined
   if (!sorted.length) return;
   return 'ORDER BY ' + sorted.map(sort => `${C(sort.id)} ${sort.desc ? 'DESC' : 'ASC'}`).join(', ');
 }
+
+export function applySorting(xs: any[], sorted: SortingRule[]): any[] {
+  const firstSortingRule = sorted[0];
+  if (!firstSortingRule) return xs;
+  const { id, desc } = firstSortingRule;
+  return sort(
+    xs,
+    desc ? (d1, d2) => descending(d1[id], d2[id]) : (d1, d2) => ascending(d1[id], d2[id]),
+  );
+}
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index 3d0e6799af6..bfa2262dad1 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -74,6 +74,7 @@ import {
   formatInteger,
   formatMillions,
   formatPercent,
+  getApiArray,
   getDruidErrorMessage,
   groupByAsMap,
   hasPopoverOpen,
@@ -441,15 +442,15 @@ GROUP BY 1, 2`;
           setIntermediateQuery(query);
           datasources = await queryDruidSql({ query }, cancelToken);
         } else if (capabilities.hasCoordinatorAccess()) {
-          const datasourcesResp = await Api.instance.get(
+          const datasourcesResp = await getApiArray(
             '/druid/coordinator/v1/datasources?simple',
-            { cancelToken },
+            cancelToken,
           );
           const loadstatusResp = await Api.instance.get('/druid/coordinator/v1/loadstatus?simple', {
             cancelToken,
           });
           const loadstatus = loadstatusResp.data;
-          datasources = datasourcesResp.data.map((d: any): DatasourceQueryResultRow => {
+          datasources = datasourcesResp.map((d: any): DatasourceQueryResultRow => {
             const totalDataSize = deepGet(d, 'properties.segments.size') || -1;
             const segmentsToLoad = Number(loadstatus[d.name] || 0);
             const availableSegments = Number(deepGet(d, 'properties.segments.count'));
@@ -527,9 +528,10 @@ GROUP BY 1, 2`;
           if (capabilities.hasOverlordAccess()) {
             auxiliaryQueries.push(async (datasourcesAndDefaultRules, cancelToken) => {
               try {
-                const taskList = (
-                  await Api.instance.get(`/druid/indexer/v1/tasks?state=running`, { cancelToken })
-                ).data;
+                const taskList = await getApiArray(
+                  `/druid/indexer/v1/tasks?state=running`,
+                  cancelToken,
+                );
 
                 const runningTasksByDatasource = groupByAsMap(
                   taskList,
@@ -568,10 +570,10 @@ GROUP BY 1, 2`;
           if (showUnused) {
             try {
               unused = (
-                await Api.instance.get<string[]>(
+                await getApiArray<string>(
                   '/druid/coordinator/v1/metadata/datasources?includeUnused',
                 )
-              ).data.filter(d => !seen[d]);
+              ).filter(d => !seen[d]);
             } catch {
               AppToaster.show({
                 icon: IconNames.ERROR,
diff --git a/web-console/src/views/home-view/datasources-card/datasources-card.tsx b/web-console/src/views/home-view/datasources-card/datasources-card.tsx
index 811187117e7..d2a219776c5 100644
--- a/web-console/src/views/home-view/datasources-card/datasources-card.tsx
+++ b/web-console/src/views/home-view/datasources-card/datasources-card.tsx
@@ -21,8 +21,7 @@ import React from 'react';
 
 import type { Capabilities } from '../../../helpers';
 import { useQueryManager } from '../../../hooks';
-import { Api } from '../../../singletons';
-import { pluralIfNeeded, queryDruidSql } from '../../../utils';
+import { getApiArray, pluralIfNeeded, queryDruidSql } from '../../../utils';
 import { HomeViewCard } from '../home-view-card/home-view-card';
 
 export interface DatasourcesCardProps {
@@ -33,7 +32,7 @@ export const DatasourcesCard = React.memo(function DatasourcesCard(props: Dataso
   const [datasourceCountState] = useQueryManager<Capabilities, number>({
     initQuery: props.capabilities,
     processQuery: async (capabilities, cancelToken) => {
-      let datasources: any[];
+      let datasources: string[];
       if (capabilities.hasSql()) {
         datasources = await queryDruidSql(
           {
@@ -42,10 +41,7 @@ export const DatasourcesCard = React.memo(function DatasourcesCard(props: Dataso
           cancelToken,
         );
       } else if (capabilities.hasCoordinatorAccess()) {
-        const datasourcesResp = await Api.instance.get('/druid/coordinator/v1/datasources', {
-          cancelToken,
-        });
-        datasources = datasourcesResp.data;
+        datasources = await getApiArray<string>('/druid/coordinator/v1/datasources', cancelToken);
       } else {
         throw new Error(`must have SQL or coordinator access`);
       }
diff --git a/web-console/src/views/home-view/segments-card/segments-card.tsx b/web-console/src/views/home-view/segments-card/segments-card.tsx
index d5d157a523b..6a86fe5f18e 100644
--- a/web-console/src/views/home-view/segments-card/segments-card.tsx
+++ b/web-console/src/views/home-view/segments-card/segments-card.tsx
@@ -23,7 +23,7 @@ import React from 'react';
 import type { Capabilities } from '../../../helpers';
 import { useQueryManager } from '../../../hooks';
 import { Api } from '../../../singletons';
-import { deepGet, pluralIfNeeded, queryDruidSql } from '../../../utils';
+import { deepGet, getApiArray, pluralIfNeeded, queryDruidSql } from '../../../utils';
 import { HomeViewCard } from '../home-view-card/home-view-card';
 
 export interface SegmentCounts {
@@ -62,11 +62,10 @@ WHERE is_active = 1`,
         const loadstatus = loadstatusResp.data;
         const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
 
-        const datasourcesMetaResp = await Api.instance.get(
+        const datasourcesMeta = await getApiArray(
           '/druid/coordinator/v1/datasources?simple',
-          { cancelToken },
+          cancelToken,
         );
-        const datasourcesMeta = datasourcesMetaResp.data;
         const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
           deepGet(curr, 'properties.segments.count'),
         );
diff --git a/web-console/src/views/home-view/services-card/services-card.tsx b/web-console/src/views/home-view/services-card/services-card.tsx
index 18a66b83218..a04680237a4 100644
--- a/web-console/src/views/home-view/services-card/services-card.tsx
+++ b/web-console/src/views/home-view/services-card/services-card.tsx
@@ -22,8 +22,7 @@ import React from 'react';
 import { PluralPairIfNeeded } from '../../../components';
 import type { Capabilities } from '../../../helpers';
 import { useQueryManager } from '../../../hooks';
-import { Api } from '../../../singletons';
-import { lookupBy, queryDruidSql } from '../../../utils';
+import { getApiArray, lookupBy, queryDruidSql } from '../../../utils';
 import { HomeViewCard } from '../home-view-card/home-view-card';
 
 export interface ServiceCounts {
@@ -45,10 +44,10 @@ export const ServicesCard = React.memo(function ServicesCard(props: ServicesCard
   const [serviceCountState] = useQueryManager<Capabilities, ServiceCounts>({
     processQuery: async (capabilities, cancelToken) => {
       if (capabilities.hasSql()) {
-        const serviceCountsFromQuery: {
+        const serviceCountsFromQuery = await queryDruidSql<{
           service_type: string;
           count: number;
-        }[] = await queryDruidSql(
+        }>(
           {
             query: `SELECT server_type AS "service_type", COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
           },
@@ -60,12 +59,10 @@ export const ServicesCard = React.memo(function ServicesCard(props: ServicesCard
           x => x.count,
         );
       } else if (capabilities.hasCoordinatorAccess()) {
-        const services = (
-          await Api.instance.get('/druid/coordinator/v1/servers?simple', { cancelToken })
-        ).data;
+        const services = await getApiArray('/druid/coordinator/v1/servers?simple', cancelToken);
 
         const middleManager = capabilities.hasOverlordAccess()
-          ? (await Api.instance.get('/druid/indexer/v1/workers', { cancelToken })).data
+          ? await getApiArray('/druid/indexer/v1/workers', cancelToken)
           : [];
 
         return {
diff --git a/web-console/src/views/home-view/supervisors-card/supervisors-card.tsx b/web-console/src/views/home-view/supervisors-card/supervisors-card.tsx
index ab63e0205ac..dca25e32d36 100644
--- a/web-console/src/views/home-view/supervisors-card/supervisors-card.tsx
+++ b/web-console/src/views/home-view/supervisors-card/supervisors-card.tsx
@@ -19,10 +19,10 @@
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
+import type { IngestionSpec } from '../../../druid-models';
 import type { Capabilities } from '../../../helpers';
 import { useQueryManager } from '../../../hooks';
-import { Api } from '../../../singletons';
-import { pluralIfNeeded, queryDruidSql } from '../../../utils';
+import { getApiArray, partition, pluralIfNeeded, queryDruidSql } from '../../../utils';
 import { HomeViewCard } from '../home-view-card/home-view-card';
 
 export interface SupervisorCounts {
@@ -50,11 +50,14 @@ FROM sys.supervisors`,
           )
         )[0];
       } else if (capabilities.hasOverlordAccess()) {
-        const resp = await Api.instance.get('/druid/indexer/v1/supervisor?full', { cancelToken });
-        const data = resp.data;
+        const supervisors = await getApiArray<{ spec: IngestionSpec }>(
+          '/druid/indexer/v1/supervisor?full',
+          cancelToken,
+        );
+        const [running, suspended] = partition(supervisors, d => !d.spec.suspended);
         return {
-          running: data.filter((d: any) => d.spec.suspended === false).length,
-          suspended: data.filter((d: any) => d.spec.suspended === true).length,
+          running: running.length,
+          suspended: suspended.length,
         };
       } else {
         throw new Error(`must have SQL or overlord access`);
diff --git a/web-console/src/views/home-view/tasks-card/tasks-card.tsx b/web-console/src/views/home-view/tasks-card/tasks-card.tsx
index fa91b8a141e..901a4e15581 100644
--- a/web-console/src/views/home-view/tasks-card/tasks-card.tsx
+++ b/web-console/src/views/home-view/tasks-card/tasks-card.tsx
@@ -25,8 +25,7 @@ import type { CapacityInfo } from '../../../druid-models';
 import type { Capabilities } from '../../../helpers';
 import { getClusterCapacity } from '../../../helpers';
 import { useQueryManager } from '../../../hooks';
-import { Api } from '../../../singletons';
-import { lookupBy, pluralIfNeeded, queryDruidSql } from '../../../utils';
+import { getApiArray, groupByAsMap, lookupBy, pluralIfNeeded, queryDruidSql } from '../../../utils';
 import { HomeViewCard } from '../home-view-card/home-view-card';
 
 function getTaskStatus(d: any) {
@@ -62,14 +61,12 @@ GROUP BY 1`,
       x => x.count,
     );
   } else if (capabilities.hasOverlordAccess()) {
-    const tasks: any[] = (await Api.instance.get('/druid/indexer/v1/tasks', { cancelToken })).data;
-    return {
-      success: tasks.filter(d => getTaskStatus(d) === 'SUCCESS').length,
-      failed: tasks.filter(d => getTaskStatus(d) === 'FAILED').length,
-      running: tasks.filter(d => getTaskStatus(d) === 'RUNNING').length,
-      pending: tasks.filter(d => getTaskStatus(d) === 'PENDING').length,
-      waiting: tasks.filter(d => getTaskStatus(d) === 'WAITING').length,
-    };
+    const tasks: any[] = await getApiArray('/druid/indexer/v1/tasks', cancelToken);
+    return groupByAsMap(
+      tasks,
+      d => getTaskStatus(d).toLowerCase(),
+      xs => xs.length,
+    );
   } else {
     throw new Error(`must have SQL or overlord access`);
   }
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx b/web-console/src/views/load-data-view/load-data-view.tsx
index b4bd0d841b6..3767a9852e9 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -157,6 +157,7 @@ import {
   EMPTY_ARRAY,
   EMPTY_OBJECT,
   filterMap,
+  getApiArray,
   getDruidErrorMessage,
   localStorageGetJson,
   LocalStorageKeys,
@@ -3559,8 +3560,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
 
     let existingDatasources: string[];
     try {
-      existingDatasources = (await Api.instance.get<string[]>('/druid/coordinator/v1/datasources'))
-        .data;
+      existingDatasources = await getApiArray<string>('/druid/coordinator/v1/datasources');
     } catch {
       return;
     }
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx b/web-console/src/views/lookups-view/lookups-view.tsx
index 2fbfb70703e..f9d188078a7 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -41,6 +41,7 @@ import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../.
 import { Api, AppToaster } from '../../singletons';
 import {
   deepGet,
+  getApiArray,
   getDruidErrorMessage,
   hasPopoverOpen,
   isLookupsUninitialized,
@@ -124,14 +125,12 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
 
     this.lookupsQueryManager = new QueryManager({
       processQuery: async (_, cancelToken) => {
-        const tiersResp = await Api.instance.get(
+        const tiersResp = await getApiArray(
           '/druid/coordinator/v1/lookups/config?discover=true',
-          { cancelToken },
+          cancelToken,
         );
         const tiers =
-          tiersResp.data && tiersResp.data.length > 0
-            ? tiersResp.data.sort(tierNameCompare)
-            : [DEFAULT_LOOKUP_TIER];
+          tiersResp.length > 0 ? tiersResp.sort(tierNameCompare) : [DEFAULT_LOOKUP_TIER];
 
         const lookupResp = await Api.instance.get('/druid/coordinator/v1/lookups/config/all', {
           cancelToken,
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 8f9aeb5abd4..af93f65b8fb 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -58,12 +58,13 @@ import {
 import { Api } from '../../singletons';
 import type { NumberLike, TableState } from '../../utils';
 import {
+  applySorting,
   compact,
   countBy,
-  deepGet,
   filterMap,
   formatBytes,
   formatInteger,
+  getApiArray,
   hasPopoverOpen,
   isNumberLikeNaN,
   LocalStorageBackedVisibility,
@@ -133,6 +134,9 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[
     'Shard spec',
     'Partition',
     'Size',
+    'Replication factor',
+    'Is realtime',
+    'Is overshadowed',
   ],
 };
 
@@ -365,75 +369,60 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
 
           return result as SegmentQueryResultRow[];
         } else if (capabilities.hasCoordinatorAccess()) {
-          let datasourceList: string[] = (
-            await Api.instance.get('/druid/coordinator/v1/metadata/datasources', { cancelToken })
-          ).data;
-
+          let datasourceList: string[] = [];
           const datasourceFilter = filtered.find(({ id }) => id === 'datasource');
           if (datasourceFilter) {
-            datasourceList = datasourceList.filter(datasource =>
+            datasourceList = (
+              await getApiArray('/druid/coordinator/v1/metadata/datasources', cancelToken)
+            ).filter((datasource: string) =>
               booleanCustomTableFilter(datasourceFilter, datasource),
             );
           }
 
-          if (sorted.length && sorted[0].id === 'datasource') {
-            datasourceList.sort(
-              sorted[0].desc ? (d1, d2) => d2.localeCompare(d1) : (d1, d2) => d1.localeCompare(d2),
-            );
-          }
-
-          const maxResults = (page + 1) * pageSize;
-          let results: SegmentQueryResultRow[] = [];
-
-          const n = Math.min(datasourceList.length, maxResults);
-          for (let i = 0; i < n && results.length < maxResults; i++) {
-            const segments = (
-              await Api.instance.get(
-                `/druid/coordinator/v1/datasources/${Api.encodePath(datasourceList[i])}?full`,
-                { cancelToken },
-              )
-            ).data?.segments;
-            if (!Array.isArray(segments)) continue;
-
-            let segmentQueryResultRows: SegmentQueryResultRow[] = segments.map((segment: any) => {
-              const [start, end] = segment.interval.split('/');
-              return {
-                segment_id: segment.identifier,
-                datasource: segment.dataSource,
-                start,
-                end,
-                interval: segment.interval,
-                version: segment.version,
-                shard_spec: deepGet(segment, 'shardSpec'),
-                partition_num: deepGet(segment, 'shardSpec.partitionNum') || 0,
-                size: segment.size,
-                num_rows: -1,
-                avg_row_size: -1,
-                num_replicas: -1,
-                replication_factor: -1,
-                is_available: -1,
-                is_active: -1,
-                is_realtime: -1,
-                is_published: -1,
-                is_overshadowed: -1,
-              };
-            });
-
-            if (filtered.length) {
-              segmentQueryResultRows = segmentQueryResultRows.filter((d: SegmentQueryResultRow) => {
-                return filtered.every(filter => {
-                  return booleanCustomTableFilter(
-                    filter,
-                    d[filter.id as keyof SegmentQueryResultRow],
-                  );
-                });
+          let results = (
+            await getApiArray(
+              `/druid/coordinator/v1/metadata/segments?includeOvershadowedStatus&includeRealtimeSegments${datasourceList
+                .map(d => `&datasources=${Api.encodePath(d)}`)
+                .join('')}`,
+              cancelToken,
+            )
+          ).map((segment: any) => {
+            const [start, end] = segment.interval.split('/');
+            return {
+              segment_id: segment.identifier,
+              datasource: segment.dataSource,
+              start,
+              end,
+              interval: segment.interval,
+              version: segment.version,
+              shard_spec: segment.shardSpec,
+              partition_num: segment.shardSpec?.partitionNum || 0,
+              size: segment.size,
+              num_rows: -1,
+              avg_row_size: -1,
+              num_replicas: -1,
+              replication_factor: segment.replicationFactor,
+              is_available: -1,
+              is_active: -1,
+              is_realtime: Number(segment.realtime),
+              is_published: -1,
+              is_overshadowed: Number(segment.overshadowed),
+            };
+          });
+
+          if (filtered.length) {
+            results = results.filter((d: SegmentQueryResultRow) => {
+              return filtered.every(filter => {
+                return booleanCustomTableFilter(
+                  filter,
+                  d[filter.id as keyof SegmentQueryResultRow],
+                );
               });
-            }
-
-            results = results.concat(segmentQueryResultRows);
+            });
           }
 
-          return results.slice(page * pageSize, maxResults);
+          const maxResults = (page + 1) * pageSize;
+          return applySorting(results, sorted).slice(page * pageSize, maxResults);
         } else {
           throw new Error('must have SQL or coordinator access to load this view');
         }
@@ -585,7 +574,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             show: visibleColumns.shown('Segment ID'),
             accessor: 'segment_id',
             width: 280,
-            sortable: hasSql,
             filterable: allowGeneralFilter,
             Cell: row => (
               <TableClickableCell
@@ -618,7 +606,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             show: groupByInterval,
             accessor: 'interval',
             width: 120,
-            sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
             Cell: this.renderFilterableCell('interval'),
@@ -629,7 +616,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             accessor: 'start',
             headerClassName: 'enable-comparisons',
             width: 180,
-            sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
             Cell: this.renderFilterableCell('start', true),
@@ -640,7 +626,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             accessor: 'end',
             headerClassName: 'enable-comparisons',
             width: 180,
-            sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
             Cell: this.renderFilterableCell('end', true),
@@ -650,7 +635,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             show: visibleColumns.shown('Version'),
             accessor: 'version',
             width: 180,
-            sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
             Cell: this.renderFilterableCell('version', true),
@@ -796,7 +780,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             accessor: 'partition_num',
             width: 60,
             filterable: false,
-            sortable: hasSql,
             className: 'padded',
           },
           {
@@ -804,7 +787,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             show: visibleColumns.shown('Size'),
             accessor: 'size',
             filterable: false,
-            sortable: hasSql,
             defaultSortDesc: true,
             width: 120,
             className: 'padded',
@@ -864,7 +846,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           },
           {
             Header: twoLines('Replication factor', <i>(desired)</i>),
-            show: hasSql && visibleColumns.shown('Replication factor'),
+            show: visibleColumns.shown('Replication factor'),
             accessor: 'replication_factor',
             width: 80,
             filterable: false,
@@ -891,7 +873,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           },
           {
             Header: 'Is realtime',
-            show: hasSql && visibleColumns.shown('Is realtime'),
+            show: visibleColumns.shown('Is realtime'),
             id: 'is_realtime',
             accessor: row => String(Boolean(row.is_realtime)),
             Filter: BooleanFilterInput,
@@ -909,7 +891,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           },
           {
             Header: 'Is overshadowed',
-            show: hasSql && visibleColumns.shown('Is overshadowed'),
+            show: visibleColumns.shown('Is overshadowed'),
             id: 'is_overshadowed',
             accessor: row => String(Boolean(row.is_overshadowed)),
             Filter: BooleanFilterInput,
diff --git a/web-console/src/views/services-view/services-view.tsx b/web-console/src/views/services-view/services-view.tsx
index 88a5be35c52..000dadeb12c 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -48,6 +48,7 @@ import {
   formatBytes,
   formatBytesCompact,
   formatDurationWithMsIfNeeded,
+  getApiArray,
   hasPopoverOpen,
   LocalStorageBackedVisibility,
   LocalStorageKeys,
@@ -257,24 +258,24 @@ ORDER BY
         if (capabilities.hasSql()) {
           services = await queryDruidSql({ query: ServicesView.SERVICE_SQL }, cancelToken);
         } else if (capabilities.hasCoordinatorAccess()) {
-          services = (
-            await Api.instance.get('/druid/coordinator/v1/servers?simple', { cancelToken })
-          ).data.map((s: any): ServiceResultRow => {
-            const hostParts = s.host.split(':');
-            const port = parseInt(hostParts[1], 10);
-            return {
-              service: s.host,
-              service_type: s.type === 'indexer-executor' ? 'peon' : s.type,
-              tier: s.tier,
-              host: hostParts[0],
-              plaintext_port: port < 9000 ? port : -1,
-              tls_port: port < 9000 ? -1 : port,
-              curr_size: s.currSize,
-              max_size: s.maxSize,
-              start_time: '1970:01:01T00:00:00Z',
-              is_leader: 0,
-            };
-          });
+          services = (await getApiArray('/druid/coordinator/v1/servers?simple', cancelToken)).map(
+            (s: any): ServiceResultRow => {
+              const hostParts = s.host.split(':');
+              const port = parseInt(hostParts[1], 10);
+              return {
+                service: s.host,
+                service_type: s.type === 'indexer-executor' ? 'peon' : s.type,
+                tier: s.tier,
+                host: hostParts[0],
+                plaintext_port: port < 9000 ? port : -1,
+                tls_port: port < 9000 ? -1 : port,
+                curr_size: s.currSize,
+                max_size: s.maxSize,
+                start_time: '1970:01:01T00:00:00Z',
+                is_leader: 0,
+              };
+            },
+          );
         } else {
           throw new Error(`must have SQL or coordinator access`);
         }
@@ -308,9 +309,10 @@ ORDER BY
         if (capabilities.hasOverlordAccess()) {
           auxiliaryQueries.push(async (services, cancelToken) => {
             try {
-              const workerInfos = (
-                await Api.instance.get<WorkerInfo[]>('/druid/indexer/v1/workers', { cancelToken })
-              ).data;
+              const workerInfos = await getApiArray<WorkerInfo>(
+                '/druid/indexer/v1/workers',
+                cancelToken,
+              );
 
               const workerInfoLookup: Record<string, WorkerInfo> = lookupBy(
                 workerInfos,
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx
index c9d75af9680..334339bc7cc 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -70,6 +70,7 @@ import {
   formatBytes,
   formatInteger,
   formatRate,
+  getApiArray,
   getDruidErrorMessage,
   hasPopoverOpen,
   isNumberLike,
@@ -275,26 +276,22 @@ export class SupervisorsView extends React.PureComponent<
             return { ...supervisor, spec: JSONBig.parse(spec) };
           });
         } else if (capabilities.hasOverlordAccess()) {
-          const supervisorList = (
-            await Api.instance.get('/druid/indexer/v1/supervisor?full', { cancelToken })
-          ).data;
-          if (!Array.isArray(supervisorList)) {
-            throw new Error(`Unexpected result from /druid/indexer/v1/supervisor?full`);
-          }
-          supervisors = supervisorList.map((sup: any) => {
-            return {
-              supervisor_id: deepGet(sup, 'id'),
-              type: deepGet(sup, 'spec.tuningConfig.type'),
-              source:
-                deepGet(sup, 'spec.ioConfig.topic') ||
-                deepGet(sup, 'spec.ioConfig.stream') ||
-                'n/a',
-              state: deepGet(sup, 'state'),
-              detailed_state: deepGet(sup, 'detailedState'),
-              spec: sup.spec,
-              suspended: Boolean(deepGet(sup, 'suspended')),
-            };
-          });
+          supervisors = (await getApiArray('/druid/indexer/v1/supervisor?full', cancelToken)).map(
+            (sup: any) => {
+              return {
+                supervisor_id: deepGet(sup, 'id'),
+                type: deepGet(sup, 'spec.tuningConfig.type'),
+                source:
+                  deepGet(sup, 'spec.ioConfig.topic') ||
+                  deepGet(sup, 'spec.ioConfig.stream') ||
+                  'n/a',
+                state: deepGet(sup, 'state'),
+                detailed_state: deepGet(sup, 'detailedState'),
+                spec: sup.spec,
+                suspended: Boolean(deepGet(sup, 'suspended')),
+              };
+            },
+          );
 
           const firstSorted = sorted[0];
           if (firstSorted) {
diff --git a/web-console/src/views/tasks-view/tasks-view.tsx b/web-console/src/views/tasks-view/tasks-view.tsx
index 2bad1a29f66..0e44c96413f 100644
--- a/web-console/src/views/tasks-view/tasks-view.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.tsx
@@ -42,6 +42,7 @@ import { SMALL_TABLE_PAGE_SIZE, SMALL_TABLE_PAGE_SIZE_OPTIONS } from '../../reac
 import { Api, AppToaster } from '../../singletons';
 import {
   formatDuration,
+  getApiArray,
   getDruidErrorMessage,
   hasPopoverOpen,
   LocalStorageBackedVisibility,
@@ -174,8 +175,19 @@ ORDER BY
             cancelToken,
           );
         } else if (capabilities.hasOverlordAccess()) {
-          const resp = await Api.instance.get(`/druid/indexer/v1/tasks`, { cancelToken });
-          return TasksView.parseTasks(resp.data);
+          return (await getApiArray(`/druid/indexer/v1/tasks`, cancelToken)).map(d => {
+            return {
+              task_id: d.id,
+              group_id: d.groupId,
+              type: d.type,
+              created_time: d.createdTime,
+              datasource: d.dataSource,
+              duration: d.duration ? d.duration : 0,
+              error_msg: d.errorMsg,
+              location: d.location.host ? `${d.location.host}:${d.location.port}` : null,
+              status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
+            };
+          });
         } else {
           throw new Error(`must have SQL or overlord access`);
         }
@@ -188,22 +200,6 @@ ORDER BY
     });
   }
 
-  static parseTasks = (data: any[]): TaskQueryResultRow[] => {
-    return data.map(d => {
-      return {
-        task_id: d.id,
-        group_id: d.groupId,
-        type: d.type,
-        created_time: d.createdTime,
-        datasource: d.dataSource,
-        duration: d.duration ? d.duration : 0,
-        error_msg: d.errorMsg,
-        location: d.location.host ? `${d.location.host}:${d.location.port}` : null,
-        status: d.statusCode === 'RUNNING' ? d.runnerStatusCode : d.statusCode,
-      };
-    });
-  };
-
   componentDidMount(): void {
     const { capabilities } = this.props;
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 07/42: improve segment view

Posted by vo...@apache.org.
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 c89e0588747ec95eeda5ef64eac2242ef4ac075e
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Oct 28 14:04:44 2024 -0700

    improve segment view
---
 web-console/src/utils/table-helpers.ts             |  11 +-
 .../src/views/segments-view/segments-view.tsx      | 148 +++++++++++++--------
 2 files changed, 93 insertions(+), 66 deletions(-)

diff --git a/web-console/src/utils/table-helpers.ts b/web-console/src/utils/table-helpers.ts
index 45e8758bf6f..117b34f2d1a 100644
--- a/web-console/src/utils/table-helpers.ts
+++ b/web-console/src/utils/table-helpers.ts
@@ -18,7 +18,7 @@
 
 import type { QueryResult, SqlExpression } from '@druid-toolkit/query';
 import { C } from '@druid-toolkit/query';
-import type { Filter } from 'react-table';
+import type { Filter, SortingRule } from 'react-table';
 
 import { filterMap, formatNumber, isNumberLike, oneOf } from './general';
 import { deepSet } from './object-change';
@@ -67,19 +67,14 @@ export function getNumericColumnBraces(
   return numericColumnBraces;
 }
 
-export interface Sorted {
-  id: string;
-  desc: boolean;
-}
-
 export interface TableState {
   page: number;
   pageSize: number;
   filtered: Filter[];
-  sorted: Sorted[];
+  sorted: SortingRule[];
 }
 
-export function sortedToOrderByClause(sorted: Sorted[]): string | undefined {
+export function sortedToOrderByClause(sorted: SortingRule[]): string | undefined {
   if (!sorted.length) return;
   return 'ORDER BY ' + sorted.map(sort => `${C(sort.id)} ${sort.desc ? 'DESC' : 'ASC'}`).join(', ');
 }
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 74373e097a7..18a44e66180 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
 import { C, L, SqlComparison, SqlExpression } from '@druid-toolkit/query';
 import * as JSONBig from 'json-bigint-native';
 import React from 'react';
-import type { Filter } from 'react-table';
+import type { Filter, SortingRule } from 'react-table';
 import ReactTable from 'react-table';
 
 import {
@@ -127,6 +127,43 @@ function formatRangeDimensionValue(dimension: any, value: any): string {
   return `${C(String(dimension))}=${L(String(value))}`;
 }
 
+function segmentFiltersToExpression(filters: Filter[]): SqlExpression {
+  return SqlExpression.and(
+    ...filterMap(filters, filter => {
+      if (filter.id === 'shard_type') {
+        // Special handling for shard_type that needs to be searched for in the shard_spec
+        // Creates filters like `shard_spec LIKE '%"type":"numbered"%'`
+        const modeAndNeedle = parseFilterModeAndNeedle(filter);
+        if (!modeAndNeedle) return;
+        const shardSpecColumn = C('shard_spec');
+        switch (modeAndNeedle.mode) {
+          case '=':
+            return SqlComparison.like(shardSpecColumn, `%"type":"${modeAndNeedle.needle}"%`);
+
+          case '!=':
+            return SqlComparison.notLike(shardSpecColumn, `%"type":"${modeAndNeedle.needle}"%`);
+
+          default:
+            return SqlComparison.like(shardSpecColumn, `%"type":"${modeAndNeedle.needle}%`);
+        }
+      } else if (filter.id.startsWith('is_')) {
+        switch (filter.value) {
+          case '=false':
+            return C(filter.id).equal(0);
+
+          case '=true':
+            return C(filter.id).equal(1);
+
+          default:
+            return;
+        }
+      } else {
+        return sqlQueryCustomTableFilter(filter);
+      }
+    }),
+  );
+}
+
 interface SegmentsQuery extends TableState {
   visibleColumns: LocalStorageBackedVisibility;
   capabilities: Capabilities;
@@ -167,11 +204,16 @@ export interface SegmentsViewState {
   segmentTableActionDialogId?: string;
   datasourceTableActionDialogId?: string;
   actions: BasicAction[];
-  terminateSegmentId?: string;
-  terminateDatasourceId?: string;
+
   visibleColumns: LocalStorageBackedVisibility;
   groupByInterval: boolean;
   showSegmentTimeline: boolean;
+  page: number;
+  pageSize: number;
+  sorted: SortingRule[];
+
+  terminateSegmentId?: string;
+  terminateDatasourceId?: string;
   showFullShardSpec?: string;
 }
 
@@ -237,8 +279,6 @@ END AS "time_span"`,
 
   private readonly segmentsQueryManager: QueryManager<SegmentsQuery, SegmentQueryResultRow[]>;
 
-  private lastTableState: TableState | undefined;
-
   constructor(props: SegmentsViewProps) {
     super(props);
 
@@ -251,6 +291,13 @@ END AS "time_span"`,
       ),
       groupByInterval: false,
       showSegmentTimeline: false,
+      page: 0,
+      pageSize: STANDARD_TABLE_PAGE_SIZE,
+      sorted: [
+        props.capabilities.hasSql()
+          ? { id: 'start', desc: true }
+          : { id: 'datasource', desc: false },
+      ],
     };
 
     this.segmentsQueryManager = new QueryManager({
@@ -259,48 +306,16 @@ END AS "time_span"`,
         const { page, pageSize, filtered, sorted, visibleColumns, capabilities, groupByInterval } =
           query;
 
-        if (capabilities.hasSql()) {
-          const whereParts = filterMap(filtered, (f: Filter) => {
-            if (f.id === 'shard_type') {
-              // Special handling for shard_type that needs to be searched for in the shard_spec
-              // Creates filters like `shard_spec LIKE '%"type":"numbered"%'`
-              const modeAndNeedle = parseFilterModeAndNeedle(f);
-              if (!modeAndNeedle) return;
-              const shardSpecColumn = C('shard_spec');
-              switch (modeAndNeedle.mode) {
-                case '=':
-                  return SqlComparison.like(shardSpecColumn, `%"type":"${modeAndNeedle.needle}"%`);
-
-                case '!=':
-                  return SqlComparison.notLike(
-                    shardSpecColumn,
-                    `%"type":"${modeAndNeedle.needle}"%`,
-                  );
-
-                default:
-                  return SqlComparison.like(shardSpecColumn, `%"type":"${modeAndNeedle.needle}%`);
-              }
-            } else if (f.id.startsWith('is_')) {
-              switch (f.value) {
-                case '=false':
-                  return C(f.id).equal(0);
-
-                case '=true':
-                  return C(f.id).equal(1);
+        console.log('run', filtered);
 
-                default:
-                  return;
-              }
-            } else {
-              return sqlQueryCustomTableFilter(f);
-            }
-          });
+        if (capabilities.hasSql()) {
+          const whereExpression = segmentFiltersToExpression(filtered);
 
           let queryParts: string[];
 
           let filterClause = '';
-          if (whereParts.length) {
-            filterClause = SqlExpression.and(...whereParts).toString();
+          if (whereExpression.toString() !== 'TRUE') {
+            filterClause = whereExpression.toString();
           }
 
           let effectiveSorted = sorted;
@@ -440,20 +455,38 @@ END AS "time_span"`,
     });
   }
 
+  componentDidMount() {
+    this.fetchData();
+  }
+
   componentWillUnmount(): void {
     this.segmentsQueryManager.terminate();
   }
 
-  private readonly fetchData = (groupByInterval: boolean, tableState?: TableState) => {
-    const { capabilities } = this.props;
-    const { visibleColumns } = this.state;
-    if (tableState) this.lastTableState = tableState;
-    if (!this.lastTableState) return;
-    const { page, pageSize, filtered, sorted } = this.lastTableState;
+  componentDidUpdate(
+    prevProps: Readonly<SegmentsViewProps>,
+    prevState: Readonly<SegmentsViewState>,
+  ) {
+    const { filters } = this.props;
+    const { groupByInterval, page, pageSize, sorted } = this.state;
+    if (
+      !segmentFiltersToExpression(filters).equals(segmentFiltersToExpression(prevProps.filters)) ||
+      groupByInterval !== prevState.groupByInterval ||
+      page !== prevState.page ||
+      pageSize !== prevState.pageSize ||
+      sortedToOrderByClause(sorted) !== sortedToOrderByClause(prevState.sorted)
+    ) {
+      this.fetchData();
+    }
+  }
+
+  private readonly fetchData = () => {
+    const { capabilities, filters } = this.props;
+    const { visibleColumns, groupByInterval, page, pageSize, sorted } = this.state;
     this.segmentsQueryManager.runQuery({
       page,
       pageSize,
-      filtered,
+      filtered: filters,
       sorted,
       visibleColumns,
       capabilities,
@@ -499,7 +532,7 @@ END AS "time_span"`,
 
   renderSegmentsTable() {
     const { capabilities, filters, onFiltersChange } = this.props;
-    const { segmentsState, visibleColumns, groupByInterval } = this.state;
+    const { segmentsState, visibleColumns, groupByInterval, page, pageSize, sorted } = this.state;
 
     const segments = segmentsState.data || [];
 
@@ -532,14 +565,15 @@ END AS "time_span"`,
         filterable
         filtered={filters}
         onFilteredChange={onFiltersChange}
-        defaultSorted={[hasSql ? { id: 'start', desc: true } : { id: 'datasource', desc: false }]}
-        onFetchData={tableState => {
-          this.fetchData(groupByInterval, tableState);
-        }}
+        sorted={sorted}
+        onSortedChange={sorted => this.setState({ sorted })}
         showPageJump={false}
         ofText=""
         pivotBy={groupByInterval ? ['interval'] : []}
-        defaultPageSize={STANDARD_TABLE_PAGE_SIZE}
+        page={page}
+        onPageChange={page => this.setState({ page })}
+        pageSize={pageSize}
+        onPageSizeChange={pageSize => this.setState({ pageSize })}
         pageSizeOptions={STANDARD_TABLE_PAGE_SIZE_OPTIONS}
         showPagination
         columns={[
@@ -984,7 +1018,6 @@ END AS "time_span"`,
               active={!groupByInterval}
               onClick={() => {
                 this.setState({ groupByInterval: false });
-                this.fetchData(false);
               }}
             >
               None
@@ -993,7 +1026,6 @@ END AS "time_span"`,
               active={groupByInterval}
               onClick={() => {
                 this.setState({ groupByInterval: true });
-                this.fetchData(true);
               }}
             >
               Interval
@@ -1015,7 +1047,7 @@ END AS "time_span"`,
             }
             onClose={added => {
               if (!added) return;
-              this.fetchData(groupByInterval);
+              this.fetchData();
             }}
             tableColumnsHidden={visibleColumns.getHiddenColumns()}
           />


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 41/42: fixes

Posted by vo...@apache.org.
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 bc202c9e4923050b60f19a5ff58a5038a51389b9
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 18 12:49:16 2024 -0800

    fixes
---
 .../components/segment-timeline/portal-bubble.scss | 27 ++++++++-
 .../components/segment-timeline/portal-bubble.tsx  | 27 +++++++--
 .../segment-timeline/segment-bar-chart-render.scss | 10 +++-
 .../segment-timeline/segment-bar-chart-render.tsx  | 69 ++++++++++++++++------
 .../segment-timeline/segment-bar-chart.tsx         | 31 +++-------
 .../segment-timeline/segment-timeline.tsx          | 10 ++--
 6 files changed, 119 insertions(+), 55 deletions(-)

diff --git a/web-console/src/components/segment-timeline/portal-bubble.scss b/web-console/src/components/segment-timeline/portal-bubble.scss
index 38663be81eb..8fc2451bdb5 100644
--- a/web-console/src/components/segment-timeline/portal-bubble.scss
+++ b/web-console/src/components/segment-timeline/portal-bubble.scss
@@ -21,8 +21,6 @@
 .portal-bubble {
   position: absolute;
   @include card-like;
-  padding: 5px;
-  white-space: nowrap;
 
   .#{$bp-ns}-dark & {
     background: $dark-gray1;
@@ -39,4 +37,29 @@
   &.mute {
     pointer-events: none;
   }
+
+  & > .bubble-title-bar {
+    position: relative;
+    padding: 5px 5px 0 5px;
+    font-weight: bold;
+
+    &.with-close {
+      padding-right: 26px;
+
+      .close-button {
+        position: absolute;
+        top: 0;
+        right: 0;
+      }
+    }
+  }
+
+  & > .bubble-content {
+    padding: 5px;
+    white-space: nowrap;
+  }
+
+  .bubble-title-bar + .bubble-content {
+    padding-top: 0;
+  }
 }
diff --git a/web-console/src/components/segment-timeline/portal-bubble.tsx b/web-console/src/components/segment-timeline/portal-bubble.tsx
index e35c0246919..f12f6150404 100644
--- a/web-console/src/components/segment-timeline/portal-bubble.tsx
+++ b/web-console/src/components/segment-timeline/portal-bubble.tsx
@@ -16,6 +16,8 @@
  * limitations under the License.
  */
 
+import { Button } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import type { ReactNode } from 'react';
 import { useRef } from 'react';
@@ -27,13 +29,14 @@ import './portal-bubble.scss';
 
 interface PortalBubbleProps {
   className?: string;
-  openOn: { x: number; y: number; text: ReactNode } | undefined;
+  openOn: { x: number; y: number; title?: string; text: ReactNode } | undefined;
   direction?: 'up' | 'down';
+  onClose?(): void;
   mute?: boolean;
 }
 
 export const PortalBubble = function PortalBubble(props: PortalBubbleProps) {
-  const { className, openOn, direction = 'up', mute } = props;
+  const { className, openOn, direction = 'up', onClose, mute } = props;
   const ref = useRef<HTMLDivElement | null>(null);
   if (!openOn) return null;
 
@@ -44,11 +47,27 @@ export const PortalBubble = function PortalBubble(props: PortalBubbleProps) {
 
   return createPortal(
     <div
-      className={classNames('portal-bubble', className, direction, { mute })}
+      className={classNames('portal-bubble', className, direction, {
+        mute: mute && !onClose,
+      })}
       ref={ref}
       style={{ left: x, top: openOn.y }}
     >
-      {openOn.text}
+      {(openOn.title || onClose) && (
+        <div className={classNames('bubble-title-bar', { 'with-close': Boolean(onClose) })}>
+          {openOn.title}
+          {onClose && (
+            <Button
+              className="close-button"
+              icon={IconNames.CROSS}
+              small
+              minimal
+              onClick={onClose}
+            />
+          )}
+        </div>
+      )}
+      <div className="bubble-content">{openOn.text}</div>
     </div>,
     document.body,
   );
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 53d5f93d941..ec9f2d77446 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
@@ -89,7 +89,7 @@
     }
 
     .now-line {
-      stroke: orange;
+      stroke: $orange4;
       stroke-dasharray: 2, 2;
       opacity: 0.7;
     }
@@ -153,3 +153,11 @@
     pointer-events: none;
   }
 }
+
+.segment-bar-chart-bubble {
+  .button-bar {
+    padding-top: 5px;
+    display: flex;
+    gap: 5px;
+  }
+}
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 ac4ce6d8ed7..df4842988c3 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
@@ -16,8 +16,9 @@
  * limitations under the License.
  */
 
-import { Button } from '@blueprintjs/core';
+import { Button, Intent } from '@blueprintjs/core';
 import type { NonNullDateRange } from '@blueprintjs/datetime';
+import { IconNames } from '@blueprintjs/icons';
 import IntervalTree from '@flatten-js/interval-tree';
 import classNames from 'classnames';
 import { max, sort, sum } from 'd3-array';
@@ -205,12 +206,13 @@ export interface DatasourceRules {
 }
 
 export interface SegmentBarChartRenderProps {
+  intervalRows: IntervalRow[];
+  datasourceRules: DatasourceRules | undefined;
+
   stage: Stage;
   dateRange: NonNullDateRange;
   changeDateRange(dateRange: NonNullDateRange): void;
   shownIntervalStat: IntervalStat;
-  intervalRows: IntervalRow[];
-  datasourceRules: DatasourceRules | undefined;
   shownDatasource: string | undefined;
   changeShownDatasource(datasource: string | undefined): void;
 }
@@ -497,25 +499,27 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   console.log('Bar chart render');
 
-  let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined;
+  let hoveredOpenOn: { x: number; y: number; title?: string; text: ReactNode } | undefined;
   if (svgRef.current) {
     const rect = svgRef.current.getBoundingClientRect();
 
     if (bubbleInfo) {
       const hoveredIntervalBars = bubbleInfo.intervalBars;
 
+      let title: string | undefined;
       let text: ReactNode;
       if (hoveredIntervalBars.length === 0) {
-        text = bubbleInfo.timeLabel;
+        title = bubbleInfo.timeLabel;
+        text = '';
       } else if (hoveredIntervalBars.length === 1) {
         const hoveredIntervalBar = hoveredIntervalBars[0];
+        title = `${formatStartDuration(
+          hoveredIntervalBar.start,
+          hoveredIntervalBar.originalTimeSpan,
+        )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`;
         text = (
           <>
-            <div>{`${formatStartDuration(
-              hoveredIntervalBar.start,
-              hoveredIntervalBar.originalTimeSpan,
-            )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
-            <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>
+            {!shownDatasource && <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>}
             <div>{`Size: ${
               hoveredIntervalBar.realtime
                 ? 'estimated for realtime'
@@ -528,10 +532,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       } else {
         const datasources = uniq(hoveredIntervalBars.map(b => b.datasource));
         const agg = aggregateSegmentStats(hoveredIntervalBars);
+        title = bubbleInfo.timeLabel;
         text = (
           <>
-            <div>{bubbleInfo.timeLabel}</div>
-            <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div>
+            {!shownDatasource && (
+              <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div>
+            )}
             <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
             <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
             <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
@@ -545,31 +551,50 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           CHART_MARGIN.left +
           timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)),
         y: rect.y + CHART_MARGIN.top - 10,
+        title,
         text,
       };
     } else if (selection) {
+      const selectedBars = intervalTree.search([
+        selection.start.valueOf() + 1,
+        selection.end.valueOf() - 1,
+      ]) as IntervalBar[];
+      const datasources = uniq(selectedBars.map(b => b.datasource));
+      const agg = aggregateSegmentStats(selectedBars);
       hoveredOpenOn = {
         x:
           rect.x +
           CHART_MARGIN.left +
           timeScale(new Date((selection.start.valueOf() + selection.end.valueOf()) / 2)),
         y: rect.y + CHART_MARGIN.top - 10,
+        title: `${formatIsoDateOnly(selection.start)} → ${formatIsoDateOnly(selection.end)}`,
         text: (
           <>
-            <div>{`${formatIsoDateOnly(selection.start)} → ${formatIsoDateOnly(
-              selection.end,
-            )}`}</div>
+            {!shownDatasource && (
+              <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div>
+            )}
+            <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
+            <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
+            <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
             {selection.done && (
-              <div>
+              <div className="button-bar">
                 <Button
+                  icon={IconNames.ZOOM_IN}
                   text="Zoom in"
+                  intent={Intent.PRIMARY}
+                  small
                   onClick={() => {
                     if (!selection) return;
                     setSelection(undefined);
                     changeDateRange([selection.start, selection.end]);
                   }}
                 />
-                <Button text="Cancel" onClick={() => setSelection(undefined)} />
+                <Button
+                  text="Open in segments view"
+                  small
+                  rightIcon={IconNames.ARROW_TOP_RIGHT}
+                  onClick={() => null}
+                />
               </div>
             )}
           </>
@@ -660,7 +685,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                 />
               );
             })}
-            {bubbleInfo &&
+            {bubbleInfo?.intervalBars.length === 1 &&
               bubbleInfo.intervalBars.map((intervalBar, i) => (
                 <rect key={i} className="hovered-bar" {...segmentBarToRect(intervalBar)} />
               ))}
@@ -699,7 +724,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div className="no-data-text">There are no segments in the selected range</div>
         </div>
       )}
-      <PortalBubble openOn={hoveredOpenOn} mute={!selection?.done} direction="up" />
+      <PortalBubble
+        className="segment-bar-chart-bubble"
+        openOn={hoveredOpenOn}
+        onClose={selection?.done ? () => setSelection(undefined) : undefined}
+        mute
+        direction="up"
+      />
     </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 5a8c162f0c2..20b1dec7045 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -16,7 +16,6 @@
  * limitations under the License.
  */
 
-import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { C, F, L, N, sql, SqlExpression, SqlQuery } from '@druid-toolkit/query';
 import { useMemo } from 'react';
 
@@ -25,34 +24,21 @@ import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { Api } from '../../singletons';
 import { Duration, filterMap, getApiArray, queryDruidSql, TZ_UTC } from '../../utils';
-import type { Stage } from '../../utils/stage';
 import { Loader } from '../loader/loader';
 
-import type { IntervalRow, IntervalStat } from './common';
+import type { IntervalRow } from './common';
+import type { SegmentBarChartRenderProps } from './segment-bar-chart-render';
 import { SegmentBarChartRender } from './segment-bar-chart-render';
 
 import './segment-bar-chart.scss';
 
-interface SegmentBarChartProps {
+interface SegmentBarChartProps
+  extends Omit<SegmentBarChartRenderProps, 'intervalRows' | 'datasourceRules'> {
   capabilities: Capabilities;
-  stage: Stage;
-  dateRange: NonNullDateRange;
-  changeDateRange(newDateRange: NonNullDateRange): void;
-  shownIntervalStat: IntervalStat;
-  shownDatasource: string | undefined;
-  changeShownDatasource(datasource: string | undefined): void;
 }
 
 export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) {
-  const {
-    capabilities,
-    dateRange,
-    changeDateRange,
-    stage,
-    shownIntervalStat,
-    shownDatasource,
-    changeShownDatasource,
-  } = props;
+  const { capabilities, dateRange, shownDatasource, ...otherProps } = props;
 
   const intervalsQuery = useMemo(
     () => ({ capabilities, dateRange, shownDatasource: shownDatasource }),
@@ -158,14 +144,11 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     <>
       {intervalRows && (
         <SegmentBarChartRender
-          stage={stage}
-          dateRange={dateRange}
-          changeDateRange={changeDateRange}
-          shownIntervalStat={shownIntervalStat}
           intervalRows={intervalRows}
           datasourceRules={datasourceRules}
+          dateRange={dateRange}
           shownDatasource={shownDatasource}
-          changeShownDatasource={changeShownDatasource}
+          {...otherProps}
         />
       )}
       {intervalRowsState.loading && <Loader />}
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index ab06abf52c3..72be20a3d83 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -56,11 +56,6 @@ import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
 
-interface SegmentTimelineProps {
-  capabilities: Capabilities;
-  datasource: string | undefined;
-}
-
 const DEFAULT_SHOWN_DURATION = new Duration('P1Y');
 const SHOWN_DURATION_OPTIONS: Duration[] = [
   new Duration('P1D'),
@@ -85,6 +80,11 @@ function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean
   return dr1[0].valueOf() === dr2[0].valueOf() && dr2[1].valueOf() === dr2[1].valueOf();
 }
 
+interface SegmentTimelineProps {
+  capabilities: Capabilities;
+  datasource: string | undefined;
+}
+
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
   const { capabilities, datasource } = props;
   const [stage, setStage] = useState<Stage | undefined>();


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 37/42: portal bubble

Posted by vo...@apache.org.
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 4a1286048a6b647943cdc5aae20a95c2aa2689fb
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Nov 13 13:12:17 2024 -0800

    portal bubble
---
 .../{bubble.scss => portal-bubble.scss}            |  2 +-
 .../{bubble.tsx => portal-bubble.tsx}              | 20 ++++++---
 .../segment-timeline/segment-bar-chart-render.tsx  | 52 +++++++++++++++++-----
 web-console/src/utils/duration/duration.ts         | 41 +++++++++--------
 4 files changed, 77 insertions(+), 38 deletions(-)

diff --git a/web-console/src/components/segment-timeline/bubble.scss b/web-console/src/components/segment-timeline/portal-bubble.scss
similarity index 98%
rename from web-console/src/components/segment-timeline/bubble.scss
rename to web-console/src/components/segment-timeline/portal-bubble.scss
index 5961ae4fbe4..38663be81eb 100644
--- a/web-console/src/components/segment-timeline/bubble.scss
+++ b/web-console/src/components/segment-timeline/portal-bubble.scss
@@ -18,7 +18,7 @@
 
 @import '../../variables';
 
-.bubble {
+.portal-bubble {
   position: absolute;
   @include card-like;
   padding: 5px;
diff --git a/web-console/src/components/segment-timeline/bubble.tsx b/web-console/src/components/segment-timeline/portal-bubble.tsx
similarity index 68%
rename from web-console/src/components/segment-timeline/bubble.tsx
rename to web-console/src/components/segment-timeline/portal-bubble.tsx
index efb72431379..e35c0246919 100644
--- a/web-console/src/components/segment-timeline/bubble.tsx
+++ b/web-console/src/components/segment-timeline/portal-bubble.tsx
@@ -18,25 +18,35 @@
 
 import classNames from 'classnames';
 import type { ReactNode } from 'react';
+import { useRef } from 'react';
 import { createPortal } from 'react-dom';
 
-import './bubble.scss';
+import { clamp } from '../../utils';
 
-interface BubbleProps {
+import './portal-bubble.scss';
+
+interface PortalBubbleProps {
   className?: string;
   openOn: { x: number; y: number; text: ReactNode } | undefined;
   direction?: 'up' | 'down';
   mute?: boolean;
 }
 
-export const Bubble = function Bubble(props: BubbleProps) {
+export const PortalBubble = function PortalBubble(props: PortalBubbleProps) {
   const { className, openOn, direction = 'up', mute } = props;
+  const ref = useRef<HTMLDivElement | null>(null);
   if (!openOn) return null;
 
+  const div: HTMLDivElement | null = ref.current;
+  const myWidth = div ? div.offsetWidth : 200;
+
+  const x = clamp(openOn.x, myWidth / 2, window.innerWidth - myWidth / 2);
+
   return createPortal(
     <div
-      className={classNames('bubble', className, direction, { mute })}
-      style={{ left: openOn.x, top: openOn.y }}
+      className={classNames('portal-bubble', className, direction, { mute })}
+      ref={ref}
+      style={{ left: x, top: openOn.y }}
     >
       {openOn.text}
     </div>,
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 6dcadc9b629..dfbb50fc628 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
@@ -38,15 +38,14 @@ import {
   groupByAsMap,
   minute,
   month,
-  prettyFormatIsoDate,
   TZ_UTC,
 } from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
-import { Bubble } from './bubble';
 import { ChartAxis } from './chart-axis';
 import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
 import { aggregateSegmentStats, formatIntervalStat } from './common';
+import { PortalBubble } from './portal-bubble';
 
 import './segment-bar-chart-render.scss';
 
@@ -63,6 +62,38 @@ const POSSIBLE_GRANULARITIES = [
 
 const EXTEND_X_SCALE_DOMAIN_BY = 1;
 
+function formatStartDuration(start: Date, duration: Duration): string {
+  let sliceLength;
+  const { singleSpan } = duration;
+  switch (singleSpan) {
+    case 'year':
+      sliceLength = 4;
+      break;
+
+    case 'month':
+      sliceLength = 7;
+      break;
+
+    case 'day':
+      sliceLength = 10;
+      break;
+
+    case 'hour':
+      sliceLength = 13;
+      break;
+
+    case 'minute':
+      sliceLength = 16;
+      break;
+
+    default:
+      sliceLength = 19;
+      break;
+  }
+
+  return `${start.toISOString().slice(0, sliceLength)}/${duration}`;
+}
+
 // ---------------------------------------
 // Load rule stuff
 
@@ -413,11 +444,10 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       y: rect.y + CHART_MARGIN.top - 10,
       text: (
         <>
-          <div>{`${prettyFormatIsoDate(hoveredIntervalBar.start)
-            .replace(/:00:00$/, '')
-            .replace(/ 00$/, '')}/${hoveredIntervalBar.originalTimeSpan}${
-            hoveredIntervalBar.realtime ? ' (realtime)' : ''
-          }`}</div>
+          <div>{`${formatStartDuration(
+            hoveredIntervalBar.start,
+            hoveredIntervalBar.originalTimeSpan,
+          )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
           <div>Datasource: {hoveredIntervalBar.datasource}</div>
           <div>{`Size: ${
             hoveredIntervalBar.realtime
@@ -469,7 +499,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
             className="gridline-x"
             transform="translate(0,0)"
             axis={axisLeft(statScale)
-              .ticks(3)
+              .tickValues(statScale.ticks(3).filter(v => v !== 0))
               .tickSize(-innerStage.width)
               .tickFormat(() => '')
               .tickSizeOuter(0)}
@@ -491,7 +521,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <ChartAxis
             className="axis-y"
             axis={axisLeft(statScale)
-              .ticks(5)
+              .ticks(3)
               .tickFormat(e => formatTickRate(e.valueOf()))}
           />
           <g className="bar-group">
@@ -567,8 +597,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div className="no-data-text">There are no segments in the selected range</div>
         </div>
       )}
-      <Bubble openOn={hoveredOpenOn} mute direction="up" />
-      <Bubble openOn={bubbleOpenOn} mute direction="down" />
+      <PortalBubble openOn={hoveredOpenOn} mute direction="up" />
+      <PortalBubble openOn={bubbleOpenOn} mute direction="down" />
     </div>
   );
 };
diff --git a/web-console/src/utils/duration/duration.ts b/web-console/src/utils/duration/duration.ts
index e582556b3f1..bc7236821f1 100755
--- a/web-console/src/utils/duration/duration.ts
+++ b/web-console/src/utils/duration/duration.ts
@@ -21,23 +21,22 @@ import { capitalizeFirst, pluralIfNeeded } from '../general';
 
 export const TZ_UTC = 'Etc/UTC';
 
-const SPANS_WITH_WEEK = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
-const SPANS_WITHOUT_WEEK = ['year', 'month', 'day', 'hour', 'minute', 'second'];
-const SPANS_WITHOUT_WEEK_OR_MONTH = ['year', 'day', 'hour', 'minute', 'second'];
-const SPANS_UP_TO_DAY = ['day', 'hour', 'minute', 'second'];
-
-export interface DurationValue {
-  year?: number;
-  month?: number;
-  week?: number;
-  day?: number;
-  hour?: number;
-  minute?: number;
-  second?: number;
-
-  // Indexable
-  [span: string]: number | undefined;
-}
+export type DurationSpan = 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second';
+
+const SPANS_WITH_WEEK: DurationSpan[] = [
+  'year',
+  'month',
+  'week',
+  'day',
+  'hour',
+  'minute',
+  'second',
+];
+const SPANS_WITHOUT_WEEK: DurationSpan[] = ['year', 'month', 'day', 'hour', 'minute', 'second'];
+const SPANS_WITHOUT_WEEK_OR_MONTH: DurationSpan[] = ['year', 'day', 'hour', 'minute', 'second'];
+const SPANS_UP_TO_DAY: DurationSpan[] = ['day', 'hour', 'minute', 'second'];
+
+export type DurationValue = Partial<Record<DurationSpan, number>>;
 
 const periodWeekRegExp = /^P(\d+)W$/;
 const periodRegExp = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
@@ -116,8 +115,8 @@ function removeZeros(spans: DurationValue): DurationValue {
   return newSpans;
 }
 
-function fitIntoSpans(length: number, spansToCheck: string[]): Record<string, number> {
-  const spans: Record<string, number> = {};
+function fitIntoSpans(length: number, spansToCheck: DurationSpan[]): DurationValue {
+  const spans: DurationValue = {};
 
   let lengthLeft = length;
   for (let i = 0; i < spansToCheck.length; i++) {
@@ -138,7 +137,7 @@ function fitIntoSpans(length: number, spansToCheck: string[]): Record<string, nu
  * Represents an ISO duration like P1DT3H
  */
 export class Duration {
-  public readonly singleSpan?: string;
+  public readonly singleSpan?: DurationSpan;
   public readonly spans: Readonly<DurationValue>;
 
   static fromCanonicalLength(length: number, skipMonths = false): Duration {
@@ -180,7 +179,7 @@ export class Duration {
     const effectiveSpans: DurationValue =
       typeof spans === 'string' ? getSpansFromString(spans) : removeZeros(spans);
 
-    const usedSpans = Object.keys(effectiveSpans);
+    const usedSpans = Object.keys(effectiveSpans) as DurationSpan[];
     if (!usedSpans.length) throw new Error('Duration can not be empty');
     if (usedSpans.length === 1) {
       this.singleSpan = usedSpans[0];


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 36/42: increment store only on appropriate engine

Posted by vo...@apache.org.
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 797fabae237e0b565cc7ab902c95ec1fe26f7fb1
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Nov 12 15:26:20 2024 -0800

    increment store only on appropriate engine
---
 .../current-dart-panel/current-dart-panel.tsx      | 12 ++++--------
 .../views/workbench-view/query-tab/query-tab.tsx   | 13 +++++++------
 .../recent-query-task-panel.tsx                    | 18 ++++--------------
 .../src/views/workbench-view/work-state-store.ts   | 22 +++++++++++++++++-----
 4 files changed, 32 insertions(+), 33 deletions(-)

diff --git a/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx
index 321ad40fc8f..5f104b75f2d 100644
--- a/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx
+++ b/web-console/src/views/workbench-view/current-dart-panel/current-dart-panel.tsx
@@ -20,7 +20,7 @@ import { Button, Icon, Intent, Menu, MenuDivider, MenuItem, Popover } from '@blu
 import { type IconName, IconNames } from '@blueprintjs/icons';
 import classNames from 'classnames';
 import copy from 'copy-to-clipboard';
-import React, { useCallback, useState } from 'react';
+import React, { useState } from 'react';
 import { useStore } from 'zustand';
 
 import { Loader } from '../../../components';
@@ -30,7 +30,7 @@ import { Api, AppToaster } from '../../../singletons';
 import { formatDuration, prettyFormatIsoDate } from '../../../utils';
 import { CancelQueryDialog } from '../cancel-query-dialog/cancel-query-dialog';
 import { DartDetailsDialog } from '../dart-details-dialog/dart-details-dialog';
-import { workStateStore } from '../work-state-store';
+import { getMsqDartVersion, WORK_STATE_STORE } from '../work-state-store';
 
 import './current-dart-panel.scss';
 
@@ -59,13 +59,8 @@ export const CurrentDartPanel = React.memo(function CurrentViberPanel(
   const [showSql, setShowSql] = useState<string | undefined>();
   const [confirmCancelId, setConfirmCancelId] = useState<string | undefined>();
 
-  const workStateVersion = useStore(
-    workStateStore,
-    useCallback(state => state.version, []),
-  );
-
   const [dartQueryEntriesState, queryManager] = useQueryManager<number, DartQueryEntry[]>({
-    query: workStateVersion,
+    query: useStore(WORK_STATE_STORE, getMsqDartVersion),
     processQuery: async (_, cancelToken) => {
       return (await Api.instance.get('/druid/v2/sql/dart', { cancelToken })).data.queries;
     },
@@ -178,6 +173,7 @@ export const CurrentDartPanel = React.memo(function CurrentViberPanel(
                 message: 'Query canceled',
                 intent: Intent.SUCCESS,
               });
+              queryManager.rerunLastQuery();
             } catch {
               AppToaster.show({
                 message: 'Could not cancel query',
diff --git a/web-console/src/views/workbench-view/query-tab/query-tab.tsx b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
index 63865054446..e0bcf54e582 100644
--- a/web-console/src/views/workbench-view/query-tab/query-tab.tsx
+++ b/web-console/src/views/workbench-view/query-tab/query-tab.tsx
@@ -61,7 +61,7 @@ import { metadataStateStore } from '../metadata-state-store';
 import { ResultTablePane } from '../result-table-pane/result-table-pane';
 import type { RunPanelProps } from '../run-panel/run-panel';
 import { RunPanel } from '../run-panel/run-panel';
-import { workStateStore } from '../work-state-store';
+import { WORK_STATE_STORE } from '../work-state-store';
 
 import './query-tab.scss';
 
@@ -362,12 +362,13 @@ export const QueryTab = React.memo(function QueryTab(props: QueryTabProps) {
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [executionState.data, executionState.error]);
 
-  const incrementWorkVersion = useStore(
-    workStateStore,
-    useCallback(state => state.increment, []),
-  );
   useEffect(() => {
-    incrementWorkVersion();
+    const effectiveEngine = query.getEffectiveEngine();
+    if (effectiveEngine === 'sql-msq-task') {
+      WORK_STATE_STORE.getState().incrementMsqTask();
+    } else if (effectiveEngine === 'sql-msq-dart') {
+      WORK_STATE_STORE.getState().incrementMsqDart();
+    }
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [executionState.loading, Boolean(executionState.intermediate)]);
 
diff --git a/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx b/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx
index 682f5fd4d38..33073e961e3 100644
--- a/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx
+++ b/web-console/src/views/workbench-view/recent-query-task-panel/recent-query-task-panel.tsx
@@ -22,7 +22,7 @@ import { IconNames } from '@blueprintjs/icons';
 import { T } from '@druid-toolkit/query';
 import classNames from 'classnames';
 import copy from 'copy-to-clipboard';
-import React, { useCallback, useState } from 'react';
+import React, { useState } from 'react';
 import { useStore } from 'zustand';
 
 import { Loader } from '../../../components';
@@ -38,7 +38,7 @@ import {
   queryDruidSql,
 } from '../../../utils';
 import { CancelQueryDialog } from '../cancel-query-dialog/cancel-query-dialog';
-import { workStateStore } from '../work-state-store';
+import { getMsqTaskVersion, WORK_STATE_STORE } from '../work-state-store';
 
 import './recent-query-task-panel.scss';
 
@@ -83,13 +83,8 @@ export const RecentQueryTaskPanel = React.memo(function RecentQueryTaskPanel(
 
   const [confirmCancelId, setConfirmCancelId] = useState<string | undefined>();
 
-  const workStateVersion = useStore(
-    workStateStore,
-    useCallback(state => state.version, []),
-  );
-
   const [queryTaskHistoryState, queryManager] = useQueryManager<number, RecentQueryEntry[]>({
-    query: workStateVersion,
+    query: useStore(WORK_STATE_STORE, getMsqTaskVersion),
     processQuery: async (_, cancelToken) => {
       return await queryDruidSql<RecentQueryEntry>(
         {
@@ -116,11 +111,6 @@ LIMIT 100`,
 
   const now = useClock();
 
-  const incrementWorkVersion = useStore(
-    workStateStore,
-    useCallback(state => state.increment, []),
-  );
-
   const queryTaskHistory = queryTaskHistoryState.getSomeData();
   return (
     <div className="recent-query-task-panel">
@@ -275,7 +265,7 @@ LIMIT 100`,
                 message: 'Query canceled',
                 intent: Intent.SUCCESS,
               });
-              incrementWorkVersion();
+              queryManager.rerunLastQuery();
             } catch {
               AppToaster.show({
                 message: 'Could not cancel query',
diff --git a/web-console/src/views/workbench-view/work-state-store.ts b/web-console/src/views/workbench-view/work-state-store.ts
index ee03a70c030..38fb8d8e673 100644
--- a/web-console/src/views/workbench-view/work-state-store.ts
+++ b/web-console/src/views/workbench-view/work-state-store.ts
@@ -19,11 +19,23 @@
 import { createStore } from 'zustand';
 
 interface WorkState {
-  version: number;
-  increment(): void;
+  msqTaskVersion: number;
+  msqDartVersion: number;
+  incrementMsqTask(): void;
+  incrementMsqDart(): void;
 }
 
-export const workStateStore = createStore<WorkState>(set => ({
-  version: 0,
-  increment: () => set(state => ({ version: state.version + 1 })),
+export const WORK_STATE_STORE = createStore<WorkState>(set => ({
+  msqTaskVersion: 0,
+  msqDartVersion: 0,
+  incrementMsqTask: () => set(state => ({ msqTaskVersion: state.msqTaskVersion + 1 })),
+  incrementMsqDart: () => set(state => ({ msqDartVersion: state.msqDartVersion + 1 })),
 }));
+
+export function getMsqTaskVersion(store: WorkState): number {
+  return store.msqTaskVersion;
+}
+
+export function getMsqDartVersion(store: WorkState): number {
+  return store.msqDartVersion;
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 18/42: fixes

Posted by vo...@apache.org.
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 7e8003112ccdccef2997844449c9e42be7de8fc2
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 1 11:40:41 2024 -0700

    fixes
---
 .../segment-timeline/segment-bar-chart-render.scss |  7 +-
 .../segment-timeline/segment-bar-chart-render.tsx  | 77 ++++++++++++++--------
 .../segment-timeline/segment-timeline.tsx          | 56 ++++++++++++++--
 .../components/source-pane/source-pane.tsx         |  4 +-
 4 files changed, 105 insertions(+), 39 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 90a6bcbe8d6..c036c313cf3 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
@@ -29,8 +29,13 @@
       user-select: none;
     }
 
+    .hover-highlight {
+      fill: white;
+      fill-opacity: 0.1;
+    }
+
     .hovered-bar {
-      fill: transparent;
+      fill: none;
       stroke: #ffffff;
       stroke-width: 1.5px;
     }
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 026b6e7d0d8..edb3a90e655 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
@@ -37,6 +37,7 @@ import {
   formatInteger,
   formatNumber,
   groupBy,
+  month,
   prettyFormatIsoDate,
   TZ_UTC,
 } from '../../utils';
@@ -49,7 +50,7 @@ import { aggregateSegmentStats } from './common';
 
 import './segment-bar-chart-render.scss';
 
-const CHART_MARGIN: Margin = { top: 40, right: 10, bottom: 25, left: 80 };
+const CHART_MARGIN: Margin = { top: 40, right: 0, bottom: 25, left: 70 };
 const MIN_BAR_WIDTH = 2;
 const POSSIBLE_GRANULARITIES = [
   new Duration('PT15M'),
@@ -60,7 +61,6 @@ const POSSIBLE_GRANULARITIES = [
   new Duration('P1Y'),
 ];
 
-const EXTEND_DATE_RANGE_BY = 0;
 const EXTEND_X_SCALE_DOMAIN_BY = 10;
 
 function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
@@ -124,22 +124,17 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const [dragging, setDragging] = useState<NonNullDateRange | undefined>();
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
   const [bubbleOpenOn, setBubbleOpenOn] = useState<
-    { x: number; y: number; text: string } | undefined
+    { start: Date; end: Date; x: number; y: number; text: string } | undefined
   >();
   const svgRef = useRef<SVGSVGElement | null>(null);
 
-  const expandedDateRange: NonNullDateRange = useMemo(() => {
-    const extend = (dateRange[1].valueOf() - dateRange[0].valueOf()) * EXTEND_DATE_RANGE_BY;
-    return [new Date(dateRange[0].valueOf() - extend), new Date(dateRange[1].valueOf() + extend)];
-  }, [dateRange]);
-
   const trimGranularity = useMemo(() => {
     return Duration.pickSmallestGranularityThatFits(
       POSSIBLE_GRANULARITIES,
-      expandedDateRange[1].valueOf() - expandedDateRange[0].valueOf(),
+      dateRange[1].valueOf() - dateRange[0].valueOf(),
       Math.floor(stage.width / MIN_BAR_WIDTH),
     ).toString();
-  }, [expandedDateRange, stage.width]);
+  }, [dateRange, stage.width]);
 
   const intervalBars = useMemo(() => {
     const trimDuration = new Duration(trimGranularity);
@@ -186,11 +181,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
   const baseTimeScale = scaleUtc()
-    .domain(expandedDateRange)
+    .domain(dateRange)
     .range([EXTEND_X_SCALE_DOMAIN_BY, innerStage.width - EXTEND_X_SCALE_DOMAIN_BY]);
 
   const timeScale = shiftOffset
-    ? baseTimeScale.copy().domain(offsetDateRange(expandedDateRange, shiftOffset))
+    ? baseTimeScale.copy().domain(offsetDateRange(dateRange, shiftOffset))
     : baseTimeScale;
 
   const maxNormalizedStat = max(
@@ -233,10 +228,16 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
+    const time = baseTimeScale.invert(x);
+    const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select';
+    setBubbleOpenOn(undefined);
     setMouseDownAt({
-      time: baseTimeScale.invert(x),
-      action: y > innerStage.height ? 'shift' : 'select',
+      time,
+      action,
     });
+    if (action === 'select') {
+      setDragging([day.floor(time, TZ_UTC), day.ceil(time, TZ_UTC)]);
+    }
   }
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
@@ -248,16 +249,19 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
     if (!mouseDownAt) {
       if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) {
+        const shifter =
+          new Duration(trimGranularity).getCanonicalLength() > day.canonicalLength * 25
+            ? month
+            : day;
         const time = baseTimeScale.invert(x);
-        const granStart = day.floor(time, TZ_UTC);
-        const granEnd = day.ceil(time, TZ_UTC);
+        const start = shifter.floor(time, TZ_UTC);
+        const end = shifter.ceil(time, TZ_UTC);
         setBubbleOpenOn({
-          x:
-            rect.x +
-            CHART_MARGIN.left +
-            baseTimeScale((granStart.valueOf() + granEnd.valueOf()) / 2),
+          start,
+          end,
+          x: rect.x + CHART_MARGIN.left + baseTimeScale((start.valueOf() + end.valueOf()) / 2),
           y: rect.y + CHART_MARGIN.top + innerStage.height + 10,
-          text: granStart.toISOString().slice(0, 10),
+          text: start.toISOString().slice(0, shifter === day ? 10 : 7),
         });
       } else {
         setBubbleOpenOn(undefined);
@@ -298,23 +302,30 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     }
   });
 
+  function startEndToXWidth({ start, end }: { start: Date; end: Date }) {
+    const xStart = clamp(timeScale(start), 0, innerStage.width);
+    const xEnd = clamp(timeScale(end), 0, innerStage.width);
+
+    return {
+      x: xStart,
+      width: Math.max(xEnd - xStart - 1, 1),
+    };
+  }
+
   function segmentBarToRect(intervalBar: IntervalBar) {
-    const xStart = clamp(timeScale(intervalBar.start), 0, innerStage.width);
-    const xEnd = clamp(timeScale(intervalBar.end), 0, innerStage.width);
     const y0 = statScale(intervalBar.offset[shownIntervalStat]);
     const y = statScale(
       intervalBar.normalized[shownIntervalStat] + intervalBar.offset[shownIntervalStat],
     );
 
     return {
-      x: xStart,
+      ...startEndToXWidth(intervalBar),
       y: y,
-      width: Math.max(xEnd - xStart - 1, 1),
       height: Math.abs(y0 - y),
     };
   }
 
-  console.log('here');
+  console.log('Bar chart render');
 
   return (
     <div className="segment-bar-chart-render">
@@ -360,6 +371,14 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
               .tickFormat(e => formatTickRate(e.valueOf()))}
           />
           <g className="bar-group">
+            {bubbleOpenOn && (
+              <rect
+                className="hover-highlight"
+                {...startEndToXWidth(bubbleOpenOn)}
+                y={0}
+                height={innerStage.height}
+              />
+            )}
             {intervalBars.map((intervalBar, i) => {
               return (
                 <rect
@@ -397,13 +416,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
             {!!shiftOffset && (
               <rect
                 className="shifter"
-                x={shiftOffset > 0 ? timeScale(expandedDateRange[1]) : 0}
+                x={shiftOffset > 0 ? timeScale(dateRange[1]) : 0}
                 y={0}
                 height={innerStage.height}
                 width={
                   shiftOffset > 0
-                    ? innerStage.width - timeScale(expandedDateRange[1])
-                    : timeScale(expandedDateRange[0])
+                    ? innerStage.width - timeScale(dateRange[1])
+                    : timeScale(dateRange[0])
                 }
               />
             )}
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index e113fdea571..5fbc2587ea2 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -33,12 +33,15 @@ import type React from 'react';
 import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
+import { useQueryManager } from '../../hooks';
+import { Api } from '../../singletons';
 import {
   day,
   Duration,
   isNonNullRange,
   localToUtcDateRange,
   prettyFormatIsoDate,
+  queryDruidSql,
   TZ_UTC,
   utcToLocalDateRange,
 } from '../../utils';
@@ -79,11 +82,27 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   );
   const [showCustomDatePicker, setShowCustomDatePicker] = useState(false);
 
-  const datasources: string[] = ['wiki', 'kttm']; // ToDo
+  const [datasourcesState] = useQueryManager<Capabilities, string[]>({
+    initQuery: capabilities,
+    processQuery: async (capabilities, cancelToken) => {
+      if (capabilities.hasSql()) {
+        const tables = await queryDruidSql<{ TABLE_NAME: string }>(
+          {
+            query: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'TABLE'`,
+          },
+          cancelToken,
+        );
+
+        return tables.map(d => d.TABLE_NAME);
+      } else {
+        return (await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken })).data;
+      }
+    },
+  });
 
   const DatasourceSelect: React.FC = () => {
     const showAll = 'Show all';
-    const datasourcesWzAll = [showAll].concat(datasources);
+    const datasourcesWzAll = [showAll].concat(datasourcesState.data || []);
     return (
       <Select<string>
         items={datasourcesWzAll}
@@ -116,11 +135,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           }
         }}
       >
-        <Button
-          text={focusDatasource === null ? showAll : focusDatasource}
-          fill
-          rightIcon={IconNames.CARET_DOWN}
-        />
+        <Button text={focusDatasource ?? showAll} rightIcon={IconNames.CARET_DOWN} />
       </Select>
     );
   };
@@ -128,6 +143,32 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   return (
     <div className="segment-timeline">
       <div className="control-bar">
+        <ButtonGroup>
+          <Button
+            icon={IconNames.CARET_LEFT}
+            small
+            onClick={() => {
+              const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
+              setDateRange([d.shift(dateRange[0], TZ_UTC, -1), dateRange[0]]);
+            }}
+          />
+          <Button
+            icon={IconNames.ZOOM_OUT}
+            small
+            onClick={() => {
+              const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
+              setDateRange([d.shift(dateRange[0], TZ_UTC, -1), dateRange[1]]);
+            }}
+          />
+          <Button
+            icon={IconNames.CARET_RIGHT}
+            small
+            onClick={() => {
+              const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
+              setDateRange([dateRange[1], d.shift(dateRange[1], TZ_UTC, 1)]);
+            }}
+          />
+        </ButtonGroup>
         <ButtonGroup>
           {SHOWN_DURATION_OPTIONS.map((d, i) => (
             <Button
@@ -159,6 +200,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           >
             <Button
               icon={IconNames.CALENDAR}
+              small
               data-tooltip={`Select a custom date range\nCurrent range: ${prettyFormatIsoDate(
                 dateRange[0],
               )} - ${prettyFormatIsoDate(dateRange[1])}`}
diff --git a/web-console/src/views/explore-view/components/source-pane/source-pane.tsx b/web-console/src/views/explore-view/components/source-pane/source-pane.tsx
index c97f736d9bc..5922152360b 100644
--- a/web-console/src/views/explore-view/components/source-pane/source-pane.tsx
+++ b/web-console/src/views/explore-view/components/source-pane/source-pane.tsx
@@ -53,8 +53,8 @@ export interface SourcePaneProps {
 export const SourcePane = React.memo(function SourcePane(props: SourcePaneProps) {
   const { selectedSource, onSelectTable, onShowSourceQuery, fill, minimal, disabled } = props;
 
-  const [tables] = useQueryManager<string, string[]>({
-    initQuery: '',
+  const [tables] = useQueryManager<null, string[]>({
+    initQuery: null,
     processQuery: async () => {
       const tables = await queryDruidSql<{ TABLE_NAME: string }>({
         query: `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'TABLE'`,


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 39/42: progress

Posted by vo...@apache.org.
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 ad0517a3cb3b01f2febc338882cea3cad85c897d
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 15 22:20:31 2024 -0800

    progress
---
 .../src/components/segment-timeline/common.ts      |   4 +
 .../segment-timeline/segment-bar-chart-render.scss |   5 +
 .../segment-timeline/segment-bar-chart-render.tsx  | 193 +++++++++++++--------
 .../segment-timeline/segment-bar-chart.tsx         |  34 ++--
 .../segment-timeline/segment-timeline.tsx          |   4 +-
 5 files changed, 145 insertions(+), 95 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index b4cea01062b..48aa5ce48d4 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -81,3 +81,7 @@ export interface TrimmedIntervalRow extends IntervalRow {
 export interface IntervalBar extends TrimmedIntervalRow {
   offset: Record<IntervalStat, number>;
 }
+
+export function formatIsoDateOnly(date: Date): string {
+  return date.toISOString().slice(0, 10);
+}
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 576cf54e146..53d5f93d941 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
@@ -53,6 +53,11 @@
       fill: transparent;
       stroke: #ffffff;
       stroke-width: 1px;
+      opacity: 0.8;
+
+      &.done {
+        opacity: 1;
+      }
     }
 
     .shifter {
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 bc73d3db7a4..ac4ce6d8ed7 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
@@ -16,6 +16,7 @@
  * limitations under the License.
  */
 
+import { Button } from '@blueprintjs/core';
 import type { NonNullDateRange } from '@blueprintjs/datetime';
 import IntervalTree from '@flatten-js/interval-tree';
 import classNames from 'classnames';
@@ -46,7 +47,7 @@ import type { Margin, Stage } from '../../utils/stage';
 
 import { ChartAxis } from './chart-axis';
 import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
-import { aggregateSegmentStats, formatIntervalStat } from './common';
+import { aggregateSegmentStats, formatIntervalStat, formatIsoDateOnly } from './common';
 import { PortalBubble } from './portal-bubble';
 
 import './segment-bar-chart-render.scss';
@@ -192,6 +193,12 @@ interface BubbleInfo {
   intervalBars: IntervalBar[];
 }
 
+interface SelectionRange {
+  start: Date;
+  end: Date;
+  done?: boolean;
+}
+
 export interface DatasourceRules {
   loadRules: Rule[];
   defaultLoadRules: Rule[];
@@ -224,7 +231,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const [mouseDownAt, setMouseDownAt] = useState<
     { time: Date; action: 'select' | 'shift' } | undefined
   >();
-  const [dragging, setDragging] = useState<NonNullDateRange | undefined>();
+  const [selection, setSelection] = useState<SelectionRange | undefined>();
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
   const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>();
   const now = useClock(minute.canonicalLength);
@@ -239,10 +246,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   }, [dateRange, stage.width]);
 
   const { intervalBars, intervalTree } = useMemo(() => {
-    const shownIntervalRows = shownDatasource
-      ? intervalRows.filter(({ datasource }) => datasource === shownDatasource)
-      : intervalRows;
-
+    const shownIntervalRows = intervalRows.filter(
+      ({ start, end, datasource }) =>
+        start <= dateRange[1] &&
+        dateRange[0] < end &&
+        (!shownDatasource || datasource === shownDatasource),
+    );
     const averageRowSizeByDatasource = groupByAsMap(
       shownIntervalRows.filter(intervalRow => intervalRow.size > 0 && intervalRow.rows > 0),
       intervalRow => intervalRow.datasource,
@@ -298,7 +307,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     );
 
     return stackIntervalRows(fullyGroupedSegmentRows);
-  }, [intervalRows, trimGranularity, shownDatasource]);
+  }, [intervalRows, trimGranularity, dateRange, shownDatasource]);
 
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
@@ -334,16 +343,21 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     const svg = svgRef.current;
     if (!svg) return;
     e.preventDefault();
-    const rect = svg.getBoundingClientRect();
-    const x = e.clientX - rect.x - CHART_MARGIN.left;
-    const y = e.clientY - rect.y - CHART_MARGIN.top;
-    const time = baseTimeScale.invert(x);
-    const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select';
-    setBubbleInfo(undefined);
-    setMouseDownAt({
-      time,
-      action,
-    });
+
+    if (selection) {
+      setSelection(undefined);
+    } else {
+      const rect = svg.getBoundingClientRect();
+      const x = e.clientX - rect.x - CHART_MARGIN.left;
+      const y = e.clientY - rect.y - CHART_MARGIN.top;
+      const time = baseTimeScale.invert(x);
+      const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select';
+      setBubbleInfo(undefined);
+      setMouseDownAt({
+        time,
+        action,
+      });
+    }
   }
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
@@ -361,12 +375,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
         setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
       } else {
         if (mouseDownAt.time < b) {
-          setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, TZ_UTC)]);
+          setSelection({ start: day.floor(mouseDownAt.time, TZ_UTC), end: day.ceil(b, TZ_UTC) });
         } else {
-          setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, TZ_UTC)]);
+          setSelection({ start: day.floor(b, TZ_UTC), end: day.ceil(mouseDownAt.time, TZ_UTC) });
         }
       }
-    } else {
+    } else if (!selection) {
       if (
         0 <= x &&
         x <= innerStage.width &&
@@ -421,16 +435,15 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
 
-    if (shiftOffset || dragging) {
-      setDragging(undefined);
+    if (shiftOffset || selection) {
       setShiftOffset(undefined);
       if (mouseDownAt.action === 'shift' || e.shiftKey) {
         if (shiftOffset) {
           changeDateRange(offsetDateRange(dateRange, shiftOffset));
         }
       } else {
-        if (dragging) {
-          changeDateRange(dragging);
+        if (selection) {
+          setSelection({ ...selection, done: true });
         }
       }
     } else if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) {
@@ -455,7 +468,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   useGlobalEventListener('keydown', (e: KeyboardEvent) => {
     if (e.key === 'Escape' && mouseDownAt) {
       setMouseDownAt(undefined);
-      setDragging(undefined);
+      setSelection(undefined);
     }
   });
 
@@ -485,53 +498,84 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   console.log('Bar chart render');
 
   let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined;
-  if (bubbleInfo && svgRef.current) {
-    const hoveredIntervalBars = bubbleInfo.intervalBars;
-
+  if (svgRef.current) {
     const rect = svgRef.current.getBoundingClientRect();
-    let text: ReactNode;
-    if (hoveredIntervalBars.length === 0) {
-      text = bubbleInfo.timeLabel;
-    } else if (hoveredIntervalBars.length === 1) {
-      const hoveredIntervalBar = hoveredIntervalBars[0];
-      text = (
-        <>
-          <div>{`${formatStartDuration(
-            hoveredIntervalBar.start,
-            hoveredIntervalBar.originalTimeSpan,
-          )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
-          <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>
-          <div>{`Size: ${
-            hoveredIntervalBar.realtime
-              ? 'estimated for realtime'
-              : formatIntervalStat('size', hoveredIntervalBar.size)
-          }`}</div>
-          <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div>
-          <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div>
-        </>
-      );
-    } else {
-      const datasources = uniq(hoveredIntervalBars.map(b => b.datasource));
-      const agg = aggregateSegmentStats(hoveredIntervalBars);
-      text = (
-        <>
-          <div>{bubbleInfo.timeLabel}</div>
-          <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div>
-          <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
-          <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
-          <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
-        </>
-      );
-    }
 
-    hoveredOpenOn = {
-      x:
-        rect.x +
-        CHART_MARGIN.left +
-        timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)),
-      y: rect.y + CHART_MARGIN.top - 10,
-      text,
-    };
+    if (bubbleInfo) {
+      const hoveredIntervalBars = bubbleInfo.intervalBars;
+
+      let text: ReactNode;
+      if (hoveredIntervalBars.length === 0) {
+        text = bubbleInfo.timeLabel;
+      } else if (hoveredIntervalBars.length === 1) {
+        const hoveredIntervalBar = hoveredIntervalBars[0];
+        text = (
+          <>
+            <div>{`${formatStartDuration(
+              hoveredIntervalBar.start,
+              hoveredIntervalBar.originalTimeSpan,
+            )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
+            <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>
+            <div>{`Size: ${
+              hoveredIntervalBar.realtime
+                ? 'estimated for realtime'
+                : formatIntervalStat('size', hoveredIntervalBar.size)
+            }`}</div>
+            <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div>
+            <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div>
+          </>
+        );
+      } else {
+        const datasources = uniq(hoveredIntervalBars.map(b => b.datasource));
+        const agg = aggregateSegmentStats(hoveredIntervalBars);
+        text = (
+          <>
+            <div>{bubbleInfo.timeLabel}</div>
+            <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div>
+            <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
+            <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
+            <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
+          </>
+        );
+      }
+
+      hoveredOpenOn = {
+        x:
+          rect.x +
+          CHART_MARGIN.left +
+          timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)),
+        y: rect.y + CHART_MARGIN.top - 10,
+        text,
+      };
+    } else if (selection) {
+      hoveredOpenOn = {
+        x:
+          rect.x +
+          CHART_MARGIN.left +
+          timeScale(new Date((selection.start.valueOf() + selection.end.valueOf()) / 2)),
+        y: rect.y + CHART_MARGIN.top - 10,
+        text: (
+          <>
+            <div>{`${formatIsoDateOnly(selection.start)} → ${formatIsoDateOnly(
+              selection.end,
+            )}`}</div>
+            {selection.done && (
+              <div>
+                <Button
+                  text="Zoom in"
+                  onClick={() => {
+                    if (!selection) return;
+                    setSelection(undefined);
+                    changeDateRange([selection.start, selection.end]);
+                  }}
+                />
+                <Button text="Cancel" onClick={() => setSelection(undefined)} />
+              </div>
+            )}
+          </>
+        ),
+      };
+    }
   }
 
   function renderLoadRule(loadRule: Rule, i: number, isDefault: boolean) {
@@ -620,13 +664,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
               bubbleInfo.intervalBars.map((intervalBar, i) => (
                 <rect key={i} className="hovered-bar" {...segmentBarToRect(intervalBar)} />
               ))}
-            {dragging && (
+            {selection && (
               <rect
-                className="selection"
-                x={timeScale(dragging[0])}
+                className={classNames('selection', { done: selection.done })}
+                {...startEndToXWidth(selection)}
                 y={0}
                 height={innerStage.height}
-                width={timeScale(dragging[1]) - timeScale(dragging[0])}
               />
             )}
             {!!shiftOffset && (
@@ -656,7 +699,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div className="no-data-text">There are no segments in the selected range</div>
         </div>
       )}
-      <PortalBubble openOn={hoveredOpenOn} mute direction="up" />
+      <PortalBubble openOn={hoveredOpenOn} mute={!selection?.done} direction="up" />
     </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 5029a1a2d76..5a8c162f0c2 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -69,7 +69,6 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
               sql`"start" <= '${dateRange[1].toISOString()}' AND '${dateRange[0].toISOString()}' < "end"`,
               C('start').unequal(START_OF_TIME_DATE),
               C('end').unequal(END_OF_TIME_DATE),
-              // C('is_published').equal(1),
               C('is_overshadowed').equal(0),
               shownDatasource ? C('datasource').equal(L(shownDatasource)) : undefined,
             ),
@@ -146,10 +145,6 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     };
   }, [allLoadRulesState.data, shownDatasource]);
 
-  if (intervalRowsState.loading) {
-    return <Loader />;
-  }
-
   if (intervalRowsState.error) {
     return (
       <div className="empty-placeholder">
@@ -158,19 +153,22 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     );
   }
 
-  const intervalRows = intervalRowsState.data;
-  if (!intervalRows) return null;
-
+  const intervalRows = intervalRowsState.getSomeData();
   return (
-    <SegmentBarChartRender
-      stage={stage}
-      dateRange={dateRange}
-      changeDateRange={changeDateRange}
-      shownIntervalStat={shownIntervalStat}
-      intervalRows={intervalRows}
-      datasourceRules={datasourceRules}
-      shownDatasource={shownDatasource}
-      changeShownDatasource={changeShownDatasource}
-    />
+    <>
+      {intervalRows && (
+        <SegmentBarChartRender
+          stage={stage}
+          dateRange={dateRange}
+          changeDateRange={changeDateRange}
+          shownIntervalStat={shownIntervalStat}
+          intervalRows={intervalRows}
+          datasourceRules={datasourceRules}
+          shownDatasource={shownDatasource}
+          changeShownDatasource={changeShownDatasource}
+        />
+      )}
+      {intervalRowsState.loading && <Loader />}
+    </>
   );
 };
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index ae8cde2a9a7..0d8ee73caca 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -51,7 +51,7 @@ import { Stage } from '../../utils/stage';
 import { Loader } from '../loader/loader';
 
 import type { IntervalStat } from './common';
-import { getIntervalStatTitle, INTERVAL_STATS } from './common';
+import { formatIsoDateOnly, getIntervalStatTitle, INTERVAL_STATS } from './common';
 import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
@@ -78,7 +78,7 @@ function getDateRange(shownDuration: Duration): NonNullDateRange {
 }
 
 function formatDateRange(dateRange: NonNullDateRange): string {
-  return `${dateRange[0].toISOString().slice(0, 10)} → ${dateRange[1].toISOString().slice(0, 10)}`;
+  return `${formatIsoDateOnly(dateRange[0])} → ${formatIsoDateOnly(dateRange[1])}`;
 }
 
 function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 30/42: fix zooming

Posted by vo...@apache.org.
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 9c5875555e8f68d436f6a5d84ea31c3f46b71ec4
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Thu Nov 7 09:23:01 2024 -0800

    fix zooming
---
 .../segment-timeline/segment-timeline.tsx          | 48 +++++++++++-----------
 1 file changed, 24 insertions(+), 24 deletions(-)

diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 4446d7fff99..fd07f9cdf15 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -167,45 +167,45 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
     initDatasourceDateRangeState.data ||
     (initDatasourceDateRangeState.isLoading() ? undefined : defaultDateRange);
 
+  let previousDateRange: NonNullDateRange | undefined;
+  let zoomedOutDateRange: NonNullDateRange | undefined;
+  let nextDateRange: NonNullDateRange | undefined;
+  if (effectiveDateRange) {
+    const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
+    const shiftStartBack = d.shift(effectiveDateRange[0], TZ_UTC, -1);
+    const shiftEndForward = d.shift(effectiveDateRange[1], TZ_UTC);
+    const now = day.ceil(new Date(), TZ_UTC);
+    previousDateRange = [shiftStartBack, effectiveDateRange[0]];
+    zoomedOutDateRange = [shiftStartBack, shiftEndForward < now ? shiftEndForward : now];
+    nextDateRange = [effectiveDateRange[1], shiftEndForward];
+  }
+
   return (
     <div className="segment-timeline">
       <div className="control-bar">
         <ButtonGroup>
           <Button
             icon={IconNames.CARET_LEFT}
-            data-tooltip="Previous time period"
+            data-tooltip={
+              previousDateRange && `Previous time period\n${formatDateRange(previousDateRange)}`
+            }
             small
-            disabled={!effectiveDateRange}
-            onClick={() => {
-              if (!effectiveDateRange) return;
-              const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
-              setDateRange([d.shift(effectiveDateRange[0], TZ_UTC, -1), effectiveDateRange[0]]);
-            }}
+            disabled={!previousDateRange}
+            onClick={() => setDateRange(previousDateRange)}
           />
           <Button
             icon={IconNames.ZOOM_OUT}
-            data-tooltip="Zoom out"
+            data-tooltip={zoomedOutDateRange && `Zoom out\n${formatDateRange(zoomedOutDateRange)}`}
             small
-            disabled={!effectiveDateRange}
-            onClick={e => {
-              if (!effectiveDateRange) return;
-              const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
-              setDateRange([
-                d.shift(effectiveDateRange[0], TZ_UTC, -1),
-                e.altKey ? d.shift(effectiveDateRange[1], TZ_UTC, 1) : effectiveDateRange[1],
-              ]);
-            }}
+            disabled={!zoomedOutDateRange}
+            onClick={() => setDateRange(zoomedOutDateRange)}
           />
           <Button
             icon={IconNames.CARET_RIGHT}
-            data-tooltip="Next time period"
+            data-tooltip={nextDateRange && `Next time period\n${formatDateRange(nextDateRange)}`}
             small
-            disabled={!effectiveDateRange}
-            onClick={() => {
-              if (!effectiveDateRange) return;
-              const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
-              setDateRange([effectiveDateRange[1], d.shift(effectiveDateRange[1], TZ_UTC, 1)]);
-            }}
+            disabled={!nextDateRange}
+            onClick={() => setDateRange(nextDateRange)}
           />
         </ButtonGroup>
         <ButtonGroup>


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 14/42: goodies

Posted by vo...@apache.org.
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 1b68c63d47c5d3885a35c8803cc16281bf0ec8e9
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Thu Oct 31 09:08:03 2024 -0700

    goodies
---
 .../segment-timeline/segment-bar-chart-render.tsx  | 30 +++++++++++++++-------
 .../segment-timeline/segment-timeline.scss         |  1 +
 2 files changed, 22 insertions(+), 9 deletions(-)

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 dd813261125..72fff4e891c 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
@@ -58,6 +58,9 @@ const POSSIBLE_GRANULARITIES = [
   new Duration('P1Y'),
 ];
 
+const EXTEND_DATE_RANGE_BY = 0;
+const EXTEND_X_SCALE_DOMAIN_BY = 10;
+
 const COLORS = [
   '#b33040',
   '#d25c4d',
@@ -140,13 +143,18 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
   const svgRef = useRef<SVGSVGElement | null>(null);
 
+  const expandedDateRange: NonNullDateRange = useMemo(() => {
+    const extend = (dateRange[1].valueOf() - dateRange[0].valueOf()) * EXTEND_DATE_RANGE_BY;
+    return [new Date(dateRange[0].valueOf() - extend), new Date(dateRange[1].valueOf() + extend)];
+  }, [dateRange]);
+
   const trimGranularity = useMemo(() => {
     return Duration.pickSmallestGranularityThatFits(
       POSSIBLE_GRANULARITIES,
-      dateRange[1].valueOf() - dateRange[0].valueOf(),
+      expandedDateRange[1].valueOf() - expandedDateRange[0].valueOf(),
       Math.floor(stage.width / MIN_BAR_WIDTH),
     ).toString();
-  }, [dateRange, stage.width]);
+  }, [expandedDateRange, stage.width]);
 
   console.log(trimGranularity);
 
@@ -186,10 +194,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
-  const baseTimeScale = scaleUtc().domain(dateRange).range([0, innerStage.width]);
+  const baseTimeScale = scaleUtc()
+    .domain(expandedDateRange)
+    .range([EXTEND_X_SCALE_DOMAIN_BY, innerStage.width - EXTEND_X_SCALE_DOMAIN_BY]);
 
   const timeScale = shiftOffset
-    ? baseTimeScale.copy().domain(offsetDateRange(dateRange, shiftOffset))
+    ? baseTimeScale.copy().domain(offsetDateRange(expandedDateRange, shiftOffset))
     : baseTimeScale;
 
   const maxStat = max(intervalBars, d => d[shownIntervalStat] + d.offset[shownIntervalStat]);
@@ -374,12 +384,14 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
             {!!shiftOffset && (
               <rect
                 className="shifter"
-                x={timeScale(shiftOffset > 0 ? dateRange[1] : dateRange[0].valueOf() + shiftOffset)}
+                x={shiftOffset > 0 ? timeScale(expandedDateRange[1]) : 0}
                 y={0}
                 height={innerStage.height}
-                width={Math.abs(
-                  timeScale(dateRange[0]) - timeScale(dateRange[0].valueOf() + shiftOffset),
-                )}
+                width={
+                  shiftOffset > 0
+                    ? innerStage.width - timeScale(expandedDateRange[1])
+                    : timeScale(expandedDateRange[0])
+                }
               />
             )}
           </g>
@@ -403,7 +415,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       ) : undefined}
       {!intervalRows.length && (
         <div className="empty-placeholder">
-          <div className="no-data-text">There are no segments in the selected date range</div>
+          <div className="no-data-text">There are no segments in the selected range</div>
         </div>
       )}
     </div>
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index 8295fa3cdfd..6582394869e 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -24,6 +24,7 @@
     height: 40px;
     display: flex;
     align-items: start;
+    padding: 5px;
     gap: 10px;
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 13/42: no highlight

Posted by vo...@apache.org.
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 6a1821a571fd37f11a20333be26ace6f2e5ab704
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 30 20:03:28 2024 -0700

    no highlight
---
 .../segment-timeline/segment-bar-chart-render.scss |  34 +++--
 .../segment-timeline/segment-bar-chart-render.tsx  |  50 ++++---
 .../segment-timeline/segment-bar-chart.tsx         |   2 +-
 .../segment-timeline/segment-timeline.scss         |  36 ++----
 .../segment-timeline/segment-timeline.tsx          | 144 +++++++++++++--------
 5 files changed, 158 insertions(+), 108 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 a30ee800f0a..90a6bcbe8d6 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
@@ -16,21 +16,12 @@
  * limitations under the License.
  */
 
+@import '../../variables';
+
 .segment-bar-chart-render {
   position: relative;
   overflow: hidden;
 
-  .bar-chart-tooltip {
-    position: absolute;
-    left: 20px;
-    right: 0;
-
-    div {
-      display: inline-block;
-      width: 230px;
-    }
-  }
-
   svg {
     position: absolute;
 
@@ -78,4 +69,25 @@
       }
     }
   }
+
+  .bar-chart-tooltip {
+    position: absolute;
+    left: 20px;
+    right: 0;
+
+    div {
+      display: inline-block;
+      width: 230px;
+    }
+  }
+
+  .empty-placeholder {
+    @include pin-full;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 20px;
+    user-select: none;
+    pointer-events: none;
+  }
 }
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 8753f8bca64..dd813261125 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
@@ -31,7 +31,6 @@ import {
   clamp,
   day,
   Duration,
-  formatByteRate,
   formatBytes,
   formatInteger,
   formatNumber,
@@ -48,7 +47,7 @@ import { aggregateSegmentStats, normalizeIntervalRow } from './common';
 
 import './segment-bar-chart-render.scss';
 
-const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 80 };
+const CHART_MARGIN: Margin = { top: 40, right: 10, bottom: 25, left: 80 };
 const MIN_BAR_WIDTH = 2;
 const POSSIBLE_GRANULARITIES = [
   new Duration('PT15M'),
@@ -210,6 +209,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   };
 
   const formatTickRate = (n: number) => {
+    n = (n * new Duration(trimGranularity).getCanonicalLength()) / 1000;
     switch (shownIntervalStat) {
       case 'segments':
         return formatNumber(n) + ' seg/s';
@@ -218,13 +218,14 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
         return formatNumber(n) + ' row/s';
 
       case 'size':
-        return formatByteRate(n);
+        return formatBytes(n);
     }
   };
 
   function handleMouseDown(e: React.MouseEvent) {
     const svg = svgRef.current;
     if (!svg) return;
+    e.preventDefault();
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
@@ -236,6 +237,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
     if (!mouseDownAt) return;
+    e.preventDefault();
+
     const svg = svgRef.current;
     if (!svg) return;
     const rect = svg.getBoundingClientRect();
@@ -254,6 +257,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   useGlobalEventListener('mouseup', (e: MouseEvent) => {
     if (!mouseDownAt) return;
+    e.preventDefault();
+
     setMouseDownAt(undefined);
 
     if (!shiftOffset && !dragging) return;
@@ -286,22 +291,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   return (
     <div className="segment-bar-chart-render">
-      {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: ${prettyFormatIsoDate(hoverOn.start)}/${hoverOn.originalTimeSpan}`}</div>
-          <div>
-            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(
-              hoverOn[shownIntervalStat] * hoverOn.shownSeconds,
-            )}`}
-          </div>
-        </div>
-      ) : undefined}
       <svg
         ref={svgRef}
         width={stage.width}
@@ -318,7 +307,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
             className="gridline-x"
             transform="translate(0,0)"
             axis={axisLeft(statScale)
-              .ticks(5)
+              .ticks(3)
               .tickSize(-innerStage.width)
               .tickFormat(() => '')
               .tickSizeOuter(0)}
@@ -396,6 +385,27 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           </g>
         </g>
       </svg>
+      {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: ${prettyFormatIsoDate(hoverOn.start)}/${hoverOn.originalTimeSpan}`}</div>
+          <div>
+            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(
+              hoverOn[shownIntervalStat] * hoverOn.shownSeconds,
+            )}`}
+          </div>
+        </div>
+      ) : undefined}
+      {!intervalRows.length && (
+        <div className="empty-placeholder">
+          <div className="no-data-text">There are no segments in the selected date range</div>
+        </div>
+      )}
     </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 d88a023f883..73c69bb8973 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -125,7 +125,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
   if (intervalRowsState.error) {
     return (
       <div className="empty-placeholder">
-        <span className="no-data-text">{`Error when loading data: ${intervalRowsState.getErrorMessage()}`}</span>
+        <span className="error-text">{`Error when loading data: ${intervalRowsState.getErrorMessage()}`}</span>
       </div>
     );
   }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index 583ce290ac9..8295fa3cdfd 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -19,6 +19,14 @@
 @import '../../variables';
 
 .segment-timeline {
+  .control-bar {
+    @include card-like;
+    height: 40px;
+    display: flex;
+    align-items: start;
+    gap: 10px;
+  }
+
   .loading-error {
     position: fixed;
     top: 50%;
@@ -26,34 +34,16 @@
     transform: translate(-50%, -50%);
   }
 
-  .side-control {
-    @include card-like;
-    height: 100%;
-    padding: 10px;
-  }
-
   .chart-container {
     position: absolute;
+    top: 40px;
     width: 100%;
-    height: 100%;
+    bottom: 0;
     overflow: hidden;
-  }
-
-  .segment-bar-chart,
-  .segment-bar-chart-render {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-  }
-
-  .empty-placeholder {
-    height: 100%;
 
-    .no-data-text {
-      position: absolute;
-      left: 30vw;
-      top: 15vh;
-      font-size: 20px;
+    .segment-bar-chart,
+    .segment-bar-chart-render {
+      @include pin-full;
     }
   }
 }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 5344916c95e..83208304d69 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,26 +16,33 @@
  * limitations under the License.
  */
 
-import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from '@blueprintjs/core';
+import {
+  Button,
+  ButtonGroup,
+  FormGroup,
+  MenuItem,
+  Popover,
+  ResizeSensor,
+  SegmentedControl,
+} from '@blueprintjs/core';
 import type { NonNullDateRange } from '@blueprintjs/datetime';
-import { DateRangeInput3 } from '@blueprintjs/datetime2';
+import { DateRangePicker3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
 import { Select } from '@blueprintjs/select';
-import enUS from 'date-fns/locale/en-US';
 import type React from 'react';
 import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
 import {
   day,
+  Duration,
   isNonNullRange,
   localToUtcDateRange,
-  month,
+  prettyFormatIsoDate,
   TZ_UTC,
   utcToLocalDateRange,
 } from '../../utils';
 import { Stage } from '../../utils/stage';
-import { SplitterLayout } from '../splitter-layout/splitter-layout';
 
 import type { IntervalStat } from './common';
 import { SegmentBarChart } from './segment-bar-chart';
@@ -46,11 +53,20 @@ interface SegmentTimelineProps {
   capabilities: Capabilities;
 }
 
-const DEFAULT_TIME_SPAN_MONTHS = 3;
+const DEFAULT_SHOWN_DURATION = new Duration('P3M');
+const SHOWN_DURATION_OPTIONS: Duration[] = [
+  new Duration('P1D'),
+  new Duration('P1W'),
+  new Duration('P1M'),
+  new Duration('P3M'),
+  new Duration('P1Y'),
+  new Duration('P5Y'),
+  new Duration('P10Y'),
+];
 
-function getDefaultDateRange(): NonNullDateRange {
+function getDateRange(shownDuration: Duration): NonNullDateRange {
   const end = day.ceil(new Date(), TZ_UTC);
-  return [month.shift(end, TZ_UTC, -DEFAULT_TIME_SPAN_MONTHS), end];
+  return [shownDuration.shift(end, TZ_UTC, -1), end];
 }
 
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
@@ -58,7 +74,10 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   const [stage, setStage] = useState<Stage | undefined>();
   const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
   const [activeDatasource, setActiveDatasource] = useState<string | undefined>();
-  const [dateRange, setDateRange] = useState<NonNullDateRange>(getDefaultDateRange);
+  const [dateRange, setDateRange] = useState<NonNullDateRange>(
+    getDateRange(DEFAULT_SHOWN_DURATION),
+  );
+  const [showCustomDatePicker, setShowCustomDatePicker] = useState(false);
 
   const datasources: string[] = ['wiki', 'kttm']; // ToDo
 
@@ -96,7 +115,6 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             return normalizedTitle.includes(normalizedQuery);
           }
         }}
-        fill
       >
         <Button
           text={activeDatasource === null ? showAll : activeDatasource}
@@ -108,39 +126,50 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   };
 
   return (
-    <SplitterLayout
-      className="segment-timeline"
-      primaryMinSize={400}
-      secondaryInitialSize={220}
-      secondaryMaxSize={400}
-    >
-      <ResizeSensor
-        onResize={(entries: ResizeObserverEntry[]) => {
-          const rect = entries[0].contentRect;
-          setStage(new Stage(rect.width, rect.height));
-        }}
-      >
-        <div className="chart-container">
-          {stage && (
-            <SegmentBarChart
-              capabilities={capabilities}
-              stage={stage}
-              dateRange={dateRange}
-              changeDateRange={setDateRange}
-              shownIntervalStat={activeSegmentStat}
-              changeActiveDatasource={(datasource: string | undefined) =>
-                setActiveDatasource(activeDatasource ? undefined : datasource)
-              }
+    <div className="segment-timeline">
+      <div className="control-bar">
+        <ButtonGroup>
+          {SHOWN_DURATION_OPTIONS.map((d, i) => (
+            <Button
+              key={i}
+              text={d.toString().replace('P', '')}
+              data-tooltip={`Show last ${d.getDescription()}`}
+              small
+              onClick={() => setDateRange(getDateRange(d))}
             />
-          )}
-        </div>
-      </ResizeSensor>
-      <div className="side-control">
-        <FormGroup label="Show">
+          ))}
+          <Popover
+            isOpen={showCustomDatePicker}
+            onInteraction={setShowCustomDatePicker}
+            content={
+              <DateRangePicker3
+                defaultValue={utcToLocalDateRange(dateRange)}
+                onChange={newDateRange => {
+                  const newUtcDateRange = localToUtcDateRange(newDateRange);
+                  if (!isNonNullRange(newUtcDateRange)) return;
+                  setDateRange(newUtcDateRange);
+                  setShowCustomDatePicker(false);
+                }}
+                contiguousCalendarMonths={false}
+                reverseMonthAndYearMenus
+                timePickerProps={undefined}
+                shortcuts={false}
+              />
+            }
+          >
+            <Button
+              icon={IconNames.CALENDAR}
+              data-tooltip={`Select a custom date range\nCurrent range: ${prettyFormatIsoDate(
+                dateRange[0],
+              )} - ${prettyFormatIsoDate(dateRange[1])}`}
+            />
+          </Popover>
+        </ButtonGroup>
+        <FormGroup label="Show" inline>
           <SegmentedControl
             value={activeSegmentStat}
             onValueChange={s => setActiveSegmentStat(s as IntervalStat)}
-            fill
+            small
             options={[
               {
                 label: 'Size',
@@ -157,22 +186,31 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             ]}
           />
         </FormGroup>
-        <FormGroup label="Interval">
-          <DateRangeInput3
-            value={utcToLocalDateRange(dateRange)}
-            onChange={newDateRange => {
-              const newUtcDateRange = localToUtcDateRange(newDateRange);
-              if (!isNonNullRange(newUtcDateRange)) return;
-              setDateRange(newUtcDateRange);
-            }}
-            fill
-            locale={enUS}
-          />
-        </FormGroup>
-        <FormGroup label="Datasource">
+        <FormGroup label="Datasource" inline>
           <DatasourceSelect />
         </FormGroup>
       </div>
-    </SplitterLayout>
+      <ResizeSensor
+        onResize={(entries: ResizeObserverEntry[]) => {
+          const rect = entries[0].contentRect;
+          setStage(new Stage(rect.width, rect.height));
+        }}
+      >
+        <div className="chart-container">
+          {stage && (
+            <SegmentBarChart
+              capabilities={capabilities}
+              stage={stage}
+              dateRange={dateRange}
+              changeDateRange={setDateRange}
+              shownIntervalStat={activeSegmentStat}
+              changeActiveDatasource={(datasource: string | undefined) =>
+                setActiveDatasource(activeDatasource ? undefined : datasource)
+              }
+            />
+          )}
+        </div>
+      </ResizeSensor>
+    </div>
   );
 };


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 15/42: cleaner normalization

Posted by vo...@apache.org.
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 15885be68a39151a031565dfb6603c2e8a0cb5cc
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Thu Oct 31 09:36:25 2024 -0700

    cleaner normalization
---
 .../src/components/segment-timeline/common.ts      | 10 +------
 .../segment-timeline/segment-bar-chart-render.tsx  | 34 ++++++++++++++--------
 2 files changed, 23 insertions(+), 21 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index 966ade312ff..efd2a0b1b45 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -41,17 +41,9 @@ export interface IntervalRow extends Record<IntervalStat, number> {
 
 export interface TrimmedIntervalRow extends IntervalRow {
   shownSeconds: number;
+  normalized: Record<IntervalStat, number>;
 }
 
 export interface IntervalBar extends TrimmedIntervalRow {
   offset: Record<IntervalStat, number>;
 }
-
-export function normalizeIntervalRow(sr: TrimmedIntervalRow): TrimmedIntervalRow {
-  return {
-    ...sr,
-    segments: sr.segments / sr.shownSeconds,
-    size: sr.size / sr.shownSeconds,
-    rows: sr.rows / sr.shownSeconds,
-  };
-}
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 72fff4e891c..485bbe4afdb 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
@@ -43,7 +43,7 @@ import type { Margin, Stage } from '../../utils/stage';
 
 import { ChartAxis } from './chart-axis';
 import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
-import { aggregateSegmentStats, normalizeIntervalRow } from './common';
+import { aggregateSegmentStats } from './common';
 
 import './segment-bar-chart-render.scss';
 
@@ -103,14 +103,16 @@ function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalB
 
   const intervalTree = new IntervalTree();
   return sortedIntervalRows.map(intervalRow => {
-    intervalRow = normalizeIntervalRow(intervalRow);
     const startMs = intervalRow.start.valueOf();
     const endMs = intervalRow.end.valueOf();
-    const intervalRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as IntervalRow[];
+    const intervalRowsBelow = intervalTree.search([
+      startMs + 1,
+      startMs + 2,
+    ]) as TrimmedIntervalRow[];
     intervalTree.insert([startMs, endMs], intervalRow);
     return {
       ...intervalRow,
-      offset: aggregateSegmentStats(intervalRowsBelow),
+      offset: aggregateSegmentStats(intervalRowsBelow.map(i => i.normalized)),
     };
   });
 }
@@ -156,18 +158,22 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     ).toString();
   }, [expandedDateRange, stage.width]);
 
-  console.log(trimGranularity);
-
   const intervalBars = useMemo(() => {
     const trimDuration = new Duration(trimGranularity);
     const trimmedIntervalRows = intervalRows.map(intervalRow => {
       const start = trimDuration.floor(intervalRow.start, TZ_UTC);
       const end = trimDuration.ceil(intervalRow.end, TZ_UTC);
+      const shownSeconds = (end.valueOf() - start.valueOf()) / 1000;
       return {
         ...intervalRow,
         start,
         end,
         shownSeconds: (end.valueOf() - start.valueOf()) / 1000,
+        normalized: {
+          segments: intervalRow.segments / shownSeconds,
+          size: intervalRow.size / shownSeconds,
+          rows: intervalRow.rows / shownSeconds,
+        },
       };
     });
 
@@ -185,6 +191,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
         return {
           ...firstIntervalRow,
           ...aggregateSegmentStats(trimmedIntervalRows),
+          normalized: aggregateSegmentStats(trimmedIntervalRows.map(t => t.normalized)),
         };
       },
     );
@@ -202,10 +209,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     ? baseTimeScale.copy().domain(offsetDateRange(expandedDateRange, shiftOffset))
     : baseTimeScale;
 
-  const maxStat = max(intervalBars, d => d[shownIntervalStat] + d.offset[shownIntervalStat]);
+  const maxNormalizedStat = max(
+    intervalBars,
+    d => d.normalized[shownIntervalStat] + d.offset[shownIntervalStat],
+  );
   const statScale = scaleLinear()
     .rangeRound([innerStage.height, 0])
-    .domain([0, maxStat ?? 1]);
+    .domain([0, maxNormalizedStat ?? 1]);
 
   const formatTick = (n: number) => {
     switch (shownIntervalStat) {
@@ -289,7 +299,9 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     const xStart = clamp(timeScale(intervalBar.start), 0, innerStage.width);
     const xEnd = clamp(timeScale(intervalBar.end), 0, innerStage.width);
     const y0 = statScale(intervalBar.offset[shownIntervalStat]);
-    const y = statScale(intervalBar[shownIntervalStat] + intervalBar.offset[shownIntervalStat]);
+    const y = statScale(
+      intervalBar.normalized[shownIntervalStat] + intervalBar.offset[shownIntervalStat],
+    );
 
     return {
       x: xStart,
@@ -407,9 +419,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div>Datasource: {hoverOn.datasource}</div>
           <div>{`Time: ${prettyFormatIsoDate(hoverOn.start)}/${hoverOn.originalTimeSpan}`}</div>
           <div>
-            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(
-              hoverOn[shownIntervalStat] * hoverOn.shownSeconds,
-            )}`}
+            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(hoverOn[shownIntervalStat])}`}
           </div>
         </div>
       ) : undefined}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 11/42: add file

Posted by vo...@apache.org.
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 17051be2648be8e1f3437b08002ef6959bc22199
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 30 11:57:26 2024 -0700

    add file
---
 .../timezone-menu-items.spec.tsx.snap              | 563 +++++++++++++++++++++
 1 file changed, 563 insertions(+)

diff --git a/web-console/src/views/workbench-view/timezone-menu-items/__snapshots__/timezone-menu-items.spec.tsx.snap b/web-console/src/views/workbench-view/timezone-menu-items/__snapshots__/timezone-menu-items.spec.tsx.snap
new file mode 100644
index 00000000000..ca6e0c2c985
--- /dev/null
+++ b/web-console/src/views/workbench-view/timezone-menu-items/__snapshots__/timezone-menu-items.spec.tsx.snap
@@ -0,0 +1,563 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TimezoneMenuItems matches snapshot 1`] = `
+[
+  <Blueprint5.MenuDivider
+    title="Timezone type"
+  />,
+  <Blueprint5.MenuItem
+    active={false}
+    disabled={false}
+    icon="blank"
+    label="Etc/UTC"
+    multiline={false}
+    onClick={[Function]}
+    popoverProps={{}}
+    shouldDismissPopover={false}
+    text="Default"
+  />,
+  <Blueprint5.MenuItem
+    active={false}
+    disabled={false}
+    icon="blank"
+    multiline={false}
+    popoverProps={{}}
+    shouldDismissPopover={true}
+    text="Named"
+  >
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-8:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Juneau"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-7:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Los_Angeles"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-7:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Phoenix"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-6:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Yellowknife"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-6:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Denver"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-6:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Mexico_City"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-5:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Chicago"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-4:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/New_York"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC-3:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="America/Argentina/Buenos_Aires"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+0:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Etc/UTC"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+0:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Europe/London"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+0:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text={
+        <React.Fragment>
+          UTC
+           
+          <Blueprint5.Icon
+            autoLoad={true}
+            data-tooltip="Browser timezone"
+            icon="star"
+            tagName="span"
+          />
+        </React.Fragment>
+      }
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+1:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Europe/Paris"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+2:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Asia/Jerusalem"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+5:30"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Asia/Kolkata"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+8:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Asia/Shanghai"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+8:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Asia/Hong_Kong"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+9:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Asia/Seoul"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+9:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Asia/Tokyo"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+10:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Pacific/Guam"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      label="UTC+11:00"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="Australia/Sydney"
+    />
+  </Blueprint5.MenuItem>,
+  <Blueprint5.MenuItem
+    active={false}
+    disabled={false}
+    icon="blank"
+    multiline={false}
+    popoverProps={{}}
+    shouldDismissPopover={true}
+    text="Offset"
+  >
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-12:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-11:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-10:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-09:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-08:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-07:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-06:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-05:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-04:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-03:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-02:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="-01:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text={
+        <React.Fragment>
+          +00:00
+           
+          <Blueprint5.Icon
+            autoLoad={true}
+            data-tooltip="Browser offset"
+            icon="star"
+            tagName="span"
+          />
+        </React.Fragment>
+      }
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+01:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+02:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+03:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+04:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+05:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+06:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+07:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+08:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+09:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+10:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+11:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+12:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+13:00"
+    />
+    <Blueprint5.MenuItem
+      active={false}
+      disabled={false}
+      icon="blank"
+      multiline={false}
+      onClick={[Function]}
+      popoverProps={{}}
+      shouldDismissPopover={false}
+      text="+14:00"
+    />
+  </Blueprint5.MenuItem>,
+]
+`;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 25/42: good fixes

Posted by vo...@apache.org.
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 cac4f78be4004adba878875bb888b100ce9f97f6
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 4 19:33:09 2024 -0800

    good fixes
---
 .../src/components/segment-timeline/common.ts      | 15 ++++++++++
 .../segment-timeline/segment-bar-chart-render.tsx  | 33 +++++++++-------------
 web-console/src/entry.scss                         |  1 +
 3 files changed, 30 insertions(+), 19 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index 69a1d20c457..c808161ce17 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -19,6 +19,7 @@
 import { sum } from 'd3-array';
 
 import type { Duration } from '../../utils';
+import { formatBytes, formatInteger } from '../../utils';
 
 export type IntervalStat = 'segments' | 'size' | 'rows';
 
@@ -50,6 +51,20 @@ export function aggregateSegmentStats(
   };
 }
 
+export function formatIntervalStat(stat: IntervalStat, n: number) {
+  switch (stat) {
+    case 'segments':
+    case 'rows':
+      return formatInteger(n);
+
+    case 'size':
+      return formatBytes(n);
+
+    default:
+      return '';
+  }
+}
+
 export interface IntervalRow extends Record<IntervalStat, number> {
   start: Date;
   end: Date;
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 dbf8302d109..adfb5657309 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
@@ -34,7 +34,6 @@ import {
   Duration,
   filterMap,
   formatBytes,
-  formatInteger,
   formatNumber,
   groupBy,
   month,
@@ -46,12 +45,12 @@ import type { Margin, Stage } from '../../utils/stage';
 import { Bubble } from './bubble';
 import { ChartAxis } from './chart-axis';
 import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
-import { aggregateSegmentStats } from './common';
+import { aggregateSegmentStats, formatIntervalStat, INTERVAL_STATS } from './common';
 
 import './segment-bar-chart-render.scss';
 
 const CHART_MARGIN: Margin = { top: 10, right: 0, bottom: 25, left: 70 };
-const MIN_BAR_WIDTH = 2;
+const MIN_BAR_WIDTH = 3;
 const POSSIBLE_GRANULARITIES = [
   new Duration('PT15M'),
   new Duration('PT1H'),
@@ -196,17 +195,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     .rangeRound([innerStage.height, 0])
     .domain([0, maxNormalizedStat ?? 1]);
 
-  const formatTick = (n: number) => {
-    switch (shownIntervalStat) {
-      case 'segments':
-      case 'rows':
-        return formatInteger(n);
-
-      case 'size':
-        return formatBytes(n);
-    }
-  };
-
   const formatTickRate = (n: number) => {
     switch (shownIntervalStat) {
       case 'segments':
@@ -301,6 +289,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     }
   });
 
+  useGlobalEventListener('keydown', (e: KeyboardEvent) => {
+    if (e.key === 'Escape' && mouseDownAt) {
+      setMouseDownAt(undefined);
+      setDragging(undefined);
+    }
+  });
+
   function startEndToXWidth({ start, end }: { start: Date; end: Date }) {
     const xStart = clamp(timeScale(start), 0, innerStage.width);
     const xEnd = clamp(timeScale(end), 0, innerStage.width);
@@ -343,11 +338,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div>{`Time: ${prettyFormatIsoDate(hoveredIntervalBar.start)}/${
             hoveredIntervalBar.originalTimeSpan
           }`}</div>
-          <div>
-            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(
-              hoveredIntervalBar[shownIntervalStat],
-            )}`}
-          </div>
+          {INTERVAL_STATS.map(stat => (
+            <div key={stat}>
+              {`${capitalizeFirst(stat)}: ${formatIntervalStat(stat, hoveredIntervalBar[stat])}`}
+            </div>
+          ))}
         </>
       ),
     };
diff --git a/web-console/src/entry.scss b/web-console/src/entry.scss
index 8d368a4146b..46ea9d298d0 100644
--- a/web-console/src/entry.scss
+++ b/web-console/src/entry.scss
@@ -56,6 +56,7 @@ body {
   position: absolute;
   height: 100%;
   width: 100%;
+  z-index: 0;
 
   .console-application {
     position: absolute;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 03/42: small fixes

Posted by vo...@apache.org.
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 88f993c4d1818d177001f47409ca532999a3f62a
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 23 13:42:06 2024 -0700

    small fixes
---
 web-console/src/components/segment-timeline/segment-timeline.scss | 1 +
 web-console/src/components/segment-timeline/segment-timeline.tsx  | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index f739277bea7..6239e717ae8 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -28,6 +28,7 @@
 
   .side-control {
     @include card-like;
+    height: 100%;
     padding: 10px;
   }
 
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 56f6801b426..edb7dfcf60f 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -564,7 +564,7 @@ ORDER BY "start" DESC`;
           items={datasourcesWzAll}
           onItemSelect={handleItemSelected}
           itemRenderer={datasourceRenderer}
-          noResults={<MenuItem disabled text="No results." roleStructure="listoption" />}
+          noResults={<MenuItem disabled text="No results" roleStructure="listoption" />}
           itemPredicate={filterDatasource}
           fill
         >


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 05/42: step

Posted by vo...@apache.org.
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


(druid) 31/42: better controlls

Posted by vo...@apache.org.
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 <va...@ogievetsky.com>
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: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 06/42: fix

Posted by vo...@apache.org.
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 b0c756fea073572cff1d38944bfacc7e2020020c
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Oct 28 10:37:56 2024 -0700

    fix
---
 .../src/components/segment-timeline/chart-axis.tsx |   2 +-
 .../segment-timeline/segment-bar-chart-render.scss |  21 +++
 .../segment-timeline/segment-bar-chart-render.tsx  | 152 ++++++++++++++++-----
 .../segment-timeline/segment-bar-chart.tsx         |  24 ++--
 web-console/src/utils/general.tsx                  |   9 --
 5 files changed, 149 insertions(+), 59 deletions(-)

diff --git a/web-console/src/components/segment-timeline/chart-axis.tsx b/web-console/src/components/segment-timeline/chart-axis.tsx
index 0e48e10fde8..b8ee4e9cbb1 100644
--- a/web-console/src/components/segment-timeline/chart-axis.tsx
+++ b/web-console/src/components/segment-timeline/chart-axis.tsx
@@ -20,9 +20,9 @@ import type { Axis } from 'd3-axis';
 import { select } from 'd3-selection';
 
 interface ChartAxisProps {
+  className?: string;
   transform?: string;
   axis: Axis<any>;
-  className?: string;
 }
 
 export const ChartAxis = function ChartAxis(props: ChartAxisProps) {
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 9cd7742e851..a30ee800f0a 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
@@ -50,6 +50,27 @@
       stroke-width: 1px;
     }
 
+    .shifter {
+      fill: white;
+      fill-opacity: 0.2;
+      filter: blur(1px);
+    }
+
+    .time-shift-indicator {
+      fill: white;
+      fill-opacity: 0.001;
+      cursor: grab;
+
+      &:hover {
+        fill-opacity: 0.1;
+      }
+
+      &.shifting {
+        fill-opacity: 0.2;
+        cursor: grabbing;
+      }
+    }
+
     .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 4c17c5d44b1..f1e67c9665a 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
@@ -17,14 +17,24 @@
  */
 
 import type { NonNullDateRange } from '@blueprintjs/datetime';
+import classNames from 'classnames';
 import { max } from 'd3-array';
 import { axisBottom, axisLeft } from 'd3-axis';
-import { scaleLinear, scaleUtc } from 'd3-scale';
+import { scaleLinear, scaleOrdinal, scaleUtc } from 'd3-scale';
 import type React from 'react';
-import { useRef, useState } from 'react';
+import { useMemo, useRef, useState } from 'react';
 
 import { useGlobalEventListener } from '../../hooks';
-import { ceilDay, floorDay, formatBytes, formatInteger } from '../../utils';
+import {
+  capitalizeFirst,
+  ceilDay,
+  clamp,
+  floorDay,
+  formatByteRate,
+  formatBytes,
+  formatInteger,
+  formatNumber,
+} from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
 import { ChartAxis } from './chart-axis';
@@ -32,7 +42,7 @@ import type { SegmentBar, SegmentStat } from './common';
 
 import './segment-bar-chart-render.scss';
 
-const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 10 };
+const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 80 };
 
 const COLORS = [
   '#b33040',
@@ -53,10 +63,14 @@ const COLORS = [
   '#87606c',
 ];
 
+function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
+  return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
+}
+
 interface SegmentBarChartRenderProps {
   stage: Stage;
   dateRange: NonNullDateRange;
-  changeDateRange(newDateRange: NonNullDateRange): void;
+  changeDateRange(dateRange: NonNullDateRange): void;
   shownSegmentStat: SegmentStat;
   segmentBars: SegmentBar[];
   changeActiveDatasource(datasource: string | undefined): void;
@@ -74,13 +88,25 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     changeActiveDatasource,
   } = props;
   const [hoverOn, setHoverOn] = useState<SegmentBar>();
-  const [mouseDownAt, setMouseDownAt] = useState<Date | undefined>();
+  const [mouseDownAt, setMouseDownAt] = useState<
+    { time: Date; action: 'select' | 'shift' } | undefined
+  >();
   const [dragging, setDragging] = useState<NonNullDateRange | undefined>();
+  const [shiftOffset, setShiftOffset] = useState<number | undefined>();
   const svgRef = useRef<SVGSVGElement | null>(null);
 
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
-  const timeScale = scaleUtc().domain(dateRange).range([0, innerStage.width]);
+  const baseTimeScale = scaleUtc().domain(dateRange).range([0, innerStage.width]);
+
+  const timeScale = shiftOffset
+    ? baseTimeScale.copy().domain(offsetDateRange(dateRange, shiftOffset))
+    : baseTimeScale;
+
+  const colorizer = useMemo(() => {
+    const s = scaleOrdinal().range(COLORS);
+    return (d: SegmentBar) => (d.datasource ? s(d.datasource) : COLORS[0]) as string;
+  }, []);
 
   const maxStat = max(segmentBars, d => d[shownSegmentStat] + d.offset[shownSegmentStat]);
   const statScale = scaleLinear()
@@ -88,11 +114,26 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     .domain([0, maxStat ?? 1]);
 
   const formatTick = (n: number) => {
-    if (isNaN(n)) return '';
-    if (shownSegmentStat === 'count') {
-      return formatInteger(n);
-    } else {
-      return formatBytes(n);
+    switch (shownSegmentStat) {
+      case 'count':
+      case 'rows':
+        return formatInteger(n);
+
+      case 'size':
+        return formatBytes(n);
+    }
+  };
+
+  const formatTickRate = (n: number) => {
+    switch (shownSegmentStat) {
+      case 'count':
+        return formatNumber(n) + ' seg/s';
+
+      case 'rows':
+        return formatNumber(n) + ' row/s';
+
+      case 'size':
+        return formatByteRate(n);
     }
   };
 
@@ -101,7 +142,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     if (!svg) return;
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
-    setMouseDownAt(timeScale.invert(x));
+    const y = e.clientY - rect.y - CHART_MARGIN.top;
+    setMouseDownAt({
+      time: baseTimeScale.invert(x),
+      action: y > innerStage.height ? 'shift' : 'select',
+    });
   }
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
@@ -110,34 +155,46 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     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)]);
+    const b = baseTimeScale.invert(x);
+    if (mouseDownAt.action === 'shift' || e.shiftKey) {
+      setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
     } else {
-      setDragging([floorDay(b), ceilDay(mouseDownAt)]);
+      if (mouseDownAt.time < b) {
+        setDragging([floorDay(mouseDownAt.time), ceilDay(b)]);
+      } else {
+        setDragging([floorDay(b), ceilDay(mouseDownAt.time)]);
+      }
     }
   });
 
-  useGlobalEventListener('mouseup', () => {
-    if (mouseDownAt) {
-      setMouseDownAt(undefined);
-    }
-    if (dragging) {
-      setDragging(undefined);
-      changeDateRange(dragging);
+  useGlobalEventListener('mouseup', (e: MouseEvent) => {
+    if (!mouseDownAt) return;
+    setMouseDownAt(undefined);
+
+    if (!shiftOffset && !dragging) return;
+    setDragging(undefined);
+    setShiftOffset(undefined);
+    if (mouseDownAt.action === 'shift' || e.shiftKey) {
+      if (shiftOffset) {
+        changeDateRange(offsetDateRange(dateRange, shiftOffset));
+      }
+    } else {
+      if (dragging) {
+        changeDateRange(dragging);
+      }
     }
   });
 
   function segmentBarToRect(segmentBar: SegmentBar) {
-    const xStart = timeScale(segmentBar.start);
-    const xEnd = timeScale(segmentBar.end);
+    const xStart = clamp(timeScale(segmentBar.start), 0, innerStage.width);
+    const xEnd = clamp(timeScale(segmentBar.end), 0, innerStage.width);
     const y0 = statScale(segmentBar.offset[shownSegmentStat]);
     const y = statScale(segmentBar[shownSegmentStat] + segmentBar.offset[shownSegmentStat]);
 
     return {
       x: xStart,
       y: y,
-      width: Math.max(xEnd - xStart, 1),
+      width: Math.max(xEnd - xStart - 1, 1),
       height: Math.abs(y0 - y),
     };
   }
@@ -154,7 +211,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div>Datasource: {hoverOn.datasource}</div>
           <div>Time: {hoverOn.start.toISOString()}</div>
           <div>
-            {`${shownSegmentStat === 'count' ? 'Count' : 'Size'}: ${formatTick(
+            {`${capitalizeFirst(shownSegmentStat)}: ${formatTick(
               hoverOn[shownSegmentStat] * hoverOn.durationSeconds,
             )}`}
           </div>
@@ -186,6 +243,21 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
             transform={`translate(0,${innerStage.height})`}
             axis={axisBottom(timeScale)}
           />
+          <rect
+            className={classNames('time-shift-indicator', {
+              shifting: typeof shiftOffset === 'number',
+            })}
+            x={0}
+            y={innerStage.height}
+            width={innerStage.width}
+            height={CHART_MARGIN.bottom}
+          />
+          <ChartAxis
+            className="axis-y"
+            axis={axisLeft(statScale)
+              .ticks(5)
+              .tickFormat(e => formatTickRate(e.valueOf()))}
+          />
           <g className="bar-group">
             {segmentBars.map((segmentBar, i) => {
               return (
@@ -193,13 +265,16 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                   key={i}
                   className="bar-unit"
                   {...segmentBarToRect(segmentBar)}
-                  style={{ fill: COLORS[i % COLORS.length] }}
+                  style={{ fill: colorizer(segmentBar) }}
                   onClick={
                     segmentBar.datasource
                       ? () => changeActiveDatasource(segmentBar.datasource)
                       : undefined
                   }
-                  onMouseOver={() => setHoverOn(segmentBar)}
+                  onMouseOver={() => {
+                    if (mouseDownAt) return;
+                    setHoverOn(segmentBar);
+                  }}
                 />
               );
             })}
@@ -213,13 +288,24 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                 }}
               />
             )}
-            {(dragging || mouseDownAt) && (
+            {dragging && (
               <rect
                 className="selection"
-                x={timeScale(dragging?.[0] || mouseDownAt!)}
+                x={timeScale(dragging[0])}
+                y={0}
+                height={innerStage.height}
+                width={timeScale(dragging[1]) - timeScale(dragging[0])}
+              />
+            )}
+            {!!shiftOffset && (
+              <rect
+                className="shifter"
+                x={timeScale(shiftOffset > 0 ? dateRange[1] : dateRange[0].valueOf() + shiftOffset)}
                 y={0}
                 height={innerStage.height}
-                width={dragging ? timeScale(dragging[1]) - timeScale(dragging[0]) : 1}
+                width={Math.abs(
+                  timeScale(dateRange[0]) - timeScale(dateRange[0].valueOf() + shiftOffset),
+                )}
               />
             )}
           </g>
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 32d6f053b18..4b24849b723 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -134,7 +134,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     [capabilities, dateRange, breakByDataSource],
   );
 
-  const [segmentRowsState] = useQueryManager({
+  const [segmentBarsState] = useQueryManager({
     query: intervalsQuery,
     processQuery: async ({ capabilities, dateRange, breakByDataSource }, cancelToken) => {
       const trimDuration: TrimDuration = 'PT1H';
@@ -142,7 +142,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
       if (capabilities.hasSql()) {
         const query = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
-            sql`'${dateRange[0].toISOString()}' <= "start" AND "end" <= '${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`,
+            sql`'${dateRange[0].toISOString()}' <= "end" AND "start" <= '${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`,
           )
           .addSelect(C('start'), { addToGroupBy: 'end' })
           .addSelect(C('end'), { addToGroupBy: 'end' })
@@ -218,28 +218,20 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     },
   });
 
-  if (segmentRowsState.loading) {
+  if (segmentBarsState.loading) {
     return <Loader />;
   }
 
-  if (segmentRowsState.error) {
+  if (segmentBarsState.error) {
     return (
       <div className="empty-placeholder">
-        <span className="no-data-text">{`Error when loading data: ${segmentRowsState.getErrorMessage()}`}</span>
+        <span className="no-data-text">{`Error when loading data: ${segmentBarsState.getErrorMessage()}`}</span>
       </div>
     );
   }
 
-  const segmentRows = segmentRowsState.data;
-  if (!segmentRows) return null;
-
-  if (!segmentRows.length) {
-    return (
-      <div className="empty-placeholder">
-        <span className="no-data-text">There are no segments for the selected interval</span>
-      </div>
-    );
-  }
+  const segmentBars = segmentBarsState.data;
+  if (!segmentBars) return null;
 
   return (
     <SegmentBarChartRender
@@ -247,7 +239,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
       dateRange={dateRange}
       changeDateRange={changeDateRange}
       shownSegmentStat={shownSegmentStat}
-      segmentBars={segmentRows as any}
+      segmentBars={segmentBars}
       changeActiveDatasource={changeActiveDatasource}
     />
   );
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 18a1ac65eca..b742013b2e8 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -656,12 +656,3 @@ 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;
-}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 01/42: init refactor of segment timeline

Posted by vo...@apache.org.
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 fb7371370a3987695141608167b35266946aa777
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Oct 22 14:34:57 2024 -0700

    init refactor of segment timeline
---
 web-console/lib/keywords.ts                        |   8 ++
 .../src/components/segment-timeline/bar-group.tsx  |  19 ++--
 .../components/segment-timeline/bar-unit.spec.tsx  |  33 -------
 .../src/components/segment-timeline/chart-axis.tsx |   7 +-
 .../segment-timeline/{bar-unit.tsx => common.ts}   |  45 ++++-----
 .../segment-timeline/segment-timeline.scss         |   8 +-
 .../segment-timeline/segment-timeline.tsx          |  83 ++++++----------
 .../segment-timeline/stacked-bar-chart.tsx         | 104 ++++++++-------------
 .../views/datasources-view/datasources-view.tsx    |   2 +-
 9 files changed, 119 insertions(+), 190 deletions(-)

diff --git a/web-console/lib/keywords.ts b/web-console/lib/keywords.ts
index 06d7ccdcc94..5985ae0a6aa 100644
--- a/web-console/lib/keywords.ts
+++ b/web-console/lib/keywords.ts
@@ -100,15 +100,23 @@ export const SQL_EXPRESSION_PARTS = [
   'TRAILING',
   'EPOCH',
   'SECOND',
+  'SECONDS',
   'MINUTE',
+  'MINUTES',
   'HOUR',
+  'HOURS',
   'DAY',
+  'DAYS',
   'DOW',
   'DOY',
   'WEEK',
+  'WEEKS',
   'MONTH',
+  'MONTHS',
   'QUARTER',
+  'QUARTERS',
   'YEAR',
+  'YEARS',
   'TIMESTAMP',
   'INTERVAL',
   'CSV',
diff --git a/web-console/src/components/segment-timeline/bar-group.tsx b/web-console/src/components/segment-timeline/bar-group.tsx
index d0cf867e2b2..3c61859115a 100644
--- a/web-console/src/components/segment-timeline/bar-group.tsx
+++ b/web-console/src/components/segment-timeline/bar-group.tsx
@@ -19,26 +19,18 @@
 import type { AxisScale } from 'd3-axis';
 import React from 'react';
 
-import { BarUnit } from './bar-unit';
-import type { BarUnitData, HoveredBarInfo } from './stacked-bar-chart';
+import type { BarUnitData, HoveredBarInfo } from './common';
 
 interface BarGroupProps {
   dataToRender: BarUnitData[];
   changeActiveDatasource: (dataSource: string) => void;
-  formatTick: (e: number) => string;
   xScale: AxisScale<Date>;
   yScale: AxisScale<number>;
   barWidth: number;
-  onHoverBar?: (e: any) => void;
-  offHoverBar?: () => void;
-  hoverOn?: HoveredBarInfo | null;
+  onHoverBar: (e: HoveredBarInfo) => void;
 }
 
 export class BarGroup extends React.Component<BarGroupProps> {
-  shouldComponentUpdate(nextProps: BarGroupProps): boolean {
-    return nextProps.hoverOn === this.props.hoverOn;
-  }
-
   render() {
     const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, barWidth } =
       this.props;
@@ -47,6 +39,8 @@ export class BarGroup extends React.Component<BarGroupProps> {
     return dataToRender.map((entry: BarUnitData, i: number) => {
       const y0 = yScale(entry.y0 || 0) || 0;
       const x = xScale(new Date(entry.x + 'T00:00:00Z'));
+      if (typeof x === 'undefined') return;
+
       const y = yScale((entry.y0 || 0) + entry.y) || 0;
       const height = Math.max(y0 - y, 0);
       const barInfo: HoveredBarInfo = {
@@ -59,15 +53,16 @@ export class BarGroup extends React.Component<BarGroupProps> {
         dailySize: entry.dailySize,
       };
       return (
-        <BarUnit
+        <rect
           key={i}
+          className="bar-unit"
           x={x}
           y={y}
           width={barWidth}
           height={height}
           style={{ fill: entry.color }}
           onClick={() => changeActiveDatasource(entry.datasource)}
-          onHover={() => onHoverBar && onHoverBar(barInfo)}
+          onMouseOver={() => onHoverBar(barInfo)}
         />
       );
     });
diff --git a/web-console/src/components/segment-timeline/bar-unit.spec.tsx b/web-console/src/components/segment-timeline/bar-unit.spec.tsx
deleted file mode 100644
index d5926dcf69f..00000000000
--- a/web-console/src/components/segment-timeline/bar-unit.spec.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { render } from '@testing-library/react';
-
-import { BarUnit } from './bar-unit';
-
-describe('BarUnit', () => {
-  it('matches snapshot', () => {
-    const barGroup = (
-      <svg>
-        <BarUnit x={10} y={10} width={10} height={10} />
-      </svg>
-    );
-    const { container } = render(barGroup);
-    expect(container.firstChild).toMatchSnapshot();
-  });
-});
diff --git a/web-console/src/components/segment-timeline/chart-axis.tsx b/web-console/src/components/segment-timeline/chart-axis.tsx
index bc333d33b77..18dc7d3e076 100644
--- a/web-console/src/components/segment-timeline/chart-axis.tsx
+++ b/web-console/src/components/segment-timeline/chart-axis.tsx
@@ -16,22 +16,23 @@
  * limitations under the License.
  */
 
+import type { Axis } from 'd3-axis';
 import { select } from 'd3-selection';
 import React from 'react';
 
 interface ChartAxisProps {
   transform?: string;
-  scale: any;
+  axis: Axis<any>;
   className?: string;
 }
 
 export const ChartAxis = React.memo(function ChartAxis(props: ChartAxisProps) {
-  const { transform, scale, className } = props;
+  const { transform, axis, className } = props;
   return (
     <g
       className={`chart-axis ${className}`}
       transform={transform}
-      ref={node => select(node).call(scale)}
+      ref={node => select(node).call(axis as any)}
     />
   );
 });
diff --git a/web-console/src/components/segment-timeline/bar-unit.tsx b/web-console/src/components/segment-timeline/common.ts
similarity index 63%
rename from web-console/src/components/segment-timeline/bar-unit.tsx
rename to web-console/src/components/segment-timeline/common.ts
index 8591f68cc2e..9add957e2c1 100644
--- a/web-console/src/components/segment-timeline/bar-unit.tsx
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -16,30 +16,31 @@
  * limitations under the License.
  */
 
-interface BarChartUnitProps {
-  x: number | undefined;
+export interface BarUnitData {
+  x: number;
   y: number;
+  y0?: number;
   width: number;
-  height: number;
-  style?: any;
-  onClick?: () => void;
-  onHover?: () => void;
-  offHover?: () => void;
+  datasource: string;
+  color: string;
+  dailySize: number;
 }
 
-export function BarUnit(props: BarChartUnitProps) {
-  const { x, y, width, height, style, onClick, onHover, offHover } = props;
-  return (
-    <rect
-      className="bar-unit"
-      x={x}
-      y={y}
-      width={width}
-      height={height}
-      style={style}
-      onClick={onClick}
-      onMouseOver={onHover}
-      onMouseLeave={offHover}
-    />
-  );
+export interface Margin {
+  top: number;
+  right: number;
+  bottom: number;
+  left: number;
 }
+
+export interface HoveredBarInfo {
+  xCoordinate: number;
+  yCoordinate: number;
+  height: number;
+  datasource: string;
+  xValue: number;
+  yValue: number;
+  dailySize: number;
+}
+
+export type SegmentStat = 'sizeData' | 'countData';
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index aa437052d61..24ee569ad3a 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -16,9 +16,12 @@
  * limitations under the License.
  */
 
+@import '../../variables';
+
 .segment-timeline {
   display: grid;
-  grid-template-columns: 1fr 220px;
+  grid-template-columns: 1fr 240px;
+  gap: 8px;
 
   .loader {
     width: 85%;
@@ -39,6 +42,7 @@
   }
 
   .side-control {
-    padding-top: 20px;
+    @include card-like;
+    padding: 10px;
   }
 }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 8aee0c66d47..f59a59e2399 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -31,8 +31,6 @@ import type { Capabilities } from '../../helpers';
 import { Api } from '../../singletons';
 import {
   ceilToUtcDay,
-  formatBytes,
-  formatInteger,
   isNonNullRange,
   localToUtcDateRange,
   queryDruidSql,
@@ -42,7 +40,7 @@ import {
 } from '../../utils';
 import { Loader } from '../loader/loader';
 
-import type { BarUnitData } from './stacked-bar-chart';
+import type { BarUnitData, SegmentStat } from './common';
 import { StackedBarChart } from './stacked-bar-chart';
 
 import './segment-timeline.scss';
@@ -51,8 +49,6 @@ interface SegmentTimelineProps {
   capabilities: Capabilities;
 }
 
-type ActiveDataType = 'sizeData' | 'countData';
-
 interface SegmentTimelineState {
   chartHeight: number;
   chartWidth: number;
@@ -60,8 +56,8 @@ interface SegmentTimelineState {
   datasources: string[];
   stackedData?: Record<string, BarUnitData[]>;
   singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
-  activeDatasource: string | null;
-  activeDataType: ActiveDataType;
+  activeDatasource?: string;
+  activeSegmentStat: SegmentStat;
   dataToRender: BarUnitData[];
   loading: boolean;
   error?: Error;
@@ -246,7 +242,7 @@ ORDER BY "start" DESC`;
     any
   >;
 
-  private readonly chartMargin = { top: 40, right: 15, bottom: 20, left: 60 };
+  private readonly chartMargin = { top: 40, right: 0, bottom: 20, left: 60 };
 
   constructor(props: SegmentTimelineProps) {
     super(props);
@@ -260,8 +256,7 @@ ORDER BY "start" DESC`;
       stackedData: {},
       singleDatasourceData: {},
       dataToRender: [],
-      activeDatasource: null,
-      activeDataType: 'sizeData',
+      activeSegmentStat: 'sizeData',
       loading: true,
       xScale: null,
       yScale: null,
@@ -355,10 +350,10 @@ ORDER BY "start" DESC`;
   }
 
   componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void {
-    const { activeDatasource, activeDataType, singleDatasourceData, stackedData } = this.state;
+    const { activeDatasource, activeSegmentStat, singleDatasourceData, stackedData } = this.state;
     if (
       prevState.data !== this.state.data ||
-      prevState.activeDataType !== this.state.activeDataType ||
+      prevState.activeSegmentStat !== this.state.activeSegmentStat ||
       prevState.activeDatasource !== this.state.activeDatasource ||
       prevState.chartWidth !== this.state.chartWidth ||
       prevState.chartHeight !== this.state.chartHeight
@@ -366,10 +361,10 @@ ORDER BY "start" DESC`;
       const scales: BarChartScales | undefined = this.calculateScales();
       const dataToRender: BarUnitData[] | undefined = activeDatasource
         ? singleDatasourceData
-          ? singleDatasourceData[activeDataType][activeDatasource]
+          ? singleDatasourceData[activeSegmentStat][activeDatasource]
           : undefined
         : stackedData
-        ? stackedData[activeDataType]
+        ? stackedData[activeSegmentStat]
         : undefined;
 
       if (scales && dataToRender) {
@@ -387,31 +382,26 @@ ORDER BY "start" DESC`;
       chartWidth,
       chartHeight,
       data,
-      activeDataType,
+      activeSegmentStat,
       activeDatasource,
       singleDatasourceData,
       dateRange,
     } = this.state;
     if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) return;
-    const activeData = data[activeDataType];
+    const activeData = data[activeSegmentStat];
 
-    let yDomain: number[] = [
-      0,
+    let yMax =
       activeData.length === 0
-        ? 0
-        : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total,
-    ];
+        ? 100
+        : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total;
 
     if (
-      activeDatasource !== null &&
-      singleDatasourceData![activeDataType][activeDatasource] !== undefined
+      activeDatasource &&
+      singleDatasourceData![activeSegmentStat][activeDatasource] !== undefined
     ) {
-      yDomain = [
-        0,
-        singleDatasourceData![activeDataType][activeDatasource].reduce((max: any, d: any) =>
-          max.y > d.y ? max : d,
-        ).y,
-      ];
+      yMax = singleDatasourceData![activeSegmentStat][activeDatasource].reduce((max: any, d: any) =>
+        max.y > d.y ? max : d,
+      ).y;
     }
 
     const xScale: AxisScale<Date> = scaleUtc()
@@ -420,7 +410,7 @@ ORDER BY "start" DESC`;
 
     const yScale: AxisScale<number> = scaleLinear()
       .rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0])
-      .domain(yDomain);
+      .domain([0, yMax]);
 
     return {
       xScale,
@@ -428,16 +418,6 @@ ORDER BY "start" DESC`;
     };
   }
 
-  private readonly formatTick = (n: number) => {
-    if (isNaN(n)) return '';
-    const { activeDataType } = this.state;
-    if (activeDataType === 'countData') {
-      return formatInteger(n);
-    } else {
-      return formatBytes(n);
-    }
-  };
-
   private readonly handleResize = (entries: ResizeObserverEntry[]) => {
     const chartRect = entries[0].contentRect;
     this.setState({
@@ -452,7 +432,7 @@ ORDER BY "start" DESC`;
       chartHeight,
       loading,
       dataToRender,
-      activeDataType,
+      activeSegmentStat,
       error,
       xScale,
       yScale,
@@ -485,7 +465,7 @@ ORDER BY "start" DESC`;
       );
     }
 
-    if (data![activeDataType].length === 0) {
+    if (data![activeSegmentStat].length === 0) {
       return (
         <div>
           <span className="no-data-text">There are no segments for the selected interval</span>
@@ -494,8 +474,8 @@ ORDER BY "start" DESC`;
     }
 
     if (
-      activeDatasource !== null &&
-      data![activeDataType].every((d: any) => d[activeDatasource] === undefined)
+      activeDatasource &&
+      data![activeSegmentStat].every((d: any) => d[activeDatasource] === undefined)
     ) {
       return (
         <div>
@@ -519,13 +499,12 @@ ORDER BY "start" DESC`;
           svgHeight={chartHeight}
           svgWidth={chartWidth}
           margin={this.chartMargin}
-          changeActiveDatasource={(datasource: string | null) =>
+          changeActiveDatasource={(datasource: string | undefined) =>
             this.setState(prevState => ({
-              activeDatasource: prevState.activeDatasource ? null : datasource,
+              activeDatasource: prevState.activeDatasource ? undefined : datasource,
             }))
           }
-          activeDataType={activeDataType}
-          formatTick={(n: number) => this.formatTick(n)}
+          shownSegmentStat={activeSegmentStat}
           xScale={xScale}
           yScale={yScale}
           barWidth={barWidth}
@@ -536,7 +515,7 @@ ORDER BY "start" DESC`;
 
   render() {
     const { capabilities } = this.props;
-    const { datasources, activeDataType, activeDatasource, dateRange, selectedDateRange } =
+    const { datasources, activeSegmentStat, activeDatasource, dateRange, selectedDateRange } =
       this.state;
 
     const filterDatasource: ItemPredicate<string> = (query, val, _index, exactMatch) => {
@@ -574,7 +553,7 @@ ORDER BY "start" DESC`;
       const showAll = 'Show all';
       const handleItemSelected = (selectedItem: string) => {
         this.setState({
-          activeDatasource: selectedItem === showAll ? null : selectedItem,
+          activeDatasource: selectedItem === showAll ? undefined : selectedItem,
         });
       };
       const datasourcesWzAll = [showAll].concat(datasources);
@@ -602,9 +581,9 @@ ORDER BY "start" DESC`;
         <div className="side-control">
           <FormGroup label="Show">
             <SegmentedControl
-              value={activeDataType}
+              value={activeSegmentStat}
               onValueChange={activeDataType =>
-                this.setState({ activeDataType: activeDataType as ActiveDataType })
+                this.setState({ activeSegmentStat: activeDataType as SegmentStat })
               }
               options={[
                 {
diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
index 8018aaee5f6..571c50f65a5 100644
--- a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
@@ -20,47 +20,21 @@ import type { AxisScale } from 'd3-axis';
 import { axisBottom, axisLeft } from 'd3-axis';
 import React, { useState } from 'react';
 
+import { formatBytes, formatInteger } from '../../utils';
+
 import { BarGroup } from './bar-group';
 import { ChartAxis } from './chart-axis';
+import type { BarUnitData, HoveredBarInfo, Margin, SegmentStat } from './common';
 
 import './stacked-bar-chart.scss';
 
-export interface BarUnitData {
-  x: number;
-  y: number;
-  y0?: number;
-  width: number;
-  datasource: string;
-  color: string;
-  dailySize?: number;
-}
-
-export interface BarChartMargin {
-  top: number;
-  right: number;
-  bottom: number;
-  left: number;
-}
-
-export interface HoveredBarInfo {
-  xCoordinate?: number;
-  yCoordinate?: number;
-  height?: number;
-  width?: number;
-  datasource?: string;
-  xValue?: number;
-  yValue?: number;
-  dailySize?: number;
-}
-
 interface StackedBarChartProps {
   svgWidth: number;
   svgHeight: number;
-  margin: BarChartMargin;
-  activeDataType?: string;
+  margin: Margin;
+  shownSegmentStat: SegmentStat;
   dataToRender: BarUnitData[];
-  changeActiveDatasource: (e: string | null) => void;
-  formatTick: (e: number) => string;
+  changeActiveDatasource: (e: string | undefined) => void;
   xScale: AxisScale<Date>;
   yScale: AxisScale<number>;
   barWidth: number;
@@ -71,11 +45,10 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart(
   ref,
 ) {
   const {
-    activeDataType,
+    shownSegmentStat,
     svgWidth,
     svgHeight,
     margin,
-    formatTick,
     xScale,
     yScale,
     dataToRender,
@@ -84,11 +57,36 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart(
   } = props;
   const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
 
+  const formatTick = (n: number) => {
+    if (isNaN(n)) return '';
+    if (shownSegmentStat === 'countData') {
+      return formatInteger(n);
+    } else {
+      return formatBytes(n);
+    }
+  };
+
   const width = svgWidth - margin.left - margin.right;
   const height = svgHeight - margin.top - margin.bottom;
 
-  function renderBarChart() {
-    return (
+  return (
+    <div className="stacked-bar-chart" ref={ref as any}>
+      {hoverOn && (
+        <div className="bar-chart-tooltip">
+          <div>Datasource: {hoverOn.datasource}</div>
+          <div>Time: {hoverOn.xValue}</div>
+          <div>
+            {`${
+              shownSegmentStat === 'countData' ? 'Daily total count:' : 'Daily total size:'
+            } ${formatTick(hoverOn.dailySize)}`}
+          </div>
+          <div>
+            {`${shownSegmentStat === 'countData' ? 'Count:' : 'Size:'} ${formatTick(
+              hoverOn.yValue,
+            )}`}
+          </div>
+        </div>
+      )}
       <svg
         width={svgWidth}
         height={svgHeight}
@@ -102,7 +100,7 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart(
           <ChartAxis
             className="gridline-x"
             transform="translate(0, 0)"
-            scale={axisLeft(yScale)
+            axis={axisLeft(yScale)
               .ticks(5)
               .tickSize(-width)
               .tickFormat(() => '')
@@ -111,30 +109,28 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart(
           <BarGroup
             dataToRender={dataToRender}
             changeActiveDatasource={changeActiveDatasource}
-            formatTick={formatTick}
             xScale={xScale}
             yScale={yScale}
             onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)}
-            hoverOn={hoverOn}
             barWidth={barWidth}
           />
           <ChartAxis
             className="axis-x"
             transform={`translate(0, ${height})`}
-            scale={axisBottom(xScale)}
+            axis={axisBottom(xScale)}
           />
           <ChartAxis
             className="axis-y"
-            scale={axisLeft(yScale)
+            axis={axisLeft(yScale)
               .ticks(5)
-              .tickFormat((e: number) => formatTick(e))}
+              .tickFormat(e => formatTick(e))}
           />
           {hoverOn && (
             <g
               className="hovered-bar"
               onClick={() => {
                 setHoverOn(undefined);
-                changeActiveDatasource(hoverOn.datasource ?? null);
+                changeActiveDatasource(hoverOn.datasource);
               }}
             >
               <rect
@@ -147,28 +143,6 @@ export const StackedBarChart = React.forwardRef(function StackedBarChart(
           )}
         </g>
       </svg>
-    );
-  }
-
-  return (
-    <div className="stacked-bar-chart" ref={ref as any}>
-      {hoverOn && (
-        <div className="bar-chart-tooltip">
-          <div>Datasource: {hoverOn.datasource}</div>
-          <div>Time: {hoverOn.xValue}</div>
-          <div>
-            {`${
-              activeDataType === 'countData' ? 'Daily total count:' : 'Daily total size:'
-            } ${formatTick(hoverOn.dailySize!)}`}
-          </div>
-          <div>
-            {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${formatTick(
-              hoverOn.yValue!,
-            )}`}
-          </div>
-        </div>
-      )}
-      {renderBarChart()}
     </div>
   );
 });
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index 3fb6a5be40d..cafc69cade5 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -418,7 +418,7 @@ GROUP BY 1, 2`;
         LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
         ['Segment size', 'Segment granularity'],
       ),
-      showSegmentTimeline: false,
+      showSegmentTimeline: true,
 
       actions: [],
     };


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 29/42: fixes

Posted by vo...@apache.org.
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 02b963d818885b60380e2c01021142088a6bef89
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Nov 6 20:43:24 2024 -0800

    fixes
---
 .../segment-timeline/segment-bar-chart-render.tsx  |   3 +-
 .../segment-timeline/segment-bar-chart.tsx         |   4 +-
 .../segment-timeline/segment-timeline.spec.tsx     |   2 +-
 .../segment-timeline/segment-timeline.tsx          | 141 ++++++++++++++++-----
 .../views/datasources-view/datasources-view.tsx    |   9 +-
 .../src/views/segments-view/segments-view.tsx      |   2 +-
 6 files changed, 117 insertions(+), 44 deletions(-)

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 054e06a1725..4666656e246 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
@@ -213,7 +213,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const baseTimeScale = scaleUtc()
     .domain(dateRange)
     .range([EXTEND_X_SCALE_DOMAIN_BY, innerStage.width - EXTEND_X_SCALE_DOMAIN_BY]);
-
   const timeScale = shiftOffset
     ? baseTimeScale.copy().domain(offsetDateRange(dateRange, shiftOffset))
     : baseTimeScale;
@@ -224,7 +223,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   );
   const statScale = scaleLinear()
     .rangeRound([innerStage.height, 0])
-    .domain([0, maxNormalizedStat ?? 1]);
+    .domain([0, (maxNormalizedStat ?? 1) * 1.05]);
 
   const formatTickRate = (n: number) => {
     switch (shownIntervalStat) {
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 b745014d0a2..87fb41bc278 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -83,8 +83,6 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
           .addSelect(F.sum(C('num_rows')).as('rows'))
           .toString();
 
-        console.log(query);
-
         return (await queryDruidSql({ query }, cancelToken)).map(sr => {
           const start = new Date(sr.start);
           const end = new Date(sr.end);
@@ -96,7 +94,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
             realtime: Boolean(sr.realtime),
             originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
           } as IntervalRow;
-        }); // 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 {
         return filterMap(
           await getApiArray(
diff --git a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
index 966e3113153..dbbb7c35fe3 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
@@ -25,7 +25,7 @@ import { SegmentTimeline } from './segment-timeline';
 describe('SegmentTimeline', () => {
   it('matches snapshot', () => {
     const segmentTimeline = (
-      <SegmentTimeline capabilities={Capabilities.FULL} initDatasource={undefined} />
+      <SegmentTimeline capabilities={Capabilities.FULL} datasource={undefined} />
     );
     const { container } = render(segmentTimeline);
     expect(container.firstChild).toMatchSnapshot();
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 75964a70205..4446d7fff99 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -29,8 +29,10 @@ import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { DateRangePicker3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
 import { Select } from '@blueprintjs/select';
-import { useState } from 'react';
+import { C, L, N, SqlExpression, SqlQuery } from '@druid-toolkit/query';
+import { useEffect, useMemo, useState } from 'react';
 
+import { END_OF_TIME_DATE, START_OF_TIME_DATE } from '../../druid-models';
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import {
@@ -40,12 +42,12 @@ import {
   getApiArray,
   isNonNullRange,
   localToUtcDateRange,
-  prettyFormatIsoDate,
   queryDruidSql,
   TZ_UTC,
   utcToLocalDateRange,
 } from '../../utils';
 import { Stage } from '../../utils/stage';
+import { Loader } from '../loader/loader';
 
 import type { IntervalStat } from './common';
 import { getIntervalStatTitle, INTERVAL_STATS } from './common';
@@ -55,7 +57,7 @@ import './segment-timeline.scss';
 
 interface SegmentTimelineProps {
   capabilities: Capabilities;
-  initDatasource: string | undefined;
+  datasource: string | undefined;
 }
 
 const DEFAULT_SHOWN_DURATION = new Duration('P3M');
@@ -74,16 +76,30 @@ function getDateRange(shownDuration: Duration): NonNullDateRange {
   return [shownDuration.shift(end, TZ_UTC, -1), end];
 }
 
+function formatDateRange(dateRange: NonNullDateRange): string {
+  return `${dateRange[0].toISOString().slice(0, 10)} → ${dateRange[1].toISOString().slice(0, 10)}`;
+}
+
+function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean {
+  return dr1[0].valueOf() === dr2[0].valueOf() && dr2[1].valueOf() === dr2[1].valueOf();
+}
+
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
-  const { capabilities, initDatasource } = props;
+  const { capabilities, datasource } = props;
   const [stage, setStage] = useState<Stage | undefined>();
   const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
-  const [shownDatasource, setShownDatasource] = useState<string | undefined>(initDatasource);
-  const [dateRange, setDateRange] = useState<NonNullDateRange>(
-    getDateRange(DEFAULT_SHOWN_DURATION),
-  );
+  const [shownDatasource, setShownDatasource] = useState<string | undefined>(datasource);
+  const [dateRange, setDateRange] = useState<NonNullDateRange | undefined>();
   const [showCustomDatePicker, setShowCustomDatePicker] = useState(false);
 
+  useEffect(() => {
+    setShownDatasource(datasource);
+  }, [datasource]);
+
+  const defaultDateRange = useMemo(() => {
+    return getDateRange(DEFAULT_SHOWN_DURATION);
+  }, []);
+
   const [datasourcesState] = useQueryManager<Capabilities, string[]>({
     initQuery: capabilities,
     processQuery: async (capabilities, cancelToken) => {
@@ -102,6 +118,55 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
     },
   });
 
+  const [initDatasourceDateRangeState] = useQueryManager<string, NonNullDateRange>({
+    query: dateRange ? undefined : shownDatasource,
+    processQuery: async (datasource, cancelToken) => {
+      if (capabilities.hasSql()) {
+        const baseQuery = SqlQuery.from(N('sys').table('segments'))
+          .changeWhereExpression(
+            SqlExpression.and(
+              C('start').unequal(START_OF_TIME_DATE),
+              C('end').unequal(END_OF_TIME_DATE),
+              C('is_overshadowed').equal(0),
+              C('datasource').equal(L(datasource)),
+            ),
+          )
+          .changeLimitValue(1);
+
+        const startQuery = baseQuery
+          .addSelect(C('start'), { addToOrderBy: 'end', direction: 'ASC' })
+          .toString();
+
+        const startRes = await queryDruidSql<{ start: string }>({ query: startQuery }, cancelToken);
+        if (startRes.length !== 1) {
+          return getDateRange(DEFAULT_SHOWN_DURATION);
+        }
+
+        const start = day.floor(new Date(startRes[0].start), TZ_UTC);
+
+        const endQuery = baseQuery
+          .addSelect(C('end'), { addToOrderBy: 'end', direction: 'DESC' })
+          .toString();
+
+        const endRes = await queryDruidSql<{ end: string }>({ query: endQuery }, cancelToken);
+        if (endRes.length !== 1) {
+          return [start, day.ceil(new Date(), TZ_UTC)]; // Should not really get here
+        }
+
+        const end = day.ceil(new Date(endRes[0].end), TZ_UTC);
+        return [start, end];
+      } else {
+        // ToDo: fill me
+        throw new Error('fill me');
+      }
+    },
+  });
+
+  const effectiveDateRange =
+    dateRange ||
+    initDatasourceDateRangeState.data ||
+    (initDatasourceDateRangeState.isLoading() ? undefined : defaultDateRange);
+
   return (
     <div className="segment-timeline">
       <div className="control-bar">
@@ -110,20 +175,24 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             icon={IconNames.CARET_LEFT}
             data-tooltip="Previous time period"
             small
+            disabled={!effectiveDateRange}
             onClick={() => {
-              const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
-              setDateRange([d.shift(dateRange[0], TZ_UTC, -1), dateRange[0]]);
+              if (!effectiveDateRange) return;
+              const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
+              setDateRange([d.shift(effectiveDateRange[0], TZ_UTC, -1), effectiveDateRange[0]]);
             }}
           />
           <Button
             icon={IconNames.ZOOM_OUT}
             data-tooltip="Zoom out"
             small
+            disabled={!effectiveDateRange}
             onClick={e => {
-              const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
+              if (!effectiveDateRange) return;
+              const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
               setDateRange([
-                d.shift(dateRange[0], TZ_UTC, -1),
-                e.altKey ? d.shift(dateRange[1], TZ_UTC, 1) : dateRange[1],
+                d.shift(effectiveDateRange[0], TZ_UTC, -1),
+                e.altKey ? d.shift(effectiveDateRange[1], TZ_UTC, 1) : effectiveDateRange[1],
               ]);
             }}
           />
@@ -131,28 +200,36 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             icon={IconNames.CARET_RIGHT}
             data-tooltip="Next time period"
             small
+            disabled={!effectiveDateRange}
             onClick={() => {
-              const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
-              setDateRange([dateRange[1], d.shift(dateRange[1], TZ_UTC, 1)]);
+              if (!effectiveDateRange) return;
+              const d = Duration.fromRange(effectiveDateRange[0], effectiveDateRange[1], TZ_UTC);
+              setDateRange([effectiveDateRange[1], d.shift(effectiveDateRange[1], TZ_UTC, 1)]);
             }}
           />
         </ButtonGroup>
         <ButtonGroup>
-          {SHOWN_DURATION_OPTIONS.map((d, i) => (
-            <Button
-              key={i}
-              text={d.toString().replace('P', '')}
-              data-tooltip={`Show last ${d.getDescription()}`}
-              small
-              onClick={() => setDateRange(getDateRange(d))}
-            />
-          ))}
+          {SHOWN_DURATION_OPTIONS.map((d, i) => {
+            const dr = getDateRange(d);
+            return (
+              <Button
+                key={i}
+                text={d.toString().replace('P', '')}
+                active={effectiveDateRange && dateRangesEqual(effectiveDateRange, dr)}
+                data-tooltip={`Show last ${d.getDescription()}\n${formatDateRange(dr)}`}
+                small
+                onClick={() => setDateRange(dr)}
+              />
+            );
+          })}
           <Popover
             isOpen={showCustomDatePicker}
             onInteraction={setShowCustomDatePicker}
             content={
               <DateRangePicker3
-                defaultValue={utcToLocalDateRange(dateRange)}
+                defaultValue={utcToLocalDateRange(
+                  effectiveDateRange || getDateRange(DEFAULT_SHOWN_DURATION),
+                )}
                 onChange={newDateRange => {
                   const newUtcDateRange = localToUtcDateRange(newDateRange);
                   if (!isNonNullRange(newUtcDateRange)) return;
@@ -168,14 +245,9 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           >
             <Button
               icon={IconNames.CALENDAR}
+              text={effectiveDateRange ? formatDateRange(effectiveDateRange) : '? → ?'}
               small
-              data-tooltip={
-                showCustomDatePicker
-                  ? undefined
-                  : `Select a custom date range\nCurrent range: ${prettyFormatIsoDate(
-                      dateRange[0],
-                    )} - ${prettyFormatIsoDate(dateRange[1])}`
-              }
+              data-tooltip={showCustomDatePicker ? undefined : `Select a custom date range`}
             />
           </Popover>
         </ButtonGroup>
@@ -248,11 +320,11 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
         }}
       >
         <div className="chart-container">
-          {stage && (
+          {stage && effectiveDateRange && (
             <SegmentBarChart
               capabilities={capabilities}
               stage={stage}
-              dateRange={dateRange}
+              dateRange={effectiveDateRange}
               changeDateRange={setDateRange}
               shownIntervalStat={activeSegmentStat}
               shownDatasource={shownDatasource}
@@ -261,6 +333,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               }
             />
           )}
+          {initDatasourceDateRangeState.isLoading() && <Loader />}
         </div>
       </ResizeSensor>
     </div>
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index de30832ce39..e79592d3aa5 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -1147,7 +1147,7 @@ GROUP BY 1, 2`;
   }
 
   private renderDatasourcesTable() {
-    const { goToSegments, goToTasks, capabilities, filters, onFiltersChange } = this.props;
+    const { goToTasks, capabilities, filters, onFiltersChange } = this.props;
     const { datasourcesAndDefaultRulesState, showUnused, visibleColumns, showSegmentTimeline } =
       this.state;
 
@@ -1240,7 +1240,10 @@ GROUP BY 1, 2`;
               const hasZeroReplicationRule = RuleUtil.hasZeroReplicaRule(rules, defaultRules);
               const descriptor = hasZeroReplicationRule ? 'pre-cached' : 'available';
               const segmentsEl = (
-                <a onClick={() => goToSegments(datasource)}>
+                <a
+                  onClick={() => this.setState({ showSegmentTimeline: { datasource } })}
+                  data-tooltip="Show in segment timeline"
+                >
                   {pluralIfNeeded(num_segments, 'segment')}
                 </a>
               );
@@ -1742,7 +1745,7 @@ GROUP BY 1, 2`;
           {showSegmentTimeline && (
             <SegmentTimeline
               capabilities={capabilities}
-              initDatasource={showSegmentTimeline.datasource}
+              datasource={showSegmentTimeline.datasource}
             />
           )}
           {this.renderDatasourcesTable()}
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 98e8fa44bb0..6f3cd468360 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -1054,7 +1054,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           secondaryMinSize={10}
         >
           {showSegmentTimeline && (
-            <SegmentTimeline capabilities={capabilities} initDatasource={undefined} />
+            <SegmentTimeline capabilities={capabilities} datasource={undefined} />
           )}
           {this.renderSegmentsTable()}
         </SplitterLayout>


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 32/42: misc fixes

Posted by vo...@apache.org.
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 06562491fda88a56d65c2146b9f2a4b8cf3cdc7b
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 8 11:51:31 2024 -0800

    misc fixes
---
 web-console/src/components/auto-form/auto-form.tsx |  1 -
 .../src/components/rule-editor/rule-editor.scss    |  4 ++
 .../src/components/rule-editor/rule-editor.tsx     |  2 +-
 .../segment-timeline/segment-bar-chart-render.scss |  1 +
 .../segment-timeline/segment-bar-chart-render.tsx  |  2 +-
 .../segment-timeline/segment-timeline.tsx          | 55 +++++++++++-----------
 .../table-clickable-cell/table-clickable-cell.tsx  |  5 +-
 .../views/datasources-view/datasources-view.tsx    | 18 +++++--
 .../src/views/lookups-view/lookups-view.tsx        |  1 +
 .../src/views/segments-view/segments-view.tsx      |  6 +++
 .../views/supervisors-view/supervisors-view.tsx    |  5 +-
 web-console/src/views/tasks-view/tasks-view.tsx    |  1 +
 .../execution-stages-pane.tsx                      |  2 +-
 13 files changed, 63 insertions(+), 40 deletions(-)

diff --git a/web-console/src/components/auto-form/auto-form.tsx b/web-console/src/components/auto-form/auto-form.tsx
index 4c51d2cbf5a..7026ca5d5dc 100644
--- a/web-console/src/components/auto-form/auto-form.tsx
+++ b/web-console/src/components/auto-form/auto-form.tsx
@@ -539,7 +539,6 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
           text={showMore ? 'Show less' : 'Show more'}
           rightIcon={showMore ? IconNames.CHEVRON_UP : IconNames.CHEVRON_DOWN}
           minimal
-          fill
           onClick={() => {
             this.setState(({ showMore }) => ({ showMore: !showMore }));
           }}
diff --git a/web-console/src/components/rule-editor/rule-editor.scss b/web-console/src/components/rule-editor/rule-editor.scss
index e5757bc2999..cb021e85c01 100644
--- a/web-console/src/components/rule-editor/rule-editor.scss
+++ b/web-console/src/components/rule-editor/rule-editor.scss
@@ -28,4 +28,8 @@
   .include-future {
     margin-left: 15px;
   }
+
+  .rule-detail {
+    border: 1px solid rgba(255, 255, 255, 0.15);
+  }
 }
diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx
index 0181e4341ba..8cea5321cfd 100644
--- a/web-console/src/components/rule-editor/rule-editor.tsx
+++ b/web-console/src/components/rule-editor/rule-editor.tsx
@@ -170,7 +170,7 @@ export const RuleEditor = React.memo(function RuleEditor(props: RuleEditorProps)
       </div>
 
       <Collapse isOpen={isOpen}>
-        <Card elevation={2}>
+        <Card className="rule-detail" elevation={2}>
           <FormGroup>
             <ControlGroup>
               <HTMLSelect
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 7667dc5b1f3..75da5e4e666 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
@@ -109,6 +109,7 @@
       padding-left: 2px;
       top: 0;
       height: 100%;
+      text-overflow: ellipsis;
     }
   }
 
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 7bdef7e2db2..7ba310048c0 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
@@ -458,7 +458,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           backgroundColor: colors[i % colors.length],
         }}
       >
-        {width > 90 ? title : undefined}
+        {title}
       </div>
     );
   }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index d4294750a7b..065e8e5a5ae 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -245,7 +245,31 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           />
         </Popover>
         <div className="expander" />
-
+        <ButtonGroup>
+          <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)}`}
+            small
+            disabled={!nextDateRange}
+            onClick={() => setDateRange(nextDateRange)}
+          />
+        </ButtonGroup>
         <ButtonGroup>
           {SHOWN_DURATION_OPTIONS.map((d, i) => {
             const dr = getDateRange(d);
@@ -284,38 +308,15 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             <Button
               icon={IconNames.CALENDAR}
               text={
-                effectiveDateRange ? formatDateRange(effectiveDateRange) : '????-??-?? → ????-??-??'
+                effectiveDateRange
+                  ? formatDateRange(effectiveDateRange)
+                  : `Loading datasource date range`
               }
               small
               data-tooltip={showCustomDatePicker ? undefined : `Select a custom date range`}
             />
           </Popover>
         </ButtonGroup>
-        <ButtonGroup>
-          <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)}`}
-            small
-            disabled={!nextDateRange}
-            onClick={() => setDateRange(nextDateRange)}
-          />
-        </ButtonGroup>
       </div>
       <ResizeSensor
         onResize={(entries: ResizeObserverEntry[]) => {
diff --git a/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx b/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
index aec706a50fc..38c69e297e0 100644
--- a/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
+++ b/web-console/src/components/table-clickable-cell/table-clickable-cell.tsx
@@ -28,7 +28,7 @@ export interface TableClickableCellProps {
   className?: string;
   onClick: MouseEventHandler<any>;
   hoverIcon?: IconName;
-  title?: string;
+  tooltip?: string;
   disabled?: boolean;
   children?: ReactNode;
 }
@@ -36,12 +36,13 @@ export interface TableClickableCellProps {
 export const TableClickableCell = React.memo(function TableClickableCell(
   props: TableClickableCellProps,
 ) {
-  const { className, onClick, hoverIcon, disabled, children, ...rest } = props;
+  const { className, onClick, hoverIcon, disabled, children, tooltip, ...rest } = props;
 
   return (
     <div
       className={classNames('table-clickable-cell', className, { disabled })}
       onClick={disabled ? undefined : onClick}
+      data-tooltip={tooltip}
       {...rest}
     >
       {children}
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index 30f17061d4f..9891368e585 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -245,6 +245,7 @@ interface DatasourcesAndDefaultRules {
 interface RetentionDialogOpenOn {
   readonly datasource: string;
   readonly rules: Rule[];
+  readonly defaultRules: Rule[];
 }
 
 interface CompactionConfigDialogOpenOn {
@@ -927,6 +928,7 @@ GROUP BY 1, 2`;
           retentionDialogOpenOn: {
             datasource: '_default',
             rules: defaultRules,
+            defaultRules,
           },
         };
       });
@@ -1036,10 +1038,13 @@ GROUP BY 1, 2`;
             icon: IconNames.AUTOMATIC_UPDATES,
             title: 'Edit retention rules',
             onAction: () => {
+              const defaultRules = this.state.datasourcesAndDefaultRulesState.data?.defaultRules;
+              if (!defaultRules) return;
               this.setState({
                 retentionDialogOpenOn: {
                   datasource,
                   rules: rules || [],
+                  defaultRules,
                 },
               });
             },
@@ -1103,9 +1108,8 @@ GROUP BY 1, 2`;
 
   private renderRetentionDialog() {
     const { capabilities } = this.props;
-    const { retentionDialogOpenOn, datasourcesAndDefaultRulesState } = this.state;
-    const defaultRules = datasourcesAndDefaultRulesState.data?.defaultRules;
-    if (!retentionDialogOpenOn || !defaultRules) return;
+    const { retentionDialogOpenOn } = this.state;
+    if (!retentionDialogOpenOn) return;
 
     return (
       <RetentionDialog
@@ -1113,7 +1117,7 @@ GROUP BY 1, 2`;
         rules={retentionDialogOpenOn.rules}
         capabilities={capabilities}
         onEditDefaults={this.editDefaultRules}
-        defaultRules={defaultRules}
+        defaultRules={retentionDialogOpenOn.defaultRules}
         onCancel={() => this.setState({ retentionDialogOpenOn: undefined })}
         onSave={this.saveRules}
       />
@@ -1205,6 +1209,7 @@ GROUP BY 1, 2`;
               <TableClickableCell
                 onClick={() => this.onDetail(original)}
                 hoverIcon={IconNames.SEARCH_TEMPLATE}
+                tooltip="Show detail"
               >
                 {showSegmentTimeline ? (
                   <>
@@ -1334,7 +1339,7 @@ GROUP BY 1, 2`;
                 <TableClickableCell
                   onClick={() => goToTasks(original.datasource)}
                   hoverIcon={IconNames.ARROW_TOP_RIGHT}
-                  title="Go to tasks"
+                  tooltip="Go to tasks"
                 >
                   {formatRunningTasks(runningTasks)}
                 </TableClickableCell>
@@ -1514,6 +1519,7 @@ GROUP BY 1, 2`;
               if (!compaction) return;
               return (
                 <TableClickableCell
+                  tooltip="Open compaction configuration"
                   disabled={!compaction}
                   onClick={() => {
                     if (!compaction) return;
@@ -1630,6 +1636,7 @@ GROUP BY 1, 2`;
 
               return (
                 <TableClickableCell
+                  tooltip="Open retention (load rule) configuration"
                   disabled={!defaultRules}
                   onClick={() => {
                     if (!defaultRules) return;
@@ -1637,6 +1644,7 @@ GROUP BY 1, 2`;
                       retentionDialogOpenOn: {
                         datasource,
                         rules,
+                        defaultRules,
                       },
                     });
                   }}
diff --git a/web-console/src/views/lookups-view/lookups-view.tsx b/web-console/src/views/lookups-view/lookups-view.tsx
index f9d188078a7..99c3d2b03f3 100644
--- a/web-console/src/views/lookups-view/lookups-view.tsx
+++ b/web-console/src/views/lookups-view/lookups-view.tsx
@@ -387,6 +387,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
             width: 200,
             Cell: ({ value, original }) => (
               <TableClickableCell
+                tooltip="Show detail"
                 onClick={() => this.onDetail(original)}
                 hoverIcon={IconNames.SEARCH_TEMPLATE}
               >
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 6f3cd468360..3233c587a4e 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -577,6 +577,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             filterable: allowGeneralFilter,
             Cell: row => (
               <TableClickableCell
+                tooltip="Show detail"
                 onClick={() => this.onDetail(row.value, row.row.datasource)}
                 hoverIcon={IconNames.SEARCH_TEMPLATE}
               >
@@ -688,6 +689,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                   return (
                     <TableClickableCell
                       className="range-detail"
+                      tooltip="Show full shardSpec"
                       onClick={onShowFullShardSpec}
                       hoverIcon={IconNames.EYE_OPEN}
                     >
@@ -704,6 +706,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                   return (
                     <TableClickableCell
                       className="range-detail"
+                      tooltip="Show full shardSpec"
                       onClick={onShowFullShardSpec}
                       hoverIcon={IconNames.EYE_OPEN}
                     >
@@ -725,6 +728,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                   if (!Array.isArray(partitionDimensions)) return JSONBig.stringify(value);
                   return (
                     <TableClickableCell
+                      tooltip="Show full shardSpec"
                       onClick={onShowFullShardSpec}
                       hoverIcon={IconNames.EYE_OPEN}
                     >
@@ -742,6 +746,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                 case 'tombstone':
                   return (
                     <TableClickableCell
+                      tooltip="Show full shardSpec"
                       onClick={onShowFullShardSpec}
                       hoverIcon={IconNames.EYE_OPEN}
                     >
@@ -752,6 +757,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                 default:
                   return (
                     <TableClickableCell
+                      tooltip="Show full shardSpec"
                       onClick={onShowFullShardSpec}
                       hoverIcon={IconNames.EYE_OPEN}
                     >
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx
index 334339bc7cc..045ec45e1f8 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -715,6 +715,7 @@ export class SupervisorsView extends React.PureComponent<
             show: visibleColumns.shown('Supervisor ID'),
             Cell: ({ value, original }) => (
               <TableClickableCell
+                tooltip="Show detail"
                 onClick={() => this.onSupervisorDetail(original)}
                 hoverIcon={IconNames.SEARCH_TEMPLATE}
               >
@@ -811,9 +812,9 @@ export class SupervisorsView extends React.PureComponent<
               }
               return (
                 <TableClickableCell
+                  tooltip="Go to tasks"
                   onClick={() => goToTasks(original.supervisor_id, `index_${original.type}`)}
                   hoverIcon={IconNames.ARROW_TOP_RIGHT}
-                  title="Go to tasks"
                 >
                   {label}
                 </TableClickableCell>
@@ -923,9 +924,9 @@ export class SupervisorsView extends React.PureComponent<
               if (!value) return null;
               return (
                 <TableClickableCell
+                  tooltip="Show errors"
                   onClick={() => this.onSupervisorDetail(original)}
                   hoverIcon={IconNames.SEARCH_TEMPLATE}
-                  title="See errors"
                 >
                   {pluralIfNeeded(value.length, 'error')}
                 </TableClickableCell>
diff --git a/web-console/src/views/tasks-view/tasks-view.tsx b/web-console/src/views/tasks-view/tasks-view.tsx
index 0e44c96413f..5142da787cd 100644
--- a/web-console/src/views/tasks-view/tasks-view.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.tsx
@@ -372,6 +372,7 @@ ORDER BY
             width: 440,
             Cell: ({ value, original }) => (
               <TableClickableCell
+                tooltip="Show detail"
                 onClick={() => this.onTaskDetail(original)}
                 hoverIcon={IconNames.SEARCH_TEMPLATE}
               >
diff --git a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
index 5ea2e97bb09..d7c447d090c 100644
--- a/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
+++ b/web-console/src/views/workbench-view/execution-stages-pane/execution-stages-pane.tsx
@@ -251,7 +251,7 @@ export const ExecutionStagesPane = React.memo(function ExecutionStagesPane(
               return (
                 <TableClickableCell
                   hoverIcon={IconNames.SHARE}
-                  data-tooltip={`Go to task: ${taskId}`}
+                  tooltip={`Go to task: ${taskId}`}
                   onClick={() => {
                     goToTask(taskId);
                   }}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 34/42: fixes

Posted by vo...@apache.org.
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 020d7ac5477ea9362c9b438b53405b40b2f92b12
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Nov 12 12:21:35 2024 -0800

    fixes
---
 .../segment-timeline/segment-bar-chart-render.scss | 24 ++++++++++
 .../segment-timeline/segment-bar-chart-render.tsx  | 36 ++++-----------
 .../segment-timeline/segment-timeline.tsx          | 53 ++++++++++++++--------
 web-console/src/utils/date.ts                      |  8 ++++
 4 files changed, 76 insertions(+), 45 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 de13ad4c123..576cf54e146 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
@@ -111,6 +111,30 @@
       top: 0;
       height: 100%;
       text-overflow: ellipsis;
+
+      &.load {
+        background-color: $green1;
+
+        &:nth-child(even) {
+          background-color: $green3;
+        }
+      }
+
+      &.drop {
+        background-color: $dark-gray5;
+
+        &:nth-child(even) {
+          background-color: $gray1;
+        }
+      }
+
+      &.broadcast {
+        background-color: $indigo1;
+
+        &:nth-child(even) {
+          background-color: $indigo3;
+        }
+      }
     }
   }
 
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 a397c619101..6dcadc9b629 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
@@ -66,27 +66,9 @@ 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'];
-  }
+function loadRuleToBaseType(loadRule: Rule): string {
+  const m = /^(load|drop|broadcast)/.exec(loadRule.type);
+  return m ? m[1] : 'load';
 }
 
 const NEGATIVE_INFINITY_DATE = new Date(Date.UTC(1000, 0, 1));
@@ -431,9 +413,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       y: rect.y + CHART_MARGIN.top - 10,
       text: (
         <>
-          <div>{`${prettyFormatIsoDate(hoveredIntervalBar.start)}/${
-            hoveredIntervalBar.originalTimeSpan
-          }${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
+          <div>{`${prettyFormatIsoDate(hoveredIntervalBar.start)
+            .replace(/:00:00$/, '')
+            .replace(/ 00$/, '')}/${hoveredIntervalBar.originalTimeSpan}${
+            hoveredIntervalBar.realtime ? ' (realtime)' : ''
+          }`}</div>
           <div>Datasource: {hoveredIntervalBar.datasource}</div>
           <div>{`Size: ${
             hoveredIntervalBar.realtime
@@ -450,17 +434,15 @@ 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"
+        className={classNames('load-rule', loadRuleToBaseType(loadRule))}
         data-tooltip={title}
         style={{
           left: x + CHART_MARGIN.left,
           width,
-          backgroundColor: colors[i % colors.length],
         }}
       >
         {title}
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 065e8e5a5ae..44d84cdc7d6 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -42,6 +42,7 @@ import {
   getApiArray,
   isNonNullRange,
   localToUtcDateRange,
+  maxDate,
   queryDruidSql,
   TZ_UTC,
   utcToLocalDateRange,
@@ -60,7 +61,7 @@ interface SegmentTimelineProps {
   datasource: string | undefined;
 }
 
-const DEFAULT_SHOWN_DURATION = new Duration('P3M');
+const DEFAULT_SHOWN_DURATION = new Duration('P1Y');
 const SHOWN_DURATION_OPTIONS: Duration[] = [
   new Duration('P1D'),
   new Duration('P1W'),
@@ -118,9 +119,11 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
     },
   });
 
-  const [initDatasourceDateRangeState] = useQueryManager<string, NonNullDateRange>({
-    query: dateRange ? undefined : shownDatasource,
+  const [initDatasourceDateRangeState] = useQueryManager<string | null, NonNullDateRange>({
+    query: dateRange ? undefined : shownDatasource ?? null,
     processQuery: async (datasource, cancelToken) => {
+      let queriedStart: Date;
+      let queriedEnd: Date;
       if (capabilities.hasSql()) {
         const baseQuery = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
@@ -128,37 +131,41 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               C('start').unequal(START_OF_TIME_DATE),
               C('end').unequal(END_OF_TIME_DATE),
               C('is_overshadowed').equal(0),
-              C('datasource').equal(L(datasource)),
+              datasource ? C('datasource').equal(L(datasource)) : undefined,
             ),
           )
           .changeLimitValue(1);
 
-        const startQuery = baseQuery
-          .addSelect(C('start'), { addToOrderBy: 'end', direction: 'ASC' })
+        const endQuery = baseQuery
+          .addSelect(C('end'), { addToOrderBy: 'end', direction: 'DESC' })
           .toString();
 
-        const startRes = await queryDruidSql<{ start: string }>({ query: startQuery }, cancelToken);
-        if (startRes.length !== 1) {
+        const endRes = await queryDruidSql<{ end: string }>({ query: endQuery }, cancelToken);
+        if (endRes.length !== 1) {
           return getDateRange(DEFAULT_SHOWN_DURATION);
         }
 
-        const start = day.floor(new Date(startRes[0].start), TZ_UTC);
+        queriedEnd = day.ceil(new Date(endRes[0].end), TZ_UTC);
 
-        const endQuery = baseQuery
-          .addSelect(C('end'), { addToOrderBy: 'end', direction: 'DESC' })
+        const startQuery = baseQuery
+          .addSelect(C('start'), { addToOrderBy: 'end', direction: 'ASC' })
           .toString();
 
-        const endRes = await queryDruidSql<{ end: string }>({ query: endQuery }, cancelToken);
-        if (endRes.length !== 1) {
-          return [start, day.ceil(new Date(), TZ_UTC)]; // Should not really get here
+        const startRes = await queryDruidSql<{ start: string }>({ query: startQuery }, cancelToken);
+        if (startRes.length !== 1) {
+          return [DEFAULT_SHOWN_DURATION.shift(queriedEnd, TZ_UTC, -1), queriedEnd]; // Should not really get here
         }
 
-        const end = day.ceil(new Date(endRes[0].end), TZ_UTC);
-        return [start, end];
+        queriedStart = day.floor(new Date(startRes[0].start), TZ_UTC);
       } else {
         // ToDo: fill me
         throw new Error('fill me');
       }
+
+      return [
+        maxDate(queriedStart, DEFAULT_SHOWN_DURATION.shift(queriedEnd, TZ_UTC, -1)),
+        queriedEnd,
+      ];
     },
   });
 
@@ -277,8 +284,8 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               <Button
                 key={i}
                 text={d.toString().replace('P', '')}
-                active={effectiveDateRange && dateRangesEqual(effectiveDateRange, dr)}
                 data-tooltip={`Show last ${d.getDescription()}\n${formatDateRange(dr)}`}
+                active={effectiveDateRange && dateRangesEqual(effectiveDateRange, dr)}
                 small
                 onClick={() => setDateRange(dr)}
               />
@@ -312,10 +319,20 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
                   ? formatDateRange(effectiveDateRange)
                   : `Loading datasource date range`
               }
-              small
               data-tooltip={showCustomDatePicker ? undefined : `Select a custom date range`}
+              small
             />
           </Popover>
+          <Button
+            icon={IconNames.PIN}
+            data-tooltip={
+              dateRange ? 'Pin the date range' : 'Auto determine date range to fit datasource'
+            }
+            active={Boolean(dateRange)}
+            disabled={!effectiveDateRange}
+            small
+            onClick={() => setDateRange(dateRange ? undefined : effectiveDateRange)}
+          />
         </ButtonGroup>
       </div>
       <ResizeSensor
diff --git a/web-console/src/utils/date.ts b/web-console/src/utils/date.ts
index e59c01a74df..08f1d1df43b 100644
--- a/web-console/src/utils/date.ts
+++ b/web-console/src/utils/date.ts
@@ -98,3 +98,11 @@ export function localDateRangeToInterval(localRange: DateRange): string {
     localEndDate ? localToUtcDate(localEndDate).toISOString().slice(0, 19) : ''
   }`;
 }
+
+export function maxDate(a: Date, b: Date): Date {
+  return a > b ? a : b;
+}
+
+export function minDate(a: Date, b: Date): Date {
+  return a < b ? a : b;
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 12/42: auto trimming

Posted by vo...@apache.org.
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 f08a337e44542b0dcbbf082040ff050346bd05b9
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 30 14:53:11 2024 -0700

    auto trimming
---
 .../src/components/segment-timeline/common.ts      |  30 +++--
 .../segment-timeline/segment-bar-chart-render.tsx  | 147 ++++++++++++++++-----
 .../segment-timeline/segment-bar-chart.tsx         |  91 ++++---------
 .../segment-timeline/segment-timeline.tsx          |  14 +-
 web-console/src/utils/general.spec.ts              |   7 -
 web-console/src/utils/general.tsx                  |   4 -
 6 files changed, 165 insertions(+), 128 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index bfb6d84e5f3..966ade312ff 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -18,34 +18,40 @@
 
 import { sum } from 'd3-array';
 
-export type SegmentStat = 'count' | 'size' | 'rows';
+import type { Duration } from '../../utils';
+
+export type IntervalStat = 'segments' | 'size' | 'rows';
 
 export function aggregateSegmentStats(
-  xs: readonly Record<SegmentStat, number>[],
-): Record<SegmentStat, number> {
+  xs: readonly Record<IntervalStat, number>[],
+): Record<IntervalStat, number> {
   return {
-    count: sum(xs, s => s.count),
+    segments: sum(xs, s => s.segments),
     size: sum(xs, s => s.size),
     rows: sum(xs, s => s.rows),
   };
 }
 
-export interface IntervalRow extends Record<SegmentStat, number> {
+export interface IntervalRow extends Record<IntervalStat, number> {
   start: Date;
   end: Date;
-  durationSeconds: number;
   datasource: string;
+  originalTimeSpan: Duration;
+}
+
+export interface TrimmedIntervalRow extends IntervalRow {
+  shownSeconds: number;
 }
 
-export interface SegmentBar extends IntervalRow {
-  offset: Record<SegmentStat, number>;
+export interface IntervalBar extends TrimmedIntervalRow {
+  offset: Record<IntervalStat, number>;
 }
 
-export function normalizedSegmentRow(sr: IntervalRow): IntervalRow {
+export function normalizeIntervalRow(sr: TrimmedIntervalRow): TrimmedIntervalRow {
   return {
     ...sr,
-    count: sr.count / sr.durationSeconds,
-    size: sr.size / sr.durationSeconds,
-    rows: sr.rows / sr.durationSeconds,
+    segments: sr.segments / sr.shownSeconds,
+    size: sr.size / sr.shownSeconds,
+    rows: sr.rows / sr.shownSeconds,
   };
 }
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 7864021622d..8753f8bca64 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
@@ -17,10 +17,11 @@
  */
 
 import type { NonNullDateRange } from '@blueprintjs/datetime';
+import IntervalTree from '@flatten-js/interval-tree';
 import classNames from 'classnames';
 import { max } from 'd3-array';
 import { axisBottom, axisLeft } from 'd3-axis';
-import { scaleLinear, scaleOrdinal, scaleUtc } from 'd3-scale';
+import { scaleLinear, scaleUtc } from 'd3-scale';
 import type React from 'react';
 import { useMemo, useRef, useState } from 'react';
 
@@ -29,20 +30,34 @@ import {
   capitalizeFirst,
   clamp,
   day,
+  Duration,
   formatByteRate,
   formatBytes,
   formatInteger,
   formatNumber,
+  groupBy,
+  hashJoaat,
+  prettyFormatIsoDate,
   TZ_UTC,
 } from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
 import { ChartAxis } from './chart-axis';
-import type { SegmentBar, SegmentStat } from './common';
+import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
+import { aggregateSegmentStats, normalizeIntervalRow } from './common';
 
 import './segment-bar-chart-render.scss';
 
 const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 80 };
+const MIN_BAR_WIDTH = 2;
+const POSSIBLE_GRANULARITIES = [
+  new Duration('PT15M'),
+  new Duration('PT1H'),
+  new Duration('PT6H'),
+  new Duration('P1D'),
+  new Duration('P1M'),
+  new Duration('P1Y'),
+];
 
 const COLORS = [
   '#b33040',
@@ -63,16 +78,47 @@ const COLORS = [
   '#87606c',
 ];
 
+const COLORIZER = ({ datasource }: IntervalBar) => {
+  const hash = hashJoaat(datasource);
+  return COLORS[hash % COLORS.length];
+};
+
 function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
   return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
 }
 
+function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalBar[] {
+  const sortedIntervalRows = trimmedIntervalRows.sort((a, b) => {
+    const shownSecondsDiff = b.shownSeconds - a.shownSeconds;
+    if (shownSecondsDiff) return shownSecondsDiff;
+
+    const timeSpanDiff =
+      b.originalTimeSpan.getCanonicalLength() - a.originalTimeSpan.getCanonicalLength();
+    if (timeSpanDiff) return timeSpanDiff;
+
+    return b.datasource.localeCompare(a.datasource);
+  });
+
+  const intervalTree = new IntervalTree();
+  return sortedIntervalRows.map(intervalRow => {
+    intervalRow = normalizeIntervalRow(intervalRow);
+    const startMs = intervalRow.start.valueOf();
+    const endMs = intervalRow.end.valueOf();
+    const intervalRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as IntervalRow[];
+    intervalTree.insert([startMs, endMs], intervalRow);
+    return {
+      ...intervalRow,
+      offset: aggregateSegmentStats(intervalRowsBelow),
+    };
+  });
+}
+
 interface SegmentBarChartRenderProps {
   stage: Stage;
   dateRange: NonNullDateRange;
   changeDateRange(dateRange: NonNullDateRange): void;
-  shownSegmentStat: SegmentStat;
-  segmentBars: SegmentBar[];
+  shownIntervalStat: IntervalStat;
+  intervalRows: IntervalRow[];
   changeActiveDatasource(datasource: string | undefined): void;
 }
 
@@ -81,13 +127,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 ) {
   const {
     stage,
-    shownSegmentStat,
+    shownIntervalStat,
     dateRange,
     changeDateRange,
-    segmentBars,
+    intervalRows,
     changeActiveDatasource,
   } = props;
-  const [hoverOn, setHoverOn] = useState<SegmentBar>();
+  const [hoverOn, setHoverOn] = useState<IntervalBar>();
   const [mouseDownAt, setMouseDownAt] = useState<
     { time: Date; action: 'select' | 'shift' } | undefined
   >();
@@ -95,6 +141,50 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
   const svgRef = useRef<SVGSVGElement | null>(null);
 
+  const trimGranularity = useMemo(() => {
+    return Duration.pickSmallestGranularityThatFits(
+      POSSIBLE_GRANULARITIES,
+      dateRange[1].valueOf() - dateRange[0].valueOf(),
+      Math.floor(stage.width / MIN_BAR_WIDTH),
+    ).toString();
+  }, [dateRange, stage.width]);
+
+  console.log(trimGranularity);
+
+  const intervalBars = useMemo(() => {
+    const trimDuration = new Duration(trimGranularity);
+    const trimmedIntervalRows = intervalRows.map(intervalRow => {
+      const start = trimDuration.floor(intervalRow.start, TZ_UTC);
+      const end = trimDuration.ceil(intervalRow.end, TZ_UTC);
+      return {
+        ...intervalRow,
+        start,
+        end,
+        shownSeconds: (end.valueOf() - start.valueOf()) / 1000,
+      };
+    });
+
+    const fullyGroupedSegmentRows = groupBy(
+      trimmedIntervalRows,
+      trimmedIntervalRow =>
+        [
+          trimmedIntervalRow.start.toISOString(),
+          trimmedIntervalRow.end.toISOString(),
+          trimmedIntervalRow.originalTimeSpan,
+          trimmedIntervalRow.datasource,
+        ].join('/'),
+      (trimmedIntervalRows): TrimmedIntervalRow => {
+        const firstIntervalRow = trimmedIntervalRows[0];
+        return {
+          ...firstIntervalRow,
+          ...aggregateSegmentStats(trimmedIntervalRows),
+        };
+      },
+    );
+
+    return stackIntervalRows(fullyGroupedSegmentRows);
+  }, [intervalRows, trimGranularity]);
+
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
   const baseTimeScale = scaleUtc().domain(dateRange).range([0, innerStage.width]);
@@ -103,19 +193,14 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     ? baseTimeScale.copy().domain(offsetDateRange(dateRange, shiftOffset))
     : baseTimeScale;
 
-  const colorizer = useMemo(() => {
-    const s = scaleOrdinal().range(COLORS);
-    return (d: SegmentBar) => s(d.datasource) as string;
-  }, []);
-
-  const maxStat = max(segmentBars, d => d[shownSegmentStat] + d.offset[shownSegmentStat]);
+  const maxStat = max(intervalBars, d => d[shownIntervalStat] + d.offset[shownIntervalStat]);
   const statScale = scaleLinear()
     .rangeRound([innerStage.height, 0])
     .domain([0, maxStat ?? 1]);
 
   const formatTick = (n: number) => {
-    switch (shownSegmentStat) {
-      case 'count':
+    switch (shownIntervalStat) {
+      case 'segments':
       case 'rows':
         return formatInteger(n);
 
@@ -125,8 +210,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   };
 
   const formatTickRate = (n: number) => {
-    switch (shownSegmentStat) {
-      case 'count':
+    switch (shownIntervalStat) {
+      case 'segments':
         return formatNumber(n) + ' seg/s';
 
       case 'rows':
@@ -185,11 +270,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     }
   });
 
-  function segmentBarToRect(segmentBar: SegmentBar) {
-    const xStart = clamp(timeScale(segmentBar.start), 0, innerStage.width);
-    const xEnd = clamp(timeScale(segmentBar.end), 0, innerStage.width);
-    const y0 = statScale(segmentBar.offset[shownSegmentStat]);
-    const y = statScale(segmentBar[shownSegmentStat] + segmentBar.offset[shownSegmentStat]);
+  function segmentBarToRect(intervalBar: IntervalBar) {
+    const xStart = clamp(timeScale(intervalBar.start), 0, innerStage.width);
+    const xEnd = clamp(timeScale(intervalBar.end), 0, innerStage.width);
+    const y0 = statScale(intervalBar.offset[shownIntervalStat]);
+    const y = statScale(intervalBar[shownIntervalStat] + intervalBar.offset[shownIntervalStat]);
 
     return {
       x: xStart,
@@ -209,10 +294,10 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       ) : hoverOn ? (
         <div className="bar-chart-tooltip">
           <div>Datasource: {hoverOn.datasource}</div>
-          <div>Time: {hoverOn.start.toISOString()}</div>
+          <div>{`Time: ${prettyFormatIsoDate(hoverOn.start)}/${hoverOn.originalTimeSpan}`}</div>
           <div>
-            {`${capitalizeFirst(shownSegmentStat)}: ${formatTick(
-              hoverOn[shownSegmentStat] * hoverOn.durationSeconds,
+            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(
+              hoverOn[shownIntervalStat] * hoverOn.shownSeconds,
             )}`}
           </div>
         </div>
@@ -259,21 +344,21 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
               .tickFormat(e => formatTickRate(e.valueOf()))}
           />
           <g className="bar-group">
-            {segmentBars.map((segmentBar, i) => {
+            {intervalBars.map((intervalBar, i) => {
               return (
                 <rect
                   key={i}
                   className="bar-unit"
-                  {...segmentBarToRect(segmentBar)}
-                  style={{ fill: colorizer(segmentBar) }}
+                  {...segmentBarToRect(intervalBar)}
+                  style={{ fill: COLORIZER(intervalBar) }}
                   onClick={
-                    segmentBar.datasource
-                      ? () => changeActiveDatasource(segmentBar.datasource)
+                    intervalBar.datasource
+                      ? () => changeActiveDatasource(intervalBar.datasource)
                       : undefined
                   }
                   onMouseOver={() => {
                     if (mouseDownAt) return;
-                    setHoverOn(segmentBar);
+                    setHoverOn(intervalBar);
                   }}
                 />
               );
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 3ba9b16f082..d88a023f883 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -18,50 +18,26 @@
 
 import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query';
-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 { Duration, filterMap, groupBy, queryDruidSql, TZ_UTC } from '../../utils';
+import { Duration, filterMap, queryDruidSql, TZ_UTC } from '../../utils';
 import type { Stage } from '../../utils/stage';
 import { Loader } from '../loader/loader';
 
-import type { IntervalRow, SegmentBar, SegmentStat } from './common';
-import { aggregateSegmentStats, normalizedSegmentRow } from './common';
+import type { IntervalStat } from './common';
 import { SegmentBarChartRender } from './segment-bar-chart-render';
 
 import './segment-bar-chart.scss';
 
-function stackIntervalRows(segmentRows: IntervalRow[]): 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 IntervalRow[];
-    intervalTree.insert([startMs, endMs], segmentRow);
-    return {
-      ...segmentRow,
-      offset: aggregateSegmentStats(segmentRowsBelow),
-    };
-  });
-}
-
 interface SegmentBarChartProps {
   capabilities: Capabilities;
   stage: Stage;
   dateRange: NonNullDateRange;
   changeDateRange(newDateRange: NonNullDateRange): void;
-  shownSegmentStat: SegmentStat;
+  shownIntervalStat: IntervalStat;
   changeActiveDatasource: (datasource: string | undefined) => void;
 }
 
@@ -71,17 +47,15 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     dateRange,
     changeDateRange,
     stage,
-    shownSegmentStat,
+    shownIntervalStat,
     changeActiveDatasource,
   } = props;
 
   const intervalsQuery = useMemo(() => ({ capabilities, dateRange }), [capabilities, dateRange]);
 
-  const [intervalBarsState] = useQueryManager({
+  const [intervalRowsState] = useQueryManager({
     query: intervalsQuery,
     processQuery: async ({ capabilities, dateRange }, cancelToken) => {
-      const trimDuration = new Duration('PT1H');
-      let intervalRows: IntervalRow[];
       if (capabilities.hasSql()) {
         const query = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
@@ -90,25 +64,27 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
           .addSelect(C('start'), { addToGroupBy: 'end' })
           .addSelect(C('end'), { addToGroupBy: 'end' })
           .addSelect(C('datasource'), { addToGroupBy: 'end' })
-          .addSelect(F.count().as('count'))
+          .addSelect(F.count().as('segments'))
           .addSelect(F.sum(C('size')).as('size'))
           .addSelect(F.sum(C('num_rows')).as('rows'));
 
-        intervalRows = (await queryDruidSql({ query: query.toString() })).map(sr => {
-          const start = trimDuration.floor(new Date(sr.start), TZ_UTC);
-          const end = trimDuration.ceil(new Date(sr.end), TZ_UTC);
+        return (await queryDruidSql({ query: query.toString() }, cancelToken)).map(sr => {
+          const start = new Date(sr.start);
+          const end = new Date(sr.end);
+
           return {
             ...sr,
             start,
             end,
-            durationSeconds: (end.valueOf() - start.valueOf()) / 1000,
+            originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
           };
         }); // 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 })
         ).data;
-        intervalRows = (
+
+        return (
           await Promise.all(
             datasources.map(async datasource => {
               const intervalMap = (
@@ -121,17 +97,16 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
               ).data;
 
               return filterMap(Object.entries(intervalMap), ([interval, v]) => {
-                // ToDo: Filter on start end
                 const [startStr, endStr] = interval.split('/');
-                const start = trimDuration.floor(new Date(startStr), TZ_UTC);
-                const end = trimDuration.ceil(new Date(endStr), TZ_UTC);
+                const start = new Date(startStr);
+                const end = new Date(endStr);
+                // ToDo: Filter on start end
                 const { count, size, rows } = v as any;
                 return {
                   start,
                   end,
-                  durationSeconds: (end.valueOf() - start.valueOf()) / 1000,
                   datasource,
-                  count,
+                  segments: count,
                   size,
                   rows,
                 };
@@ -140,49 +115,31 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
           )
         ).flat();
       }
-
-      const fullyGroupedSegmentRows = groupBy(
-        intervalRows,
-        intervalRow =>
-          [
-            intervalRow.start.toISOString(),
-            intervalRow.end.toISOString(),
-            intervalRow.datasource || '',
-          ].join('/'),
-        (intervalRows): IntervalRow => {
-          return {
-            ...intervalRows[0],
-            ...aggregateSegmentStats(intervalRows),
-          };
-        },
-      );
-
-      return stackIntervalRows(fullyGroupedSegmentRows);
     },
   });
 
-  if (intervalBarsState.loading) {
+  if (intervalRowsState.loading) {
     return <Loader />;
   }
 
-  if (intervalBarsState.error) {
+  if (intervalRowsState.error) {
     return (
       <div className="empty-placeholder">
-        <span className="no-data-text">{`Error when loading data: ${intervalBarsState.getErrorMessage()}`}</span>
+        <span className="no-data-text">{`Error when loading data: ${intervalRowsState.getErrorMessage()}`}</span>
       </div>
     );
   }
 
-  const segmentBars = intervalBarsState.data;
-  if (!segmentBars) return null;
+  const intervalRows = intervalRowsState.data;
+  if (!intervalRows) return null;
 
   return (
     <SegmentBarChartRender
       stage={stage}
       dateRange={dateRange}
       changeDateRange={changeDateRange}
-      shownSegmentStat={shownSegmentStat}
-      segmentBars={segmentBars}
+      shownIntervalStat={shownIntervalStat}
+      intervalRows={intervalRows}
       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 db6653c912b..5344916c95e 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -37,7 +37,7 @@ import {
 import { Stage } from '../../utils/stage';
 import { SplitterLayout } from '../splitter-layout/splitter-layout';
 
-import type { SegmentStat } from './common';
+import type { IntervalStat } from './common';
 import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
@@ -56,7 +56,7 @@ function getDefaultDateRange(): NonNullDateRange {
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
   const { capabilities } = props;
   const [stage, setStage] = useState<Stage | undefined>();
-  const [activeSegmentStat, setActiveSegmentStat] = useState<SegmentStat>('size');
+  const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
   const [activeDatasource, setActiveDatasource] = useState<string | undefined>();
   const [dateRange, setDateRange] = useState<NonNullDateRange>(getDefaultDateRange);
 
@@ -127,7 +127,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               stage={stage}
               dateRange={dateRange}
               changeDateRange={setDateRange}
-              shownSegmentStat={activeSegmentStat}
+              shownIntervalStat={activeSegmentStat}
               changeActiveDatasource={(datasource: string | undefined) =>
                 setActiveDatasource(activeDatasource ? undefined : datasource)
               }
@@ -139,7 +139,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
         <FormGroup label="Show">
           <SegmentedControl
             value={activeSegmentStat}
-            onValueChange={s => setActiveSegmentStat(s as SegmentStat)}
+            onValueChange={s => setActiveSegmentStat(s as IntervalStat)}
             fill
             options={[
               {
@@ -147,12 +147,12 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
                 value: 'size',
               },
               {
-                label: 'Rows',
+                label: 'Num. rows',
                 value: 'rows',
               },
               {
-                label: 'Count',
-                value: 'count',
+                label: 'Num. segments',
+                value: 'segments',
               },
             ]}
           />
diff --git a/web-console/src/utils/general.spec.ts b/web-console/src/utils/general.spec.ts
index 4b97cb19acb..e7ad92e7ab7 100644
--- a/web-console/src/utils/general.spec.ts
+++ b/web-console/src/utils/general.spec.ts
@@ -29,7 +29,6 @@ import {
   hashJoaat,
   moveElement,
   moveToIndex,
-  objectHash,
   offsetToRowColumn,
   parseCsvLine,
   swapElements,
@@ -178,12 +177,6 @@ describe('general', () => {
     });
   });
 
-  describe('objectHash', () => {
-    it('works', () => {
-      expect(objectHash({ hello: 'world1' })).toEqual('cc14ad13');
-    });
-  });
-
   describe('offsetToRowColumn', () => {
     it('works', () => {
       const str = 'Hello\nThis is a test\nstring.';
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 0bc959b6045..9e2e5e7f525 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -620,10 +620,6 @@ export function hashJoaat(str: string): number {
   return (hash & 4294967295) >>> 0;
 }
 
-export function objectHash(obj: any): string {
-  return hashJoaat(JSONBig.stringify(obj)).toString(16).padStart(8);
-}
-
 export function hasPopoverOpen(): boolean {
   return Boolean(document.querySelector(`${Classes.PORTAL} ${Classes.OVERLAY} ${Classes.POPOVER}`));
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 24/42: normalize to days

Posted by vo...@apache.org.
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 109022c4c4ec8c1310dba2ccf982139b902a4174
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 4 16:12:10 2024 -0800

    normalize to days
---
 .../src/components/segment-timeline/common.ts      |  2 +-
 .../segment-timeline/segment-bar-chart-render.tsx  | 23 +++++++++++-----------
 2 files changed, 12 insertions(+), 13 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index 7371aada27f..69a1d20c457 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -58,7 +58,7 @@ export interface IntervalRow extends Record<IntervalStat, number> {
 }
 
 export interface TrimmedIntervalRow extends IntervalRow {
-  shownSeconds: number;
+  shownDays: number;
   normalized: Record<IntervalStat, number>;
 }
 
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 0152188e8e2..dbf8302d109 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
@@ -22,7 +22,7 @@ import classNames from 'classnames';
 import { max } from 'd3-array';
 import { axisBottom, axisLeft } from 'd3-axis';
 import { scaleLinear, scaleUtc } from 'd3-scale';
-import type { ReactNode } from 'react';
+import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
 import { useMemo, useRef, useState } from 'react';
 
 import { getDatasourceColor } from '../../druid-models';
@@ -69,8 +69,8 @@ function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDa
 
 function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalBar[] {
   const sortedIntervalRows = trimmedIntervalRows.sort((a, b) => {
-    const shownSecondsDiff = b.shownSeconds - a.shownSeconds;
-    if (shownSecondsDiff) return shownSecondsDiff;
+    const shownDaysDiff = b.shownDays - a.shownDays;
+    if (shownDaysDiff) return shownDaysDiff;
 
     const timeSpanDiff =
       b.originalTimeSpan.getCanonicalLength() - a.originalTimeSpan.getCanonicalLength();
@@ -142,16 +142,16 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       if (focusDatasource && intervalRow.datasource !== focusDatasource) return;
       const start = trimDuration.floor(intervalRow.start, TZ_UTC);
       const end = trimDuration.ceil(intervalRow.end, TZ_UTC);
-      const shownSeconds = (end.valueOf() - start.valueOf()) / 1000;
+      const shownDays = (end.valueOf() - start.valueOf()) / day.canonicalLength;
       return {
         ...intervalRow,
         start,
         end,
-        shownSeconds: (end.valueOf() - start.valueOf()) / 1000,
+        shownDays,
         normalized: {
-          segments: intervalRow.segments / shownSeconds,
-          size: intervalRow.size / shownSeconds,
-          rows: intervalRow.rows / shownSeconds,
+          segments: intervalRow.segments / shownDays,
+          size: intervalRow.size / shownDays,
+          rows: intervalRow.rows / shownDays,
         },
       };
     });
@@ -208,20 +208,19 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   };
 
   const formatTickRate = (n: number) => {
-    n = (n * new Duration(trimGranularity).getCanonicalLength()) / 1000;
     switch (shownIntervalStat) {
       case 'segments':
-        return formatNumber(n) + ' seg/s';
+        return formatNumber(n); // + ' seg/day';
 
       case 'rows':
-        return formatNumber(n) + ' row/s';
+        return formatNumber(n); // + ' row/day';
 
       case 'size':
         return formatBytes(n);
     }
   };
 
-  function handleMouseDown(e: React.MouseEvent) {
+  function handleMouseDown(e: ReactMouseEvent) {
     const svg = svgRef.current;
     if (!svg || hoveredIntervalBar) return;
     e.preventDefault();


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 22/42: preogress

Posted by vo...@apache.org.
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 d80b2beb9566b4ef94ed1f9ab49051cda48433b1
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 4 12:38:13 2024 -0800

    preogress
---
 .../src/components/segment-timeline/bubble.tsx     |  9 ++-
 .../segment-timeline/segment-bar-chart-render.tsx  | 64 +++++++++++-------
 .../segment-timeline/segment-bar-chart.tsx         | 77 +++++++++++-----------
 .../segment-timeline/segment-timeline.tsx          |  9 +--
 4 files changed, 89 insertions(+), 70 deletions(-)

diff --git a/web-console/src/components/segment-timeline/bubble.tsx b/web-console/src/components/segment-timeline/bubble.tsx
index f3a9c287b13..efb72431379 100644
--- a/web-console/src/components/segment-timeline/bubble.tsx
+++ b/web-console/src/components/segment-timeline/bubble.tsx
@@ -17,22 +17,25 @@
  */
 
 import classNames from 'classnames';
+import type { ReactNode } from 'react';
 import { createPortal } from 'react-dom';
 
 import './bubble.scss';
 
 interface BubbleProps {
-  openOn: { x: number; y: number; text: string };
+  className?: string;
+  openOn: { x: number; y: number; text: ReactNode } | undefined;
   direction?: 'up' | 'down';
   mute?: boolean;
 }
 
 export const Bubble = function Bubble(props: BubbleProps) {
-  const { openOn, direction = 'up', mute } = props;
+  const { className, openOn, direction = 'up', mute } = props;
+  if (!openOn) return null;
 
   return createPortal(
     <div
-      className={classNames('bubble', direction, { mute })}
+      className={classNames('bubble', className, direction, { mute })}
       style={{ left: openOn.x, top: openOn.y }}
     >
       {openOn.text}
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 edb3a90e655..0152188e8e2 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
@@ -22,7 +22,7 @@ import classNames from 'classnames';
 import { max } from 'd3-array';
 import { axisBottom, axisLeft } from 'd3-axis';
 import { scaleLinear, scaleUtc } from 'd3-scale';
-import type React from 'react';
+import type { ReactNode } from 'react';
 import { useMemo, useRef, useState } from 'react';
 
 import { getDatasourceColor } from '../../druid-models';
@@ -50,7 +50,7 @@ import { aggregateSegmentStats } from './common';
 
 import './segment-bar-chart-render.scss';
 
-const CHART_MARGIN: Margin = { top: 40, right: 0, bottom: 25, left: 70 };
+const CHART_MARGIN: Margin = { top: 10, right: 0, bottom: 25, left: 70 };
 const MIN_BAR_WIDTH = 2;
 const POSSIBLE_GRANULARITIES = [
   new Duration('PT15M'),
@@ -117,7 +117,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     focusDatasource,
     changeFocusDatasource,
   } = props;
-  const [hoverOn, setHoverOn] = useState<IntervalBar>();
+  const [hoveredIntervalBar, setHoveredIntervalBar] = useState<IntervalBar>();
   const [mouseDownAt, setMouseDownAt] = useState<
     { time: Date; action: 'select' | 'shift' } | undefined
   >();
@@ -223,7 +223,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   function handleMouseDown(e: React.MouseEvent) {
     const svg = svgRef.current;
-    if (!svg) return;
+    if (!svg || hoveredIntervalBar) return;
     e.preventDefault();
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
@@ -327,6 +327,33 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   console.log('Bar chart render');
 
+  let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined;
+  if (hoveredIntervalBar && svgRef.current) {
+    const rect = svgRef.current.getBoundingClientRect();
+    hoveredOpenOn = {
+      x:
+        rect.x +
+        CHART_MARGIN.left +
+        timeScale(
+          new Date((hoveredIntervalBar.start.valueOf() + hoveredIntervalBar.end.valueOf()) / 2),
+        ),
+      y: rect.y + CHART_MARGIN.top - 10,
+      text: (
+        <>
+          <div>Datasource: {hoveredIntervalBar.datasource}</div>
+          <div>{`Time: ${prettyFormatIsoDate(hoveredIntervalBar.start)}/${
+            hoveredIntervalBar.originalTimeSpan
+          }`}</div>
+          <div>
+            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(
+              hoveredIntervalBar[shownIntervalStat],
+            )}`}
+          </div>
+        </>
+      ),
+    };
+  }
+
   return (
     <div className="segment-bar-chart-render">
       <svg
@@ -339,7 +366,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       >
         <g
           transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}
-          onMouseLeave={() => setHoverOn(undefined)}
+          onMouseLeave={() => setHoveredIntervalBar(undefined)}
         >
           <ChartAxis
             className="gridline-x"
@@ -389,18 +416,18 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                   onClick={() => changeFocusDatasource(intervalBar.datasource)}
                   onMouseOver={() => {
                     if (mouseDownAt) return;
-                    setHoverOn(intervalBar);
+                    setHoveredIntervalBar(intervalBar);
                   }}
                 />
               );
             })}
-            {hoverOn && (
+            {hoveredIntervalBar && (
               <rect
                 className="hovered-bar"
-                {...segmentBarToRect(hoverOn)}
+                {...segmentBarToRect(hoveredIntervalBar)}
                 onClick={() => {
-                  setHoverOn(undefined);
-                  changeFocusDatasource(hoverOn.datasource);
+                  setHoveredIntervalBar(undefined);
+                  changeFocusDatasource(hoveredIntervalBar.datasource);
                 }}
               />
             )}
@@ -429,26 +456,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           </g>
         </g>
       </svg>
-      {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: ${prettyFormatIsoDate(hoverOn.start)}/${hoverOn.originalTimeSpan}`}</div>
-          <div>
-            {`${capitalizeFirst(shownIntervalStat)}: ${formatTick(hoverOn[shownIntervalStat])}`}
-          </div>
-        </div>
-      ) : undefined}
       {!intervalRows.length && (
         <div className="empty-placeholder">
           <div className="no-data-text">There are no segments in the selected range</div>
         </div>
       )}
-      {bubbleOpenOn && <Bubble openOn={bubbleOpenOn} mute direction="down" />}
+      <Bubble openOn={hoveredOpenOn} mute direction="up" />
+      <Bubble openOn={bubbleOpenOn} mute direction="down" />
     </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 bcbe645c58e..047fda7fdd2 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -17,9 +17,10 @@
  */
 
 import type { NonNullDateRange } from '@blueprintjs/datetime';
-import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query';
+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 type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { Api } from '../../singletons';
@@ -53,15 +54,25 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     changeFocusDatasource,
   } = props;
 
-  const intervalsQuery = useMemo(() => ({ capabilities, dateRange }), [capabilities, dateRange]);
+  const intervalsQuery = useMemo(
+    () => ({ capabilities, dateRange, focusDatasource }),
+    [capabilities, dateRange, focusDatasource],
+  );
 
   const [intervalRowsState] = useQueryManager({
     query: intervalsQuery,
-    processQuery: async ({ capabilities, dateRange }, cancelToken) => {
+    processQuery: async ({ capabilities, dateRange, focusDatasource }, cancelToken) => {
       if (capabilities.hasSql()) {
         const query = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
-            sql`'${dateRange[0].toISOString()}' <= "end" AND "start" <= '${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`,
+            SqlExpression.and(
+              sql`'${dateRange[0].toISOString()}' <= "end" AND "start" <= '${dateRange[1].toISOString()}'`,
+              C('start').unequal(START_OF_TIME_DATE),
+              C('end').unequal(END_OF_TIME_DATE),
+              C('is_published').equal(1),
+              C('is_overshadowed').equal(0),
+              focusDatasource ? C('datasource').equal(L(focusDatasource)) : undefined,
+            ),
           )
           .addSelect(C('start'), { addToGroupBy: 'end' })
           .addSelect(C('end'), { addToGroupBy: 'end' })
@@ -82,41 +93,31 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
           };
         }); // 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 = await getApiArray<string>(
-          `/druid/coordinator/v1/datasources`,
-          cancelToken,
+        return filterMap(
+          await getApiArray(
+            `/druid/coordinator/v1/metadata/segments?${
+              focusDatasource ? `datasources=${Api.encodePath(focusDatasource)}` : ''
+            }`,
+            cancelToken,
+          ),
+          (segment: any) => {
+            const [startStr, endStr] = segment.interval.split('/');
+            if (startStr === START_OF_TIME_DATE && endStr === END_OF_TIME_DATE) return;
+            const start = new Date(startStr);
+            const end = new Date(endStr);
+            if (!(dateRange[0] <= end && start <= dateRange[1])) return;
+
+            return {
+              start,
+              end,
+              datasource: segment.dataSource,
+              originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
+              segments: 1,
+              size: segment.size,
+              num_rows: segment.num_rows || 0, // segment.num_rows is really null on this API :-(
+            };
+          },
         );
-
-        return (
-          await Promise.all(
-            datasources.map(async datasource => {
-              const intervalMap = (
-                await Api.instance.get(
-                  `/druid/coordinator/v1/datasources/${Api.encodePath(
-                    datasource,
-                  )}/intervals?simple`,
-                  { cancelToken },
-                )
-              ).data;
-
-              return filterMap(Object.entries(intervalMap), ([interval, v]) => {
-                const [startStr, endStr] = interval.split('/');
-                const start = new Date(startStr);
-                const end = new Date(endStr);
-                // ToDo: Filter on start end
-                const { count, size, rows } = v as any;
-                return {
-                  start,
-                  end,
-                  datasource,
-                  segments: count,
-                  size,
-                  rows,
-                };
-              });
-            }),
-          )
-        ).flat();
       }
     },
   });
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 2a683b011ea..92761368d53 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -67,6 +67,8 @@ const SHOWN_DURATION_OPTIONS: Duration[] = [
   new Duration('P10Y'),
 ];
 
+const SHOW_ALL = 'Show all';
+
 function getDateRange(shownDuration: Duration): NonNullDateRange {
   const end = day.ceil(new Date(), TZ_UTC);
   return [shownDuration.shift(end, TZ_UTC, -1), end];
@@ -101,13 +103,12 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   });
 
   const DatasourceSelect: React.FC = () => {
-    const showAll = 'Show all';
-    const datasourcesWzAll = [showAll].concat(datasourcesState.data || []);
+    const datasourcesWzAll = [SHOW_ALL].concat(datasourcesState.data || []);
     return (
       <Select<string>
         items={datasourcesWzAll}
         onItemSelect={(selectedItem: string) => {
-          setFocusDatasource(selectedItem === showAll ? undefined : selectedItem);
+          setFocusDatasource(selectedItem === SHOW_ALL ? undefined : selectedItem);
         }}
         itemRenderer={(val, { handleClick, handleFocus, modifiers }) => {
           if (!modifiers.matchesPredicate) return null;
@@ -135,7 +136,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           }
         }}
       >
-        <Button text={focusDatasource ?? showAll} rightIcon={IconNames.CARET_DOWN} />
+        <Button text={focusDatasource ?? SHOW_ALL} rightIcon={IconNames.CARET_DOWN} />
       </Select>
     );
   };


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 23/42: more fixes

Posted by vo...@apache.org.
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 91af28e729930a8d86b58df3c4732a281a5cd509
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 4 15:55:57 2024 -0800

    more fixes
---
 .../src/components/segment-timeline/common.ts      |  18 +++
 .../segment-timeline/segment-timeline.scss         |   4 +-
 .../segment-timeline/segment-timeline.tsx          | 129 +++++++++------------
 3 files changed, 78 insertions(+), 73 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index efd2a0b1b45..7371aada27f 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -22,6 +22,24 @@ import type { Duration } from '../../utils';
 
 export type IntervalStat = 'segments' | 'size' | 'rows';
 
+export const INTERVAL_STATS: IntervalStat[] = ['segments', 'size', 'rows'];
+
+export function getIntervalStatTitle(intervalStat: IntervalStat): string {
+  switch (intervalStat) {
+    case 'segments':
+      return 'Num. segments';
+
+    case 'size':
+      return 'Size';
+
+    case 'rows':
+      return 'Rows';
+
+    default:
+      return intervalStat;
+  }
+}
+
 export function aggregateSegmentStats(
   xs: readonly Record<IntervalStat, number>[],
 ): Record<IntervalStat, number> {
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index 6582394869e..b1844016041 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -21,7 +21,7 @@
 .segment-timeline {
   .control-bar {
     @include card-like;
-    height: 40px;
+    height: 34px;
     display: flex;
     align-items: start;
     padding: 5px;
@@ -37,7 +37,7 @@
 
   .chart-container {
     position: absolute;
-    top: 40px;
+    top: 34px;
     width: 100%;
     bottom: 0;
     overflow: hidden;
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 92761368d53..81b26c04d8b 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,25 +16,17 @@
  * limitations under the License.
  */
 
-import {
-  Button,
-  ButtonGroup,
-  FormGroup,
-  MenuItem,
-  Popover,
-  ResizeSensor,
-  SegmentedControl,
-} from '@blueprintjs/core';
+import { Button, ButtonGroup, Menu, MenuItem, Popover, ResizeSensor } from '@blueprintjs/core';
 import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { DateRangePicker3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
 import { Select } from '@blueprintjs/select';
-import type React from 'react';
 import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import {
+  checkedCircleIcon,
   day,
   Duration,
   getApiArray,
@@ -48,6 +40,7 @@ import {
 import { Stage } from '../../utils/stage';
 
 import type { IntervalStat } from './common';
+import { getIntervalStatTitle, INTERVAL_STATS } from './common';
 import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
@@ -102,45 +95,6 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
     },
   });
 
-  const DatasourceSelect: React.FC = () => {
-    const datasourcesWzAll = [SHOW_ALL].concat(datasourcesState.data || []);
-    return (
-      <Select<string>
-        items={datasourcesWzAll}
-        onItemSelect={(selectedItem: string) => {
-          setFocusDatasource(selectedItem === SHOW_ALL ? undefined : selectedItem);
-        }}
-        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={focusDatasource ?? SHOW_ALL} rightIcon={IconNames.CARET_DOWN} />
-      </Select>
-    );
-  };
-
   return (
     <div className="segment-timeline">
       <div className="control-bar">
@@ -208,30 +162,63 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             />
           </Popover>
         </ButtonGroup>
-        <FormGroup label="Show" inline>
-          <SegmentedControl
-            value={activeSegmentStat}
-            onValueChange={s => setActiveSegmentStat(s as IntervalStat)}
+        <Popover
+          content={
+            <Menu>
+              {INTERVAL_STATS.map(stat => (
+                <MenuItem
+                  key={stat}
+                  icon={checkedCircleIcon(stat === activeSegmentStat)}
+                  text={getIntervalStatTitle(stat)}
+                  onClick={() => setActiveSegmentStat(stat)}
+                />
+              ))}
+            </Menu>
+          }
+        >
+          <Button
+            text={`Stat: ${getIntervalStatTitle(activeSegmentStat)}`}
+            small
+            rightIcon={IconNames.CARET_DOWN}
+          />
+        </Popover>
+        <Select<string>
+          items={[SHOW_ALL].concat(datasourcesState.data || [])}
+          onItemSelect={(selectedItem: string) => {
+            setFocusDatasource(selectedItem === SHOW_ALL ? undefined : selectedItem);
+          }}
+          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: ${focusDatasource ?? 'all'}`}
             small
-            options={[
-              {
-                label: 'Size',
-                value: 'size',
-              },
-              {
-                label: 'Num. rows',
-                value: 'rows',
-              },
-              {
-                label: 'Num. segments',
-                value: 'segments',
-              },
-            ]}
+            rightIcon={IconNames.CARET_DOWN}
           />
-        </FormGroup>
-        <FormGroup label="Datasource" inline>
-          <DatasourceSelect />
-        </FormGroup>
+        </Select>
       </div>
       <ResizeSensor
         onResize={(entries: ResizeObserverEntry[]) => {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 40/42: fix todo

Posted by vo...@apache.org.
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 7513ec5c4f06655f1a75432556324f582fef5e03
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 15 22:43:52 2024 -0800

    fix todo
---
 web-console/src/components/segment-timeline/segment-timeline.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 0d8ee73caca..ab06abf52c3 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -158,8 +158,8 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
 
         queriedStart = day.floor(new Date(startRes[0].start), TZ_UTC);
       } else {
-        // ToDo: fill me
-        throw new Error('fill me');
+        // Don't bother querying if there is no SQL
+        return getDateRange(DEFAULT_SHOWN_DURATION);
       }
 
       return [


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 16/42: simple bubble

Posted by vo...@apache.org.
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 2243da03823a0e10a2e17880f17cc5d6d0c85f6b
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Thu Oct 31 13:39:13 2024 -0700

    simple bubble
---
 .../src/components/segment-timeline/bubble.scss    | 37 +++++++++++++++++
 .../src/components/segment-timeline/bubble.tsx     | 42 +++++++++++++++++++
 .../segment-timeline/segment-bar-chart-render.tsx  | 48 ++++++++++++++++++----
 .../segment-timeline/segment-bar-chart.tsx         |  9 ++--
 .../segment-timeline/segment-timeline.tsx          | 11 ++---
 .../src/views/segments-view/segments-view.tsx      |  2 -
 6 files changed, 130 insertions(+), 19 deletions(-)

diff --git a/web-console/src/components/segment-timeline/bubble.scss b/web-console/src/components/segment-timeline/bubble.scss
new file mode 100644
index 00000000000..5dcf9986551
--- /dev/null
+++ b/web-console/src/components/segment-timeline/bubble.scss
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import '../../variables';
+
+.bubble {
+  position: absolute;
+  @include card-like;
+  padding: 5px;
+
+  &.up {
+    transform: translate(-50%, -100%);
+  }
+
+  &.down {
+    transform: translate(-50%, 0);
+  }
+
+  &.mute {
+    pointer-events: none;
+  }
+}
diff --git a/web-console/src/components/segment-timeline/bubble.tsx b/web-console/src/components/segment-timeline/bubble.tsx
new file mode 100644
index 00000000000..f3a9c287b13
--- /dev/null
+++ b/web-console/src/components/segment-timeline/bubble.tsx
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import classNames from 'classnames';
+import { createPortal } from 'react-dom';
+
+import './bubble.scss';
+
+interface BubbleProps {
+  openOn: { x: number; y: number; text: string };
+  direction?: 'up' | 'down';
+  mute?: boolean;
+}
+
+export const Bubble = function Bubble(props: BubbleProps) {
+  const { openOn, direction = 'up', mute } = props;
+
+  return createPortal(
+    <div
+      className={classNames('bubble', direction, { mute })}
+      style={{ left: openOn.x, top: openOn.y }}
+    >
+      {openOn.text}
+    </div>,
+    document.body,
+  );
+};
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 485bbe4afdb..44407e22233 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
@@ -31,6 +31,7 @@ import {
   clamp,
   day,
   Duration,
+  filterMap,
   formatBytes,
   formatInteger,
   formatNumber,
@@ -41,6 +42,7 @@ import {
 } from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
+import { Bubble } from './bubble';
 import { ChartAxis } from './chart-axis';
 import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
 import { aggregateSegmentStats } from './common';
@@ -123,7 +125,8 @@ interface SegmentBarChartRenderProps {
   changeDateRange(dateRange: NonNullDateRange): void;
   shownIntervalStat: IntervalStat;
   intervalRows: IntervalRow[];
-  changeActiveDatasource(datasource: string | undefined): void;
+  focusDatasource: string | undefined;
+  changeFocusDatasource(datasource: string | undefined): void;
 }
 
 export const SegmentBarChartRender = function SegmentBarChartRender(
@@ -135,7 +138,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     dateRange,
     changeDateRange,
     intervalRows,
-    changeActiveDatasource,
+    focusDatasource,
+    changeFocusDatasource,
   } = props;
   const [hoverOn, setHoverOn] = useState<IntervalBar>();
   const [mouseDownAt, setMouseDownAt] = useState<
@@ -143,6 +147,9 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   >();
   const [dragging, setDragging] = useState<NonNullDateRange | undefined>();
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
+  const [bubbleOpenOn, setBubbleOpenOn] = useState<
+    { x: number; y: number; text: string } | undefined
+  >();
   const svgRef = useRef<SVGSVGElement | null>(null);
 
   const expandedDateRange: NonNullDateRange = useMemo(() => {
@@ -160,7 +167,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   const intervalBars = useMemo(() => {
     const trimDuration = new Duration(trimGranularity);
-    const trimmedIntervalRows = intervalRows.map(intervalRow => {
+    const trimmedIntervalRows = filterMap(intervalRows, intervalRow => {
+      if (focusDatasource && intervalRow.datasource !== focusDatasource) return;
       const start = trimDuration.floor(intervalRow.start, TZ_UTC);
       const end = trimDuration.ceil(intervalRow.end, TZ_UTC);
       const shownSeconds = (end.valueOf() - start.valueOf()) / 1000;
@@ -197,7 +205,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     );
 
     return stackIntervalRows(fullyGroupedSegmentRows);
-  }, [intervalRows, trimGranularity]);
+  }, [intervalRows, trimGranularity, focusDatasource]);
 
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
@@ -256,13 +264,32 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   }
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
-    if (!mouseDownAt) return;
-    e.preventDefault();
-
     const svg = svgRef.current;
     if (!svg) return;
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
+    const y = e.clientY - rect.y - CHART_MARGIN.top;
+
+    if (!mouseDownAt) {
+      if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) {
+        const time = baseTimeScale.invert(x);
+        const granStart = day.floor(time, TZ_UTC);
+        const granEnd = day.ceil(time, TZ_UTC);
+        setBubbleOpenOn({
+          x:
+            rect.x +
+            CHART_MARGIN.left +
+            baseTimeScale((granStart.valueOf() + granEnd.valueOf()) / 2),
+          y: rect.y + CHART_MARGIN.top + innerStage.height + 10,
+          text: granStart.toISOString().slice(0, 10),
+        });
+      } else {
+        setBubbleOpenOn(undefined);
+      }
+      return;
+    }
+    e.preventDefault();
+
     const b = baseTimeScale.invert(x);
     if (mouseDownAt.action === 'shift' || e.shiftKey) {
       setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
@@ -311,6 +338,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     };
   }
 
+  console.log('here');
+
   return (
     <div className="segment-bar-chart-render">
       <svg
@@ -364,7 +393,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                   style={{ fill: COLORIZER(intervalBar) }}
                   onClick={
                     intervalBar.datasource
-                      ? () => changeActiveDatasource(intervalBar.datasource)
+                      ? () => changeFocusDatasource(intervalBar.datasource)
                       : undefined
                   }
                   onMouseOver={() => {
@@ -380,7 +409,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                 {...segmentBarToRect(hoverOn)}
                 onClick={() => {
                   setHoverOn(undefined);
-                  changeActiveDatasource(hoverOn.datasource);
+                  changeFocusDatasource(hoverOn.datasource);
                 }}
               />
             )}
@@ -428,6 +457,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div className="no-data-text">There are no segments in the selected range</div>
         </div>
       )}
+      {bubbleOpenOn && <Bubble openOn={bubbleOpenOn} mute direction="down" />}
     </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 73c69bb8973..17f2866abee 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -38,7 +38,8 @@ interface SegmentBarChartProps {
   dateRange: NonNullDateRange;
   changeDateRange(newDateRange: NonNullDateRange): void;
   shownIntervalStat: IntervalStat;
-  changeActiveDatasource: (datasource: string | undefined) => void;
+  focusDatasource: string | undefined;
+  changeFocusDatasource: (datasource: string | undefined) => void;
 }
 
 export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) {
@@ -48,7 +49,8 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     changeDateRange,
     stage,
     shownIntervalStat,
-    changeActiveDatasource,
+    focusDatasource,
+    changeFocusDatasource,
   } = props;
 
   const intervalsQuery = useMemo(() => ({ capabilities, dateRange }), [capabilities, dateRange]);
@@ -140,7 +142,8 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
       changeDateRange={changeDateRange}
       shownIntervalStat={shownIntervalStat}
       intervalRows={intervalRows}
-      changeActiveDatasource={changeActiveDatasource}
+      focusDatasource={focusDatasource}
+      changeFocusDatasource={changeFocusDatasource}
     />
   );
 };
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 83208304d69..e113fdea571 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -73,7 +73,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   const { capabilities } = props;
   const [stage, setStage] = useState<Stage | undefined>();
   const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
-  const [activeDatasource, setActiveDatasource] = useState<string | undefined>();
+  const [focusDatasource, setFocusDatasource] = useState<string | undefined>();
   const [dateRange, setDateRange] = useState<NonNullDateRange>(
     getDateRange(DEFAULT_SHOWN_DURATION),
   );
@@ -88,7 +88,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
       <Select<string>
         items={datasourcesWzAll}
         onItemSelect={(selectedItem: string) => {
-          setActiveDatasource(selectedItem === showAll ? undefined : selectedItem);
+          setFocusDatasource(selectedItem === showAll ? undefined : selectedItem);
         }}
         itemRenderer={(val, { handleClick, handleFocus, modifiers }) => {
           if (!modifiers.matchesPredicate) return null;
@@ -117,7 +117,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
         }}
       >
         <Button
-          text={activeDatasource === null ? showAll : activeDatasource}
+          text={focusDatasource === null ? showAll : focusDatasource}
           fill
           rightIcon={IconNames.CARET_DOWN}
         />
@@ -204,8 +204,9 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               dateRange={dateRange}
               changeDateRange={setDateRange}
               shownIntervalStat={activeSegmentStat}
-              changeActiveDatasource={(datasource: string | undefined) =>
-                setActiveDatasource(activeDatasource ? undefined : datasource)
+              focusDatasource={focusDatasource}
+              changeFocusDatasource={(datasource: string | undefined) =>
+                setFocusDatasource(focusDatasource ? undefined : datasource)
               }
             />
           )}
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index ded3ac5474c..18dcc4c4cc3 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -272,8 +272,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
         const { page, pageSize, filtered, sorted, visibleColumns, capabilities, groupByInterval } =
           query;
 
-        console.log('run', filtered);
-
         if (capabilities.hasSql()) {
           const whereExpression = segmentFiltersToExpression(filtered);
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 19/42: fixes

Posted by vo...@apache.org.
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 f33a31aff4f7d269bf254c61f04329a94f64b2e6
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 1 14:10:45 2024 -0700

    fixes
---
 web-console/src/utils/query-manager/query-manager.ts        |  2 +-
 web-console/src/views/datasources-view/datasources-view.tsx |  4 ++--
 web-console/src/views/segments-view/segments-view.tsx       |  4 ++--
 web-console/src/views/supervisors-view/supervisors-view.tsx | 10 +++++++++-
 4 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/web-console/src/utils/query-manager/query-manager.ts b/web-console/src/utils/query-manager/query-manager.ts
index 5b141c7c89d..1023b23412a 100644
--- a/web-console/src/utils/query-manager/query-manager.ts
+++ b/web-console/src/utils/query-manager/query-manager.ts
@@ -288,7 +288,7 @@ export class QueryManager<Q, R, I = never, E extends Error = Error> {
     return this.lastQuery;
   }
 
-  public getLastIntermediateQuery(): any {
+  public getLastIntermediateQuery(): unknown {
     return this.lastIntermediateQuery;
   }
 
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index f58caee1ae8..3d0e6799af6 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -852,9 +852,9 @@ GROUP BY 1, 2`;
           <MenuItem
             icon={IconNames.APPLICATION}
             text="View SQL query for table"
-            disabled={!lastDatasourcesQuery}
+            disabled={typeof lastDatasourcesQuery !== 'string'}
             onClick={() => {
-              if (!lastDatasourcesQuery) return;
+              if (typeof lastDatasourcesQuery !== 'string') return;
               goToQuery({ queryString: lastDatasourcesQuery });
             }}
           />
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 0f7751bf76c..90cb9311391 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -965,9 +965,9 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           <MenuItem
             icon={IconNames.APPLICATION}
             text="View SQL query for table"
-            disabled={!lastSegmentsQuery}
+            disabled={typeof lastSegmentsQuery !== 'string'}
             onClick={() => {
-              if (!lastSegmentsQuery) return;
+              if (typeof lastSegmentsQuery !== 'string') return;
               goToQuery({ queryString: lastSegmentsQuery });
             }}
           />
diff --git a/web-console/src/views/supervisors-view/supervisors-view.tsx b/web-console/src/views/supervisors-view/supervisors-view.tsx
index 8c557fe3c54..c9d75af9680 100644
--- a/web-console/src/views/supervisors-view/supervisors-view.tsx
+++ b/web-console/src/views/supervisors-view/supervisors-view.tsx
@@ -361,6 +361,10 @@ export class SupervisorsView extends React.PureComponent<
     });
   }
 
+  componentDidMount() {
+    this.fetchData();
+  }
+
   componentWillUnmount(): void {
     this.supervisorQueryManager.terminate();
   }
@@ -968,7 +972,11 @@ export class SupervisorsView extends React.PureComponent<
             <MenuItem
               icon={IconNames.APPLICATION}
               text="View SQL query for table"
-              onClick={() => goToQuery({ queryString: lastSupervisorQuery })}
+              disabled={typeof lastSupervisorQuery !== 'string'}
+              onClick={() => {
+                if (typeof lastSupervisorQuery !== 'string') return;
+                goToQuery({ queryString: lastSupervisorQuery });
+              }}
             />
           )}
           <MenuItem


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 38/42: goodies

Posted by vo...@apache.org.
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 7fe805aeb0ba94a6274ddc18f8d3a2619e52d05e
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 15 15:53:22 2024 -0800

    goodies
---
 .../segment-timeline/segment-bar-chart-render.tsx  | 224 +++++++++++++--------
 .../segment-timeline/segment-timeline.tsx          |   4 +-
 2 files changed, 142 insertions(+), 86 deletions(-)

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 dfbb50fc628..bc73d3db7a4 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
@@ -38,7 +38,9 @@ import {
   groupByAsMap,
   minute,
   month,
+  pluralIfNeeded,
   TZ_UTC,
+  uniq,
 } from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
@@ -139,7 +141,10 @@ function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDa
   return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
 }
 
-function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalBar[] {
+function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): {
+  intervalBars: IntervalBar[];
+  intervalTree: IntervalTree;
+} {
   // Total size of the datasource will be user as an ordering tiebreaker
   const datasourceToTotalSize = groupByAsMap(
     trimmedIntervalRows,
@@ -162,19 +167,29 @@ function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalB
   });
 
   const intervalTree = new IntervalTree();
-  return sortedIntervalRows.map(intervalRow => {
+  const intervalBars = sortedIntervalRows.map(intervalRow => {
     const startMs = intervalRow.start.valueOf();
     const endMs = intervalRow.end.valueOf();
-    const intervalRowsBelow = intervalTree.search([
-      startMs + 1,
-      startMs + 2,
-    ]) as TrimmedIntervalRow[];
-    intervalTree.insert([startMs, endMs], intervalRow);
-    return {
+    const intervalRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as IntervalBar[];
+    const intervalBar: IntervalBar = {
       ...intervalRow,
       offset: aggregateSegmentStats(intervalRowsBelow.map(i => i.normalized)),
     };
+    intervalTree.insert([startMs, endMs], intervalBar);
+    return intervalBar;
   });
+
+  return {
+    intervalBars,
+    intervalTree,
+  };
+}
+
+interface BubbleInfo {
+  start: Date;
+  end: Date;
+  timeLabel: string;
+  intervalBars: IntervalBar[];
 }
 
 export interface DatasourceRules {
@@ -206,15 +221,12 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     shownDatasource,
     changeShownDatasource,
   } = props;
-  const [hoveredIntervalBar, setHoveredIntervalBar] = useState<IntervalBar>();
   const [mouseDownAt, setMouseDownAt] = useState<
     { time: Date; action: 'select' | 'shift' } | undefined
   >();
   const [dragging, setDragging] = useState<NonNullDateRange | undefined>();
   const [shiftOffset, setShiftOffset] = useState<number | undefined>();
-  const [bubbleOpenOn, setBubbleOpenOn] = useState<
-    { start: Date; end: Date; x: number; y: number; text: string } | undefined
-  >();
+  const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>();
   const now = useClock(minute.canonicalLength);
   const svgRef = useRef<SVGSVGElement | null>(null);
 
@@ -226,7 +238,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     ).toString();
   }, [dateRange, stage.width]);
 
-  const intervalBars = useMemo(() => {
+  const { intervalBars, intervalTree } = useMemo(() => {
     const shownIntervalRows = shownDatasource
       ? intervalRows.filter(({ datasource }) => datasource === shownDatasource)
       : intervalRows;
@@ -320,21 +332,18 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   function handleMouseDown(e: ReactMouseEvent) {
     const svg = svgRef.current;
-    if (!svg || hoveredIntervalBar) return;
+    if (!svg) return;
     e.preventDefault();
     const rect = svg.getBoundingClientRect();
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
     const time = baseTimeScale.invert(x);
     const action = y > innerStage.height || e.shiftKey ? 'shift' : 'select';
-    setBubbleOpenOn(undefined);
+    setBubbleInfo(undefined);
     setMouseDownAt({
       time,
       action,
     });
-    if (action === 'select') {
-      setDragging([day.floor(time, TZ_UTC), day.ceil(time, TZ_UTC)]);
-    }
   }
 
   useGlobalEventListener('mousemove', (e: MouseEvent) => {
@@ -344,37 +353,59 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     const x = e.clientX - rect.x - CHART_MARGIN.left;
     const y = e.clientY - rect.y - CHART_MARGIN.top;
 
-    if (!mouseDownAt) {
-      if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) {
+    if (mouseDownAt) {
+      e.preventDefault();
+
+      const b = baseTimeScale.invert(x);
+      if (mouseDownAt.action === 'shift' || e.shiftKey) {
+        setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
+      } else {
+        if (mouseDownAt.time < b) {
+          setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, TZ_UTC)]);
+        } else {
+          setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, TZ_UTC)]);
+        }
+      }
+    } else {
+      if (
+        0 <= x &&
+        x <= innerStage.width &&
+        0 <= y &&
+        y <= innerStage.height + CHART_MARGIN.bottom
+      ) {
+        const time = baseTimeScale.invert(x);
         const shifter =
           new Duration(trimGranularity).getCanonicalLength() > day.canonicalLength * 25
             ? month
             : day;
-        const time = baseTimeScale.invert(x);
         const start = shifter.floor(time, TZ_UTC);
         const end = shifter.ceil(time, TZ_UTC);
-        setBubbleOpenOn({
+
+        let intervalBars: IntervalBar[] = [];
+        if (y <= innerStage.height) {
+          const bars = intervalTree.search([
+            time.valueOf() + 1,
+            time.valueOf() + 2,
+          ]) as IntervalBar[];
+
+          if (bars.length) {
+            const stat = statScale.invert(y);
+            const hoverBar = bars.find(
+              bar =>
+                bar.offset[shownIntervalStat] <= stat &&
+                stat < bar.offset[shownIntervalStat] + bar.normalized[shownIntervalStat],
+            );
+            intervalBars = hoverBar ? [hoverBar] : bars;
+          }
+        }
+        setBubbleInfo({
           start,
           end,
-          x: rect.x + CHART_MARGIN.left + baseTimeScale((start.valueOf() + end.valueOf()) / 2),
-          y: rect.y + CHART_MARGIN.top + innerStage.height + 10,
-          text: start.toISOString().slice(0, shifter === day ? 10 : 7),
+          timeLabel: start.toISOString().slice(0, shifter === day ? 10 : 7),
+          intervalBars,
         });
       } else {
-        setBubbleOpenOn(undefined);
-      }
-      return;
-    }
-    e.preventDefault();
-
-    const b = baseTimeScale.invert(x);
-    if (mouseDownAt.action === 'shift' || e.shiftKey) {
-      setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
-    } else {
-      if (mouseDownAt.time < b) {
-        setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, TZ_UTC)]);
-      } else {
-        setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, TZ_UTC)]);
+        setBubbleInfo(undefined);
       }
     }
   });
@@ -382,19 +413,41 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   useGlobalEventListener('mouseup', (e: MouseEvent) => {
     if (!mouseDownAt) return;
     e.preventDefault();
-
     setMouseDownAt(undefined);
 
-    if (!shiftOffset && !dragging) return;
-    setDragging(undefined);
-    setShiftOffset(undefined);
-    if (mouseDownAt.action === 'shift' || e.shiftKey) {
-      if (shiftOffset) {
-        changeDateRange(offsetDateRange(dateRange, shiftOffset));
+    const svg = svgRef.current;
+    if (!svg) return;
+    const rect = svg.getBoundingClientRect();
+    const x = e.clientX - rect.x - CHART_MARGIN.left;
+    const y = e.clientY - rect.y - CHART_MARGIN.top;
+
+    if (shiftOffset || dragging) {
+      setDragging(undefined);
+      setShiftOffset(undefined);
+      if (mouseDownAt.action === 'shift' || e.shiftKey) {
+        if (shiftOffset) {
+          changeDateRange(offsetDateRange(dateRange, shiftOffset));
+        }
+      } else {
+        if (dragging) {
+          changeDateRange(dragging);
+        }
       }
-    } else {
-      if (dragging) {
-        changeDateRange(dragging);
+    } else if (0 <= x && x <= innerStage.width && 0 <= y && y <= innerStage.height) {
+      const time = baseTimeScale.invert(x);
+
+      const bars = intervalTree.search([time.valueOf() + 1, time.valueOf() + 2]) as IntervalBar[];
+
+      if (bars.length) {
+        const stat = statScale.invert(y);
+        const hoverBar = bars.find(
+          bar =>
+            bar.offset[shownIntervalStat] <= stat &&
+            stat < bar.offset[shownIntervalStat] + bar.normalized[shownIntervalStat],
+        );
+        if (hoverBar) {
+          changeShownDatasource(shownDatasource ? undefined : hoverBar.datasource);
+        }
       }
     }
   });
@@ -432,23 +485,22 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   console.log('Bar chart render');
 
   let hoveredOpenOn: { x: number; y: number; text: ReactNode } | undefined;
-  if (hoveredIntervalBar && svgRef.current) {
+  if (bubbleInfo && svgRef.current) {
+    const hoveredIntervalBars = bubbleInfo.intervalBars;
+
     const rect = svgRef.current.getBoundingClientRect();
-    hoveredOpenOn = {
-      x:
-        rect.x +
-        CHART_MARGIN.left +
-        timeScale(
-          new Date((hoveredIntervalBar.start.valueOf() + hoveredIntervalBar.end.valueOf()) / 2),
-        ),
-      y: rect.y + CHART_MARGIN.top - 10,
-      text: (
+    let text: ReactNode;
+    if (hoveredIntervalBars.length === 0) {
+      text = bubbleInfo.timeLabel;
+    } else if (hoveredIntervalBars.length === 1) {
+      const hoveredIntervalBar = hoveredIntervalBars[0];
+      text = (
         <>
           <div>{`${formatStartDuration(
             hoveredIntervalBar.start,
             hoveredIntervalBar.originalTimeSpan,
           )}${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
-          <div>Datasource: {hoveredIntervalBar.datasource}</div>
+          <div>{`Datasource: ${hoveredIntervalBar.datasource}`}</div>
           <div>{`Size: ${
             hoveredIntervalBar.realtime
               ? 'estimated for realtime'
@@ -457,7 +509,28 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div>
           <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div>
         </>
-      ),
+      );
+    } else {
+      const datasources = uniq(hoveredIntervalBars.map(b => b.datasource));
+      const agg = aggregateSegmentStats(hoveredIntervalBars);
+      text = (
+        <>
+          <div>{bubbleInfo.timeLabel}</div>
+          <div>{`Totals for ${pluralIfNeeded(datasources.length, 'datasource')}:`}</div>
+          <div>{`Size: ${formatIntervalStat('size', agg.size)}`}</div>
+          <div>{`Rows: ${formatIntervalStat('rows', agg.rows)}`}</div>
+          <div>{`Segments: ${formatIntervalStat('segments', agg.segments)}`}</div>
+        </>
+      );
+    }
+
+    hoveredOpenOn = {
+      x:
+        rect.x +
+        CHART_MARGIN.left +
+        timeScale(new Date((bubbleInfo.start.valueOf() + bubbleInfo.end.valueOf()) / 2)),
+      y: rect.y + CHART_MARGIN.top - 10,
+      text,
     };
   }
 
@@ -491,10 +564,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
         preserveAspectRatio="xMinYMin meet"
         onMouseDown={handleMouseDown}
       >
-        <g
-          transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}
-          onMouseLeave={() => setHoveredIntervalBar(undefined)}
-        >
+        <g transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}>
           <ChartAxis
             className="gridline-x"
             transform="translate(0,0)"
@@ -525,10 +595,10 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
               .tickFormat(e => formatTickRate(e.valueOf()))}
           />
           <g className="bar-group">
-            {bubbleOpenOn && (
+            {bubbleInfo && (
               <rect
                 className="hover-highlight"
-                {...startEndToXWidth(bubbleOpenOn)}
+                {...startEndToXWidth(bubbleInfo)}
                 y={0}
                 height={innerStage.height}
               />
@@ -543,24 +613,13 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                   className={classNames('bar-unit', { realtime: intervalBar.realtime })}
                   {...segmentBarToRect(intervalBar)}
                   fill={getDatasourceColor(intervalBar.datasource)}
-                  onClick={() => changeShownDatasource(intervalBar.datasource)}
-                  onMouseOver={() => {
-                    if (mouseDownAt) return;
-                    setHoveredIntervalBar(intervalBar);
-                  }}
                 />
               );
             })}
-            {hoveredIntervalBar && (
-              <rect
-                className="hovered-bar"
-                {...segmentBarToRect(hoveredIntervalBar)}
-                onClick={() => {
-                  setHoveredIntervalBar(undefined);
-                  changeShownDatasource(hoveredIntervalBar.datasource);
-                }}
-              />
-            )}
+            {bubbleInfo &&
+              bubbleInfo.intervalBars.map((intervalBar, i) => (
+                <rect key={i} className="hovered-bar" {...segmentBarToRect(intervalBar)} />
+              ))}
             {dragging && (
               <rect
                 className="selection"
@@ -598,7 +657,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
         </div>
       )}
       <PortalBubble openOn={hoveredOpenOn} mute direction="up" />
-      <PortalBubble openOn={bubbleOpenOn} mute direction="down" />
     </div>
   );
 };
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 44d84cdc7d6..ae8cde2a9a7 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -350,9 +350,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               changeDateRange={setDateRange}
               shownIntervalStat={activeSegmentStat}
               shownDatasource={shownDatasource}
-              changeShownDatasource={(datasource: string | undefined) =>
-                setShownDatasource(shownDatasource ? undefined : datasource)
-              }
+              changeShownDatasource={setShownDatasource}
             />
           )}
           {initDatasourceDateRangeState.isLoading() && <Loader />}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 27/42: fixes

Posted by vo...@apache.org.
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 dc1a507081bfb98c7fd78bbb0c0697ccf96d4902
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Nov 5 13:05:24 2024 -0800

    fixes
---
 .../src/components/segment-timeline/bubble.scss    |  5 ++
 .../src/components/segment-timeline/common.ts      |  1 +
 .../segment-timeline/segment-bar-chart-render.scss | 15 ++++
 .../segment-timeline/segment-bar-chart-render.tsx  | 84 ++++++++++++-------
 .../segment-timeline/segment-bar-chart.tsx         | 42 ++++++----
 .../segment-timeline/segment-timeline.tsx          | 96 ++++++++++++----------
 6 files changed, 151 insertions(+), 92 deletions(-)

diff --git a/web-console/src/components/segment-timeline/bubble.scss b/web-console/src/components/segment-timeline/bubble.scss
index 5dcf9986551..5961ae4fbe4 100644
--- a/web-console/src/components/segment-timeline/bubble.scss
+++ b/web-console/src/components/segment-timeline/bubble.scss
@@ -22,6 +22,11 @@
   position: absolute;
   @include card-like;
   padding: 5px;
+  white-space: nowrap;
+
+  .#{$bp-ns}-dark & {
+    background: $dark-gray1;
+  }
 
   &.up {
     transform: translate(-50%, -100%);
diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index c808161ce17..b4cea01062b 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -69,6 +69,7 @@ export interface IntervalRow extends Record<IntervalStat, number> {
   start: Date;
   end: Date;
   datasource: string;
+  realtime: boolean;
   originalTimeSpan: Duration;
 }
 
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 e6badd45a7c..2e4626d5f02 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
@@ -22,6 +22,15 @@
   position: relative;
   overflow: hidden;
 
+  @keyframes pulseOpacity {
+    0% {
+      opacity: 0.8;
+    }
+    100% {
+      opacity: 0.95;
+    }
+  }
+
   svg {
     position: absolute;
 
@@ -79,6 +88,12 @@
       stroke-dasharray: 2, 2;
       opacity: 0.7;
     }
+
+    .bar-unit {
+      &.realtime {
+        animation: pulseOpacity 3s alternate infinite;
+      }
+    }
   }
 
   .empty-placeholder {
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 8b29ed428dd..fc5cfd9ccd6 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
@@ -28,11 +28,9 @@ import { useMemo, useRef, useState } from 'react';
 import { getDatasourceColor } from '../../druid-models';
 import { useClock, useGlobalEventListener } from '../../hooks';
 import {
-  capitalizeFirst,
   clamp,
   day,
   Duration,
-  filterMap,
   formatBytes,
   formatNumber,
   groupBy,
@@ -47,7 +45,7 @@ import type { Margin, Stage } from '../../utils/stage';
 import { Bubble } from './bubble';
 import { ChartAxis } from './chart-axis';
 import type { IntervalBar, IntervalRow, IntervalStat, TrimmedIntervalRow } from './common';
-import { aggregateSegmentStats, formatIntervalStat, INTERVAL_STATS } from './common';
+import { aggregateSegmentStats, formatIntervalStat } from './common';
 
 import './segment-bar-chart-render.scss';
 
@@ -62,7 +60,7 @@ const POSSIBLE_GRANULARITIES = [
   new Duration('P1Y'),
 ];
 
-const EXTEND_X_SCALE_DOMAIN_BY = 4;
+const EXTEND_X_SCALE_DOMAIN_BY = 1;
 
 function offsetDateRange(dateRange: NonNullDateRange, offset: number): NonNullDateRange {
   return [new Date(dateRange[0].valueOf() + offset), new Date(dateRange[1].valueOf() + offset)];
@@ -84,7 +82,10 @@ function stackIntervalRows(trimmedIntervalRows: TrimmedIntervalRow[]): IntervalB
       b.originalTimeSpan.getCanonicalLength() - a.originalTimeSpan.getCanonicalLength();
     if (timeSpanDiff) return timeSpanDiff;
 
-    return datasourceToTotalSize[b.datasource] - datasourceToTotalSize[a.datasource];
+    const totalSizeDiff = datasourceToTotalSize[b.datasource] - datasourceToTotalSize[a.datasource];
+    if (totalSizeDiff) return totalSizeDiff;
+
+    return Number(a.realtime) - Number(b.realtime);
   });
 
   const intervalTree = new IntervalTree();
@@ -109,8 +110,8 @@ interface SegmentBarChartRenderProps {
   changeDateRange(dateRange: NonNullDateRange): void;
   shownIntervalStat: IntervalStat;
   intervalRows: IntervalRow[];
-  focusDatasource: string | undefined;
-  changeFocusDatasource(datasource: string | undefined): void;
+  shownDatasource: string | undefined;
+  changeShownDatasource(datasource: string | undefined): void;
 }
 
 export const SegmentBarChartRender = function SegmentBarChartRender(
@@ -122,8 +123,8 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     dateRange,
     changeDateRange,
     intervalRows,
-    focusDatasource,
-    changeFocusDatasource,
+    shownDatasource,
+    changeShownDatasource,
   } = props;
   const [hoveredIntervalBar, setHoveredIntervalBar] = useState<IntervalBar>();
   const [mouseDownAt, setMouseDownAt] = useState<
@@ -146,21 +147,40 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
   }, [dateRange, stage.width]);
 
   const intervalBars = useMemo(() => {
+    const shownIntervalRows = shownDatasource
+      ? intervalRows.filter(({ datasource }) => datasource === shownDatasource)
+      : intervalRows;
+
+    const averageRowSizeByDatasource = groupByAsMap(
+      shownIntervalRows.filter(intervalRow => intervalRow.size > 0 && intervalRow.rows > 0),
+      intervalRow => intervalRow.datasource,
+      intervalRows => sum(intervalRows, d => d.size) / sum(intervalRows, d => d.rows),
+    );
+
     const trimDuration = new Duration(trimGranularity);
-    const trimmedIntervalRows = filterMap(intervalRows, intervalRow => {
-      if (focusDatasource && intervalRow.datasource !== focusDatasource) return;
-      const start = trimDuration.floor(intervalRow.start, TZ_UTC);
-      const end = trimDuration.ceil(intervalRow.end, TZ_UTC);
-      const shownDays = (end.valueOf() - start.valueOf()) / day.canonicalLength;
+    const trimmedIntervalRows = shownIntervalRows.map(intervalRow => {
+      const { start, end, segments, size, rows } = intervalRow;
+      const startTrimmed = trimDuration.floor(start, TZ_UTC);
+      let endTrimmed = trimDuration.ceil(end, TZ_UTC);
+
+      // Special handling to catch WEEK intervals when trimming to month.
+      if (trimGranularity === 'P1M' && intervalRow.originalTimeSpan.toString() === 'P7D') {
+        endTrimmed = trimDuration.shift(startTrimmed, TZ_UTC);
+      }
+
+      const shownDays = (endTrimmed.valueOf() - startTrimmed.valueOf()) / day.canonicalLength;
+      const shownSize =
+        size === 0 ? rows * averageRowSizeByDatasource[intervalRow.datasource] : size;
       return {
         ...intervalRow,
-        start,
-        end,
+        start: startTrimmed,
+        end: endTrimmed,
         shownDays,
+        size: shownSize,
         normalized: {
-          segments: intervalRow.segments / shownDays,
-          size: intervalRow.size / shownDays,
-          rows: intervalRow.rows / shownDays,
+          size: shownSize / shownDays,
+          rows: rows / shownDays,
+          segments: segments / shownDays,
         },
       };
     });
@@ -173,6 +193,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
           trimmedIntervalRow.end.toISOString(),
           trimmedIntervalRow.originalTimeSpan,
           trimmedIntervalRow.datasource,
+          trimmedIntervalRow.realtime,
         ].join('/'),
       (trimmedIntervalRows): TrimmedIntervalRow => {
         const firstIntervalRow = trimmedIntervalRows[0];
@@ -185,7 +206,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     );
 
     return stackIntervalRows(fullyGroupedSegmentRows);
-  }, [intervalRows, trimGranularity, focusDatasource]);
+  }, [intervalRows, trimGranularity, shownDatasource]);
 
   const innerStage = stage.applyMargin(CHART_MARGIN);
 
@@ -325,7 +346,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     return {
       ...startEndToXWidth(intervalBar),
       y: y,
-      height: Math.abs(y0 - y),
+      height: y0 - y,
     };
   }
 
@@ -345,21 +366,22 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       text: (
         <>
           <div>Datasource: {hoveredIntervalBar.datasource}</div>
-          <div>{`Time: ${prettyFormatIsoDate(hoveredIntervalBar.start)}/${
+          <div>{`${prettyFormatIsoDate(hoveredIntervalBar.start)}/${
             hoveredIntervalBar.originalTimeSpan
+          }${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
+          <div>{`Size: ${
+            hoveredIntervalBar.realtime
+              ? 'estimated for realtime'
+              : formatIntervalStat('size', hoveredIntervalBar.size)
           }`}</div>
-          {INTERVAL_STATS.map(stat => (
-            <div key={stat}>
-              {`${capitalizeFirst(stat)}: ${formatIntervalStat(stat, hoveredIntervalBar[stat])}`}
-            </div>
-          ))}
+          <div>{`Rows: ${formatIntervalStat('rows', hoveredIntervalBar.rows)}`}</div>
+          <div>{`Segments: ${formatIntervalStat('segments', hoveredIntervalBar.segments)}`}</div>
         </>
       ),
     };
   }
 
   const nowX = timeScale(now);
-
   return (
     <div className="segment-bar-chart-render">
       <svg
@@ -419,10 +441,10 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
               return (
                 <rect
                   key={i}
-                  className="bar-unit"
+                  className={classNames('bar-unit', { realtime: intervalBar.realtime })}
                   {...segmentBarToRect(intervalBar)}
                   style={{ fill: getDatasourceColor(intervalBar.datasource) }}
-                  onClick={() => changeFocusDatasource(intervalBar.datasource)}
+                  onClick={() => changeShownDatasource(intervalBar.datasource)}
                   onMouseOver={() => {
                     if (mouseDownAt) return;
                     setHoveredIntervalBar(intervalBar);
@@ -436,7 +458,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                 {...segmentBarToRect(hoveredIntervalBar)}
                 onClick={() => {
                   setHoveredIntervalBar(undefined);
-                  changeFocusDatasource(hoveredIntervalBar.datasource);
+                  changeShownDatasource(hoveredIntervalBar.datasource);
                 }}
               />
             )}
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 ba73c8422c6..b745014d0a2 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -28,7 +28,7 @@ import { Duration, filterMap, getApiArray, queryDruidSql, TZ_UTC } from '../../u
 import type { Stage } from '../../utils/stage';
 import { Loader } from '../loader/loader';
 
-import type { IntervalStat } from './common';
+import type { IntervalRow, IntervalStat } from './common';
 import { SegmentBarChartRender } from './segment-bar-chart-render';
 
 import './segment-bar-chart.scss';
@@ -39,8 +39,8 @@ interface SegmentBarChartProps {
   dateRange: NonNullDateRange;
   changeDateRange(newDateRange: NonNullDateRange): void;
   shownIntervalStat: IntervalStat;
-  focusDatasource: string | undefined;
-  changeFocusDatasource: (datasource: string | undefined) => void;
+  shownDatasource: string | undefined;
+  changeShownDatasource(datasource: string | undefined): void;
 }
 
 export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) {
@@ -50,18 +50,18 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     changeDateRange,
     stage,
     shownIntervalStat,
-    focusDatasource,
-    changeFocusDatasource,
+    shownDatasource,
+    changeShownDatasource,
   } = props;
 
   const intervalsQuery = useMemo(
-    () => ({ capabilities, dateRange, focusDatasource }),
-    [capabilities, dateRange, focusDatasource],
+    () => ({ capabilities, dateRange, shownDatasource: shownDatasource }),
+    [capabilities, dateRange, shownDatasource],
   );
 
   const [intervalRowsState] = useQueryManager({
     query: intervalsQuery,
-    processQuery: async ({ capabilities, dateRange, focusDatasource }, cancelToken) => {
+    processQuery: async ({ capabilities, dateRange, shownDatasource }, cancelToken) => {
       if (capabilities.hasSql()) {
         const query = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
@@ -69,19 +69,23 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
               sql`"start" <= '${dateRange[1].toISOString()}' AND '${dateRange[0].toISOString()}' < "end"`,
               C('start').unequal(START_OF_TIME_DATE),
               C('end').unequal(END_OF_TIME_DATE),
-              C('is_published').equal(1),
+              // C('is_published').equal(1),
               C('is_overshadowed').equal(0),
-              focusDatasource ? C('datasource').equal(L(focusDatasource)) : undefined,
+              shownDatasource ? C('datasource').equal(L(shownDatasource)) : undefined,
             ),
           )
           .addSelect(C('start'), { addToGroupBy: 'end' })
           .addSelect(C('end'), { addToGroupBy: 'end' })
           .addSelect(C('datasource'), { addToGroupBy: 'end' })
+          .addSelect(C('is_realtime').as('realtime'), { addToGroupBy: 'end' })
           .addSelect(F.count().as('segments'))
           .addSelect(F.sum(C('size')).as('size'))
-          .addSelect(F.sum(C('num_rows')).as('rows'));
+          .addSelect(F.sum(C('num_rows')).as('rows'))
+          .toString();
 
-        return (await queryDruidSql({ query: query.toString() }, cancelToken)).map(sr => {
+        console.log(query);
+
+        return (await queryDruidSql({ query }, cancelToken)).map(sr => {
           const start = new Date(sr.start);
           const end = new Date(sr.end);
 
@@ -89,14 +93,15 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
             ...sr,
             start,
             end,
+            realtime: Boolean(sr.realtime),
             originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
-          };
+          } as IntervalRow;
         }); // 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 {
         return filterMap(
           await getApiArray(
             `/druid/coordinator/v1/metadata/segments?${
-              focusDatasource ? `datasources=${Api.encodePath(focusDatasource)}` : ''
+              shownDatasource ? `datasources=${Api.encodePath(shownDatasource)}` : ''
             }`,
             cancelToken,
           ),
@@ -111,11 +116,12 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
               start,
               end,
               datasource: segment.dataSource,
+              realtime: false, // ToDo: fill me
               originalTimeSpan: Duration.fromRange(start, end, TZ_UTC),
               segments: 1,
               size: segment.size,
-              num_rows: segment.num_rows || 0, // segment.num_rows is really null on this API :-(
-            };
+              rows: segment.num_rows || 0, // segment.num_rows is really null on this API :-(
+            } as IntervalRow;
           },
         );
       }
@@ -144,8 +150,8 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
       changeDateRange={changeDateRange}
       shownIntervalStat={shownIntervalStat}
       intervalRows={intervalRows}
-      focusDatasource={focusDatasource}
-      changeFocusDatasource={changeFocusDatasource}
+      shownDatasource={shownDatasource}
+      changeShownDatasource={changeShownDatasource}
     />
   );
 };
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 81b26c04d8b..dd79f07f0dd 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,7 +16,15 @@
  * limitations under the License.
  */
 
-import { Button, ButtonGroup, Menu, MenuItem, Popover, ResizeSensor } from '@blueprintjs/core';
+import {
+  Button,
+  ButtonGroup,
+  Menu,
+  MenuItem,
+  Popover,
+  Position,
+  ResizeSensor,
+} from '@blueprintjs/core';
 import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { DateRangePicker3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
@@ -60,8 +68,6 @@ const SHOWN_DURATION_OPTIONS: Duration[] = [
   new Duration('P10Y'),
 ];
 
-const SHOW_ALL = 'Show all';
-
 function getDateRange(shownDuration: Duration): NonNullDateRange {
   const end = day.ceil(new Date(), TZ_UTC);
   return [shownDuration.shift(end, TZ_UTC, -1), end];
@@ -71,7 +77,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   const { capabilities } = props;
   const [stage, setStage] = useState<Stage | undefined>();
   const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
-  const [focusDatasource, setFocusDatasource] = useState<string | undefined>();
+  const [shownDatasource, setShownDatasource] = useState<string | undefined>();
   const [dateRange, setDateRange] = useState<NonNullDateRange>(
     getDateRange(DEFAULT_SHOWN_DURATION),
   );
@@ -163,6 +169,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           </Popover>
         </ButtonGroup>
         <Popover
+          position={Position.BOTTOM_LEFT}
           content={
             <Menu>
               {INTERVAL_STATS.map(stat => (
@@ -182,43 +189,46 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             rightIcon={IconNames.CARET_DOWN}
           />
         </Popover>
-        <Select<string>
-          items={[SHOW_ALL].concat(datasourcesState.data || [])}
-          onItemSelect={(selectedItem: string) => {
-            setFocusDatasource(selectedItem === SHOW_ALL ? undefined : selectedItem);
-          }}
-          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();
+        <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: ${focusDatasource ?? 'all'}`}
-            small
-            rightIcon={IconNames.CARET_DOWN}
-          />
-        </Select>
+              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
         onResize={(entries: ResizeObserverEntry[]) => {
@@ -234,9 +244,9 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               dateRange={dateRange}
               changeDateRange={setDateRange}
               shownIntervalStat={activeSegmentStat}
-              focusDatasource={focusDatasource}
-              changeFocusDatasource={(datasource: string | undefined) =>
-                setFocusDatasource(focusDatasource ? undefined : datasource)
+              shownDatasource={shownDatasource}
+              changeShownDatasource={(datasource: string | undefined) =>
+                setShownDatasource(shownDatasource ? undefined : datasource)
               }
             />
           )}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 02/42: more cleanup

Posted by vo...@apache.org.
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 cf8f1d8f0b1aa8aa871deb9b6fe17aa9af4523a9
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 23 11:35:36 2024 -0700

    more cleanup
---
 .../segment-timeline/segment-timeline.scss         | 30 ++++++++--------
 .../segment-timeline/segment-timeline.tsx          | 41 +++++++++++++---------
 .../views/datasources-view/datasources-view.scss   |  2 +-
 .../views/datasources-view/datasources-view.tsx    |  1 +
 .../src/views/segments-view/segments-view.scss     |  2 +-
 .../src/views/segments-view/segments-view.tsx      |  1 +
 6 files changed, 43 insertions(+), 34 deletions(-)

diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index 24ee569ad3a..f739277bea7 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -19,14 +19,6 @@
 @import '../../variables';
 
 .segment-timeline {
-  display: grid;
-  grid-template-columns: 1fr 240px;
-  gap: 8px;
-
-  .loader {
-    width: 85%;
-  }
-
   .loading-error {
     position: fixed;
     top: 50%;
@@ -34,15 +26,23 @@
     transform: translate(-50%, -50%);
   }
 
-  .no-data-text {
-    position: absolute;
-    left: 30vw;
-    top: 15vh;
-    font-size: 20px;
-  }
-
   .side-control {
     @include card-like;
     padding: 10px;
   }
+
+  .stacked-bar-chart {
+    height: 100%;
+  }
+
+  .empty-placeholder {
+    height: 100%;
+
+    .no-data-text {
+      position: absolute;
+      left: 30vw;
+      top: 15vh;
+      font-size: 20px;
+    }
+  }
 }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index f59a59e2399..56f6801b426 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,7 +16,14 @@
  * limitations under the License.
  */
 
-import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from '@blueprintjs/core';
+import {
+  Button,
+  FormGroup,
+  MenuItem,
+  ResizeSensor,
+  SegmentedControl,
+  Tag,
+} from '@blueprintjs/core';
 import type { DateRange, NonNullDateRange } from '@blueprintjs/datetime';
 import { DateRangeInput3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
@@ -39,6 +46,7 @@ import {
   utcToLocalDateRange,
 } from '../../utils';
 import { Loader } from '../loader/loader';
+import { SplitterLayout } from '../splitter-layout/splitter-layout';
 
 import type { BarUnitData, SegmentStat } from './common';
 import { StackedBarChart } from './stacked-bar-chart';
@@ -442,16 +450,12 @@ ORDER BY "start" DESC`;
     } = this.state;
 
     if (loading) {
-      return (
-        <div>
-          <Loader loading={loading} />
-        </div>
-      );
+      return <Loader />;
     }
 
     if (error) {
       return (
-        <div>
+        <div className="empty-placeholder">
           <span className="no-data-text">Error when loading data: {error.message}</span>
         </div>
       );
@@ -459,7 +463,7 @@ ORDER BY "start" DESC`;
 
     if (xScale === null || yScale === null) {
       return (
-        <div>
+        <div className="empty-placeholder">
           <span className="no-data-text">Error when calculating scales</span>
         </div>
       );
@@ -467,7 +471,7 @@ ORDER BY "start" DESC`;
 
     if (data![activeSegmentStat].length === 0) {
       return (
-        <div>
+        <div className="empty-placeholder">
           <span className="no-data-text">There are no segments for the selected interval</span>
         </div>
       );
@@ -478,9 +482,9 @@ ORDER BY "start" DESC`;
       data![activeSegmentStat].every((d: any) => d[activeDatasource] === undefined)
     ) {
       return (
-        <div>
+        <div className="empty-placeholder">
           <span className="no-data-text">
-            No data available for <i>{activeDatasource}</i>
+            No data available for <Tag minimal>{activeDatasource}</Tag>
           </span>
         </div>
       );
@@ -525,7 +529,7 @@ ORDER BY "start" DESC`;
       if (exactMatch) {
         return normalizedTitle === normalizedQuery;
       } else {
-        return ` ${normalizedTitle}`.includes(normalizedQuery);
+        return normalizedTitle.includes(normalizedQuery);
       }
     };
 
@@ -533,9 +537,7 @@ ORDER BY "start" DESC`;
       val,
       { handleClick, handleFocus, modifiers },
     ) => {
-      if (!modifiers.matchesPredicate) {
-        return null;
-      }
+      if (!modifiers.matchesPredicate) return null;
       return (
         <MenuItem
           key={val}
@@ -576,7 +578,12 @@ ORDER BY "start" DESC`;
     };
 
     return (
-      <div className="segment-timeline">
+      <SplitterLayout
+        className="segment-timeline"
+        primaryMinSize={400}
+        secondaryInitialSize={220}
+        secondaryMaxSize={400}
+      >
         {this.renderStackedBarChart()}
         <div className="side-control">
           <FormGroup label="Show">
@@ -616,7 +623,7 @@ ORDER BY "start" DESC`;
             <DatasourceSelect />
           </FormGroup>
         </div>
-      </div>
+      </SplitterLayout>
     );
   }
 }
diff --git a/web-console/src/views/datasources-view/datasources-view.scss b/web-console/src/views/datasources-view/datasources-view.scss
index b0abe78ec56..1f9d9df328e 100644
--- a/web-console/src/views/datasources-view/datasources-view.scss
+++ b/web-console/src/views/datasources-view/datasources-view.scss
@@ -23,7 +23,7 @@
   width: 100%;
   overflow: auto;
 
-  .splitter-layout {
+  .splitter-layout.timeline-datasources-splitter {
     position: absolute;
     top: $view-control-bar-height + $standard-padding;
     bottom: 0;
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index cafc69cade5..8f89a0f1c38 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -1714,6 +1714,7 @@ GROUP BY 1, 2`;
           />
         </ViewControlBar>
         <SplitterLayout
+          className="timeline-datasources-splitter"
           vertical
           percentage
           secondaryInitialSize={35}
diff --git a/web-console/src/views/segments-view/segments-view.scss b/web-console/src/views/segments-view/segments-view.scss
index a45d8860db1..f64f881fe4c 100644
--- a/web-console/src/views/segments-view/segments-view.scss
+++ b/web-console/src/views/segments-view/segments-view.scss
@@ -22,7 +22,7 @@
   height: 100%;
   width: 100%;
 
-  .splitter-layout {
+  .splitter-layout.timeline-segments-splitter {
     position: absolute;
     top: $view-control-bar-height + $standard-padding;
     bottom: 0;
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 9e0d680c854..74373e097a7 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -1021,6 +1021,7 @@ END AS "time_span"`,
           />
         </ViewControlBar>
         <SplitterLayout
+          className="timeline-segments-splitter"
           vertical
           percentage
           secondaryInitialSize={35}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 04/42: check in

Posted by vo...@apache.org.
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 289d95c7d09ab82c89f78ba5364a8b16ca855bf5
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Oct 25 15:03:53 2024 -0700

    check in
---
 .../src/components/rule-editor/rule-editor.tsx     |   4 +-
 .../src/components/segment-timeline/bar-group.tsx  |  70 ---
 .../src/components/segment-timeline/common.ts      |  34 +-
 ...ar-chart.scss => segment-bar-chart-render.scss} |   8 +-
 .../segment-timeline/segment-bar-chart-render.tsx  | 155 +++++
 .../segment-timeline/segment-bar-chart.scss}       |  10 +-
 .../segment-timeline/segment-bar-chart.tsx         | 194 ++++++
 .../segment-timeline/segment-timeline.scss         |  12 +-
 .../segment-timeline/segment-timeline.spec.tsx     |  25 -
 .../segment-timeline/segment-timeline.tsx          | 685 ++++-----------------
 .../segment-timeline/stacked-bar-chart.tsx         | 148 -----
 .../dialogs/retention-dialog/retention-dialog.tsx  |   2 +-
 web-console/src/druid-models/index.ts              |   1 +
 .../{utils => druid-models/load-rule}/load-rule.ts |   2 +-
 .../{views/explore-view/models => utils}/stage.ts  |  14 +
 .../views/datasources-view/datasources-view.tsx    |   5 +-
 .../components/module-pane/module-pane.tsx         |   3 +-
 web-console/src/views/explore-view/models/index.ts |   1 -
 .../module-repository/module-repository.ts         |   3 +-
 19 files changed, 520 insertions(+), 856 deletions(-)

diff --git a/web-console/src/components/rule-editor/rule-editor.tsx b/web-console/src/components/rule-editor/rule-editor.tsx
index c5dfb24ddf0..0181e4341ba 100644
--- a/web-console/src/components/rule-editor/rule-editor.tsx
+++ b/web-console/src/components/rule-editor/rule-editor.tsx
@@ -30,9 +30,9 @@ import {
 import { IconNames } from '@blueprintjs/icons';
 import React, { useState } from 'react';
 
+import type { Rule } from '../../druid-models';
+import { RuleUtil } from '../../druid-models';
 import { durationSanitizer } from '../../utils';
-import type { Rule } from '../../utils/load-rule';
-import { RuleUtil } from '../../utils/load-rule';
 import { SuggestibleInput } from '../suggestible-input/suggestible-input';
 
 import './rule-editor.scss';
diff --git a/web-console/src/components/segment-timeline/bar-group.tsx b/web-console/src/components/segment-timeline/bar-group.tsx
deleted file mode 100644
index 3c61859115a..00000000000
--- a/web-console/src/components/segment-timeline/bar-group.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { AxisScale } from 'd3-axis';
-import React from 'react';
-
-import type { BarUnitData, HoveredBarInfo } from './common';
-
-interface BarGroupProps {
-  dataToRender: BarUnitData[];
-  changeActiveDatasource: (dataSource: string) => void;
-  xScale: AxisScale<Date>;
-  yScale: AxisScale<number>;
-  barWidth: number;
-  onHoverBar: (e: HoveredBarInfo) => void;
-}
-
-export class BarGroup extends React.Component<BarGroupProps> {
-  render() {
-    const { dataToRender, changeActiveDatasource, xScale, yScale, onHoverBar, barWidth } =
-      this.props;
-    if (dataToRender === undefined) return null;
-
-    return dataToRender.map((entry: BarUnitData, i: number) => {
-      const y0 = yScale(entry.y0 || 0) || 0;
-      const x = xScale(new Date(entry.x + 'T00:00:00Z'));
-      if (typeof x === 'undefined') return;
-
-      const y = yScale((entry.y0 || 0) + entry.y) || 0;
-      const height = Math.max(y0 - y, 0);
-      const barInfo: HoveredBarInfo = {
-        xCoordinate: x,
-        yCoordinate: y,
-        height,
-        datasource: entry.datasource,
-        xValue: entry.x,
-        yValue: entry.y,
-        dailySize: entry.dailySize,
-      };
-      return (
-        <rect
-          key={i}
-          className="bar-unit"
-          x={x}
-          y={y}
-          width={barWidth}
-          height={height}
-          style={{ fill: entry.color }}
-          onClick={() => changeActiveDatasource(entry.datasource)}
-          onMouseOver={() => onHoverBar(barInfo)}
-        />
-      );
-    });
-  }
-}
diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index 9add957e2c1..10a29751721 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -16,31 +16,19 @@
  * limitations under the License.
  */
 
-export interface BarUnitData {
-  x: number;
-  y: number;
-  y0?: number;
-  width: number;
-  datasource: string;
-  color: string;
-  dailySize: number;
-}
+export type SegmentStat = 'count' | 'size' | 'rows';
 
-export interface Margin {
-  top: number;
-  right: number;
-  bottom: number;
-  left: number;
+export interface SegmentRow extends Record<SegmentStat, number> {
+  start: string;
+  end: string;
+  datasource?: string;
 }
 
-export interface HoveredBarInfo {
-  xCoordinate: number;
-  yCoordinate: number;
-  height: number;
-  datasource: string;
-  xValue: number;
-  yValue: number;
-  dailySize: number;
+export interface SegmentBar extends SegmentRow {
+  startDate: Date;
+  endDate: Date;
 }
 
-export type SegmentStat = 'sizeData' | 'countData';
+export interface StackedSegmentBar extends SegmentBar {
+  offset: Record<SegmentStat, number>;
+}
diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.scss b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss
similarity index 92%
rename from web-console/src/components/segment-timeline/stacked-bar-chart.scss
rename to web-console/src/components/segment-timeline/segment-bar-chart-render.scss
index 26e5f5186b5..629499e13db 100644
--- a/web-console/src/components/segment-timeline/stacked-bar-chart.scss
+++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.scss
@@ -16,13 +16,13 @@
  * limitations under the License.
  */
 
-.stacked-bar-chart {
+.segment-bar-chart-render {
   position: relative;
   overflow: hidden;
 
   .bar-chart-tooltip {
     position: absolute;
-    left: 100px;
+    left: 20px;
     right: 0;
 
     div {
@@ -34,6 +34,10 @@
   svg {
     position: absolute;
 
+    .chart-axis text {
+      user-select: none;
+    }
+
     .hovered-bar {
       fill: transparent;
       stroke: #ffffff;
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
new file mode 100644
index 00000000000..41823b50b0a
--- /dev/null
+++ b/web-console/src/components/segment-timeline/segment-bar-chart-render.tsx
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+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 { formatBytes, formatInteger } from '../../utils';
+import type { Margin, Stage } from '../../utils/stage';
+
+import { ChartAxis } from './chart-axis';
+import type { SegmentBar, SegmentStat } from './common';
+
+import './segment-bar-chart-render.scss';
+
+const CHART_MARGIN: Margin = { top: 40, right: 5, bottom: 20, left: 60 };
+
+interface SegmentBarChartRenderProps {
+  stage: Stage;
+  dateRange: NonNullDateRange;
+  shownSegmentStat: SegmentStat;
+  segmentBars: SegmentBar[];
+  changeActiveDatasource(datasource: string | undefined): void;
+}
+
+export const SegmentBarChartRender = function SegmentBarChartRender(
+  props: SegmentBarChartRenderProps,
+) {
+  const { stage, shownSegmentStat, dateRange, segmentBars, changeActiveDatasource } = props;
+  const [hoverOn, setHoverOn] = useState<SegmentBar>();
+
+  const innerStage = stage.applyMargin(CHART_MARGIN);
+
+  const timeScale: AxisScale<Date> = scaleUtc().domain(dateRange).range([0, innerStage.width]);
+
+  const maxStat = max(segmentBars, d => d[shownSegmentStat]);
+  const statScale: AxisScale<number> = scaleLinear()
+    .rangeRound([innerStage.height, 0])
+    .domain([0, maxStat ?? 1]);
+
+  const formatTick = (n: number) => {
+    if (isNaN(n)) return '';
+    if (shownSegmentStat === 'count') {
+      return formatInteger(n);
+    } else {
+      return formatBytes(n);
+    }
+  };
+
+  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;
+
+    return {
+      x: xStart,
+      y: y,
+      width: Math.max(xEnd - xStart, 1),
+      height: Math.abs(y0 - y),
+    };
+  }
+
+  return (
+    <div className="segment-bar-chart-render">
+      {hoverOn && (
+        <div className="bar-chart-tooltip">
+          <div>Datasource: {hoverOn.datasource}</div>
+          <div>Time: {hoverOn.start}</div>
+          <div>
+            {`${shownSegmentStat === 'count' ? 'Count' : 'Size'}: ${formatTick(
+              hoverOn[shownSegmentStat],
+            )}`}
+          </div>
+        </div>
+      )}
+      <svg
+        width={stage.width}
+        height={stage.height}
+        viewBox={`0 0 ${stage.width} ${stage.height}`}
+        preserveAspectRatio="xMinYMin meet"
+      >
+        <g
+          transform={`translate(${CHART_MARGIN.left},${CHART_MARGIN.top})`}
+          onMouseLeave={() => setHoverOn(undefined)}
+        >
+          <ChartAxis
+            className="gridline-x"
+            transform="translate(0,0)"
+            axis={axisLeft(statScale)
+              .ticks(5)
+              .tickSize(-innerStage.width)
+              .tickFormat(() => '')
+              .tickSizeOuter(0)}
+          />
+          <ChartAxis
+            className="axis-x"
+            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 (
+              <rect
+                key={i}
+                className="bar-unit"
+                {...segmentBarToRect(segmentBar)}
+                style={{ fill: i % 2 ? 'red' : 'blue' }}
+                onClick={
+                  segmentBar.datasource
+                    ? () => changeActiveDatasource(segmentBar.datasource)
+                    : undefined
+                }
+                onMouseOver={() => setHoverOn(segmentBar)}
+              />
+            );
+          })}
+          {hoverOn && (
+            <rect
+              className="hovered-bar"
+              {...segmentBarToRect(hoverOn)}
+              onClick={() => {
+                setHoverOn(undefined);
+                changeActiveDatasource(hoverOn.datasource);
+              }}
+            />
+          )}
+        </g>
+      </svg>
+    </div>
+  );
+};
diff --git a/web-console/src/views/explore-view/models/index.ts b/web-console/src/components/segment-timeline/segment-bar-chart.scss
similarity index 79%
copy from web-console/src/views/explore-view/models/index.ts
copy to web-console/src/components/segment-timeline/segment-bar-chart.scss
index 1e3c58ea09f..130997119e3 100644
--- a/web-console/src/views/explore-view/models/index.ts
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.scss
@@ -16,10 +16,6 @@
  * limitations under the License.
  */
 
-export * from './expression-meta';
-export * from './highlight';
-export * from './measure';
-export * from './measure-pattern';
-export * from './parameter';
-export * from './query-source';
-export * from './stage';
+.segment-bar-chart {
+  position: relative;
+}
diff --git a/web-console/src/components/segment-timeline/segment-bar-chart.tsx b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
new file mode 100644
index 00000000000..a6468ac3d4c
--- /dev/null
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { NonNullDateRange } from '@blueprintjs/datetime';
+import { C, F, N, sql, SqlQuery } from '@druid-toolkit/query';
+import { sum } from 'd3-array';
+import { useMemo } from 'react';
+
+import type { Capabilities } from '../../helpers';
+import { useQueryManager } from '../../hooks';
+import { Api } from '../../singletons';
+import { filterMap, groupBy, queryDruidSql } from '../../utils';
+import type { Stage } from '../../utils/stage';
+import { Loader } from '../loader/loader';
+
+import type { SegmentBar, SegmentRow, SegmentStat } 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
+  switch (duration) {
+    case 'PT1H':
+      return date.substring(0, 13) + ':00:00Z';
+
+    case 'P1D':
+      return date.substring(0, 10) + 'T00:00:00Z';
+
+    case 'P1M':
+      return date.substring(0, 7) + '-01T00:00:00Z';
+
+    case 'P1Y':
+      return date.substring(0, 4) + '-01-01T00:00:00Z';
+
+    default:
+      throw new Error(`Unexpected duration: ${duration}`);
+  }
+}
+
+interface SegmentBarChartProps {
+  capabilities: Capabilities;
+  stage: Stage;
+  dateRange: NonNullDateRange;
+  breakByDataSource: boolean;
+  shownSegmentStat: SegmentStat;
+  changeActiveDatasource: (datasource: string | undefined) => void;
+}
+
+export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartProps) {
+  const {
+    capabilities,
+    dateRange,
+    breakByDataSource,
+    stage,
+    shownSegmentStat,
+    changeActiveDatasource,
+  } = props;
+
+  const intervalsQuery = useMemo(
+    () => ({ capabilities, dateRange, breakByDataSource }),
+    [capabilities, dateRange, breakByDataSource],
+  );
+
+  const [segmentRowsState] = useQueryManager({
+    query: intervalsQuery,
+    processQuery: async ({ capabilities, dateRange, breakByDataSource }, cancelToken) => {
+      let segmentRows: SegmentRow[];
+      if (capabilities.hasSql()) {
+        const query = SqlQuery.from(N('sys').table('segments'))
+          .changeWhereExpression(
+            sql`'${dateRange[0].toISOString()}' <= "start" AND "end" <= '${dateRange[1].toISOString()}' AND is_published = 1 AND is_overshadowed = 0`,
+          )
+          .addSelect(C('start'), { addToGroupBy: 'end' })
+          .addSelect(C('end'), { addToGroupBy: 'end' })
+          .applyIf(breakByDataSource, q => q.addSelect(C('datasource'), { addToGroupBy: 'end' }))
+          .addSelect(F.count().as('count'))
+          .addSelect(F.sum(C('size')).as('size'))
+          .addSelect(F.sum(C('num_rows')).as('rows'));
+
+        segmentRows = await queryDruidSql({ query: query.toString() });
+      } else {
+        const datasources: string[] = (
+          await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken })
+        ).data;
+        segmentRows = (
+          await Promise.all(
+            datasources.map(async datasource => {
+              const intervalMap = (
+                await Api.instance.get(
+                  `/druid/coordinator/v1/datasources/${Api.encodePath(
+                    datasource,
+                  )}/intervals?simple`,
+                  { cancelToken },
+                )
+              ).data;
+
+              return filterMap(Object.entries(intervalMap), ([interval, v]) => {
+                // ToDo: Filter on start end
+                const [start, end] = interval.split('/');
+                const { count, size, rows } = v as any;
+                return {
+                  start,
+                  end,
+                  datasource: breakByDataSource ? datasource : undefined,
+                  count,
+                  size,
+                  rows,
+                };
+              });
+            }),
+          )
+        ).flat();
+      }
+
+      const trimDuration: TrimDuration = 'P1D';
+      return 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);
+          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),
+          };
+        },
+      );
+    },
+  });
+
+  if (segmentRowsState.loading) {
+    return <Loader />;
+  }
+
+  if (segmentRowsState.error) {
+    return (
+      <div className="empty-placeholder">
+        <span className="no-data-text">{`Error when loading data: ${segmentRowsState.getErrorMessage()}`}</span>
+      </div>
+    );
+  }
+
+  const segmentRows = segmentRowsState.data;
+  if (!segmentRows) return null;
+
+  if (!segmentRows.length) {
+    return (
+      <div className="empty-placeholder">
+        <span className="no-data-text">There are no segments for the selected interval</span>
+      </div>
+    );
+  }
+
+  console.log(segmentRows);
+  return (
+    <SegmentBarChartRender
+      stage={stage}
+      dateRange={dateRange}
+      shownSegmentStat={shownSegmentStat}
+      segmentBars={segmentRows}
+      changeActiveDatasource={changeActiveDatasource}
+    />
+  );
+};
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index 6239e717ae8..583ce290ac9 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -32,7 +32,17 @@
     padding: 10px;
   }
 
-  .stacked-bar-chart {
+  .chart-container {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    overflow: hidden;
+  }
+
+  .segment-bar-chart,
+  .segment-bar-chart-render {
+    position: absolute;
+    width: 100%;
     height: 100%;
   }
 
diff --git a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
index 4f95842801a..f247f98794a 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
@@ -16,38 +16,13 @@
  * limitations under the License.
  */
 
-import { sane } from '@druid-toolkit/query';
 import { render } from '@testing-library/react';
 
 import { Capabilities } from '../../helpers';
 
 import { SegmentTimeline } from './segment-timeline';
 
-jest.useFakeTimers('modern').setSystemTime(Date.parse('2021-06-08T12:34:56Z'));
-
 describe('SegmentTimeline', () => {
-  it('.getSqlQuery', () => {
-    expect(
-      SegmentTimeline.getSqlQuery([
-        new Date('2020-01-01T00:00:00Z'),
-        new Date('2021-02-01T00:00:00Z'),
-      ]),
-    ).toEqual(sane`
-      SELECT
-        "start", "end", "datasource",
-        COUNT(*) AS "count",
-        SUM("size") AS "size"
-      FROM sys.segments
-      WHERE
-        '2020-01-01T00:00:00.000Z' <= "start" AND
-        "end" <= '2021-02-01T00:00:00.000Z' AND
-        is_published = 1 AND
-        is_overshadowed = 0
-      GROUP BY 1, 2, 3
-      ORDER BY "start" DESC
-    `);
-  });
-
   it('matches snapshot', () => {
     const segmentTimeline = <SegmentTimeline capabilities={Capabilities.FULL} />;
     const { container } = render(segmentTimeline);
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index edb7dfcf60f..df4e1c12960 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,40 +16,27 @@
  * limitations under the License.
  */
 
-import {
-  Button,
-  FormGroup,
-  MenuItem,
-  ResizeSensor,
-  SegmentedControl,
-  Tag,
-} from '@blueprintjs/core';
-import type { DateRange, NonNullDateRange } from '@blueprintjs/datetime';
+import { Button, FormGroup, MenuItem, ResizeSensor, SegmentedControl } from '@blueprintjs/core';
+import type { NonNullDateRange } from '@blueprintjs/datetime';
 import { DateRangeInput3 } from '@blueprintjs/datetime2';
 import { IconNames } from '@blueprintjs/icons';
-import type { ItemPredicate, ItemRenderer } from '@blueprintjs/select';
 import { Select } from '@blueprintjs/select';
-import type { AxisScale } from 'd3-axis';
-import { scaleLinear, scaleUtc } from 'd3-scale';
 import enUS from 'date-fns/locale/en-US';
-import React from 'react';
+import type React from 'react';
+import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
-import { Api } from '../../singletons';
 import {
   ceilToUtcDay,
   isNonNullRange,
   localToUtcDateRange,
-  queryDruidSql,
-  QueryManager,
-  uniq,
   utcToLocalDateRange,
 } from '../../utils';
-import { Loader } from '../loader/loader';
+import { Stage } from '../../utils/stage';
 import { SplitterLayout } from '../splitter-layout/splitter-layout';
 
-import type { BarUnitData, SegmentStat } from './common';
-import { StackedBarChart } from './stacked-bar-chart';
+import type { SegmentStat } from './common';
+import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
 
@@ -57,37 +44,6 @@ interface SegmentTimelineProps {
   capabilities: Capabilities;
 }
 
-interface SegmentTimelineState {
-  chartHeight: number;
-  chartWidth: number;
-  data?: Record<string, any>;
-  datasources: string[];
-  stackedData?: Record<string, BarUnitData[]>;
-  singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
-  activeDatasource?: string;
-  activeSegmentStat: SegmentStat;
-  dataToRender: BarUnitData[];
-  loading: boolean;
-  error?: Error;
-  xScale: AxisScale<Date> | null;
-  yScale: AxisScale<number> | null;
-  dateRange: NonNullDateRange;
-  selectedDateRange?: DateRange;
-}
-
-interface BarChartScales {
-  xScale: AxisScale<Date>;
-  yScale: AxisScale<number>;
-}
-
-interface IntervalRow {
-  start: string;
-  end: string;
-  datasource: string;
-  count: number;
-  size: number;
-}
-
 const DEFAULT_TIME_SPAN_MONTHS = 3;
 
 function getDefaultDateRange(): NonNullDateRange {
@@ -97,533 +53,122 @@ function getDefaultDateRange(): NonNullDateRange {
   return [start, end];
 }
 
-export class SegmentTimeline extends React.PureComponent<
-  SegmentTimelineProps,
-  SegmentTimelineState
-> {
-  static COLORS = [
-    '#b33040',
-    '#d25c4d',
-    '#f2b447',
-    '#d9d574',
-    '#4FAA7E',
-    '#57ceff',
-    '#789113',
-    '#098777',
-    '#b33040',
-    '#d2757b',
-    '#f29063',
-    '#d9a241',
-    '#80aa61',
-    '#c4ff9e',
-    '#915412',
-    '#87606c',
-  ];
-
-  static getColor(index: number): string {
-    return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length];
-  }
-
-  static getSqlQuery(dateRange: NonNullDateRange): string {
-    return `SELECT
-  "start", "end", "datasource",
-  COUNT(*) AS "count",
-  SUM("size") AS "size"
-FROM sys.segments
-WHERE
-  '${dateRange[0].toISOString()}' <= "start" AND
-  "end" <= '${dateRange[1].toISOString()}' AND
-  is_published = 1 AND
-  is_overshadowed = 0
-GROUP BY 1, 2, 3
-ORDER BY "start" DESC`;
-  }
-
-  static processRawData(data: IntervalRow[]) {
-    if (data === null) return [];
-
-    const countData: Record<string, any> = {};
-    const sizeData: Record<string, any> = {};
-    data.forEach(entry => {
-      const start = entry.start;
-      const day = start.split('T')[0];
-      const datasource = entry.datasource;
-      const count = entry.count;
-      const segmentSize = entry.size;
-      if (countData[day] === undefined) {
-        countData[day] = {
-          day,
-          [datasource]: count,
-          total: count,
-        };
-        sizeData[day] = {
-          day,
-          [datasource]: segmentSize,
-          total: segmentSize,
-        };
-      } else {
-        const countDataEntry: number | undefined = countData[day][datasource];
-        countData[day][datasource] = count + (countDataEntry === undefined ? 0 : countDataEntry);
-        const sizeDataEntry: number | undefined = sizeData[day][datasource];
-        sizeData[day][datasource] = segmentSize + (sizeDataEntry === undefined ? 0 : sizeDataEntry);
-        countData[day].total += count;
-        sizeData[day].total += segmentSize;
-      }
-    });
-
-    const countDataArray = Object.keys(countData)
-      .reverse()
-      .map((time: any) => {
-        return countData[time];
-      });
-
-    const sizeDataArray = Object.keys(sizeData)
-      .reverse()
-      .map((time: any) => {
-        return sizeData[time];
-      });
-
-    return { countData: countDataArray, sizeData: sizeDataArray };
-  }
-
-  static calculateStackedData(
-    data: Record<string, any>,
-    datasources: string[],
-  ): Record<string, BarUnitData[]> {
-    const newStackedData: Record<string, BarUnitData[]> = {};
-    Object.keys(data).forEach((type: any) => {
-      const stackedData: any = data[type].map((d: any) => {
-        let y0 = 0;
-        return datasources.map((datasource: string, i) => {
-          const barUnitData = {
-            x: d.day,
-            y: d[datasource] === undefined ? 0 : d[datasource],
-            y0,
-            datasource,
-            color: SegmentTimeline.getColor(i),
-            dailySize: d.total,
-          };
-          y0 += d[datasource] === undefined ? 0 : d[datasource];
-          return barUnitData;
-        });
-      });
-      newStackedData[type] = stackedData.flat();
-    });
-
-    return newStackedData;
-  }
-
-  static calculateSingleDatasourceData(
-    data: Record<string, any>,
-    datasources: string[],
-  ): Record<string, Record<string, BarUnitData[]>> {
-    const singleDatasourceData: Record<string, Record<string, BarUnitData[]>> = {};
-    Object.keys(data).forEach(dataType => {
-      singleDatasourceData[dataType] = {};
-      datasources.forEach((datasource, i) => {
-        const currentData = data[dataType];
-        if (currentData.length === 0) return;
-        const dataResult = currentData.map((d: any) => {
-          let y = 0;
-          if (d[datasource] !== undefined) {
-            y = d[datasource];
-          }
-          return {
-            x: d.day,
-            y,
-            datasource,
-            color: SegmentTimeline.getColor(i),
-            dailySize: d.total,
-          };
-        });
-        if (!dataResult.every((d: any) => d.y === 0)) {
-          singleDatasourceData[dataType][datasource] = dataResult;
-        }
-      });
-    });
-
-    return singleDatasourceData;
-  }
-
-  private readonly dataQueryManager: QueryManager<
-    { capabilities: Capabilities; dateRange: NonNullDateRange },
-    any
-  >;
-
-  private readonly chartMargin = { top: 40, right: 0, bottom: 20, left: 60 };
-
-  constructor(props: SegmentTimelineProps) {
-    super(props);
-    const dateRange = getDefaultDateRange();
-
-    this.state = {
-      chartWidth: 1, // Dummy init values to be replaced
-      chartHeight: 1, // after first render
-      data: {},
-      datasources: [],
-      stackedData: {},
-      singleDatasourceData: {},
-      dataToRender: [],
-      activeSegmentStat: 'sizeData',
-      loading: true,
-      xScale: null,
-      yScale: null,
-      dateRange,
-    };
-
-    this.dataQueryManager = new QueryManager({
-      processQuery: async ({ capabilities, dateRange }, cancelToken) => {
-        let intervals: IntervalRow[];
-        let datasources: string[];
-        if (capabilities.hasSql()) {
-          intervals = await queryDruidSql(
-            {
-              query: SegmentTimeline.getSqlQuery(dateRange),
-            },
-            cancelToken,
-          );
-          datasources = uniq(intervals.map(r => r.datasource).sort());
-        } else if (capabilities.hasCoordinatorAccess()) {
-          const startIso = dateRange[0].toISOString();
-
-          datasources = (
-            await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken })
-          ).data;
-          intervals = (
-            await Promise.all(
-              datasources.map(async datasource => {
-                const intervalMap = (
-                  await Api.instance.get(
-                    `/druid/coordinator/v1/datasources/${Api.encodePath(
-                      datasource,
-                    )}/intervals?simple`,
-                    { cancelToken },
-                  )
-                ).data;
-
-                return Object.keys(intervalMap)
-                  .map(interval => {
-                    const [start, end] = interval.split('/');
-                    const { count, size } = intervalMap[interval];
-                    return {
-                      start,
-                      end,
-                      datasource,
-                      count,
-                      size,
-                    };
-                  })
-                  .filter(a => startIso < a.start);
-              }),
-            )
-          )
-            .flat()
-            .sort((a, b) => b.start.localeCompare(a.start));
-        } else {
-          throw new Error(`must have SQL or coordinator access`);
-        }
-
-        const data = SegmentTimeline.processRawData(intervals);
-        const stackedData = SegmentTimeline.calculateStackedData(data, datasources);
-        const singleDatasourceData = SegmentTimeline.calculateSingleDatasourceData(
-          data,
-          datasources,
-        );
-        return { data, datasources, stackedData, singleDatasourceData };
-      },
-      onStateChange: ({ data, loading, error }) => {
-        this.setState({
-          data: data ? data.data : undefined,
-          datasources: data ? data.datasources : [],
-          stackedData: data ? data.stackedData : undefined,
-          singleDatasourceData: data ? data.singleDatasourceData : undefined,
-          loading,
-          error,
-        });
-      },
-    });
-  }
-
-  componentDidMount(): void {
-    const { capabilities } = this.props;
-    const { dateRange } = this.state;
-
-    if (isNonNullRange(dateRange)) {
-      this.dataQueryManager.runQuery({ capabilities, dateRange });
-    }
-  }
-
-  componentWillUnmount(): void {
-    this.dataQueryManager.terminate();
-  }
-
-  componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void {
-    const { activeDatasource, activeSegmentStat, singleDatasourceData, stackedData } = this.state;
-    if (
-      prevState.data !== this.state.data ||
-      prevState.activeSegmentStat !== this.state.activeSegmentStat ||
-      prevState.activeDatasource !== this.state.activeDatasource ||
-      prevState.chartWidth !== this.state.chartWidth ||
-      prevState.chartHeight !== this.state.chartHeight
-    ) {
-      const scales: BarChartScales | undefined = this.calculateScales();
-      const dataToRender: BarUnitData[] | undefined = activeDatasource
-        ? singleDatasourceData
-          ? singleDatasourceData[activeSegmentStat][activeDatasource]
-          : undefined
-        : stackedData
-        ? stackedData[activeSegmentStat]
-        : undefined;
-
-      if (scales && dataToRender) {
-        this.setState({
-          dataToRender,
-          xScale: scales.xScale,
-          yScale: scales.yScale,
-        });
-      }
-    }
-  }
-
-  private calculateScales(): BarChartScales | undefined {
-    const {
-      chartWidth,
-      chartHeight,
-      data,
-      activeSegmentStat,
-      activeDatasource,
-      singleDatasourceData,
-      dateRange,
-    } = this.state;
-    if (!data || !Object.keys(data).length || !isNonNullRange(dateRange)) return;
-    const activeData = data[activeSegmentStat];
-
-    let yMax =
-      activeData.length === 0
-        ? 100
-        : activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total;
-
-    if (
-      activeDatasource &&
-      singleDatasourceData![activeSegmentStat][activeDatasource] !== undefined
-    ) {
-      yMax = singleDatasourceData![activeSegmentStat][activeDatasource].reduce((max: any, d: any) =>
-        max.y > d.y ? max : d,
-      ).y;
-    }
-
-    const xScale: AxisScale<Date> = scaleUtc()
-      .domain(dateRange)
-      .range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]);
-
-    const yScale: AxisScale<number> = scaleLinear()
-      .rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0])
-      .domain([0, yMax]);
+export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
+  const { capabilities } = props;
+  const [stage, setStage] = useState<Stage | undefined>();
+  const [activeSegmentStat, setActiveSegmentStat] = useState<SegmentStat>('size');
+  const [activeDatasource, setActiveDatasource] = useState<string | undefined>();
+  const [dateRange, setDateRange] = useState<NonNullDateRange>(getDefaultDateRange);
 
-    return {
-      xScale,
-      yScale,
-    };
-  }
+  const datasources: string[] = ['wiki', 'kttm'];
 
-  private readonly handleResize = (entries: ResizeObserverEntry[]) => {
-    const chartRect = entries[0].contentRect;
-    this.setState({
-      chartWidth: chartRect.width,
-      chartHeight: chartRect.height,
-    });
-  };
-
-  renderStackedBarChart() {
-    const {
-      chartWidth,
-      chartHeight,
-      loading,
-      dataToRender,
-      activeSegmentStat,
-      error,
-      xScale,
-      yScale,
-      data,
-      activeDatasource,
-      dateRange,
-    } = this.state;
-
-    if (loading) {
-      return <Loader />;
-    }
-
-    if (error) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">Error when loading data: {error.message}</span>
-        </div>
-      );
-    }
-
-    if (xScale === null || yScale === null) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">Error when calculating scales</span>
-        </div>
-      );
-    }
-
-    if (data![activeSegmentStat].length === 0) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">There are no segments for the selected interval</span>
-        </div>
-      );
-    }
-
-    if (
-      activeDatasource &&
-      data![activeSegmentStat].every((d: any) => d[activeDatasource] === undefined)
-    ) {
-      return (
-        <div className="empty-placeholder">
-          <span className="no-data-text">
-            No data available for <Tag minimal>{activeDatasource}</Tag>
-          </span>
-        </div>
-      );
-    }
-
-    const millisecondsPerDay = 24 * 60 * 60 * 1000;
-    const barCounts = (dateRange[1].getTime() - dateRange[0].getTime()) / millisecondsPerDay;
-    const barWidth = Math.max(
-      0,
-      (chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts,
-    );
+  const DatasourceSelect: React.FC = () => {
+    const showAll = 'Show all';
+    const datasourcesWzAll = [showAll].concat(datasources);
     return (
-      <ResizeSensor onResize={this.handleResize}>
-        <StackedBarChart
-          dataToRender={dataToRender}
-          svgHeight={chartHeight}
-          svgWidth={chartWidth}
-          margin={this.chartMargin}
-          changeActiveDatasource={(datasource: string | undefined) =>
-            this.setState(prevState => ({
-              activeDatasource: prevState.activeDatasource ? undefined : datasource,
-            }))
+      <Select<string>
+        items={datasourcesWzAll}
+        onItemSelect={(selectedItem: string) => {
+          setActiveDatasource(selectedItem === showAll ? undefined : selectedItem);
+        }}
+        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);
           }
-          shownSegmentStat={activeSegmentStat}
-          xScale={xScale}
-          yScale={yScale}
-          barWidth={barWidth}
+        }}
+        fill
+      >
+        <Button
+          text={activeDatasource === null ? showAll : activeDatasource}
+          fill
+          rightIcon={IconNames.CARET_DOWN}
         />
-      </ResizeSensor>
+      </Select>
     );
-  }
-
-  render() {
-    const { capabilities } = this.props;
-    const { datasources, activeSegmentStat, activeDatasource, dateRange, selectedDateRange } =
-      this.state;
-
-    const filterDatasource: ItemPredicate<string> = (query, val, _index, exactMatch) => {
-      const normalizedTitle = val.toLowerCase();
-      const normalizedQuery = query.toLowerCase();
-
-      if (exactMatch) {
-        return normalizedTitle === normalizedQuery;
-      } else {
-        return normalizedTitle.includes(normalizedQuery);
-      }
-    };
-
-    const datasourceRenderer: ItemRenderer<string> = (
-      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}
-        />
-      );
-    };
-
-    const DatasourceSelect: React.FC = () => {
-      const showAll = 'Show all';
-      const handleItemSelected = (selectedItem: string) => {
-        this.setState({
-          activeDatasource: selectedItem === showAll ? undefined : selectedItem,
-        });
-      };
-      const datasourcesWzAll = [showAll].concat(datasources);
-      return (
-        <Select<string>
-          items={datasourcesWzAll}
-          onItemSelect={handleItemSelected}
-          itemRenderer={datasourceRenderer}
-          noResults={<MenuItem disabled text="No results" roleStructure="listoption" />}
-          itemPredicate={filterDatasource}
-          fill
-        >
-          <Button
-            text={activeDatasource === null ? showAll : activeDatasource}
-            fill
-            rightIcon={IconNames.CARET_DOWN}
-          />
-        </Select>
-      );
-    };
+  };
 
-    return (
-      <SplitterLayout
-        className="segment-timeline"
-        primaryMinSize={400}
-        secondaryInitialSize={220}
-        secondaryMaxSize={400}
+  return (
+    <SplitterLayout
+      className="segment-timeline"
+      primaryMinSize={400}
+      secondaryInitialSize={220}
+      secondaryMaxSize={400}
+    >
+      <ResizeSensor
+        onResize={(entries: ResizeObserverEntry[]) => {
+          const rect = entries[0].contentRect;
+          setStage(new Stage(rect.width, rect.height));
+        }}
       >
-        {this.renderStackedBarChart()}
-        <div className="side-control">
-          <FormGroup label="Show">
-            <SegmentedControl
-              value={activeSegmentStat}
-              onValueChange={activeDataType =>
-                this.setState({ activeSegmentStat: activeDataType as SegmentStat })
+        <div className="chart-container">
+          {stage && (
+            <SegmentBarChart
+              capabilities={capabilities}
+              stage={stage}
+              dateRange={dateRange}
+              shownSegmentStat={activeSegmentStat}
+              breakByDataSource={false}
+              changeActiveDatasource={(datasource: string | undefined) =>
+                setActiveDatasource(activeDatasource ? undefined : datasource)
               }
-              options={[
-                {
-                  label: 'Total size',
-                  value: 'sizeData',
-                },
-                {
-                  label: 'Segment count',
-                  value: 'countData',
-                },
-              ]}
-              fill
-            />
-          </FormGroup>
-          <FormGroup label="Interval">
-            <DateRangeInput3
-              value={utcToLocalDateRange(selectedDateRange || dateRange)}
-              onChange={newDateRange => {
-                const newUtcDateRange = localToUtcDateRange(newDateRange);
-                if (!isNonNullRange(newUtcDateRange)) return;
-                this.setState({ dateRange: newUtcDateRange, selectedDateRange: undefined }, () => {
-                  this.dataQueryManager.runQuery({ capabilities, dateRange: newUtcDateRange });
-                });
-              }}
-              fill
-              locale={enUS}
             />
-          </FormGroup>
-          <FormGroup label="Datasource">
-            <DatasourceSelect />
-          </FormGroup>
+          )}
         </div>
-      </SplitterLayout>
-    );
-  }
-}
+      </ResizeSensor>
+      <div className="side-control">
+        <FormGroup label="Show">
+          <SegmentedControl
+            value={activeSegmentStat}
+            onValueChange={s => setActiveSegmentStat(s as SegmentStat)}
+            fill
+            options={[
+              {
+                label: 'Size',
+                value: 'size',
+              },
+              {
+                label: 'Count',
+                value: 'count',
+              },
+            ]}
+          />
+        </FormGroup>
+        <FormGroup label="Interval">
+          <DateRangeInput3
+            value={utcToLocalDateRange(dateRange)}
+            onChange={newDateRange => {
+              const newUtcDateRange = localToUtcDateRange(newDateRange);
+              if (!isNonNullRange(newUtcDateRange)) return;
+              setDateRange(newUtcDateRange);
+            }}
+            fill
+            locale={enUS}
+          />
+        </FormGroup>
+        <FormGroup label="Datasource">
+          <DatasourceSelect />
+        </FormGroup>
+      </div>
+    </SplitterLayout>
+  );
+};
diff --git a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
deleted file mode 100644
index 571c50f65a5..00000000000
--- a/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { AxisScale } from 'd3-axis';
-import { axisBottom, axisLeft } from 'd3-axis';
-import React, { useState } from 'react';
-
-import { formatBytes, formatInteger } from '../../utils';
-
-import { BarGroup } from './bar-group';
-import { ChartAxis } from './chart-axis';
-import type { BarUnitData, HoveredBarInfo, Margin, SegmentStat } from './common';
-
-import './stacked-bar-chart.scss';
-
-interface StackedBarChartProps {
-  svgWidth: number;
-  svgHeight: number;
-  margin: Margin;
-  shownSegmentStat: SegmentStat;
-  dataToRender: BarUnitData[];
-  changeActiveDatasource: (e: string | undefined) => void;
-  xScale: AxisScale<Date>;
-  yScale: AxisScale<number>;
-  barWidth: number;
-}
-
-export const StackedBarChart = React.forwardRef(function StackedBarChart(
-  props: StackedBarChartProps,
-  ref,
-) {
-  const {
-    shownSegmentStat,
-    svgWidth,
-    svgHeight,
-    margin,
-    xScale,
-    yScale,
-    dataToRender,
-    changeActiveDatasource,
-    barWidth,
-  } = props;
-  const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
-
-  const formatTick = (n: number) => {
-    if (isNaN(n)) return '';
-    if (shownSegmentStat === 'countData') {
-      return formatInteger(n);
-    } else {
-      return formatBytes(n);
-    }
-  };
-
-  const width = svgWidth - margin.left - margin.right;
-  const height = svgHeight - margin.top - margin.bottom;
-
-  return (
-    <div className="stacked-bar-chart" ref={ref as any}>
-      {hoverOn && (
-        <div className="bar-chart-tooltip">
-          <div>Datasource: {hoverOn.datasource}</div>
-          <div>Time: {hoverOn.xValue}</div>
-          <div>
-            {`${
-              shownSegmentStat === 'countData' ? 'Daily total count:' : 'Daily total size:'
-            } ${formatTick(hoverOn.dailySize)}`}
-          </div>
-          <div>
-            {`${shownSegmentStat === 'countData' ? 'Count:' : 'Size:'} ${formatTick(
-              hoverOn.yValue,
-            )}`}
-          </div>
-        </div>
-      )}
-      <svg
-        width={svgWidth}
-        height={svgHeight}
-        viewBox={`0 0 ${svgWidth} ${svgHeight}`}
-        preserveAspectRatio="xMinYMin meet"
-      >
-        <g
-          transform={`translate(${margin.left}, ${margin.top})`}
-          onMouseLeave={() => setHoverOn(undefined)}
-        >
-          <ChartAxis
-            className="gridline-x"
-            transform="translate(0, 0)"
-            axis={axisLeft(yScale)
-              .ticks(5)
-              .tickSize(-width)
-              .tickFormat(() => '')
-              .tickSizeOuter(0)}
-          />
-          <BarGroup
-            dataToRender={dataToRender}
-            changeActiveDatasource={changeActiveDatasource}
-            xScale={xScale}
-            yScale={yScale}
-            onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)}
-            barWidth={barWidth}
-          />
-          <ChartAxis
-            className="axis-x"
-            transform={`translate(0, ${height})`}
-            axis={axisBottom(xScale)}
-          />
-          <ChartAxis
-            className="axis-y"
-            axis={axisLeft(yScale)
-              .ticks(5)
-              .tickFormat(e => formatTick(e))}
-          />
-          {hoverOn && (
-            <g
-              className="hovered-bar"
-              onClick={() => {
-                setHoverOn(undefined);
-                changeActiveDatasource(hoverOn.datasource);
-              }}
-            >
-              <rect
-                x={hoverOn.xCoordinate}
-                y={hoverOn.yCoordinate}
-                width={barWidth}
-                height={hoverOn.height}
-              />
-            </g>
-          )}
-        </g>
-      </svg>
-    </div>
-  );
-});
diff --git a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
index 9b657622d89..5ee4d51a3a5 100644
--- a/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
+++ b/web-console/src/dialogs/retention-dialog/retention-dialog.tsx
@@ -22,12 +22,12 @@ import React, { useState } from 'react';
 
 import type { FormJsonTabs } from '../../components';
 import { ExternalLink, FormJsonSelector, JsonInput, RuleEditor } from '../../components';
+import type { Rule } from '../../druid-models';
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { getLink } from '../../links';
 import { Api } from '../../singletons';
 import { filterMap, queryDruidSql, swapElements } from '../../utils';
-import type { Rule } from '../../utils/load-rule';
 import { SnitchDialog } from '..';
 
 import './retention-dialog.scss';
diff --git a/web-console/src/druid-models/index.ts b/web-console/src/druid-models/index.ts
index dfeeeeaac83..82f14aab4e6 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -32,6 +32,7 @@ export * from './ingest-query-pattern/ingest-query-pattern';
 export * from './ingestion-spec/ingestion-spec';
 export * from './input-format/input-format';
 export * from './input-source/input-source';
+export * from './load-rule/load-rule';
 export * from './lookup-spec/lookup-spec';
 export * from './metric-spec/metric-spec';
 export * from './overlord-dynamic-config/overlord-dynamic-config';
diff --git a/web-console/src/utils/load-rule.ts b/web-console/src/druid-models/load-rule/load-rule.ts
similarity index 98%
rename from web-console/src/utils/load-rule.ts
rename to web-console/src/druid-models/load-rule/load-rule.ts
index a32422bbb6a..63bd162c272 100644
--- a/web-console/src/utils/load-rule.ts
+++ b/web-console/src/druid-models/load-rule/load-rule.ts
@@ -18,7 +18,7 @@
 
 import { sum } from 'd3-array';
 
-import { deepMove, deepSet } from './object-change';
+import { deepMove, deepSet } from '../../utils';
 
 export type RuleType =
   | 'loadForever'
diff --git a/web-console/src/views/explore-view/models/stage.ts b/web-console/src/utils/stage.ts
similarity index 80%
rename from web-console/src/views/explore-view/models/stage.ts
rename to web-console/src/utils/stage.ts
index a1d01258f0a..7b65cfebd9c 100644
--- a/web-console/src/views/explore-view/models/stage.ts
+++ b/web-console/src/utils/stage.ts
@@ -16,6 +16,13 @@
  * limitations under the License.
  */
 
+export interface Margin {
+  top: number;
+  right: number;
+  bottom: number;
+  left: number;
+}
+
 export class Stage {
   public readonly width: number;
   public readonly height: number;
@@ -28,4 +35,11 @@ export class Stage {
   public equals(other: Stage | undefined): boolean {
     return Boolean(other && this.width === other.width && this.height === other.height);
   }
+
+  public applyMargin(margin: Margin): Stage {
+    return new Stage(
+      this.width - margin.left - margin.right,
+      this.height - margin.top - margin.bottom,
+    );
+  }
 }
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index 8f89a0f1c38..a2bd3692346 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -51,8 +51,9 @@ import type {
   CompactionInfo,
   CompactionStatus,
   QueryWithContext,
+  Rule,
 } from '../../druid-models';
-import { formatCompactionInfo, zeroCompactionStatus } from '../../druid-models';
+import { formatCompactionInfo, RuleUtil, zeroCompactionStatus } from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
 import { Api, AppToaster } from '../../singletons';
@@ -82,8 +83,6 @@ import {
   twoLines,
 } from '../../utils';
 import type { BasicAction } from '../../utils/basic-action';
-import type { Rule } from '../../utils/load-rule';
-import { RuleUtil } from '../../utils/load-rule';
 
 import './datasources-view.scss';
 
diff --git a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
index b360effc87c..2a1f55c3d4a 100644
--- a/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
+++ b/web-console/src/views/explore-view/components/module-pane/module-pane.tsx
@@ -20,8 +20,9 @@ import { ResizeSensor } from '@blueprintjs/core';
 import type { QueryResult, SqlExpression, SqlQuery } from '@druid-toolkit/query';
 import React, { useMemo, useState } from 'react';
 
+import { Stage } from '../../../../utils/stage';
 import type { ParameterDefinition, ParameterValues, QuerySource } from '../../models';
-import { effectiveParameterDefault, Stage } from '../../models';
+import { effectiveParameterDefault } from '../../models';
 import { ModuleRepository } from '../../module-repository/module-repository';
 import { Issue } from '../issue/issue';
 
diff --git a/web-console/src/views/explore-view/models/index.ts b/web-console/src/views/explore-view/models/index.ts
index 1e3c58ea09f..8938818acc2 100644
--- a/web-console/src/views/explore-view/models/index.ts
+++ b/web-console/src/views/explore-view/models/index.ts
@@ -22,4 +22,3 @@ export * from './measure';
 export * from './measure-pattern';
 export * from './parameter';
 export * from './query-source';
-export * from './stage';
diff --git a/web-console/src/views/explore-view/module-repository/module-repository.ts b/web-console/src/views/explore-view/module-repository/module-repository.ts
index d050a2779c5..fd9dd19f1b3 100644
--- a/web-console/src/views/explore-view/module-repository/module-repository.ts
+++ b/web-console/src/views/explore-view/module-repository/module-repository.ts
@@ -20,7 +20,8 @@ import type { IconName } from '@blueprintjs/icons';
 import type { QueryResult, SqlExpression, SqlQuery } from '@druid-toolkit/query';
 import type { CancelToken } from 'axios';
 
-import type { ParameterDefinition, QuerySource, Stage } from '../models';
+import type { Stage } from '../../../utils/stage';
+import type { ParameterDefinition, QuerySource } from '../models';
 
 interface ModuleDefinition<P> {
   id: string;


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 08/42: progress

Posted by vo...@apache.org.
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 5c430de66ce982acef7bb87c5158b6ee81fd18d0
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Oct 29 16:16:42 2024 -0700

    progress
---
 licenses.yaml                                      |  76 ++--
 web-console/package-lock.json                      | 119 ++---
 web-console/package.json                           |   2 +-
 web-console/script/licenses                        |   1 +
 web-console/src/druid-models/index.ts              |   1 +
 .../segment/segment.ts}                            |  35 +-
 .../date-floor-shift-ceil-utc.spec.ts              | 169 +++++++
 .../date-floor-shift-ceil.spec.ts                  | 181 ++++++++
 .../date-floor-shift-ceil/date-floor-shift-ceil.ts | 296 ++++++++++++
 web-console/src/utils/duration/duration.spec.ts    | 505 +++++++++++++++++++++
 web-console/src/utils/duration/duration.ts         | 381 ++++++++++++++++
 web-console/src/utils/index.tsx                    |   2 +
 .../views/datasources-view/datasources-view.tsx    |  10 +-
 .../modules/multi-axis-chart-module.tsx            |  12 +-
 .../explore-view/modules/time-chart-module.tsx     |  17 +-
 .../src/views/explore-view/utils/duration.ts       |  46 --
 .../explore-view/utils/filter-pattern-helpers.ts   |   8 +-
 .../explore-view/utils/get-auto-granularity.ts     |   7 +-
 web-console/src/views/explore-view/utils/index.ts  |   2 -
 .../explore-view/utils/snap-to-granularity.ts      |  57 ---
 .../src/views/explore-view/utils/table-query.ts    |   7 +-
 .../src/views/segments-view/segments-view.tsx      |  60 +--
 22 files changed, 1663 insertions(+), 331 deletions(-)

diff --git a/licenses.yaml b/licenses.yaml
index 40305fc55c6..48c4ebd8390 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5224,6 +5224,16 @@ license_file_path: licenses/bin/@emotion-weak-memoize.MIT
 
 ---
 
+name: "@flatten-js/interval-tree"
+license_category: binary
+module: web-console
+license_name: MIT License
+copyright: Alex Bol
+version: 1.1.3
+license_file_path: licenses/bin/@flatten-js-interval-tree.MIT
+
+---
+
 name: "@fontsource/open-sans"
 license_category: binary
 module: web-console
@@ -5234,6 +5244,15 @@ license_file_path: licenses/bin/@fontsource-open-sans.OFL
 
 ---
 
+name: "@internationalized/date"
+license_category: binary
+module: web-console
+license_name: Apache License version 2.0
+copyright: Adobe
+version: 3.5.6
+
+---
+
 name: "@popperjs/core"
 license_category: binary
 module: web-console
@@ -5244,6 +5263,15 @@ license_file_path: licenses/bin/@popperjs-core.MIT
 
 ---
 
+name: "@swc/helpers"
+license_category: binary
+module: web-console
+license_name: Apache License version 2.0
+copyright: 강동윤
+version: 0.5.13
+
+---
+
 name: "@types/parse-json"
 license_category: binary
 module: web-console
@@ -5404,15 +5432,6 @@ license_file_path: licenses/bin/change-case.MIT
 
 ---
 
-name: "chronoshift"
-license_category: binary
-module: web-console
-license_name: Apache License version 2.0
-copyright: Vadim Ogievetsky
-version: 0.10.0
-
----
-
 name: "classnames"
 license_category: binary
 module: web-console
@@ -5801,16 +5820,6 @@ license_file_path: licenses/bin/has-flag.MIT
 
 ---
 
-name: "has-own-prop"
-license_category: binary
-module: web-console
-license_name: MIT License
-copyright: Sindre Sorhus
-version: 2.0.0
-license_file_path: licenses/bin/has-own-prop.MIT
-
----
-
 name: "hasown"
 license_category: binary
 module: web-console
@@ -5871,15 +5880,6 @@ license_file_path: licenses/bin/iconv-lite.MIT
 
 ---
 
-name: "immutable-class"
-license_category: binary
-module: web-console
-license_name: Apache License version 2.0
-copyright: Vadim Ogievetsky
-version: 0.11.2
-
----
-
 name: "import-fresh"
 license_category: binary
 module: web-console
@@ -6060,26 +6060,6 @@ license_file_path: licenses/bin/mime-types.MIT
 
 ---
 
-name: "moment-timezone"
-license_category: binary
-module: web-console
-license_name: MIT License
-copyright: Tim Wood
-version: 0.5.43
-license_file_path: licenses/bin/moment-timezone.MIT
-
----
-
-name: "moment"
-license_category: binary
-module: web-console
-license_name: MIT License
-copyright: Iskren Ivov Chernev
-version: 2.29.4
-license_file_path: licenses/bin/moment.MIT
-
----
-
 name: "no-case"
 license_category: binary
 module: web-console
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 4abbd266e0c..709bba0bf5b 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -17,9 +17,9 @@
         "@druid-toolkit/query": "^0.22.23",
         "@flatten-js/interval-tree": "^1.1.3",
         "@fontsource/open-sans": "^5.0.30",
+        "@internationalized/date": "^3.5.6",
         "ace-builds": "~1.5.3",
         "axios": "^1.7.7",
-        "chronoshift": "^0.10.0",
         "classnames": "^2.2.6",
         "copy-to-clipboard": "^3.3.3",
         "d3-array": "^3.2.4",
@@ -2449,6 +2449,15 @@
         "url": "https://github.com/sponsors/nzakas"
       }
     },
+    "node_modules/@internationalized/date": {
+      "version": "3.5.6",
+      "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.6.tgz",
+      "integrity": "sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@swc/helpers": "^0.5.0"
+      }
+    },
     "node_modules/@istanbuljs/load-nyc-config": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -3480,6 +3489,15 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.13",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
+      "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "tslib": "^2.4.0"
+      }
+    },
     "node_modules/@testing-library/dom": {
       "version": "10.4.0",
       "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -5809,16 +5827,6 @@
         "node": ">=6.0"
       }
     },
-    "node_modules/chronoshift": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/chronoshift/-/chronoshift-0.10.0.tgz",
-      "integrity": "sha512-dNvumPg7R6ACUOKbGo1zH6DtmTo5ut9/LNbzqaKGnpC9VdArIos8+kApHOVIZH4FCpm9M9XYh++jwlRHhc1PyA==",
-      "dependencies": {
-        "immutable-class": "^0.11.0",
-        "moment-timezone": "^0.5.26",
-        "tslib": "^2.3.1"
-      }
-    },
     "node_modules/ci-info": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
@@ -8916,14 +8924,6 @@
         "node": ">=4"
       }
     },
-    "node_modules/has-own-prop": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
-      "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==",
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/has-property-descriptors": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -9361,15 +9361,6 @@
       "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
       "dev": true
     },
-    "node_modules/immutable-class": {
-      "version": "0.11.2",
-      "resolved": "https://registry.npmjs.org/immutable-class/-/immutable-class-0.11.2.tgz",
-      "integrity": "sha512-CzkVPkJXzkspt6RX+ipNgtvt16+rzEBUlA3yNPLkK5/S042c9wvuyfE4F5TfMfPJ6XF86Fp+OCwu6eeAnMICuw==",
-      "dependencies": {
-        "has-own-prop": "^2.0.0",
-        "tslib": "^2.3.1"
-      }
-    },
     "node_modules/import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -12896,25 +12887,6 @@
         "mkdirp": "bin/cmd.js"
       }
     },
-    "node_modules/moment": {
-      "version": "2.29.4",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
-      "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/moment-timezone": {
-      "version": "0.5.43",
-      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
-      "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
-      "dependencies": {
-        "moment": "^2.29.4"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
     "node_modules/mrmime": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
@@ -19827,6 +19799,14 @@
       "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
       "dev": true
     },
+    "@internationalized/date": {
+      "version": "3.5.6",
+      "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.6.tgz",
+      "integrity": "sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==",
+      "requires": {
+        "@swc/helpers": "^0.5.0"
+      }
+    },
     "@istanbuljs/load-nyc-config": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -20626,6 +20606,14 @@
         }
       }
     },
+    "@swc/helpers": {
+      "version": "0.5.13",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz",
+      "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==",
+      "requires": {
+        "tslib": "^2.4.0"
+      }
+    },
     "@testing-library/dom": {
       "version": "10.4.0",
       "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
@@ -22423,16 +22411,6 @@
       "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
       "dev": true
     },
-    "chronoshift": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/chronoshift/-/chronoshift-0.10.0.tgz",
-      "integrity": "sha512-dNvumPg7R6ACUOKbGo1zH6DtmTo5ut9/LNbzqaKGnpC9VdArIos8+kApHOVIZH4FCpm9M9XYh++jwlRHhc1PyA==",
-      "requires": {
-        "immutable-class": "^0.11.0",
-        "moment-timezone": "^0.5.26",
-        "tslib": "^2.3.1"
-      }
-    },
     "ci-info": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz",
@@ -24662,11 +24640,6 @@
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
       "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
     },
-    "has-own-prop": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz",
-      "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ=="
-    },
     "has-property-descriptors": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
@@ -24994,15 +24967,6 @@
       "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==",
       "dev": true
     },
-    "immutable-class": {
-      "version": "0.11.2",
-      "resolved": "https://registry.npmjs.org/immutable-class/-/immutable-class-0.11.2.tgz",
-      "integrity": "sha512-CzkVPkJXzkspt6RX+ipNgtvt16+rzEBUlA3yNPLkK5/S042c9wvuyfE4F5TfMfPJ6XF86Fp+OCwu6eeAnMICuw==",
-      "requires": {
-        "has-own-prop": "^2.0.0",
-        "tslib": "^2.3.1"
-      }
-    },
     "import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -27569,19 +27533,6 @@
         "minimist": "^1.2.5"
       }
     },
-    "moment": {
-      "version": "2.29.4",
-      "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
-      "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
-    },
-    "moment-timezone": {
-      "version": "0.5.43",
-      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz",
-      "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==",
-      "requires": {
-        "moment": "^2.29.4"
-      }
-    },
     "mrmime": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz",
diff --git a/web-console/package.json b/web-console/package.json
index 2580151bac1..22bb09d232c 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -58,9 +58,9 @@
     "@druid-toolkit/query": "^0.22.23",
     "@flatten-js/interval-tree": "^1.1.3",
     "@fontsource/open-sans": "^5.0.30",
+    "@internationalized/date": "^3.5.6",
     "ace-builds": "~1.5.3",
     "axios": "^1.7.7",
-    "chronoshift": "^0.10.0",
     "classnames": "^2.2.6",
     "copy-to-clipboard": "^3.3.3",
     "d3-array": "^3.2.4",
diff --git a/web-console/script/licenses b/web-console/script/licenses
index 90f1420282a..f4e67862d01 100755
--- a/web-console/script/licenses
+++ b/web-console/script/licenses
@@ -193,6 +193,7 @@ checker.init(
           if (name === 'diff-match-patch') publisher = 'Google';
           if (name === 'esutils') publisher = 'Yusuke Suzuki'; // https://github.com/estools/esutils#license
           if (name === 'echarts') publisher = 'Apache Software Foundation';
+          if (name === '@internationalized/date') publisher = 'Adobe';
         }
 
         if (!publisher) {
diff --git a/web-console/src/druid-models/index.ts b/web-console/src/druid-models/index.ts
index 82f14aab4e6..3e5c7062232 100644
--- a/web-console/src/druid-models/index.ts
+++ b/web-console/src/druid-models/index.ts
@@ -37,6 +37,7 @@ export * from './lookup-spec/lookup-spec';
 export * from './metric-spec/metric-spec';
 export * from './overlord-dynamic-config/overlord-dynamic-config';
 export * from './query-context/query-context';
+export * from './segment/segment';
 export * from './stages/stages';
 export * from './supervisor-status/supervisor-status';
 export * from './task/task';
diff --git a/web-console/src/views/explore-view/utils/duration.spec.ts b/web-console/src/druid-models/segment/segment.ts
similarity index 54%
rename from web-console/src/views/explore-view/utils/duration.spec.ts
rename to web-console/src/druid-models/segment/segment.ts
index 0d7e0473131..3860d4f5127 100644
--- a/web-console/src/views/explore-view/utils/duration.spec.ts
+++ b/web-console/src/druid-models/segment/segment.ts
@@ -16,24 +16,25 @@
  * limitations under the License.
  */
 
-import { formatDuration } from './duration';
+import { Duration } from '../../utils';
 
-describe('formatDuration', () => {
-  it('works with 0', () => {
-    expect(formatDuration('PT0S')).toEqual('0 seconds');
-  });
+export const START_OF_TIME_DATE = '-146136543-09-08T08:23:32.096Z';
+export const END_OF_TIME_DATE = '146140482-04-24T15:36:27.903Z';
 
-  it('works with single span', () => {
-    expect(formatDuration('P1D')).toEqual('1 day');
-    expect(formatDuration('PT1M')).toEqual('1 minute');
-  });
+export function computeSegmentTimeSpan(start: string, end: string): string {
+  if (start === START_OF_TIME_DATE && end === END_OF_TIME_DATE) {
+    return 'All';
+  }
 
-  it('works with single span (compact)', () => {
-    expect(formatDuration('PT1M', true)).toEqual('minute');
-  });
+  const startDate = new Date(start);
+  if (isNaN(startDate.valueOf())) {
+    return 'Invalid start';
+  }
 
-  it('works with multiple spans', () => {
-    expect(formatDuration('PT2H30M15S')).toEqual('2 hours, 30 minutes, 15 seconds');
-    expect(formatDuration('PT2H30M15S', true)).toEqual('2 hours, 30 minutes, 15 seconds');
-  });
-});
+  const endDate = new Date(end);
+  if (isNaN(endDate.valueOf())) {
+    return 'Invalid end';
+  }
+
+  return Duration.fromRange(startDate, endDate, 'Etc/UTC').getDescription(true);
+}
diff --git a/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil-utc.spec.ts b/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil-utc.spec.ts
new file mode 100755
index 00000000000..5ba63b04468
--- /dev/null
+++ b/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil-utc.spec.ts
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { shifters } from './date-floor-shift-ceil';
+
+function pairwise<T>(array: T[], callback: (t1: T, t2: T) => void) {
+  for (let i = 0; i < array.length - 1; i++) {
+    callback(array[i], array[i + 1]);
+  }
+}
+
+describe('floor, shift, ceil (UTC)', () => {
+  const tz = 'Etc/UTC';
+
+  it('moves seconds', () => {
+    const dates: Date[] = [
+      new Date('2012-11-04T00:00:00Z'),
+      new Date('2012-11-04T00:00:03Z'),
+      new Date('2012-11-04T00:00:06Z'),
+      new Date('2012-11-04T00:00:09Z'),
+      new Date('2012-11-04T00:00:12Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.second.shift(d1, tz, 3)).toEqual(d2));
+  });
+
+  it('rounds minutes', () => {
+    expect(shifters.minute.round(new Date('2012-11-04T00:29:00Z'), 15, tz)).toEqual(
+      new Date('2012-11-04T00:15:00Z'),
+    );
+
+    expect(shifters.minute.round(new Date('2012-11-04T00:29:00Z'), 4, tz)).toEqual(
+      new Date('2012-11-04T00:28:00Z'),
+    );
+  });
+
+  it('moves minutes', () => {
+    const dates: Date[] = [
+      new Date('2012-11-04T00:00:00Z'),
+      new Date('2012-11-04T00:03:00Z'),
+      new Date('2012-11-04T00:06:00Z'),
+      new Date('2012-11-04T00:09:00Z'),
+      new Date('2012-11-04T00:12:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.minute.shift(d1, tz, 3)).toEqual(d2));
+  });
+
+  it('floors hour correctly', () => {
+    expect(shifters.hour.floor(new Date('2012-11-04T00:30:00Z'), tz)).toEqual(
+      new Date('2012-11-04T00:00:00Z'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T01:30:00Z'), tz)).toEqual(
+      new Date('2012-11-04T01:00:00Z'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T01:30:00Z'), tz)).toEqual(
+      new Date('2012-11-04T01:00:00Z'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T02:30:00Z'), tz)).toEqual(
+      new Date('2012-11-04T02:00:00Z'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T03:30:00Z'), tz)).toEqual(
+      new Date('2012-11-04T03:00:00Z'),
+    );
+  });
+
+  it('moves hour', () => {
+    const dates: Date[] = [
+      new Date('2012-11-04T00:00:00Z'),
+      new Date('2012-11-04T01:00:00Z'),
+      new Date('2012-11-04T02:00:00Z'),
+      new Date('2012-11-04T03:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('moves day', () => {
+    const dates: Date[] = [
+      new Date('2012-11-03T00:00:00Z'),
+      new Date('2012-11-04T00:00:00Z'),
+      new Date('2012-11-05T00:00:00Z'),
+      new Date('2012-11-06T00:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.day.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('ceils day', () => {
+    let d1 = new Date('2014-12-11T22:11:57.469Z');
+    let d2 = new Date('2014-12-12T00:00:00.000Z');
+    expect(shifters.day.ceil(d1, tz)).toEqual(d2);
+
+    d1 = new Date('2014-12-08T00:00:00.000Z');
+    d2 = new Date('2014-12-08T00:00:00.000Z');
+    expect(shifters.day.ceil(d1, tz)).toEqual(d2);
+  });
+
+  it('moves week', () => {
+    const dates: Date[] = [
+      new Date('2012-10-29T00:00:00Z'),
+      new Date('2012-11-05T00:00:00Z'),
+      new Date('2012-11-12T00:00:00Z'),
+      new Date('2012-11-19T00:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.week.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('floors week correctly', () => {
+    let d1 = new Date('2014-12-11T22:11:57.469Z');
+    let d2 = new Date('2014-12-08T00:00:00.000Z');
+    expect(shifters.week.floor(d1, tz)).toEqual(d2);
+
+    d1 = new Date('2014-12-07T12:11:57.469Z');
+    d2 = new Date('2014-12-01T00:00:00.000Z');
+    expect(shifters.week.floor(d1, tz)).toEqual(d2);
+  });
+
+  it('ceils week correctly', () => {
+    let d1 = new Date('2014-12-11T22:11:57.469Z');
+    let d2 = new Date('2014-12-15T00:00:00.000Z');
+    expect(shifters.week.ceil(d1, tz)).toEqual(d2);
+
+    d1 = new Date('2014-12-07T12:11:57.469Z');
+    d2 = new Date('2014-12-08T00:00:00.000Z');
+    expect(shifters.week.ceil(d1, tz)).toEqual(d2);
+  });
+
+  it('moves month', () => {
+    const dates: Date[] = [
+      new Date('2012-11-01T00:00:00Z'),
+      new Date('2012-12-01T00:00:00Z'),
+      new Date('2013-01-01T00:00:00Z'),
+      new Date('2013-02-01T00:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.month.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('shifts month on the 31st', () => {
+    const d1 = new Date('2016-03-31T00:00:00.000Z');
+    const d2 = new Date('2016-05-01T00:00:00.000Z');
+    expect(shifters.month.shift(d1, tz, 1)).toEqual(d2);
+  });
+
+  it('moves year', () => {
+    const dates: Date[] = [
+      new Date('2010-01-01T00:00:00Z'),
+      new Date('2011-01-01T00:00:00Z'),
+      new Date('2012-01-01T00:00:00Z'),
+      new Date('2013-01-01T00:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.year.shift(d1, tz, 1)).toEqual(d2));
+  });
+});
diff --git a/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.spec.ts b/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.spec.ts
new file mode 100755
index 00000000000..1612c9ce579
--- /dev/null
+++ b/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.spec.ts
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { shifters } from './date-floor-shift-ceil';
+
+function pairwise<T>(array: T[], callback: (t1: T, t2: T) => void) {
+  for (let i = 0; i < array.length - 1; i++) {
+    callback(array[i], array[i + 1]);
+  }
+}
+
+describe('floor/shift/ceil', () => {
+  const tz = 'America/Los_Angeles';
+
+  it('shifts seconds', () => {
+    const dates: Date[] = [
+      new Date('2012-11-04T00:00:00-07:00'),
+      new Date('2012-11-04T00:00:03-07:00'),
+      new Date('2012-11-04T00:00:06-07:00'),
+      new Date('2012-11-04T00:00:09-07:00'),
+      new Date('2012-11-04T00:00:12-07:00'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.second.shift(d1, tz, 3)).toEqual(d2));
+  });
+
+  it('shifts minutes', () => {
+    const dates: Date[] = [
+      new Date('2012-11-04T00:00:00-07:00'),
+      new Date('2012-11-04T00:03:00-07:00'),
+      new Date('2012-11-04T00:06:00-07:00'),
+      new Date('2012-11-04T00:09:00-07:00'),
+      new Date('2012-11-04T00:12:00-07:00'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.minute.shift(d1, tz, 3)).toEqual(d2));
+  });
+
+  it('floors hour correctly', () => {
+    expect(shifters.hour.floor(new Date('2012-11-04T00:30:00-07:00'), tz)).toEqual(
+      new Date('2012-11-04T00:00:00-07:00'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T01:30:00-07:00'), tz)).toEqual(
+      new Date('2012-11-04T01:00:00-07:00'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T01:30:00-08:00'), tz)).toEqual(
+      new Date('2012-11-04T01:00:00-07:00'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T02:30:00-08:00'), tz)).toEqual(
+      new Date('2012-11-04T02:00:00-08:00'),
+    );
+
+    expect(shifters.hour.floor(new Date('2012-11-04T03:30:00-08:00'), tz)).toEqual(
+      new Date('2012-11-04T03:00:00-08:00'),
+    );
+  });
+
+  it('shifting 24 hours over DST is not the same as shifting a day', () => {
+    const start = new Date('2012-11-04T07:00:00Z');
+
+    const shift1Day = shifters.day.shift(start, tz, 1);
+    const shift24Hours = shifters.hour.shift(start, tz, 24);
+
+    expect(shift1Day).toEqual(new Date('2012-11-05T08:00:00Z'));
+    expect(shift24Hours).toEqual(new Date('2012-11-05T07:00:00Z'));
+  });
+
+  it('shifts hour over DST 1', () => {
+    const dates: Date[] = [
+      new Date('2012-11-04T00:00:00-07:00'),
+      new Date('2012-11-04T08:00:00Z'),
+      new Date('2012-11-04T09:00:00Z'),
+      new Date('2012-11-04T10:00:00Z'),
+      new Date('2012-11-04T11:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('floors hour over DST 1', () => {
+    expect(shifters.hour.floor(new Date('2012-11-04T00:05:00-07:00'), tz)).toEqual(
+      new Date('2012-11-04T00:00:00-07:00'),
+    );
+    expect(shifters.hour.floor(new Date('2012-11-04T01:05:00-07:00'), tz)).toEqual(
+      new Date('2012-11-04T01:00:00-07:00'),
+    );
+    expect(shifters.hour.floor(new Date('2012-11-04T02:05:00-07:00'), tz)).toEqual(
+      new Date('2012-11-04T01:00:00-07:00'),
+    );
+    expect(shifters.hour.floor(new Date('2012-11-04T03:05:00-07:00'), tz)).toEqual(
+      new Date('2012-11-04T03:00:00-07:00'),
+    );
+  });
+
+  it('shifts hour over DST 2', () => {
+    // "2018-03-11T09:00:00Z"
+    const dates: Date[] = [
+      new Date('2018-03-11T01:00:00-07:00'),
+      new Date('2018-03-11T09:00:00Z'),
+      new Date('2018-03-11T10:00:00Z'),
+      new Date('2018-03-11T11:00:00Z'),
+      new Date('2018-03-11T12:00:00Z'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.hour.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('shifts day over DST', () => {
+    const dates: Date[] = [
+      new Date('2012-11-03T00:00:00-07:00'),
+      new Date('2012-11-04T00:00:00-07:00'),
+      new Date('2012-11-05T00:00:00-08:00'),
+      new Date('2012-11-06T00:00:00-08:00'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.day.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('shifts week over DST', () => {
+    const dates: Date[] = [
+      new Date('2012-10-29T00:00:00-07:00'),
+      new Date('2012-11-05T00:00:00-08:00'),
+      new Date('2012-11-12T00:00:00-08:00'),
+      new Date('2012-11-19T00:00:00-08:00'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.week.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('floors week correctly', () => {
+    let d1 = new Date('2014-12-11T22:11:57.469Z');
+    let d2 = new Date('2014-12-08T08:00:00.000Z');
+    expect(shifters.week.floor(d1, tz)).toEqual(d2);
+
+    d1 = new Date('2014-12-07T12:11:57.469Z');
+    d2 = new Date('2014-12-01T08:00:00.000Z');
+    expect(shifters.week.floor(d1, tz)).toEqual(d2);
+  });
+
+  it('ceils week correctly', () => {
+    let d1 = new Date('2014-12-11T22:11:57.469Z');
+    let d2 = new Date('2014-12-15T08:00:00.000Z');
+    expect(shifters.week.ceil(d1, tz)).toEqual(d2);
+
+    d1 = new Date('2014-12-07T12:11:57.469Z');
+    d2 = new Date('2014-12-08T08:00:00.000Z');
+    expect(shifters.week.ceil(d1, tz)).toEqual(d2);
+  });
+
+  it('shifts month over DST', () => {
+    const dates: Date[] = [
+      new Date('2012-11-01T00:00:00-07:00'),
+      new Date('2012-12-01T00:00:00-08:00'),
+      new Date('2013-01-01T00:00:00-08:00'),
+      new Date('2013-02-01T00:00:00-08:00'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.month.shift(d1, tz, 1)).toEqual(d2));
+  });
+
+  it('shifts year', () => {
+    const dates: Date[] = [
+      new Date('2010-01-01T00:00:00-08:00'),
+      new Date('2011-01-01T00:00:00-08:00'),
+      new Date('2012-01-01T00:00:00-08:00'),
+      new Date('2013-01-01T00:00:00-08:00'),
+    ];
+    pairwise(dates, (d1, d2) => expect(shifters.year.shift(d1, tz, 1)).toEqual(d2));
+  });
+});
diff --git a/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.ts b/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.ts
new file mode 100755
index 00000000000..3306b05267d
--- /dev/null
+++ b/web-console/src/utils/date-floor-shift-ceil/date-floor-shift-ceil.ts
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { fromDate, startOfWeek } from '@internationalized/date';
+
+export type AlignFn = (dt: Date, tz: string) => Date;
+
+export type ShiftFn = (dt: Date, tz: string, step: number) => Date;
+
+export type RoundFn = (dt: Date, roundTo: number, tz: string) => Date;
+
+export interface TimeShifterNoCeil {
+  canonicalLength: number;
+  siblings?: number;
+  floor: AlignFn;
+  round: RoundFn;
+  shift: ShiftFn;
+}
+
+export interface TimeShifter extends TimeShifterNoCeil {
+  ceil: AlignFn;
+}
+
+function isUTC(tz: string): boolean {
+  return tz === 'Etc/UTC';
+}
+
+function adjustDay(day: number): number {
+  return (day + 6) % 7;
+}
+
+function floorTo(n: number, roundTo: number): number {
+  return Math.floor(n / roundTo) * roundTo;
+}
+
+function timeShifterFiller(tm: TimeShifterNoCeil): TimeShifter {
+  const { floor, shift } = tm;
+  return {
+    ...tm,
+    ceil: (dt: Date, tz: string) => {
+      const floored = floor(dt, tz);
+      if (floored.valueOf() === dt.valueOf()) return dt; // Just like ceil(3) is 3 and not 4
+      return shift(floored, tz, 1);
+    },
+  };
+}
+
+export const second = timeShifterFiller({
+  canonicalLength: 1000,
+  siblings: 60,
+  floor: (dt, _tz) => {
+    // Seconds do not actually need a timezone because all timezones align on seconds... for now...
+    dt = new Date(dt.valueOf());
+    dt.setUTCMilliseconds(0);
+    return dt;
+  },
+  round: (dt, roundTo, _tz) => {
+    const cur = dt.getUTCSeconds();
+    const adj = floorTo(cur, roundTo);
+    if (cur !== adj) dt.setUTCSeconds(adj);
+    return dt;
+  },
+  shift: (dt, _tz, step) => {
+    dt = new Date(dt.valueOf());
+    dt.setUTCSeconds(dt.getUTCSeconds() + step);
+    return dt;
+  },
+});
+
+export const minute = timeShifterFiller({
+  canonicalLength: 60000,
+  siblings: 60,
+  floor: (dt, _tz) => {
+    // Minutes do not actually need a timezone because all timezones align on minutes... for now...
+    dt = new Date(dt.valueOf());
+    dt.setUTCSeconds(0, 0);
+    return dt;
+  },
+  round: (dt, roundTo, _tz) => {
+    const cur = dt.getUTCMinutes();
+    const adj = floorTo(cur, roundTo);
+    if (cur !== adj) dt.setUTCMinutes(adj);
+    return dt;
+  },
+  shift: (dt, _tz, step) => {
+    dt = new Date(dt.valueOf());
+    dt.setUTCMinutes(dt.getUTCMinutes() + step);
+    return dt;
+  },
+});
+
+// Movement by hour is tz independent because in every timezone an hour is 60 min
+function hourMove(dt: Date, _tz: string, step: number) {
+  dt = new Date(dt.valueOf());
+  dt.setUTCHours(dt.getUTCHours() + step);
+  return dt;
+}
+
+export const hour = timeShifterFiller({
+  canonicalLength: 3600000,
+  siblings: 24,
+  floor: (dt, tz) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCMinutes(0, 0, 0);
+      return dt;
+    } else {
+      return fromDate(dt, tz).set({ second: 0, minute: 0, millisecond: 0 }).toDate();
+    }
+  },
+  round: (dt, roundTo, tz) => {
+    if (isUTC(tz)) {
+      const cur = dt.getUTCHours();
+      const adj = floorTo(cur, roundTo);
+      if (cur !== adj) dt.setUTCHours(adj);
+    } else {
+      const cur = fromDate(dt, tz).hour;
+      const adj = floorTo(cur, roundTo);
+      if (cur !== adj) return hourMove(dt, tz, adj - cur);
+    }
+    return dt;
+  },
+  shift: hourMove,
+});
+
+export const day = timeShifterFiller({
+  canonicalLength: 24 * 3600000,
+  floor: (dt, tz) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCHours(0, 0, 0, 0);
+      return dt;
+    } else {
+      return fromDate(dt, tz).set({ hour: 0, second: 0, minute: 0, millisecond: 0 }).toDate();
+    }
+  },
+  shift: (dt, tz, step) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCDate(dt.getUTCDate() + step);
+      return dt;
+    } else {
+      return fromDate(dt, tz).add({ days: step }).toDate();
+    }
+  },
+  round: () => {
+    throw new Error('missing day round');
+  },
+});
+
+export const week = timeShifterFiller({
+  canonicalLength: 7 * 24 * 3600000,
+  floor: (dt, tz) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCHours(0, 0, 0, 0);
+      dt.setUTCDate(dt.getUTCDate() - adjustDay(dt.getUTCDay()));
+    } else {
+      const zd = fromDate(dt, tz);
+      return startOfWeek(
+        zd.set({ hour: 0, second: 0, minute: 0, millisecond: 0 }),
+        'fr-FR', // We want the week to start on Monday
+      ).toDate();
+    }
+    return dt;
+  },
+  shift: (dt, tz, step) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCDate(dt.getUTCDate() + step * 7);
+      return dt;
+    } else {
+      return fromDate(dt, tz).add({ weeks: step }).toDate();
+    }
+  },
+  round: () => {
+    throw new Error('missing week round');
+  },
+});
+
+function monthShift(dt: Date, tz: string, step: number) {
+  if (isUTC(tz)) {
+    dt = new Date(dt.valueOf());
+    dt.setUTCMonth(dt.getUTCMonth() + step);
+    return dt;
+  } else {
+    return fromDate(dt, tz).add({ months: step }).toDate();
+  }
+}
+
+export const month = timeShifterFiller({
+  canonicalLength: 30 * 24 * 3600000,
+  siblings: 12,
+  floor: (dt, tz) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCHours(0, 0, 0, 0);
+      dt.setUTCDate(1);
+      return dt;
+    } else {
+      return fromDate(dt, tz)
+        .set({ day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 })
+        .toDate();
+    }
+  },
+  round: (dt, roundTo, tz) => {
+    if (isUTC(tz)) {
+      const cur = dt.getUTCMonth();
+      const adj = floorTo(cur, roundTo);
+      if (cur !== adj) dt.setUTCMonth(adj);
+    } else {
+      const cur = fromDate(dt, tz).month - 1; // Needs to be zero indexed
+      const adj = floorTo(cur, roundTo);
+      if (cur !== adj) return monthShift(dt, tz, adj - cur);
+    }
+    return dt;
+  },
+  shift: monthShift,
+});
+
+function yearShift(dt: Date, tz: string, step: number) {
+  if (isUTC(tz)) {
+    dt = new Date(dt.valueOf());
+    dt.setUTCFullYear(dt.getUTCFullYear() + step);
+    return dt;
+  } else {
+    return fromDate(dt, tz).add({ years: step }).toDate();
+  }
+}
+
+export const year = timeShifterFiller({
+  canonicalLength: 365 * 24 * 3600000,
+  siblings: 1000,
+  floor: (dt, tz) => {
+    if (isUTC(tz)) {
+      dt = new Date(dt.valueOf());
+      dt.setUTCHours(0, 0, 0, 0);
+      dt.setUTCMonth(0, 1);
+      return dt;
+    } else {
+      return fromDate(dt, tz)
+        .set({ month: 1, day: 1, hour: 0, second: 0, minute: 0, millisecond: 0 })
+        .toDate();
+    }
+  },
+  round: (dt, roundTo, tz) => {
+    if (isUTC(tz)) {
+      const cur = dt.getUTCFullYear();
+      const adj = floorTo(cur, roundTo);
+      if (cur !== adj) dt.setUTCFullYear(adj);
+    } else {
+      const cur = fromDate(dt, tz).year;
+      const adj = floorTo(cur, roundTo);
+      if (cur !== adj) return yearShift(dt, tz, adj - cur);
+    }
+    return dt;
+  },
+  shift: yearShift,
+});
+
+export interface Shifters {
+  second: TimeShifter;
+  minute: TimeShifter;
+  hour: TimeShifter;
+  day: TimeShifter;
+  week: TimeShifter;
+  month: TimeShifter;
+  year: TimeShifter;
+
+  [key: string]: TimeShifter;
+}
+
+export const shifters: Shifters = {
+  second,
+  minute,
+  hour,
+  day,
+  week,
+  month,
+  year,
+};
diff --git a/web-console/src/utils/duration/duration.spec.ts b/web-console/src/utils/duration/duration.spec.ts
new file mode 100755
index 00000000000..8b20ac0a6b2
--- /dev/null
+++ b/web-console/src/utils/duration/duration.spec.ts
@@ -0,0 +1,505 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Duration } from './duration';
+
+describe('Duration', () => {
+  const TZ_LA = 'America/Los_Angeles';
+  const TZ_JUNEAU = 'America/Juneau';
+
+  describe('errors', () => {
+    it('throws error if invalid duration', () => {
+      expect(() => new Duration('')).toThrow("Can not parse duration ''");
+
+      expect(() => new Duration('P00')).toThrow("Can not parse duration 'P00'");
+
+      expect(() => new Duration('P')).toThrow('Duration can not be empty');
+
+      expect(() => new Duration('P0YT0H')).toThrow('Duration can not be empty');
+
+      expect(() => new Duration('P0W').shift(new Date(), TZ_LA)).toThrow(
+        'Duration can not have empty weeks',
+      );
+
+      expect(() => new Duration('P0Y0MT0H0M0S').shift(new Date(), TZ_LA)).toThrow(
+        'Duration can not be empty',
+      );
+    });
+
+    it('throws error if fromJS is not given a string', () => {
+      expect(() => new Duration(new Date() as any)).toThrow('Duration can not be empty');
+    });
+  });
+
+  describe('#toString', () => {
+    it('gives back the correct string', () => {
+      let durationStr: string;
+
+      durationStr = 'P3Y';
+      expect(new Duration(durationStr).toString()).toEqual(durationStr);
+
+      durationStr = 'P2W';
+      expect(new Duration(durationStr).toString()).toEqual(durationStr);
+
+      durationStr = 'PT5H';
+      expect(new Duration(durationStr).toString()).toEqual(durationStr);
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).toString()).toEqual(durationStr);
+    });
+
+    it('eliminates 0', () => {
+      expect(new Duration('P0DT15H').toString()).toEqual('PT15H');
+    });
+  });
+
+  describe('fromCanonicalLength', () => {
+    it('handles zero', () => {
+      expect(() => {
+        Duration.fromCanonicalLength(0);
+      }).toThrow('length must be positive');
+    });
+
+    it('works 1', () => {
+      expect(Duration.fromCanonicalLength(86400000).toString()).toEqual('P1D');
+    });
+
+    it('works 2', () => {
+      const len =
+        new Date('2018-03-01T00:00:00Z').valueOf() - new Date('2016-02-22T00:00:00Z').valueOf();
+      expect(Duration.fromCanonicalLength(len).toString()).toEqual('P2Y8D');
+    });
+
+    it('works 3', () => {
+      const len =
+        new Date('2018-09-15T00:00:00Z').valueOf() - new Date('2018-09-04T00:00:00Z').valueOf();
+      expect(Duration.fromCanonicalLength(len).toString()).toEqual('P11D');
+    });
+
+    it('works with months', () => {
+      expect(Duration.fromCanonicalLength(2592000000).toString()).toEqual('P1M');
+      expect(Duration.fromCanonicalLength(2678400000).toString()).toEqual('P1M1D');
+    });
+
+    it('works without months', () => {
+      expect(Duration.fromCanonicalLength(2592000000, true).toString()).toEqual('P30D');
+      expect(Duration.fromCanonicalLength(2678400000, true).toString()).toEqual('P31D');
+    });
+  });
+
+  describe('construct from span', () => {
+    it('parses days over DST', () => {
+      expect(
+        Duration.fromRange(
+          new Date('2012-10-29T00:00:00-07:00'),
+          new Date('2012-11-05T00:00:00-08:00'),
+          TZ_LA,
+        ).toString(),
+      ).toEqual('P7D');
+
+      expect(
+        Duration.fromRange(
+          new Date('2012-10-29T00:00:00-07:00'),
+          new Date('2012-11-12T00:00:00-08:00'),
+          TZ_LA,
+        ).toString(),
+      ).toEqual('P14D');
+    });
+
+    it('parses complex case', () => {
+      expect(
+        Duration.fromRange(
+          new Date('2012-10-29T00:00:00-07:00'),
+          new Date(new Date('2012-11-05T00:00:00-08:00').valueOf() - 1000),
+          TZ_LA,
+        ).toString(),
+      ).toEqual('P6DT24H59M59S');
+
+      expect(
+        Duration.fromRange(
+          new Date('2012-01-01T00:00:00-08:00'),
+          new Date('2013-03-04T04:05:06-08:00'),
+          TZ_LA,
+        ).toString(),
+      ).toEqual('P1Y2M3DT4H5M6S');
+    });
+  });
+
+  describe('#isFloorable', () => {
+    const floorable = 'P1Y P5Y P10Y P100Y P1M P2M P3M P4M P1D'.split(' ');
+    for (const v of floorable) {
+      it(`works on floorable ${v}`, () => {
+        expect(new Duration(v).isFloorable()).toEqual(true);
+      });
+    }
+
+    const unfloorable = 'P1Y1M P5M P2D P3D'.split(' ');
+    for (const v of unfloorable) {
+      it(`works on not floorable ${v}`, () => {
+        expect(new Duration(v).isFloorable()).toEqual(false);
+      });
+    }
+  });
+
+  describe('#floor', () => {
+    it('throws error if complex duration', () => {
+      expect(() => new Duration('P1Y2D').floor(new Date(), TZ_LA)).toThrow(
+        'Can not floor on a complex duration',
+      );
+
+      expect(() => new Duration('P3DT15H').floor(new Date(), TZ_LA)).toThrow(
+        'Can not floor on a complex duration',
+      );
+
+      expect(() => new Duration('PT5H').floor(new Date(), TZ_LA)).toThrow(
+        'Can not floor on a hour duration that does not divide into 24',
+      );
+    });
+
+    it('works for year', () => {
+      const p1y = new Duration('P1Y');
+      expect(p1y.floor(new Date('2013-09-29T01:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-01-01T00:00:00.000-08:00'),
+      );
+    });
+
+    it('works for PT2M', () => {
+      const pt2h = new Duration('PT2M');
+      expect(pt2h.floor(new Date('2013-09-29T03:03:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-09-29T03:02:00.000-07:00'),
+      );
+    });
+
+    it('works for P2H', () => {
+      const pt2h = new Duration('PT2H');
+      expect(pt2h.floor(new Date('2013-09-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-09-29T02:00:00.000-07:00'),
+      );
+    });
+
+    it('works for PT12H', () => {
+      const pt12h = new Duration('PT12H');
+      expect(pt12h.floor(new Date('2015-09-12T13:05:00-08:00'), TZ_JUNEAU)).toEqual(
+        new Date('2015-09-12T12:00:00-08:00'),
+      );
+    });
+
+    it('works for P1W', () => {
+      const p1w = new Duration('P1W');
+
+      expect(p1w.floor(new Date('2013-09-29T01:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-09-23T07:00:00.000Z'),
+      );
+
+      expect(p1w.floor(new Date('2013-10-03T01:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-09-30T00:00:00.000-07:00'),
+      );
+    });
+
+    it('works for P3M', () => {
+      const p3m = new Duration('P3M');
+      expect(p3m.floor(new Date('2013-09-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-07-01T00:00:00.000-07:00'),
+      );
+
+      expect(p3m.floor(new Date('2013-02-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2013-01-01T00:00:00.000-08:00'),
+      );
+    });
+
+    it('works for P4Y', () => {
+      const p4y = new Duration('P4Y');
+      expect(p4y.floor(new Date('2013-09-29T03:02:03.456-07:00'), TZ_LA)).toEqual(
+        new Date('2012-01-01T00:00:00.000-08:00'),
+      );
+    });
+  });
+
+  describe('#shift', () => {
+    it('works for weeks', () => {
+      let p1w = new Duration('P1W');
+      expect(p1w.shift(new Date('2012-10-29T00:00:00-07:00'), TZ_LA)).toEqual(
+        new Date('2012-11-05T00:00:00-08:00'),
+      );
+
+      p1w = new Duration('P1W');
+      expect(p1w.shift(new Date('2012-10-29T00:00:00-07:00'), TZ_LA, 2)).toEqual(
+        new Date('2012-11-12T00:00:00-08:00'),
+      );
+
+      const p2w = new Duration('P2W');
+      expect(p2w.shift(new Date('2012-10-29T05:16:17-07:00'), TZ_LA)).toEqual(
+        new Date('2012-11-12T05:16:17-08:00'),
+      );
+    });
+
+    it('works for general complex case', () => {
+      const pComplex = new Duration('P1Y2M3DT4H5M6S');
+      expect(pComplex.shift(new Date('2012-01-01T00:00:00-08:00'), TZ_LA)).toEqual(
+        new Date('2013-03-04T04:05:06-08:00'),
+      );
+    });
+  });
+
+  describe('#materialize', () => {
+    it('works for weeks', () => {
+      const p1w = new Duration('P1W');
+
+      expect(
+        p1w.materialize(
+          new Date('2012-10-29T00:00:00-07:00'),
+          new Date('2012-12-01T00:00:00-08:00'),
+          TZ_LA,
+        ),
+      ).toEqual([
+        new Date('2012-10-29T07:00:00.000Z'),
+        new Date('2012-11-05T08:00:00.000Z'),
+        new Date('2012-11-12T08:00:00.000Z'),
+        new Date('2012-11-19T08:00:00.000Z'),
+        new Date('2012-11-26T08:00:00.000Z'),
+      ]);
+
+      expect(
+        p1w.materialize(
+          new Date('2012-10-29T00:00:00-07:00'),
+          new Date('2012-12-01T00:00:00-08:00'),
+          TZ_LA,
+          2,
+        ),
+      ).toEqual([
+        new Date('2012-10-29T07:00:00.000Z'),
+        new Date('2012-11-12T08:00:00.000Z'),
+        new Date('2012-11-26T08:00:00.000Z'),
+      ]);
+    });
+  });
+
+  describe('#isAligned', () => {
+    it('works for weeks', () => {
+      const p1w = new Duration('P1W');
+      expect(p1w.isAligned(new Date('2012-10-29T00:00:00-07:00'), TZ_LA)).toEqual(true);
+      expect(p1w.isAligned(new Date('2012-10-29T00:00:00-07:00'), 'Etc/UTC')).toEqual(false);
+    });
+  });
+
+  describe('#dividesBy', () => {
+    const divisible = 'P5Y/P1Y P1D/P1D P1M/P1D P1W/P1D P1D/PT6H PT3H/PT1H'.split(' ');
+    for (const v of divisible) {
+      it(`works for ${v} (true)`, () => {
+        const p = v.split('/');
+        expect(new Duration(p[0]).dividesBy(new Duration(p[1]))).toEqual(true);
+      });
+    }
+
+    const undivisible = 'P1D/P1M PT5H/PT1H'.split(' ');
+    for (const v of undivisible) {
+      it(`works for ${v} (false)`, () => {
+        const p = v.split('/');
+        expect(new Duration(p[0]).dividesBy(new Duration(p[1]))).toEqual(false);
+      });
+    }
+  });
+
+  describe('#getCanonicalLength', () => {
+    it('gives back the correct canonical length', () => {
+      let durationStr: string;
+
+      durationStr = 'P3Y';
+      expect(new Duration(durationStr).getCanonicalLength()).toEqual(94608000000);
+
+      durationStr = 'P2W';
+      expect(new Duration(durationStr).getCanonicalLength()).toEqual(1209600000);
+
+      durationStr = 'PT5H';
+      expect(new Duration(durationStr).getCanonicalLength()).toEqual(18000000);
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getCanonicalLength()).toEqual(313200000);
+    });
+  });
+
+  describe('#add()', () => {
+    it('works with a simple duration', () => {
+      const d1 = new Duration('P1D');
+      const d2 = new Duration('P1D');
+
+      expect(d1.add(d2).toString()).toEqual('P2D');
+    });
+
+    it('works with heterogeneous spans', () => {
+      const d1 = new Duration('P1D');
+      const d2 = new Duration('P1Y');
+
+      expect(d1.add(d2).toString()).toEqual('P1Y1D');
+    });
+
+    it('works with weeks', () => {
+      let d1 = new Duration('P1W');
+      let d2 = new Duration('P2W');
+      expect(d1.add(d2).toString()).toEqual('P3W');
+
+      d1 = new Duration('P6D');
+      d2 = new Duration('P1D');
+      expect(d1.add(d2).toString()).toEqual('P1W');
+    });
+  });
+
+  describe('#subtract()', () => {
+    it('works with a simple duration', () => {
+      const d1 = new Duration('P1DT2H');
+      const d2 = new Duration('PT1H');
+
+      expect(d1.subtract(d2).toString()).toEqual('P1DT1H');
+    });
+
+    it('works with a less simple duration', () => {
+      const d1 = new Duration('P1D');
+      const d2 = new Duration('PT1H');
+
+      expect(d1.subtract(d2).toString()).toEqual('PT23H');
+    });
+
+    it('works with weeks', () => {
+      const d1 = new Duration('P1W');
+      const d2 = new Duration('P1D');
+
+      expect(d1.subtract(d2).toString()).toEqual('P6D');
+    });
+
+    it('throws an error if result is going to be negative', () => {
+      const d1 = new Duration('P1D');
+      const d2 = new Duration('P2D');
+
+      expect(() => d1.subtract(d2)).toThrow();
+    });
+  });
+
+  describe('#multiply()', () => {
+    it('works with a simple duration', () => {
+      const d = new Duration('P1D');
+      expect(d.multiply(5).toString()).toEqual('P5D');
+    });
+
+    it('works with a less simple duration', () => {
+      const d = new Duration('P1DT2H');
+      expect(d.multiply(2).toString()).toEqual('P2DT4H');
+    });
+
+    it('works with weeks', () => {
+      const d = new Duration('P1W');
+      expect(d.multiply(5).toString()).toEqual('P5W');
+    });
+
+    it('throws an error if result is going to be negative', () => {
+      const d = new Duration('P1D');
+      expect(() => d.multiply(-1)).toThrow('Multiplier must be positive non-zero');
+    });
+
+    it('gets description properly', () => {
+      const d = new Duration('P2D');
+      expect(d.multiply(2).getDescription(true)).toEqual('4 Days');
+    });
+  });
+
+  describe('#getDescription()', () => {
+    it('gives back the correct description', () => {
+      let durationStr: string;
+
+      durationStr = 'P1D';
+      expect(new Duration(durationStr).getDescription()).toEqual('day');
+
+      durationStr = 'P1DT2H';
+      expect(new Duration(durationStr).getDescription()).toEqual('1 day, 2 hours');
+
+      durationStr = 'P3Y';
+      expect(new Duration(durationStr).getDescription()).toEqual('3 years');
+
+      durationStr = 'P2W';
+      expect(new Duration(durationStr).getDescription()).toEqual('2 weeks');
+
+      durationStr = 'PT5H';
+      expect(new Duration(durationStr).getDescription()).toEqual('5 hours');
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getDescription()).toEqual('3 days, 15 hours');
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getDescription(true)).toEqual('3 Days, 15 Hours');
+    });
+  });
+
+  describe('#getSingleSpan()', () => {
+    it('gives back the correct span', () => {
+      let durationStr: string;
+
+      durationStr = 'P1D';
+      expect(new Duration(durationStr).getSingleSpan()).toEqual('day');
+
+      durationStr = 'P3Y';
+      expect(new Duration(durationStr).getSingleSpan()).toEqual('year');
+
+      durationStr = 'P2W';
+      expect(new Duration(durationStr).getSingleSpan()).toEqual('week');
+
+      durationStr = 'PT5H';
+      expect(new Duration(durationStr).getSingleSpan()).toEqual('hour');
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getSingleSpan()).toBeUndefined();
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getSingleSpan()).toBeUndefined();
+    });
+  });
+
+  describe('#getSingleSpanValue()', () => {
+    it('gives back the correct span value', () => {
+      let durationStr: string;
+
+      durationStr = 'P1D';
+      expect(new Duration(durationStr).getSingleSpanValue()).toEqual(1);
+
+      durationStr = 'P3Y';
+      expect(new Duration(durationStr).getSingleSpanValue()).toEqual(3);
+
+      durationStr = 'P2W';
+      expect(new Duration(durationStr).getSingleSpanValue()).toEqual(2);
+
+      durationStr = 'PT5H';
+      expect(new Duration(durationStr).getSingleSpanValue()).toEqual(5);
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getSingleSpanValue()).toBeUndefined();
+
+      durationStr = 'P3DT15H';
+      expect(new Duration(durationStr).getSingleSpanValue()).toBeUndefined();
+    });
+  });
+
+  describe('#limitToDays', () => {
+    it('works', () => {
+      expect(new Duration('P6D').limitToDays().toString()).toEqual('P6D');
+
+      expect(new Duration('P1M').limitToDays().toString()).toEqual('P30D');
+
+      expect(new Duration('P1Y').limitToDays().toString()).toEqual('P365D');
+
+      expect(new Duration('P1Y2M').limitToDays().toString()).toEqual('P425D');
+    });
+  });
+});
diff --git a/web-console/src/utils/duration/duration.ts b/web-console/src/utils/duration/duration.ts
new file mode 100755
index 00000000000..b93dab175ad
--- /dev/null
+++ b/web-console/src/utils/duration/duration.ts
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { second, shifters } from '../date-floor-shift-ceil/date-floor-shift-ceil';
+import { capitalizeFirst, pluralIfNeeded } from '../general';
+
+const SPANS_WITH_WEEK = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
+const SPANS_WITHOUT_WEEK = ['year', 'month', 'day', 'hour', 'minute', 'second'];
+const SPANS_WITHOUT_WEEK_OR_MONTH = ['year', 'day', 'hour', 'minute', 'second'];
+const SPANS_UP_TO_DAY = ['day', 'hour', 'minute', 'second'];
+
+export interface DurationValue {
+  year?: number;
+  month?: number;
+  week?: number;
+  day?: number;
+  hour?: number;
+  minute?: number;
+  second?: number;
+
+  // Indexable
+  [span: string]: number | undefined;
+}
+
+const periodWeekRegExp = /^P(\d+)W$/;
+const periodRegExp = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
+//                     P   (year ) (month   ) (day     )    T(hour    ) (minute  ) (second  )
+
+function getSpansFromString(durationStr: string): DurationValue {
+  const spans: DurationValue = {};
+  let matches: RegExpExecArray | null;
+  if ((matches = periodWeekRegExp.exec(durationStr))) {
+    spans.week = Number(matches[1]);
+    if (!spans.week) throw new Error('Duration can not have empty weeks');
+  } else if ((matches = periodRegExp.exec(durationStr))) {
+    const nums = matches.map(Number);
+    for (let i = 0; i < SPANS_WITHOUT_WEEK.length; i++) {
+      const span = SPANS_WITHOUT_WEEK[i];
+      const value = nums[i + 1];
+      if (value) spans[span] = value;
+    }
+  } else {
+    throw new Error("Can not parse duration '" + durationStr + "'");
+  }
+  return spans;
+}
+
+function getSpansFromStartEnd(start: Date, end: Date, timezone: string): DurationValue {
+  start = second.floor(start, timezone);
+  end = second.floor(end, timezone);
+  if (end <= start) throw new Error('start must come before end');
+
+  const spans: DurationValue = {};
+  let iterator: Date = start;
+  for (let i = 0; i < SPANS_WITHOUT_WEEK.length; i++) {
+    const span = SPANS_WITHOUT_WEEK[i];
+    let spanCount = 0;
+
+    // Shortcut
+    const length = end.valueOf() - iterator.valueOf();
+    const canonicalLength: number = shifters[span].canonicalLength;
+    if (length < canonicalLength / 4) continue;
+    const numberToFit = Math.min(0, Math.floor(length / canonicalLength) - 1);
+    let iteratorMove: Date;
+    if (numberToFit > 0) {
+      // try to skip by numberToFit
+      iteratorMove = shifters[span].shift(iterator, timezone, numberToFit);
+      if (iteratorMove <= end) {
+        spanCount += numberToFit;
+        iterator = iteratorMove;
+      }
+    }
+
+    while (true) {
+      iteratorMove = shifters[span].shift(iterator, timezone, 1);
+      if (iteratorMove <= end) {
+        iterator = iteratorMove;
+        spanCount++;
+      } else {
+        break;
+      }
+    }
+
+    if (spanCount) {
+      spans[span] = spanCount;
+    }
+  }
+  return spans;
+}
+
+function removeZeros(spans: DurationValue): DurationValue {
+  const newSpans: DurationValue = {};
+  for (let i = 0; i < SPANS_WITH_WEEK.length; i++) {
+    const span = SPANS_WITH_WEEK[i];
+    if (Number(spans[span]) > 0) {
+      newSpans[span] = spans[span];
+    }
+  }
+  return newSpans;
+}
+
+function fitIntoSpans(length: number, spansToCheck: string[]): Record<string, number> {
+  const spans: Record<string, number> = {};
+
+  let lengthLeft = length;
+  for (let i = 0; i < spansToCheck.length; i++) {
+    const span = spansToCheck[i];
+    const spanLength = shifters[span].canonicalLength;
+    const count = Math.floor(lengthLeft / spanLength);
+
+    if (count) {
+      lengthLeft -= spanLength * count;
+      spans[span] = count;
+    }
+  }
+
+  return spans;
+}
+
+/**
+ * Represents an ISO duration like P1DT3H
+ */
+export class Duration {
+  public readonly singleSpan?: string;
+  public readonly spans: Readonly<DurationValue>;
+
+  static parse(durationStr: string): Duration {
+    if (typeof durationStr !== 'string') throw new TypeError('Duration JS must be a string');
+    return new Duration(getSpansFromString(durationStr));
+  }
+
+  static fromCanonicalLength(length: number, skipMonths = false): Duration {
+    if (length <= 0) throw new Error('length must be positive');
+    let spans = fitIntoSpans(length, skipMonths ? SPANS_WITHOUT_WEEK_OR_MONTH : SPANS_WITHOUT_WEEK);
+
+    if (
+      length % shifters['week'].canonicalLength === 0 && // Weeks fits
+      (Object.keys(spans).length > 1 || // We already have a more complex span
+        spans['day']) // or... we only have days and it might be simpler to express as weeks
+    ) {
+      spans = { week: length / shifters['week'].canonicalLength };
+    }
+
+    return new Duration(spans);
+  }
+
+  static fromCanonicalLengthUpToDays(length: number): Duration {
+    if (length <= 0) throw new Error('length must be positive');
+    return new Duration(fitIntoSpans(length, SPANS_UP_TO_DAY));
+  }
+
+  static fromRange(start: Date, end: Date, timezone: string): Duration {
+    return new Duration(getSpansFromStartEnd(start, end, timezone));
+  }
+
+  constructor(spans: DurationValue | string) {
+    const effectiveSpans: DurationValue =
+      typeof spans === 'string' ? getSpansFromString(spans) : removeZeros(spans);
+
+    const usedSpans = Object.keys(effectiveSpans);
+    if (!usedSpans.length) throw new Error('Duration can not be empty');
+    if (usedSpans.length === 1) {
+      this.singleSpan = usedSpans[0];
+    } else if (effectiveSpans.week) {
+      throw new Error("Can not mix 'week' and other spans");
+    }
+    this.spans = effectiveSpans;
+  }
+
+  public toString() {
+    const strArr: string[] = ['P'];
+    const spans = this.spans;
+    if (spans.week) {
+      strArr.push(String(spans.week), 'W');
+    } else {
+      let addedT = false;
+      for (let i = 0; i < SPANS_WITHOUT_WEEK.length; i++) {
+        const span = SPANS_WITHOUT_WEEK[i];
+        const value = spans[span];
+        if (!value) continue;
+        if (!addedT && i >= 3) {
+          strArr.push('T');
+          addedT = true;
+        }
+        strArr.push(String(value), span[0].toUpperCase());
+      }
+    }
+    return strArr.join('');
+  }
+
+  public add(duration: Duration): Duration {
+    return Duration.fromCanonicalLength(this.getCanonicalLength() + duration.getCanonicalLength());
+  }
+
+  public subtract(duration: Duration): Duration {
+    const newCanonicalDuration = this.getCanonicalLength() - duration.getCanonicalLength();
+    if (newCanonicalDuration < 0) throw new Error('A duration can not be negative.');
+    return Duration.fromCanonicalLength(newCanonicalDuration);
+  }
+
+  public multiply(multiplier: number): Duration {
+    if (multiplier <= 0) throw new Error('Multiplier must be positive non-zero');
+    if (multiplier === 1) return this;
+    const newCanonicalDuration = this.getCanonicalLength() * multiplier;
+    return Duration.fromCanonicalLength(newCanonicalDuration);
+  }
+
+  public valueOf() {
+    return this.spans;
+  }
+
+  public equals(other: Duration | undefined): boolean {
+    return other instanceof Duration && this.toString() === other.toString();
+  }
+
+  public isSimple(): boolean {
+    const { singleSpan } = this;
+    if (!singleSpan) return false;
+    return this.spans[singleSpan] === 1;
+  }
+
+  public isFloorable(): boolean {
+    const { singleSpan } = this;
+    if (!singleSpan) return false;
+    const span = Number(this.spans[singleSpan]);
+    if (span === 1) return true;
+    const { siblings } = shifters[singleSpan];
+    if (!siblings) return false;
+    return siblings % span === 0;
+  }
+
+  /**
+   * Floors the date according to this duration.
+   * @param date The date to floor
+   * @param timezone The timezone within which to floor
+   */
+  public floor(date: Date, timezone: string): Date {
+    const { singleSpan } = this;
+    if (!singleSpan) throw new Error('Can not floor on a complex duration');
+    const span = this.spans[singleSpan]!;
+    const mover = shifters[singleSpan];
+    let dt = mover.floor(date, timezone);
+    if (span !== 1) {
+      if (!mover.siblings) {
+        throw new Error(`Can not floor on a ${singleSpan} duration that is not 1`);
+      }
+      if (mover.siblings % span !== 0) {
+        throw new Error(
+          `Can not floor on a ${singleSpan} duration that does not divide into ${mover.siblings}`,
+        );
+      }
+      dt = mover.round(dt, span, timezone);
+    }
+    return dt;
+  }
+
+  /**
+   * Moves the given date by 'step' times of the duration
+   * Negative step value will move back in time.
+   * @param date The date to move
+   * @param timezone The timezone within which to make the move
+   * @param step The number of times to step by the duration
+   */
+  public shift(date: Date, timezone: string, step = 1): Date {
+    const spans = this.spans;
+    for (const span of SPANS_WITH_WEEK) {
+      const value = spans[span];
+      if (value) date = shifters[span].shift(date, timezone, step * value);
+    }
+    return date;
+  }
+
+  public ceil(date: Date, timezone: string): Date {
+    const floored = this.floor(date, timezone);
+    if (floored.valueOf() === date.valueOf()) return date; // Just like ceil(3) is 3 and not 4
+    return this.shift(floored, timezone, 1);
+  }
+
+  public round(date: Date, timezone: string): Date {
+    const floorDate = this.floor(date, timezone);
+    const ceilDate = this.ceil(date, timezone);
+    const distanceToFloor = Math.abs(date.valueOf() - floorDate.valueOf());
+    const distanceToCeil = Math.abs(date.valueOf() - ceilDate.valueOf());
+    return distanceToFloor < distanceToCeil ? floorDate : ceilDate;
+  }
+
+  /**
+   * Materializes all the values of this duration form start to end
+   * @param start The date to start on
+   * @param end The date to start on
+   * @param timezone The timezone within which to materialize
+   * @param step The number of times to step by the duration
+   */
+  public materialize(start: Date, end: Date, timezone: string, step = 1): Date[] {
+    const values: Date[] = [];
+    let iter = this.floor(start, timezone);
+    while (iter <= end) {
+      values.push(iter);
+      iter = this.shift(iter, timezone, step);
+    }
+    return values;
+  }
+
+  /**
+   * Checks to see if date is aligned to this duration within the timezone (floors to itself)
+   * @param date The date to check
+   * @param timezone The timezone within which to make the check
+   */
+  public isAligned(date: Date, timezone: string): boolean {
+    return this.floor(date, timezone).valueOf() === date.valueOf();
+  }
+
+  /**
+   * Check to see if this duration can be divided by the given duration
+   * @param smaller The smaller duration to divide by
+   */
+  public dividesBy(smaller: Duration): boolean {
+    const myCanonicalLength = this.getCanonicalLength();
+    const smallerCanonicalLength = smaller.getCanonicalLength();
+    return (
+      myCanonicalLength % smallerCanonicalLength === 0 &&
+      this.isFloorable() &&
+      smaller.isFloorable()
+    );
+  }
+
+  public getCanonicalLength(): number {
+    const spans = this.spans;
+    let length = 0;
+    for (const span of SPANS_WITH_WEEK) {
+      const value = spans[span];
+      if (value) length += value * shifters[span].canonicalLength;
+    }
+    return length;
+  }
+
+  public getDescription(capitalize?: boolean): string {
+    const spans = this.spans;
+    const description: string[] = [];
+    for (const span of SPANS_WITH_WEEK) {
+      const value = spans[span];
+      const spanTitle = capitalize ? capitalizeFirst(span) : span;
+      if (value) {
+        if (value === 1 && this.singleSpan) {
+          description.push(spanTitle);
+        } else {
+          description.push(pluralIfNeeded(value, spanTitle));
+        }
+      }
+    }
+    return description.join(', ');
+  }
+
+  public getSingleSpan(): string | undefined {
+    return this.singleSpan;
+  }
+
+  public getSingleSpanValue(): number | undefined {
+    if (!this.singleSpan) return;
+    return this.spans[this.singleSpan];
+  }
+
+  public limitToDays(): Duration {
+    return Duration.fromCanonicalLengthUpToDays(this.getCanonicalLength());
+  }
+}
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index edea5ad0a52..096f0dfe063 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -19,10 +19,12 @@
 export * from './base64-url';
 export * from './column-metadata';
 export * from './date';
+export * from './date-floor-shift-ceil/date-floor-shift-ceil';
 export * from './download';
 export * from './download-query-detail-archive';
 export * from './druid-lookup';
 export * from './druid-query';
+export * from './duration/duration';
 export * from './formatter';
 export * from './general';
 export * from './local-storage-backed-visibility';
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index a2bd3692346..4477540af66 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -53,7 +53,13 @@ import type {
   QueryWithContext,
   Rule,
 } from '../../druid-models';
-import { formatCompactionInfo, RuleUtil, zeroCompactionStatus } from '../../druid-models';
+import {
+  END_OF_TIME_DATE,
+  formatCompactionInfo,
+  RuleUtil,
+  START_OF_TIME_DATE,
+  zeroCompactionStatus,
+} from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import { STANDARD_TABLE_PAGE_SIZE, STANDARD_TABLE_PAGE_SIZE_OPTIONS } from '../../react-table';
 import { Api, AppToaster } from '../../singletons';
@@ -361,7 +367,7 @@ export class DatasourcesView extends React.PureComponent<
           `COUNT(*) FILTER (WHERE is_active = 1 AND "start" LIKE '%T00:00:00.000Z' AND "end" LIKE '%T00:00:00.000Z') AS day_aligned_segments`,
           `COUNT(*) FILTER (WHERE is_active = 1 AND "start" LIKE '%-01T00:00:00.000Z' AND "end" LIKE '%-01T00:00:00.000Z') AS month_aligned_segments`,
           `COUNT(*) FILTER (WHERE is_active = 1 AND "start" LIKE '%-01-01T00:00:00.000Z' AND "end" LIKE '%-01-01T00:00:00.000Z') AS year_aligned_segments`,
-          `COUNT(*) FILTER (WHERE is_active = 1 AND "start" = '-146136543-09-08T08:23:32.096Z' AND "end" = '146140482-04-24T15:36:27.903Z') AS all_granularity_segments`,
+          `COUNT(*) FILTER (WHERE is_active = 1 AND "start" = '${START_OF_TIME_DATE}' AND "end" = '${END_OF_TIME_DATE}') AS all_granularity_segments`,
         ],
         visibleColumns.shown('Total rows') &&
           `SUM("num_rows") FILTER (WHERE is_active = 1) AS total_rows`,
diff --git a/web-console/src/views/explore-view/modules/multi-axis-chart-module.tsx b/web-console/src/views/explore-view/modules/multi-axis-chart-module.tsx
index e5f2fb47f9a..f5a25d2ea0a 100644
--- a/web-console/src/views/explore-view/modules/multi-axis-chart-module.tsx
+++ b/web-console/src/views/explore-view/modules/multi-axis-chart-module.tsx
@@ -26,6 +26,7 @@ import { useEffect, useMemo, useRef } from 'react';
 import { Loader } from '../../../components';
 import { useQueryManager } from '../../../hooks';
 import {
+  Duration,
   formatInteger,
   formatNumber,
   prettyFormatIsoDateTick,
@@ -35,7 +36,7 @@ import { Issue } from '../components';
 import { highlightStore } from '../highlight-store/highlight-store';
 import type { ExpressionMeta } from '../models';
 import { ModuleRepository } from '../module-repository/module-repository';
-import { DATE_FORMAT, getAutoGranularity, snapToGranularity } from '../utils';
+import { DATE_FORMAT, getAutoGranularity } from '../utils';
 
 import './record-table-module.scss';
 
@@ -223,12 +224,9 @@ ModuleRepository.registerModule<MultiAxisChartParameterValues>({
         // this is only used for the label and the data saved in the highlight
         // the positioning is done with the true coordinates until the user
         // releases the mouse button (in the `brushend` event)
-        const { start, end } = snapToGranularity(
-          params.areas[0].coordRange[0],
-          params.areas[0].coordRange[1],
-          timeGranularity,
-          undefined, // context.timezone,
-        );
+        const duration = new Duration(timeGranularity);
+        const start = duration.round(params.areas[0].coordRange[0], 'Etc/UTC');
+        const end = duration.round(params.areas[0].coordRange[1], 'Etc/UTC');
 
         const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, params.areas[0].coordRange[0]);
         const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, params.areas[0].coordRange[1]);
diff --git a/web-console/src/views/explore-view/modules/time-chart-module.tsx b/web-console/src/views/explore-view/modules/time-chart-module.tsx
index 1c19963b1ed..661e3ec4575 100644
--- a/web-console/src/views/explore-view/modules/time-chart-module.tsx
+++ b/web-console/src/views/explore-view/modules/time-chart-module.tsx
@@ -25,6 +25,7 @@ import { useEffect, useMemo, useRef } from 'react';
 import { Loader } from '../../../components';
 import { useQueryManager } from '../../../hooks';
 import {
+  Duration,
   formatInteger,
   formatNumber,
   prettyFormatIsoDateTick,
@@ -34,7 +35,7 @@ import { Issue } from '../components';
 import { highlightStore } from '../highlight-store/highlight-store';
 import type { ExpressionMeta } from '../models';
 import { ModuleRepository } from '../module-repository/module-repository';
-import { DATE_FORMAT, getAutoGranularity, snapToGranularity } from '../utils';
+import { DATE_FORMAT, getAutoGranularity } from '../utils';
 
 import './record-table-module.scss';
 
@@ -295,13 +296,13 @@ ModuleRepository.registerModule<TimeChartParameterValues>({
         // this is only used for the label and the data saved in the highlight
         // the positioning is done with the true coordinates until the user
         // releases the mouse button (in the `brushend` event)
-        const { start, end } = snappyHighlight
-          ? snapToGranularity(
-              params.areas[0].coordRange[0],
-              params.areas[0].coordRange[1],
-              timeGranularity,
-            )
-          : { start: params.areas[0].coordRange[0], end: params.areas[0].coordRange[1] };
+        let start = params.areas[0].coordRange[0];
+        let end = params.areas[0].coordRange[1];
+        if (snappyHighlight) {
+          const duration = new Duration(timeGranularity);
+          start = duration.round(start, 'Etc/UTC');
+          end = duration.round(end, 'Etc/UTC');
+        }
 
         const x0 = myChart.convertToPixel({ xAxisIndex: 0 }, params.areas[0].coordRange[0]);
         const x1 = myChart.convertToPixel({ xAxisIndex: 0 }, params.areas[0].coordRange[1]);
diff --git a/web-console/src/views/explore-view/utils/duration.ts b/web-console/src/views/explore-view/utils/duration.ts
deleted file mode 100644
index 621aa0a57a0..00000000000
--- a/web-console/src/views/explore-view/utils/duration.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { sum } from 'd3-array';
-
-import { filterMap, pluralIfNeeded } from '../../../utils';
-
-const SPANS = ['year', 'month', 'day', 'hour', 'minute', 'second'];
-
-export function formatDuration(duration: string, preferCompact = false): string {
-  // Regular expressions to match ISO 8601 duration parts
-  const regex = /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/;
-  const matches = regex.exec(duration);
-  if (!matches) return duration;
-
-  // Extract the relevant parts
-  const counts = SPANS.map((_, i) => parseInt(matches[i + 1] || '0', 10));
-
-  // Construct the human-readable format based on the parsed values
-  let parts: string[];
-  if (preferCompact && sum(counts) === 1) {
-    parts = filterMap(SPANS, (span, i) => (counts[i] ? span : undefined));
-  } else {
-    parts = filterMap(SPANS, (span, i) =>
-      counts[i] ? pluralIfNeeded(counts[i], span) : undefined,
-    );
-  }
-
-  // Join the parts with commas, and return the result
-  return parts.length > 0 ? parts.join(', ') : '0 seconds';
-}
diff --git a/web-console/src/views/explore-view/utils/filter-pattern-helpers.ts b/web-console/src/views/explore-view/utils/filter-pattern-helpers.ts
index 6b2245dbd38..57a654fa10e 100644
--- a/web-console/src/views/explore-view/utils/filter-pattern-helpers.ts
+++ b/web-console/src/views/explore-view/utils/filter-pattern-helpers.ts
@@ -18,8 +18,9 @@
 
 import type { Column, FilterPattern } from '@druid-toolkit/query';
 
+import { Duration } from '../../../utils';
+
 import { DATE_FORMAT } from './date-format';
-import { formatDuration } from './duration';
 
 const TIME_RELATIVE_TYPES: Record<string, string> = {
   'maxDataTime/': 'latest',
@@ -82,10 +83,9 @@ export function formatPatternWithoutNegation(pattern: FilterPattern): string {
 
     case 'timeRelative': {
       const type = TIME_RELATIVE_TYPES[`${pattern.anchor}/${pattern.alignType || ''}`];
-      return `${pattern.column} in ${type ? `${type} ` : ''}${formatDuration(
+      return `${pattern.column} in ${type ? `${type} ` : ''}${new Duration(
         pattern.rangeDuration,
-        true,
-      )}`;
+      ).getDescription()}`;
     }
 
     case 'numberRange':
diff --git a/web-console/src/views/explore-view/utils/get-auto-granularity.ts b/web-console/src/views/explore-view/utils/get-auto-granularity.ts
index f8928f3b86b..21cc2743820 100644
--- a/web-console/src/views/explore-view/utils/get-auto-granularity.ts
+++ b/web-console/src/views/explore-view/utils/get-auto-granularity.ts
@@ -18,7 +18,8 @@
 
 import type { SqlExpression } from '@druid-toolkit/query';
 import { fitFilterPattern, SqlMulti } from '@druid-toolkit/query';
-import { day, Duration, hour } from 'chronoshift';
+
+import { day, Duration, hour } from '../../../utils';
 
 function getCanonicalDuration(
   expression: SqlExpression,
@@ -32,7 +33,7 @@ function getCanonicalDuration(
       return pattern.end.valueOf() - pattern.start.valueOf();
 
     case 'timeRelative':
-      return Duration.fromJS(pattern.rangeDuration).getCanonicalLength();
+      return new Duration(pattern.rangeDuration).getCanonicalLength();
 
     case 'custom':
       if (pattern.expression instanceof SqlMulti) {
@@ -76,7 +77,5 @@ export function getAutoGranularity(where: SqlExpression, timeColumnName: string)
     return 'PT1M';
   }
 
-  console.debug('Unable to determine granularity from where clause', where.toString());
-
   return DEFAULT_GRANULARITY;
 }
diff --git a/web-console/src/views/explore-view/utils/index.ts b/web-console/src/views/explore-view/utils/index.ts
index 8469c83b927..57c72e2c0e7 100644
--- a/web-console/src/views/explore-view/utils/index.ts
+++ b/web-console/src/views/explore-view/utils/index.ts
@@ -17,7 +17,6 @@
  */
 
 export * from './date-format';
-export * from './duration';
 export * from './filter-pattern-helpers';
 export * from './general';
 export * from './get-auto-granularity';
@@ -25,6 +24,5 @@ export * from './known-aggregations';
 export * from './max-time-for-table';
 export * from './misc';
 export * from './query-log';
-export * from './snap-to-granularity';
 export * from './table-query';
 export * from './time-manipulation';
diff --git a/web-console/src/views/explore-view/utils/snap-to-granularity.ts b/web-console/src/views/explore-view/utils/snap-to-granularity.ts
deleted file mode 100644
index 15b0cce0351..00000000000
--- a/web-console/src/views/explore-view/utils/snap-to-granularity.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Duration, Timezone } from 'chronoshift';
-
-/**
- * Will try to snap start and end to the closest available dates, given the granularity.
- *
- * @param start the start date
- * @param end the end date
- * @param granularity the granularity
- * @param timezone the timezone
- * @returns an object with the start and end dates snapped to the given granularity
- */
-export function snapToGranularity(
-  start: Date,
-  end: Date,
-  granularity: string,
-  timezone?: string,
-): { start: Date; end: Date } {
-  const tz = Timezone.fromJS(timezone || 'Etc/UTC');
-  const duration = Duration.fromJS(granularity);
-
-  // get closest to start
-  const flooredStart = duration.floor(start, tz);
-  const ceiledStart = duration.shift(flooredStart, tz, 1);
-  const distanceToFlooredStart = Math.abs(start.valueOf() - flooredStart.valueOf());
-  const distanceToCeiledStart = Math.abs(start.valueOf() - ceiledStart.valueOf());
-  const closestStart = distanceToFlooredStart < distanceToCeiledStart ? flooredStart : ceiledStart;
-
-  // get closest to end
-  const flooredEnd = duration.floor(end, tz);
-  const ceiledEnd = duration.shift(flooredEnd, tz, 1);
-  const distanceToFlooredEnd = Math.abs(end.valueOf() - flooredEnd.valueOf());
-  const distanceToCeiledEnd = Math.abs(end.valueOf() - ceiledEnd.valueOf());
-  const closestEnd = distanceToFlooredEnd < distanceToCeiledEnd ? flooredEnd : ceiledEnd;
-
-  return {
-    start: closestStart,
-    end: closestEnd,
-  };
-}
diff --git a/web-console/src/views/explore-view/utils/table-query.ts b/web-console/src/views/explore-view/utils/table-query.ts
index bc5b6484c9a..633f02ff15b 100644
--- a/web-console/src/views/explore-view/utils/table-query.ts
+++ b/web-console/src/views/explore-view/utils/table-query.ts
@@ -31,11 +31,10 @@ import {
 } from '@druid-toolkit/query';
 
 import type { ColumnHint } from '../../../utils';
-import { forceSignInNumberFormatter, formatNumber, formatPercent } from '../../../utils';
+import { Duration, forceSignInNumberFormatter, formatNumber, formatPercent } from '../../../utils';
 import type { ExpressionMeta } from '../models';
 import { Measure } from '../models';
 
-import { formatDuration } from './duration';
 import { addTableScope } from './general';
 import { KNOWN_AGGREGATIONS } from './known-aggregations';
 import type { Compare } from './time-manipulation';
@@ -103,7 +102,7 @@ function makeBaseColumnHints(
       ).getUnderlyingExpression(),
     };
     if (isTimestamp(splitColumn)) {
-      hint.displayName = `${splitColumn.name} (by ${formatDuration(timeBucket, true)})`;
+      hint.displayName = `${splitColumn.name} (by ${new Duration(timeBucket).getDescription()})`;
     }
     columnHints.set(splitColumn.name, hint);
   }
@@ -291,7 +290,7 @@ function makeCompareAggregatorsAndAddHints(
   prevMeasure: SqlExpression,
   columnHints: Map<string, ColumnHint>,
 ): SqlExpression[] {
-  const group = `Previous ${formatDuration(compare, true)}`;
+  const group = `Previous ${new Duration(compare).getDescription()}`;
   const diff = mainMeasure.subtract(prevMeasure);
 
   const ret: SqlExpression[] = [];
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 18a44e66180..ded3ac5474c 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -44,6 +44,7 @@ import { AsyncActionDialog } from '../../dialogs';
 import { SegmentTableActionDialog } from '../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
 import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
 import type { QueryWithContext } from '../../druid-models';
+import { computeSegmentTimeSpan } from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import {
   booleanCustomTableFilter,
@@ -84,7 +85,7 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[
     'Start',
     'End',
     'Version',
-    { text: 'Time span', label: '𝑓(sys.segments)' },
+    'Time span',
     'Shard type',
     'Shard spec',
     'Partition',
@@ -106,7 +107,7 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[
     'Start',
     'End',
     'Version',
-    { text: 'Time span', label: '𝑓(sys.segments)' },
+    'Time span',
     'Shard type',
     'Shard spec',
     'Partition',
@@ -177,7 +178,6 @@ interface SegmentQueryResultRow {
   interval: string;
   segment_id: string;
   version: string;
-  time_span: string;
   shard_spec: string;
   partition_num: number;
   size: number;
@@ -225,16 +225,6 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
       `"start"`,
       `"end"`,
       `"version"`,
-      visibleColumns.shown('Time span') &&
-        `CASE
-  WHEN "start" = '-146136543-09-08T08:23:32.096Z' AND "end" = '146140482-04-24T15:36:27.903Z' THEN 'All'
-  WHEN "start" LIKE '%-01-01T00:00:00.000Z' AND "end" LIKE '%-01-01T00:00:00.000Z' THEN 'Year'
-  WHEN "start" LIKE '%-01T00:00:00.000Z' AND "end" LIKE '%-01T00:00:00.000Z' THEN 'Month'
-  WHEN "start" LIKE '%T00:00:00.000Z' AND "end" LIKE '%T00:00:00.000Z' THEN 'Day'
-  WHEN "start" LIKE '%:00:00.000Z' AND "end" LIKE '%:00:00.000Z' THEN 'Hour'
-  WHEN "start" LIKE '%:00.000Z' AND "end" LIKE '%:00.000Z' THEN 'Minute'
-  ELSE 'Sub minute'
-END AS "time_span"`,
       visibleColumns.shown('Shard type', 'Shard spec') && `"shard_spec"`,
       visibleColumns.shown('Partition') && `"partition_num"`,
       visibleColumns.shown('Size') && `"size"`,
@@ -253,30 +243,6 @@ END AS "time_span"`,
     return `WITH s AS (SELECT\n${columns.join(',\n')}\nFROM sys.segments)`;
   }
 
-  static computeTimeSpan(start: string, end: string): string {
-    if (start.endsWith('-01-01T00:00:00.000Z') && end.endsWith('-01-01T00:00:00.000Z')) {
-      return 'Year';
-    }
-
-    if (start.endsWith('-01T00:00:00.000Z') && end.endsWith('-01T00:00:00.000Z')) {
-      return 'Month';
-    }
-
-    if (start.endsWith('T00:00:00.000Z') && end.endsWith('T00:00:00.000Z')) {
-      return 'Day';
-    }
-
-    if (start.endsWith(':00:00.000Z') && end.endsWith(':00:00.000Z')) {
-      return 'Hour';
-    }
-
-    if (start.endsWith(':00.000Z') && end.endsWith(':00.000Z')) {
-      return 'Minute';
-    }
-
-    return 'Sub minute';
-  }
-
   private readonly segmentsQueryManager: QueryManager<SegmentsQuery, SegmentQueryResultRow[]>;
 
   constructor(props: SegmentsViewProps) {
@@ -287,7 +253,7 @@ END AS "time_span"`,
       segmentsState: QueryState.INIT,
       visibleColumns: new LocalStorageBackedVisibility(
         LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
-        ['Time span', 'Is published', 'Is overshadowed'],
+        ['Is published', 'Is overshadowed'],
       ),
       groupByInterval: false,
       showSegmentTimeline: false,
@@ -412,7 +378,6 @@ END AS "time_span"`,
                 end,
                 interval: segment.interval,
                 version: segment.version,
-                time_span: SegmentsView.computeTimeSpan(start, end),
                 shard_spec: deepGet(segment, 'shardSpec'),
                 partition_num: deepGet(segment, 'shardSpec.partitionNum') || 0,
                 size: segment.size,
@@ -615,7 +580,7 @@ END AS "time_span"`,
             show: visibleColumns.shown('Start'),
             accessor: 'start',
             headerClassName: 'enable-comparisons',
-            width: 160,
+            width: 180,
             sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
@@ -626,7 +591,7 @@ END AS "time_span"`,
             show: visibleColumns.shown('End'),
             accessor: 'end',
             headerClassName: 'enable-comparisons',
-            width: 160,
+            width: 180,
             sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
@@ -636,20 +601,21 @@ END AS "time_span"`,
             Header: 'Version',
             show: visibleColumns.shown('Version'),
             accessor: 'version',
-            width: 160,
+            width: 180,
             sortable: hasSql,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
-            Cell: this.renderFilterableCell('version'),
+            Cell: this.renderFilterableCell('version', true),
           },
           {
             Header: 'Time span',
             show: visibleColumns.shown('Time span'),
-            accessor: 'time_span',
+            id: 'time_span',
+            className: 'padded',
+            accessor: ({ start, end }) => computeSegmentTimeSpan(start, end),
             width: 100,
-            sortable: hasSql,
-            filterable: allowGeneralFilter,
-            Cell: this.renderFilterableCell('time_span'),
+            sortable: false,
+            filterable: false,
           },
           {
             Header: 'Shard type',


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 10/42: better timezone menu

Posted by vo...@apache.org.
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 8ff3f44ec39e13fca3d2cec73a9682f00616987f
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 30 11:55:22 2024 -0700

    better timezone menu
---
 web-console/jest.common.config.js                  |   2 +
 .../__snapshots__/bar-unit.spec.tsx.snap           |  13 -
 .../__snapshots__/segment-timeline.spec.tsx.snap   | 293 ++++++++++-----------
 web-console/src/utils/general.tsx                  |   8 +
 .../__snapshots__/datasources-view.spec.tsx.snap   |  15 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |  50 ++--
 .../views/workbench-view/run-panel/run-panel.tsx   |  71 +----
 .../timezone-menu-items.spec.tsx}                  |  30 ++-
 .../timezone-menu-items/timezone-menu-items.tsx    | 135 ++++++++++
 9 files changed, 355 insertions(+), 262 deletions(-)

diff --git a/web-console/jest.common.config.js b/web-console/jest.common.config.js
index 89e3dab5852..1ea8f55ad18 100644
--- a/web-console/jest.common.config.js
+++ b/web-console/jest.common.config.js
@@ -18,6 +18,8 @@
 
 const { createJsWithTsPreset } = require('ts-jest');
 
+process.env.TZ = 'UTC';
+
 module.exports = {
   testEnvironment: 'jsdom',
   transformIgnorePatterns: ['/node_modules/(?!(d3-.+)/)'],
diff --git a/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap b/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
deleted file mode 100644
index 7d98145d4b5..00000000000
--- a/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`BarUnit matches snapshot 1`] = `
-<svg>
-  <rect
-    class="bar-unit"
-    height="10"
-    width="10"
-    x="10"
-    y="10"
-  />
-</svg>
-`;
diff --git a/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap b/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
index ae30df246c2..b82bfbc10f2 100644
--- a/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
+++ b/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
@@ -2,190 +2,179 @@
 
 exports[`SegmentTimeline matches snapshot 1`] = `
 <div
-  class="segment-timeline"
+  class="splitter-layout segment-timeline splitter-layout-horizontal"
 >
-  <div>
+  <div
+    class="layout-pane layout-pane-primary"
+  >
     <div
-      class="loader"
-    >
-      <div
-        class="loader-logo"
-      >
-        <svg
-          viewBox="0 0 100 100"
-        >
-          <path
-            class="one"
-            d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
-          c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
-          c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
-          />
-          <path
-            class="two"
-            d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
-            c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
-            C63.5,58,59.9,59.5,55.7,59.5z"
-          />
-          <path
-            class="three"
-            d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
-          />
-          <path
-            class="four"
-            d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
-            C46.4,69.2,45.8,69.8,45.1,69.8z"
-          />
-        </svg>
-      </div>
-    </div>
+      class="chart-container"
+    />
   </div>
   <div
-    class="side-control"
+    class="layout-splitter"
+    role="separator"
+  />
+  <div
+    class="layout-pane"
+    style="width: 220px;"
   >
     <div
-      class="bp5-form-group"
+      class="side-control"
     >
-      <label
-        class="bp5-label"
-      >
-        Show
-         
-        <span
-          class="bp5-text-muted"
-        />
-      </label>
       <div
-        class="bp5-form-content"
+        class="bp5-form-group"
       >
+        <label
+          class="bp5-label"
+        >
+          Show
+           
+          <span
+            class="bp5-text-muted"
+          />
+        </label>
         <div
-          class="bp5-segmented-control bp5-fill"
-          role="radiogroup"
+          class="bp5-form-content"
         >
-          <button
-            aria-checked="true"
-            class="bp5-button"
-            role="radio"
-            tabindex="0"
-            type="button"
+          <div
+            class="bp5-segmented-control bp5-fill"
+            role="radiogroup"
           >
-            <span
-              class="bp5-button-text"
+            <button
+              aria-checked="true"
+              class="bp5-button"
+              role="radio"
+              tabindex="0"
+              type="button"
             >
-              Total size
-            </span>
-          </button>
-          <button
-            aria-checked="false"
-            class="bp5-button bp5-minimal"
-            role="radio"
-            tabindex="-1"
-            type="button"
-          >
-            <span
-              class="bp5-button-text"
+              <span
+                class="bp5-button-text"
+              >
+                Size
+              </span>
+            </button>
+            <button
+              aria-checked="false"
+              class="bp5-button bp5-minimal"
+              role="radio"
+              tabindex="-1"
+              type="button"
             >
-              Segment count
-            </span>
-          </button>
+              <span
+                class="bp5-button-text"
+              >
+                Rows
+              </span>
+            </button>
+            <button
+              aria-checked="false"
+              class="bp5-button bp5-minimal"
+              role="radio"
+              tabindex="-1"
+              type="button"
+            >
+              <span
+                class="bp5-button-text"
+              >
+                Count
+              </span>
+            </button>
+          </div>
         </div>
       </div>
-    </div>
-    <div
-      class="bp5-form-group"
-    >
-      <label
-        class="bp5-label"
-      >
-        Interval
-         
-        <span
-          class="bp5-text-muted"
-        />
-      </label>
       <div
-        class="bp5-form-content"
+        class="bp5-form-group"
       >
+        <label
+          class="bp5-label"
+        >
+          Interval
+           
+          <span
+            class="bp5-text-muted"
+          />
+        </label>
         <div
-          aria-expanded="false"
-          aria-haspopup="menu"
-          class="bp5-control-group bp5-date-range-input bp5-popover-target bp5-fill"
+          class="bp5-form-content"
         >
           <div
-            class="bp5-input-group bp5-fill"
+            aria-expanded="false"
+            aria-haspopup="menu"
+            class="bp5-control-group bp5-date-range-input bp5-popover-target bp5-fill"
           >
-            <input
-              aria-disabled="false"
-              autocomplete="off"
-              class="bp5-input"
-              placeholder="Start date"
-              type="text"
-              value="2021-03-09"
-            />
-          </div>
-          <div
-            class="bp5-input-group bp5-fill"
-          >
-            <input
-              aria-disabled="false"
-              autocomplete="off"
-              class="bp5-input"
-              placeholder="End date"
-              type="text"
-              value="2021-06-09"
-            />
+            <div
+              class="bp5-input-group bp5-fill"
+            >
+              <input
+                aria-disabled="false"
+                autocomplete="off"
+                class="bp5-input"
+                placeholder="Start date"
+                type="text"
+                value="2024-07-31"
+              />
+            </div>
+            <div
+              class="bp5-input-group bp5-fill"
+            >
+              <input
+                aria-disabled="false"
+                autocomplete="off"
+                class="bp5-input"
+                placeholder="End date"
+                type="text"
+                value="2024-10-31"
+              />
+            </div>
           </div>
         </div>
       </div>
-    </div>
-    <div
-      class="bp5-form-group"
-    >
-      <label
-        class="bp5-label"
-      >
-        Datasource
-         
-        <span
-          class="bp5-text-muted"
-        />
-      </label>
       <div
-        class="bp5-form-content"
+        class="bp5-form-group"
       >
+        <label
+          class="bp5-label"
+        >
+          Datasource
+           
+          <span
+            class="bp5-text-muted"
+          />
+        </label>
         <div
-          aria-controls="listbox-1"
-          aria-expanded="false"
-          aria-haspopup="listbox"
-          class="bp5-popover-target bp5-fill"
-          role="combobox"
+          class="bp5-form-content"
         >
-          <button
-            class="bp5-button bp5-fill"
-            type="button"
+          <div
+            aria-controls="listbox-0"
+            aria-expanded="false"
+            aria-haspopup="listbox"
+            class="bp5-popover-target bp5-fill"
+            role="combobox"
           >
-            <span
-              class="bp5-button-text"
+            <button
+              class="bp5-button bp5-fill"
+              type="button"
             >
-              Show all
-            </span>
-            <span
-              aria-hidden="true"
-              class="bp5-icon bp5-icon-caret-down"
-            >
-              <svg
-                data-icon="caret-down"
-                height="16"
-                role="img"
-                viewBox="0 0 16 16"
-                width="16"
+              <span
+                aria-hidden="true"
+                class="bp5-icon bp5-icon-caret-down"
               >
-                <path
-                  d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
-                  fill-rule="evenodd"
-                />
-              </svg>
-            </span>
-          </button>
+                <svg
+                  data-icon="caret-down"
+                  height="16"
+                  role="img"
+                  viewBox="0 0 16 16"
+                  width="16"
+                >
+                  <path
+                    d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+                    fill-rule="evenodd"
+                  />
+                </svg>
+              </span>
+            </button>
+          </div>
         </div>
       </div>
     </div>
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index b742013b2e8..0bc959b6045 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -373,6 +373,14 @@ export function formatDurationHybrid(ms: NumberLike): string {
   }
 }
 
+export function timezoneOffsetInMinutesToString(offsetInMinutes: number, padHour: boolean): string {
+  const sign = offsetInMinutes < 0 ? '-' : '+';
+  const absOffset = Math.abs(offsetInMinutes);
+  const h = Math.floor(absOffset / 60);
+  const m = absOffset % 60;
+  return `${sign}${padHour ? pad2(h) : h}:${pad2(m)}`;
+}
+
 function pluralize(word: string): string {
   // Ignoring irregular plurals.
   if (/(s|x|z|ch|sh)$/.test(word)) {
diff --git a/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap b/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
index 6da41f6eb76..c4a548ad886 100644
--- a/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
+++ b/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
@@ -55,7 +55,7 @@ exports[`DatasourcesView matches snapshot 1`] = `
       onChange={[Function]}
     />
     <Blueprint5.Switch
-      checked={false}
+      checked={true}
       disabled={false}
       label="Show segment timeline"
       onChange={[Function]}
@@ -109,6 +109,7 @@ exports[`DatasourcesView matches snapshot 1`] = `
     />
   </Memo(ViewControlBar)>
   <SplitterLayout
+    className="timeline-datasources-splitter"
     percentage={true}
     primaryIndex={1}
     primaryMinSize={20}
@@ -116,6 +117,18 @@ exports[`DatasourcesView matches snapshot 1`] = `
     secondaryMinSize={10}
     vertical={true}
   >
+    <SegmentTimeline
+      capabilities={
+        Capabilities {
+          "coordinator": true,
+          "maxTaskSlots": undefined,
+          "multiStageQueryDart": true,
+          "multiStageQueryTask": true,
+          "overlord": true,
+          "queryType": "nativeAndSql",
+        }
+      }
+    />
     <ReactTable
       AggregatedComponent={[Function]}
       ExpanderComponent={[Function]}
diff --git a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index e177003b7ee..7fa71826cb1 100755
--- a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -54,10 +54,7 @@ exports[`SegmentsView matches snapshot 1`] = `
           "Start",
           "End",
           "Version",
-          {
-            "label": "𝑓(sys.segments)",
-            "text": "Time span",
-          },
+          "Time span",
           "Shard type",
           "Shard spec",
           "Partition",
@@ -77,7 +74,6 @@ exports[`SegmentsView matches snapshot 1`] = `
       onClose={[Function]}
       tableColumnsHidden={
         [
-          "Time span",
           "Is published",
           "Is overshadowed",
         ]
@@ -85,6 +81,7 @@ exports[`SegmentsView matches snapshot 1`] = `
     />
   </Memo(ViewControlBar)>
   <SplitterLayout
+    className="timeline-segments-splitter"
     percentage={true}
     primaryIndex={1}
     primaryMinSize={20}
@@ -184,7 +181,7 @@ exports[`SegmentsView matches snapshot 1`] = `
             "headerClassName": "enable-comparisons",
             "show": true,
             "sortable": true,
-            "width": 160,
+            "width": 180,
           },
           {
             "Cell": [Function],
@@ -195,7 +192,7 @@ exports[`SegmentsView matches snapshot 1`] = `
             "headerClassName": "enable-comparisons",
             "show": true,
             "sortable": true,
-            "width": 160,
+            "width": 180,
           },
           {
             "Cell": [Function],
@@ -205,15 +202,16 @@ exports[`SegmentsView matches snapshot 1`] = `
             "filterable": true,
             "show": true,
             "sortable": true,
-            "width": 160,
+            "width": 180,
           },
           {
-            "Cell": [Function],
             "Header": "Time span",
-            "accessor": "time_span",
-            "filterable": true,
-            "show": false,
-            "sortable": true,
+            "accessor": [Function],
+            "className": "padded",
+            "filterable": false,
+            "id": "time_span",
+            "show": true,
+            "sortable": false,
             "width": 100,
           },
           {
@@ -374,18 +372,11 @@ exports[`SegmentsView matches snapshot 1`] = `
       defaultFilterMethod={[Function]}
       defaultFiltered={[]}
       defaultPage={0}
-      defaultPageSize={50}
+      defaultPageSize={20}
       defaultResized={[]}
       defaultSortDesc={false}
       defaultSortMethod={[Function]}
-      defaultSorted={
-        [
-          {
-            "desc": true,
-            "id": "start",
-          },
-        ]
-      }
+      defaultSorted={[]}
       expanderDefaults={
         {
           "filterable": false,
@@ -421,7 +412,7 @@ exports[`SegmentsView matches snapshot 1`] = `
       getTrProps={[Function]}
       groupedByPivotKey="_groupedByPivot"
       indexKey="_index"
-      loading={false}
+      loading={true}
       loadingText="Loading..."
       manual={true}
       multiSort={true}
@@ -431,8 +422,13 @@ exports[`SegmentsView matches snapshot 1`] = `
       ofText=""
       onFetchData={[Function]}
       onFilteredChange={[Function]}
+      onPageChange={[Function]}
+      onPageSizeChange={[Function]}
+      onSortedChange={[Function]}
       originalKey="_original"
+      page={0}
       pageJumpText="jump to page"
+      pageSize={50}
       pageSizeOptions={
         [
           50,
@@ -457,6 +453,14 @@ exports[`SegmentsView matches snapshot 1`] = `
       showPaginationBottom={true}
       showPaginationTop={false}
       sortable={true}
+      sorted={
+        [
+          {
+            "desc": true,
+            "id": "start",
+          },
+        ]
+      }
       style={{}}
       subRowsKey="_subRows"
     />
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index d586959357a..2747179d6d4 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -51,31 +51,10 @@ import { deepGet, pluralIfNeeded, removeUndefinedValues, tickIcon } from '../../
 import type { MaxTasksButtonProps } from '../max-tasks-button/max-tasks-button';
 import { MaxTasksButton } from '../max-tasks-button/max-tasks-button';
 import { QueryParametersDialog } from '../query-parameters-dialog/query-parameters-dialog';
+import { TimezoneMenuItems } from '../timezone-menu-items/timezone-menu-items';
 
 import './run-panel.scss';
 
-const NAMED_TIMEZONES: string[] = [
-  'America/Juneau', // -9.0
-  'America/Los_Angeles', // -8.0
-  'America/Yellowknife', // -7.0
-  'America/Phoenix', // -7.0
-  'America/Denver', // -7.0
-  'America/Mexico_City', // -6.0
-  'America/Chicago', // -6.0
-  'America/New_York', // -5.0
-  'America/Argentina/Buenos_Aires', // -4.0
-  'Etc/UTC', // +0.0
-  'Europe/London', // +0.0
-  'Europe/Paris', // +1.0
-  'Asia/Jerusalem', // +2.0
-  'Asia/Shanghai', // +8.0
-  'Asia/Hong_Kong', // +8.0
-  'Asia/Seoul', // +9.0
-  'Asia/Tokyo', // +9.0
-  'Pacific/Guam', // +10.0
-  'Australia/Sydney', // +11.0
-];
-
 const ARRAY_INGEST_MODE_LABEL: Record<ArrayIngestMode, string> = {
   array: 'Array',
   mvd: 'MVD',
@@ -314,25 +293,6 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
     onQueryChange(query.changeQueryContext(removeUndefinedValues(queryContext)));
   }
 
-  function offsetOptions(): JSX.Element[] {
-    const items: JSX.Element[] = [];
-
-    for (let i = -12; i <= 14; i++) {
-      const offset = `${i < 0 ? '-' : '+'}${String(Math.abs(i)).padStart(2, '0')}:00`;
-      items.push(
-        <MenuItem
-          key={offset}
-          icon={tickIcon(offset === sqlTimeZone)}
-          text={offset}
-          shouldDismissPopover={false}
-          onClick={() => changeQueryContext({ ...queryContext, sqlTimeZone: offset })}
-        />,
-      );
-    }
-
-    return items;
-  }
-
   const overloadWarning =
     query.unlimited &&
     (queryEngine === 'sql-native' ||
@@ -424,32 +384,13 @@ export const RunPanel = React.memo(function RunPanel(props: RunPanelProps) {
                     text="Timezone"
                     label={sqlTimeZone ?? defaultQueryContext.sqlTimeZone}
                   >
-                    <MenuDivider title="Timezone type" />
-                    <MenuItem
-                      icon={tickIcon(!sqlTimeZone)}
-                      text="Default"
-                      label={defaultQueryContext.sqlTimeZone}
-                      shouldDismissPopover={false}
-                      onClick={() =>
-                        changeQueryContext({ ...queryContext, sqlTimeZone: undefined })
+                    <TimezoneMenuItems
+                      sqlTimeZone={sqlTimeZone}
+                      setSqlTimeZone={sqlTimeZone =>
+                        changeQueryContext({ ...queryContext, sqlTimeZone })
                       }
+                      defaultSqlTimeZone={defaultQueryContext.sqlTimeZone}
                     />
-                    <MenuItem icon={tickIcon(String(sqlTimeZone).includes('/'))} text="Named">
-                      {NAMED_TIMEZONES.map(namedTimezone => (
-                        <MenuItem
-                          key={namedTimezone}
-                          icon={tickIcon(namedTimezone === sqlTimeZone)}
-                          text={namedTimezone}
-                          shouldDismissPopover={false}
-                          onClick={() =>
-                            changeQueryContext({ ...queryContext, sqlTimeZone: namedTimezone })
-                          }
-                        />
-                      ))}
-                    </MenuItem>
-                    <MenuItem icon={tickIcon(String(sqlTimeZone).includes(':'))} text="Offset">
-                      {offsetOptions()}
-                    </MenuItem>
                     <MenuItem
                       icon={IconNames.BLANK}
                       text="Custom"
diff --git a/web-console/jest.common.config.js b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx
similarity index 58%
copy from web-console/jest.common.config.js
copy to web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx
index 89e3dab5852..5a1a2b9ce51 100644
--- a/web-console/jest.common.config.js
+++ b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx
@@ -16,12 +16,26 @@
  * limitations under the License.
  */
 
-const { createJsWithTsPreset } = require('ts-jest');
+import { shallow } from '../../../utils/shallow-renderer';
 
-module.exports = {
-  testEnvironment: 'jsdom',
-  transformIgnorePatterns: ['/node_modules/(?!(d3-.+)/)'],
-  ...createJsWithTsPreset({
-    tsconfig: './tsconfig.test.json',
-  }),
-};
+import { TimezoneMenuItems } from './timezone-menu-items';
+
+jest.useFakeTimers('modern').setSystemTime(Date.parse('2024-06-08T12:34:56Z'));
+
+describe('TimezoneMenuItems', () => {
+  it('ensure UTC', () => {
+    expect(new Date().getTimezoneOffset()).toBe(0);
+  });
+
+  it('matches snapshot', () => {
+    const comp = shallow(
+      <TimezoneMenuItems
+        sqlTimeZone="Blah"
+        setSqlTimeZone={() => {}}
+        defaultSqlTimeZone="Etc/UTC"
+      />,
+    );
+
+    expect(comp).toMatchSnapshot();
+  });
+});
diff --git a/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx
new file mode 100644
index 00000000000..402620563ef
--- /dev/null
+++ b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Icon, MenuDivider, MenuItem } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import { fromDate, getLocalTimeZone } from '@internationalized/date';
+
+import { minute, tickIcon, timezoneOffsetInMinutesToString } from '../../../utils';
+
+const n = new Date();
+const NAMED_TIMEZONES: { timezone: string; offsetInMinutes: number }[] = [
+  'America/Juneau', // -9:00
+  'America/Los_Angeles', // -8:00
+  'America/Yellowknife', // -7:00
+  'America/Phoenix', // -7:00
+  'America/Denver', // -7:00
+  'America/Mexico_City', // -6:00
+  'America/Chicago', // -6:00
+  'America/New_York', // -5:00
+  'America/Argentina/Buenos_Aires', // -4:00
+  'Etc/UTC', // +0:00
+  'Europe/London', // +0:00
+  'Europe/Paris', // +1:00
+  'Asia/Jerusalem', // +2:00
+  'Asia/Kolkata', // +5:30
+  'Asia/Shanghai', // +8:00
+  'Asia/Hong_Kong', // +8:00
+  'Asia/Seoul', // +9:00
+  'Asia/Tokyo', // +9:00
+  'Pacific/Guam', // +10:00
+  'Australia/Sydney', // +11:00
+].map(timezone => ({
+  timezone,
+  offsetInMinutes: fromDate(n, timezone).offset / minute.canonicalLength,
+}));
+const LOCAL_TIMEZONE = getLocalTimeZone();
+const LOCAL_OFFSET_IN_MINUTES = -n.getTimezoneOffset();
+
+// Make sure the browsers timezone is in the list
+if (!NAMED_TIMEZONES.find(({ timezone }) => timezone === LOCAL_TIMEZONE)) {
+  NAMED_TIMEZONES.push({ timezone: LOCAL_TIMEZONE, offsetInMinutes: LOCAL_OFFSET_IN_MINUTES });
+}
+NAMED_TIMEZONES.sort((a, b) => a.offsetInMinutes - b.offsetInMinutes);
+
+const OFFSETS_IN_MINUTES: number[] = [];
+for (let offsetInMinutes = -12 * 60; offsetInMinutes <= 14 * 60; offsetInMinutes += 60) {
+  OFFSETS_IN_MINUTES.push(offsetInMinutes);
+}
+
+// Make sure the browser offset is in the list
+if (!OFFSETS_IN_MINUTES.includes(LOCAL_OFFSET_IN_MINUTES)) {
+  OFFSETS_IN_MINUTES.push(LOCAL_OFFSET_IN_MINUTES);
+  OFFSETS_IN_MINUTES.sort((a, b) => a - b);
+}
+
+export interface TimezoneMenuItemsProps {
+  sqlTimeZone: string | undefined;
+  setSqlTimeZone(sqlTimeZone: string | undefined): void;
+  defaultSqlTimeZone: string | undefined;
+}
+
+export const TimezoneMenuItems = function TimezoneMenuItems(props: TimezoneMenuItemsProps) {
+  const { sqlTimeZone, setSqlTimeZone, defaultSqlTimeZone } = props;
+
+  const localOffset = new Date().getTimezoneOffset();
+
+  return [
+    <MenuDivider key="type" title="Timezone type" />,
+    <MenuItem
+      key="default"
+      icon={tickIcon(!sqlTimeZone)}
+      text="Default"
+      label={defaultSqlTimeZone}
+      shouldDismissPopover={false}
+      onClick={() => setSqlTimeZone(undefined)}
+    />,
+    <MenuItem key="named" icon={tickIcon(String(sqlTimeZone).includes('/'))} text="Named">
+      {NAMED_TIMEZONES.map(({ timezone, offsetInMinutes }) => (
+        <MenuItem
+          key={timezone}
+          icon={tickIcon(timezone === sqlTimeZone)}
+          text={
+            timezone === LOCAL_TIMEZONE ? (
+              <>
+                {timezone} <Icon icon={IconNames.STAR} data-tooltip="Browser timezone" />
+              </>
+            ) : (
+              timezone
+            )
+          }
+          label={`UTC${timezoneOffsetInMinutesToString(offsetInMinutes, false)}`}
+          shouldDismissPopover={false}
+          onClick={() => setSqlTimeZone(timezone)}
+        />
+      ))}
+    </MenuItem>,
+    <MenuItem key="offset" icon={tickIcon(String(sqlTimeZone).includes(':'))} text="Offset">
+      {OFFSETS_IN_MINUTES.map(offsetInMinutes => {
+        const offset = timezoneOffsetInMinutesToString(offsetInMinutes, true);
+        return (
+          <MenuItem
+            key={offset}
+            icon={tickIcon(offset === sqlTimeZone)}
+            text={
+              localOffset === offsetInMinutes ? (
+                <>
+                  {offset} <Icon icon={IconNames.STAR} data-tooltip="Browser offset" />
+                </>
+              ) : (
+                offset
+              )
+            }
+            shouldDismissPopover={false}
+            onClick={() => setSqlTimeZone(offset)}
+          />
+        );
+      })}
+    </MenuItem>,
+  ];
+};


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 28/42: init datasource

Posted by vo...@apache.org.
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 2036585da68bbc74a48cea8c81e6e81c2c8b221b
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Tue Nov 5 15:33:50 2024 -0800

    init datasource
---
 .../segment-timeline/segment-bar-chart-render.tsx  |  2 +-
 .../segment-timeline/segment-timeline.spec.tsx     |  4 +++-
 .../segment-timeline/segment-timeline.tsx          | 27 +++++++++++++++-------
 .../views/datasources-view/datasources-view.tsx    | 17 ++++++++++----
 .../src/views/segments-view/segments-view.tsx      |  4 +++-
 5 files changed, 38 insertions(+), 16 deletions(-)

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 fc5cfd9ccd6..054e06a1725 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
@@ -365,10 +365,10 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       y: rect.y + CHART_MARGIN.top - 10,
       text: (
         <>
-          <div>Datasource: {hoveredIntervalBar.datasource}</div>
           <div>{`${prettyFormatIsoDate(hoveredIntervalBar.start)}/${
             hoveredIntervalBar.originalTimeSpan
           }${hoveredIntervalBar.realtime ? ' (realtime)' : ''}`}</div>
+          <div>Datasource: {hoveredIntervalBar.datasource}</div>
           <div>{`Size: ${
             hoveredIntervalBar.realtime
               ? 'estimated for realtime'
diff --git a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
index f247f98794a..966e3113153 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
@@ -24,7 +24,9 @@ import { SegmentTimeline } from './segment-timeline';
 
 describe('SegmentTimeline', () => {
   it('matches snapshot', () => {
-    const segmentTimeline = <SegmentTimeline capabilities={Capabilities.FULL} />;
+    const segmentTimeline = (
+      <SegmentTimeline capabilities={Capabilities.FULL} initDatasource={undefined} />
+    );
     const { container } = render(segmentTimeline);
     expect(container.firstChild).toMatchSnapshot();
   });
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index dd79f07f0dd..75964a70205 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -55,6 +55,7 @@ import './segment-timeline.scss';
 
 interface SegmentTimelineProps {
   capabilities: Capabilities;
+  initDatasource: string | undefined;
 }
 
 const DEFAULT_SHOWN_DURATION = new Duration('P3M');
@@ -74,10 +75,10 @@ function getDateRange(shownDuration: Duration): NonNullDateRange {
 }
 
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
-  const { capabilities } = props;
+  const { capabilities, initDatasource } = props;
   const [stage, setStage] = useState<Stage | undefined>();
   const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
-  const [shownDatasource, setShownDatasource] = useState<string | undefined>();
+  const [shownDatasource, setShownDatasource] = useState<string | undefined>(initDatasource);
   const [dateRange, setDateRange] = useState<NonNullDateRange>(
     getDateRange(DEFAULT_SHOWN_DURATION),
   );
@@ -107,6 +108,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
         <ButtonGroup>
           <Button
             icon={IconNames.CARET_LEFT}
+            data-tooltip="Previous time period"
             small
             onClick={() => {
               const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
@@ -115,14 +117,19 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           />
           <Button
             icon={IconNames.ZOOM_OUT}
+            data-tooltip="Zoom out"
             small
-            onClick={() => {
+            onClick={e => {
               const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
-              setDateRange([d.shift(dateRange[0], TZ_UTC, -1), dateRange[1]]);
+              setDateRange([
+                d.shift(dateRange[0], TZ_UTC, -1),
+                e.altKey ? d.shift(dateRange[1], TZ_UTC, 1) : dateRange[1],
+              ]);
             }}
           />
           <Button
             icon={IconNames.CARET_RIGHT}
+            data-tooltip="Next time period"
             small
             onClick={() => {
               const d = Duration.fromRange(dateRange[0], dateRange[1], TZ_UTC);
@@ -162,9 +169,13 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
             <Button
               icon={IconNames.CALENDAR}
               small
-              data-tooltip={`Select a custom date range\nCurrent range: ${prettyFormatIsoDate(
-                dateRange[0],
-              )} - ${prettyFormatIsoDate(dateRange[1])}`}
+              data-tooltip={
+                showCustomDatePicker
+                  ? undefined
+                  : `Select a custom date range\nCurrent range: ${prettyFormatIsoDate(
+                      dateRange[0],
+                    )} - ${prettyFormatIsoDate(dateRange[1])}`
+              }
             />
           </Popover>
         </ButtonGroup>
@@ -184,7 +195,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
           }
         >
           <Button
-            text={`Stat: ${getIntervalStatTitle(activeSegmentStat)}`}
+            text={`Show: ${getIntervalStatTitle(activeSegmentStat)}`}
             small
             rightIcon={IconNames.CARET_DOWN}
           />
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index bfa2262dad1..de30832ce39 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -324,7 +324,7 @@ export interface DatasourcesViewState {
   useUnuseInterval: string;
   showForceCompact: boolean;
   visibleColumns: LocalStorageBackedVisibility;
-  showSegmentTimeline: boolean;
+  showSegmentTimeline?: { datasource?: string };
 
   datasourceTableActionDialogId?: string;
   actions: BasicAction[];
@@ -425,7 +425,7 @@ GROUP BY 1, 2`;
         LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
         ['Segment size', 'Segment granularity'],
       ),
-      showSegmentTimeline: true,
+      showSegmentTimeline: {}, // ToDo: remove
 
       actions: [],
     };
@@ -1709,9 +1709,11 @@ GROUP BY 1, 2`;
             disabled={!capabilities.hasCoordinatorAccess()}
           />
           <Switch
-            checked={showSegmentTimeline}
+            checked={Boolean(showSegmentTimeline)}
             label="Show segment timeline"
-            onChange={() => this.setState({ showSegmentTimeline: !showSegmentTimeline })}
+            onChange={() =>
+              this.setState({ showSegmentTimeline: showSegmentTimeline ? undefined : {} })
+            }
             disabled={!capabilities.hasSqlOrCoordinatorAccess()}
           />
           <TableColumnSelector
@@ -1737,7 +1739,12 @@ GROUP BY 1, 2`;
           primaryMinSize={20}
           secondaryMinSize={10}
         >
-          {showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
+          {showSegmentTimeline && (
+            <SegmentTimeline
+              capabilities={capabilities}
+              initDatasource={showSegmentTimeline.datasource}
+            />
+          )}
           {this.renderDatasourcesTable()}
         </SplitterLayout>
         {datasourceTableActionDialogId && (
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index af93f65b8fb..98e8fa44bb0 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -1053,7 +1053,9 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           primaryMinSize={20}
           secondaryMinSize={10}
         >
-          {showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
+          {showSegmentTimeline && (
+            <SegmentTimeline capabilities={capabilities} initDatasource={undefined} />
+          )}
           {this.renderSegmentsTable()}
         </SplitterLayout>
         {this.renderTerminateSegmentAction()}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 20/42: segment wheel fixes

Posted by vo...@apache.org.
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 883e0f9a7c276b8ec61d764bdd4691d12fc65517
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Fri Nov 1 15:03:59 2024 -0700

    segment wheel fixes
---
 web-console/src/druid-models/segment/segment.ts    | 10 +++
 .../src/views/segments-view/segments-view.tsx      | 80 ++++++++++++++--------
 2 files changed, 62 insertions(+), 28 deletions(-)

diff --git a/web-console/src/druid-models/segment/segment.ts b/web-console/src/druid-models/segment/segment.ts
index 3860d4f5127..1bff0711fcd 100644
--- a/web-console/src/druid-models/segment/segment.ts
+++ b/web-console/src/druid-models/segment/segment.ts
@@ -38,3 +38,13 @@ export function computeSegmentTimeSpan(start: string, end: string): string {
 
   return Duration.fromRange(startDate, endDate, 'Etc/UTC').getDescription(true);
 }
+
+export interface ShardSpec {
+  type: string;
+  partitionNum?: number;
+  partitions?: number;
+  dimensions?: string[];
+  partitionDimensions?: string[];
+  start?: string[];
+  end?: string[];
+}
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 90cb9311391..8f9aeb5abd4 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -44,7 +44,7 @@ import {
 import { AsyncActionDialog } from '../../dialogs';
 import { SegmentTableActionDialog } from '../../dialogs/segments-table-action-dialog/segment-table-action-dialog';
 import { ShowValueDialog } from '../../dialogs/show-value-dialog/show-value-dialog';
-import type { QueryWithContext } from '../../druid-models';
+import type { QueryWithContext, ShardSpec } from '../../druid-models';
 import { computeSegmentTimeSpan, getDatasourceColor } from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import {
@@ -101,7 +101,6 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[
     'Is published',
     'Is overshadowed',
   ],
-  'no-sql': ['Segment ID', 'Datasource', 'Start', 'End', 'Version', 'Partition', 'Size'],
   'no-proxy': [
     'Segment ID',
     'Datasource',
@@ -123,8 +122,28 @@ const TABLE_COLUMNS_BY_MODE: Record<CapabilitiesMode, TableColumnSelectorColumn[
     'Is published',
     'Is overshadowed',
   ],
+  'no-sql': [
+    'Segment ID',
+    'Datasource',
+    'Start',
+    'End',
+    'Version',
+    'Time span',
+    'Shard type',
+    'Shard spec',
+    'Partition',
+    'Size',
+  ],
 };
 
+function maybeParseJsonBig(str: string): any {
+  try {
+    return JSONBig.parse(str);
+  } catch {
+    return undefined;
+  }
+}
+
 function formatRangeDimensionValue(dimension: any, value: any): string {
   return `${C(String(dimension))}=${L(String(value))}`;
 }
@@ -179,7 +198,7 @@ interface SegmentQueryResultRow {
   interval: string;
   segment_id: string;
   version: string;
-  shard_spec: string;
+  shard_spec: ShardSpec;
   partition_num: number;
   size: number;
   num_rows: NumberLike;
@@ -335,7 +354,16 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           }
           const sqlQuery = queryParts.join('\n');
           setIntermediateQuery(sqlQuery);
-          return await queryDruidSql({ query: sqlQuery }, cancelToken);
+          let result = await queryDruidSql({ query: sqlQuery }, cancelToken);
+
+          if (visibleColumns.shown('Shard type', 'Shard spec')) {
+            result = result.map(sr => ({
+              ...sr,
+              shard_spec: maybeParseJsonBig(sr.shard_spec),
+            }));
+          }
+
+          return result as SegmentQueryResultRow[];
         } else if (capabilities.hasCoordinatorAccess()) {
           let datasourceList: string[] = (
             await Api.instance.get('/druid/coordinator/v1/metadata/datasources', { cancelToken })
@@ -350,7 +378,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
 
           if (sorted.length && sorted[0].id === 'datasource') {
             datasourceList.sort(
-              sorted[0].desc ? (d1, d2) => d1.localeCompare(d2) : (d1, d2) => d2.localeCompare(d1),
+              sorted[0].desc ? (d1, d2) => d2.localeCompare(d1) : (d1, d2) => d1.localeCompare(d2),
             );
           }
 
@@ -643,14 +671,9 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             id: 'shard_type',
             width: 100,
             sortable: false,
-            accessor: d => {
-              let v: any;
-              try {
-                v = JSONBig.parse(d.shard_spec);
-              } catch {}
-
-              if (typeof v?.type !== 'string') return '-';
-              return v?.type;
+            accessor: ({ shard_spec }) => {
+              if (typeof shard_spec?.type !== 'string') return '-';
+              return shard_spec?.type;
             },
             Cell: this.renderFilterableCell('shard_type', true),
           },
@@ -663,21 +686,18 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             sortable: false,
             filterable: false,
             Cell: ({ value }) => {
-              let v: any;
-              try {
-                v = JSONBig.parse(value);
-              } catch {}
-
               const onShowFullShardSpec = () => {
                 this.setState({
                   showFullShardSpec:
-                    v && typeof v === 'object' ? JSONBig.stringify(v, undefined, 2) : String(value),
+                    value && typeof value === 'object'
+                      ? JSONBig.stringify(value, undefined, 2)
+                      : String(value),
                 });
               };
 
-              switch (v?.type) {
+              switch (value?.type) {
                 case 'range': {
-                  const dimensions: string[] = v.dimensions || [];
+                  const dimensions: string[] = value.dimensions || [];
                   const formatEdge = (values: string[]) =>
                     dimensions.map((d, i) => formatRangeDimensionValue(d, values[i])).join('; ');
 
@@ -688,10 +708,10 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                       hoverIcon={IconNames.EYE_OPEN}
                     >
                       <span className="range-label">Start:</span>
-                      {Array.isArray(v.start) ? formatEdge(v.start) : '-∞'}
+                      {Array.isArray(value.start) ? formatEdge(value.start) : '-∞'}
                       <br />
                       <span className="range-label">End:</span>
-                      {Array.isArray(v.end) ? formatEdge(v.end) : '∞'}
+                      {Array.isArray(value.end) ? formatEdge(value.end) : '∞'}
                     </TableClickableCell>
                   );
                 }
@@ -704,17 +724,21 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                       hoverIcon={IconNames.EYE_OPEN}
                     >
                       <span className="range-label">Start:</span>
-                      {v.start != null ? formatRangeDimensionValue(v.dimension, v.start) : '-∞'}
+                      {value.start != null
+                        ? formatRangeDimensionValue(value.dimension, value.start)
+                        : '-∞'}
                       <br />
                       <span className="range-label">End:</span>
-                      {v.end != null ? formatRangeDimensionValue(v.dimension, v.end) : '∞'}
+                      {value.end != null
+                        ? formatRangeDimensionValue(value.dimension, value.end)
+                        : '∞'}
                     </TableClickableCell>
                   );
                 }
 
                 case 'hashed': {
-                  const { partitionDimensions } = v;
-                  if (!Array.isArray(partitionDimensions)) return value;
+                  const { partitionDimensions } = value;
+                  if (!Array.isArray(partitionDimensions)) return JSONBig.stringify(value);
                   return (
                     <TableClickableCell
                       onClick={onShowFullShardSpec}
@@ -747,7 +771,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
                       onClick={onShowFullShardSpec}
                       hoverIcon={IconNames.EYE_OPEN}
                     >
-                      {String(value)}
+                      {JSONBig.stringify(value)}
                     </TableClickableCell>
                   );
               }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 42/42: fix render spamming

Posted by vo...@apache.org.
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 5855a5bb7ff47858e97483fa054c834dae539482
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Mon Nov 18 15:28:05 2024 -0800

    fix render spamming
---
 .../segment-timeline/segment-bar-chart-render.tsx  | 58 +++++++++++++++++-----
 .../segment-timeline/segment-bar-chart.tsx         |  2 +-
 .../segment-timeline/segment-timeline.tsx          |  6 ++-
 .../table-filterable-cell.tsx                      |  2 +-
 web-console/src/console-application.tsx            | 15 ++++--
 .../src/react-table/react-table-utils.spec.ts      |  4 ++
 web-console/src/react-table/react-table-utils.ts   | 43 ++++++++++++----
 web-console/src/utils/general.tsx                  |  4 ++
 .../views/datasources-view/datasources-view.tsx    | 16 ++++--
 .../src/views/segments-view/segments-view.tsx      | 27 ++++++++--
 10 files changed, 142 insertions(+), 35 deletions(-)

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 df4842988c3..ea6310cc843 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
@@ -31,6 +31,7 @@ import type { Rule } from '../../druid-models';
 import { getDatasourceColor, RuleUtil } from '../../druid-models';
 import { useClock, useGlobalEventListener } from '../../hooks';
 import {
+  arraysEqualByElement,
   clamp,
   day,
   Duration,
@@ -215,6 +216,7 @@ export interface SegmentBarChartRenderProps {
   shownIntervalStat: IntervalStat;
   shownDatasource: string | undefined;
   changeShownDatasource(datasource: string | undefined): void;
+  getActionButton?(start: Date, end: Date, datasource?: string): ReactNode;
 }
 
 export const SegmentBarChartRender = function SegmentBarChartRender(
@@ -229,13 +231,42 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     datasourceRules,
     shownDatasource,
     changeShownDatasource,
+    getActionButton,
   } = props;
   const [mouseDownAt, setMouseDownAt] = useState<
     { time: Date; action: 'select' | 'shift' } | undefined
   >();
   const [selection, setSelection] = useState<SelectionRange | undefined>();
-  const [shiftOffset, setShiftOffset] = useState<number | undefined>();
+
+  function setSelectionIfNeeded(newSelection: SelectionRange) {
+    if (
+      selection &&
+      selection.start.valueOf() === newSelection.start.valueOf() &&
+      selection.end.valueOf() === newSelection.end.valueOf() &&
+      selection.done === newSelection.done
+    ) {
+      return;
+    }
+    setSelection(newSelection);
+  }
+
   const [bubbleInfo, setBubbleInfo] = useState<BubbleInfo | undefined>();
+
+  function setBubbleInfoIfNeeded(newBubbleInfo: BubbleInfo) {
+    if (
+      bubbleInfo &&
+      bubbleInfo.start.valueOf() === newBubbleInfo.start.valueOf() &&
+      bubbleInfo.end.valueOf() === newBubbleInfo.end.valueOf() &&
+      bubbleInfo.timeLabel === newBubbleInfo.timeLabel &&
+      arraysEqualByElement(bubbleInfo.intervalBars, newBubbleInfo.intervalBars)
+    ) {
+      return;
+    }
+    setBubbleInfo(newBubbleInfo);
+  }
+
+  const [shiftOffset, setShiftOffset] = useState<number | undefined>();
+
   const now = useClock(minute.canonicalLength);
   const svgRef = useRef<SVGSVGElement | null>(null);
 
@@ -377,9 +408,15 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
         setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
       } else {
         if (mouseDownAt.time < b) {
-          setSelection({ start: day.floor(mouseDownAt.time, TZ_UTC), end: day.ceil(b, TZ_UTC) });
+          setSelectionIfNeeded({
+            start: day.floor(mouseDownAt.time, TZ_UTC),
+            end: day.ceil(b, TZ_UTC),
+          });
         } else {
-          setSelection({ start: day.floor(b, TZ_UTC), end: day.ceil(mouseDownAt.time, TZ_UTC) });
+          setSelectionIfNeeded({
+            start: day.floor(b, TZ_UTC),
+            end: day.ceil(mouseDownAt.time, TZ_UTC),
+          });
         }
       }
     } else if (!selection) {
@@ -414,7 +451,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
             intervalBars = hoverBar ? [hoverBar] : bars;
           }
         }
-        setBubbleInfo({
+        setBubbleInfoIfNeeded({
           start,
           end,
           timeLabel: start.toISOString().slice(0, shifter === day ? 10 : 7),
@@ -497,8 +534,6 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
     };
   }
 
-  console.log('Bar chart render');
-
   let hoveredOpenOn: { x: number; y: number; title?: string; text: ReactNode } | undefined;
   if (svgRef.current) {
     const rect = svgRef.current.getBoundingClientRect();
@@ -589,12 +624,11 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
                     changeDateRange([selection.start, selection.end]);
                   }}
                 />
-                <Button
-                  text="Open in segments view"
-                  small
-                  rightIcon={IconNames.ARROW_TOP_RIGHT}
-                  onClick={() => null}
-                />
+                {getActionButton?.(
+                  selection.start,
+                  selection.end,
+                  datasources.length === 1 ? datasources[0] : undefined,
+                )}
               </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 20b1dec7045..9be6aab54fb 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -32,7 +32,7 @@ import { SegmentBarChartRender } from './segment-bar-chart-render';
 
 import './segment-bar-chart.scss';
 
-interface SegmentBarChartProps
+export interface SegmentBarChartProps
   extends Omit<SegmentBarChartRenderProps, 'intervalRows' | 'datasourceRules'> {
   capabilities: Capabilities;
 }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 72be20a3d83..18bda4f838b 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -52,6 +52,7 @@ import { Loader } from '../loader/loader';
 
 import type { IntervalStat } from './common';
 import { formatIsoDateOnly, getIntervalStatTitle, INTERVAL_STATS } from './common';
+import type { SegmentBarChartProps } from './segment-bar-chart';
 import { SegmentBarChart } from './segment-bar-chart';
 
 import './segment-timeline.scss';
@@ -80,13 +81,13 @@ function dateRangesEqual(dr1: NonNullDateRange, dr2: NonNullDateRange): boolean
   return dr1[0].valueOf() === dr2[0].valueOf() && dr2[1].valueOf() === dr2[1].valueOf();
 }
 
-interface SegmentTimelineProps {
+interface SegmentTimelineProps extends Pick<SegmentBarChartProps, 'getActionButton'> {
   capabilities: Capabilities;
   datasource: string | undefined;
 }
 
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
-  const { capabilities, datasource } = props;
+  const { capabilities, datasource, ...otherProps } = props;
   const [stage, setStage] = useState<Stage | undefined>();
   const [activeSegmentStat, setActiveSegmentStat] = useState<IntervalStat>('size');
   const [shownDatasource, setShownDatasource] = useState<string | undefined>(datasource);
@@ -351,6 +352,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               shownIntervalStat={activeSegmentStat}
               shownDatasource={shownDatasource}
               changeShownDatasource={setShownDatasource}
+              {...otherProps}
             />
           )}
           {initDatasourceDateRangeState.isLoading() && <Loader />}
diff --git a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
index 631fa224aaf..c03f0038cc7 100644
--- a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
+++ b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
@@ -27,7 +27,7 @@ import { Deferred } from '../deferred/deferred';
 
 import './table-filterable-cell.scss';
 
-const FILTER_MODES: FilterMode[] = ['=', '!=', '<=', '>='];
+const FILTER_MODES: FilterMode[] = ['=', '!=', '<', '>='];
 const FILTER_MODES_NO_COMPARISONS: FilterMode[] = ['=', '!='];
 
 export interface TableFilterableCellProps {
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 95336713dbb..acb7d9c81a3 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -182,12 +182,21 @@ export class ConsoleApplication extends React.PureComponent<
     changeTabWithFilter('datasources', [{ id: 'datasource', value: `=${datasource}` }]);
   };
 
-  private readonly goToSegments = (datasource: string, onlyUnavailable = false) => {
+  private readonly goToSegments = ({
+    start,
+    end,
+    datasource,
+  }: {
+    start?: Date;
+    end?: Date;
+    datasource?: string;
+  }) => {
     changeTabWithFilter(
       'segments',
       compact([
-        { id: 'datasource', value: `=${datasource}` },
-        onlyUnavailable ? { id: 'is_available', value: '=false' } : undefined,
+        start && { id: 'start', value: `>=${start.toISOString()}` },
+        end && { id: 'end', value: `<${end.toISOString()}` },
+        datasource && { id: 'datasource', value: `=${datasource}` },
       ]),
     );
   };
diff --git a/web-console/src/react-table/react-table-utils.spec.ts b/web-console/src/react-table/react-table-utils.spec.ts
index 0a1bbf3f9ad..1dd245b49f7 100644
--- a/web-console/src/react-table/react-table-utils.spec.ts
+++ b/web-console/src/react-table/react-table-utils.spec.ts
@@ -75,5 +75,9 @@ describe('react-table-utils', () => {
       { id: 'x', value: '~y' },
       { id: 'z', value: '=w&' },
     ]);
+    expect(stringToTableFilters('x<3&y<=3')).toEqual([
+      { id: 'x', value: '<3' },
+      { id: 'y', value: '<=3' },
+    ]);
   });
 });
diff --git a/web-console/src/react-table/react-table-utils.ts b/web-console/src/react-table/react-table-utils.ts
index dc3debdf32c..ec1272b2282 100644
--- a/web-console/src/react-table/react-table-utils.ts
+++ b/web-console/src/react-table/react-table-utils.ts
@@ -31,9 +31,9 @@ export const STANDARD_TABLE_PAGE_SIZE_OPTIONS = [50, 100, 200];
 export const SMALL_TABLE_PAGE_SIZE = 25;
 export const SMALL_TABLE_PAGE_SIZE_OPTIONS = [25, 50, 100];
 
-export type FilterMode = '~' | '=' | '!=' | '<=' | '>=';
+export type FilterMode = '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
 
-export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<=', '>='];
+export const FILTER_MODES: FilterMode[] = ['~', '=', '!=', '<', '<=', '>', '>='];
 export const FILTER_MODES_NO_COMPARISON: FilterMode[] = ['~', '=', '!='];
 
 export function filterModeToIcon(mode: FilterMode): IconName {
@@ -44,8 +44,12 @@ export function filterModeToIcon(mode: FilterMode): IconName {
       return IconNames.EQUALS;
     case '!=':
       return IconNames.NOT_EQUAL_TO;
+    case '<':
+      return IconNames.LESS_THAN;
     case '<=':
       return IconNames.LESS_THAN_OR_EQUAL_TO;
+    case '>':
+      return IconNames.GREATER_THAN;
     case '>=':
       return IconNames.GREATER_THAN_OR_EQUAL_TO;
     default:
@@ -61,8 +65,12 @@ export function filterModeToTitle(mode: FilterMode): string {
       return 'Equals';
     case '!=':
       return 'Not equals';
+    case '<':
+      return 'Less than';
     case '<=':
       return 'Less than or equal';
+    case '>':
+      return 'Greater than';
     case '>=':
       return 'Greater than or equal';
     default:
@@ -88,7 +96,7 @@ export function parseFilterModeAndNeedle(
   filter: Filter,
   loose = false,
 ): FilterModeAndNeedle | undefined {
-  const m = /^(~|=|!=|<=|>=)?(.*)$/.exec(String(filter.value));
+  const m = /^(~|=|!=|<(?!=)|<=|>(?!=)|>=)?(.*)$/.exec(String(filter.value));
   if (!m) return;
   if (!loose && !m[2]) return;
   const mode = (m[1] as FilterMode) || '~';
@@ -111,21 +119,28 @@ export function booleanCustomTableFilter(filter: Filter, value: unknown): boolea
   const modeAndNeedle = parseFilterModeAndNeedle(filter);
   if (!modeAndNeedle) return true;
   const { mode, needle } = modeAndNeedle;
+  const strValue = String(value);
   switch (mode) {
     case '=':
-      return String(value) === needle;
+      return strValue === needle;
 
     case '!=':
-      return String(value) !== needle;
+      return strValue !== needle;
+
+    case '<':
+      return strValue < needle;
 
     case '<=':
-      return String(value) <= needle;
+      return strValue <= needle;
+
+    case '>':
+      return strValue > needle;
 
     case '>=':
-      return String(value) >= needle;
+      return strValue >= needle;
 
     default:
-      return caseInsensitiveContains(String(value), needle);
+      return caseInsensitiveContains(strValue, needle);
   }
 }
 
@@ -141,9 +156,15 @@ export function sqlQueryCustomTableFilter(filter: Filter): SqlExpression | undef
     case '!=':
       return column.unequal(needle);
 
+    case '<':
+      return column.lessThan(needle);
+
     case '<=':
       return column.lessThanOrEqual(needle);
 
+    case '>':
+      return column.greaterThan(needle);
+
     case '>=':
       return column.greaterThanOrEqual(needle);
 
@@ -164,9 +185,11 @@ export function tableFiltersToString(tableFilters: Filter[]): string {
 
 export function stringToTableFilters(str: string | undefined): Filter[] {
   if (!str) return [];
-  // '~' | '=' | '!=' | '<=' | '>=';
+  // '~' | '=' | '!=' | '<' | '<=' | '>' | '>=';
   return filterMap(str.split('&'), clause => {
-    const m = /^(\w+)((?:~|=|!=|<=|>=).*)$/.exec(clause.replace(/%2[56]/g, decodeURIComponent));
+    const m = /^(\w+)((?:~|=|!=|<(?!=)|<=|>(?!=)|>=).*)$/.exec(
+      clause.replace(/%2[56]/g, decodeURIComponent),
+    );
     if (!m) return;
     return { id: m[1], value: m[2] };
   });
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 9e2e5e7f525..89a05467649 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -59,6 +59,10 @@ export function isSimpleArray(a: any): a is (string | number | boolean)[] {
   );
 }
 
+export function arraysEqualByElement<T>(xs: T[], ys: T[]): boolean {
+  return xs.length === ys.length && xs.every((x, i) => x === ys[i]);
+}
+
 export function wait(ms: number): Promise<void> {
   return new Promise(resolve => {
     setTimeout(resolve, ms);
diff --git a/web-console/src/views/datasources-view/datasources-view.tsx b/web-console/src/views/datasources-view/datasources-view.tsx
index 9891368e585..064f95e673e 100644
--- a/web-console/src/views/datasources-view/datasources-view.tsx
+++ b/web-console/src/views/datasources-view/datasources-view.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core';
+import { Button, FormGroup, InputGroup, Intent, MenuItem, Switch, Tag } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import { SqlQuery, T } from '@druid-toolkit/query';
 import { sum } from 'd3-array';
@@ -305,7 +305,7 @@ export interface DatasourcesViewProps {
   onFiltersChange(filters: Filter[]): void;
   goToQuery(queryWithContext: QueryWithContext): void;
   goToTasks(datasource?: string): void;
-  goToSegments(datasource: string, onlyUnavailable?: boolean): void;
+  goToSegments(options: { start?: Date; end?: Date; datasource?: string }): void;
   capabilities: Capabilities;
 }
 
@@ -1692,7 +1692,7 @@ GROUP BY 1, 2`;
   }
 
   render() {
-    const { capabilities } = this.props;
+    const { capabilities, goToSegments } = this.props;
     const {
       showUnused,
       visibleColumns,
@@ -1752,6 +1752,16 @@ GROUP BY 1, 2`;
             <SegmentTimeline
               capabilities={capabilities}
               datasource={showSegmentTimeline.datasource}
+              getActionButton={(start, end, datasource) => {
+                return (
+                  <Button
+                    text="Open in segments view"
+                    small
+                    rightIcon={IconNames.ARROW_TOP_RIGHT}
+                    onClick={() => goToSegments({ start, end, datasource })}
+                  />
+                );
+              }}
             />
           )}
           {this.renderDatasourcesTable()}
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 3233c587a4e..56659415a25 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -989,6 +989,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
   }
 
   render() {
+    const { capabilities, onFiltersChange } = this.props;
     const {
       segmentTableActionDialogId,
       datasourceTableActionDialogId,
@@ -996,9 +997,8 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
       visibleColumns,
       showSegmentTimeline,
       showFullShardSpec,
+      groupByInterval,
     } = this.state;
-    const { capabilities } = this.props;
-    const { groupByInterval } = this.state;
 
     return (
       <div className="segments-view app-view">
@@ -1060,7 +1060,28 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
           secondaryMinSize={10}
         >
           {showSegmentTimeline && (
-            <SegmentTimeline capabilities={capabilities} datasource={undefined} />
+            <SegmentTimeline
+              capabilities={capabilities}
+              datasource={undefined}
+              getActionButton={(start, end, datasource) => {
+                return (
+                  <Button
+                    text="Apply fitler to table"
+                    small
+                    rightIcon={IconNames.ARROW_DOWN}
+                    onClick={() =>
+                      onFiltersChange(
+                        compact([
+                          start && { id: 'start', value: `>=${start.toISOString()}` },
+                          end && { id: 'end', value: `<${end.toISOString()}` },
+                          datasource && { id: 'datasource', value: `=${datasource}` },
+                        ]),
+                      )
+                    }
+                  />
+                );
+              }}
+            />
           )}
           {this.renderSegmentsTable()}
         </SplitterLayout>


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org


(druid) 09/42: checkpoint

Posted by vo...@apache.org.
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 ded6fc2a0d66783213c8d9c9d84b36c562e78b03
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Wed Oct 30 09:24:13 2024 -0700

    checkpoint
---
 .../src/components/segment-timeline/common.ts      |   8 +-
 .../segment-timeline/segment-bar-chart-render.tsx  |  10 +-
 .../segment-timeline/segment-bar-chart.tsx         | 117 ++++++---------------
 .../segment-timeline/segment-timeline.tsx          |  13 ++-
 web-console/src/utils/date.spec.ts                 |   9 --
 web-console/src/utils/date.ts                      |  89 ----------------
 web-console/src/utils/duration/duration.ts         |  18 +++-
 .../explore-view/utils/get-auto-granularity.ts     |  80 ++++++--------
 .../time-menu-items/time-menu-items.tsx            |  28 ++---
 9 files changed, 98 insertions(+), 274 deletions(-)

diff --git a/web-console/src/components/segment-timeline/common.ts b/web-console/src/components/segment-timeline/common.ts
index a1907846f29..bfb6d84e5f3 100644
--- a/web-console/src/components/segment-timeline/common.ts
+++ b/web-console/src/components/segment-timeline/common.ts
@@ -30,18 +30,18 @@ export function aggregateSegmentStats(
   };
 }
 
-export interface SegmentRow extends Record<SegmentStat, number> {
+export interface IntervalRow extends Record<SegmentStat, number> {
   start: Date;
   end: Date;
   durationSeconds: number;
-  datasource?: string;
+  datasource: string;
 }
 
-export interface SegmentBar extends SegmentRow {
+export interface SegmentBar extends IntervalRow {
   offset: Record<SegmentStat, number>;
 }
 
-export function normalizedSegmentRow(sr: SegmentRow): SegmentRow {
+export function normalizedSegmentRow(sr: IntervalRow): IntervalRow {
   return {
     ...sr,
     count: sr.count / sr.durationSeconds,
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 f1e67c9665a..7864021622d 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
@@ -27,13 +27,13 @@ import { useMemo, useRef, useState } from 'react';
 import { useGlobalEventListener } from '../../hooks';
 import {
   capitalizeFirst,
-  ceilDay,
   clamp,
-  floorDay,
+  day,
   formatByteRate,
   formatBytes,
   formatInteger,
   formatNumber,
+  TZ_UTC,
 } from '../../utils';
 import type { Margin, Stage } from '../../utils/stage';
 
@@ -105,7 +105,7 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
 
   const colorizer = useMemo(() => {
     const s = scaleOrdinal().range(COLORS);
-    return (d: SegmentBar) => (d.datasource ? s(d.datasource) : COLORS[0]) as string;
+    return (d: SegmentBar) => s(d.datasource) as string;
   }, []);
 
   const maxStat = max(segmentBars, d => d[shownSegmentStat] + d.offset[shownSegmentStat]);
@@ -160,9 +160,9 @@ export const SegmentBarChartRender = function SegmentBarChartRender(
       setShiftOffset(mouseDownAt.time.valueOf() - b.valueOf());
     } else {
       if (mouseDownAt.time < b) {
-        setDragging([floorDay(mouseDownAt.time), ceilDay(b)]);
+        setDragging([day.floor(mouseDownAt.time, TZ_UTC), day.ceil(b, TZ_UTC)]);
       } else {
-        setDragging([floorDay(b), ceilDay(mouseDownAt.time)]);
+        setDragging([day.floor(b, TZ_UTC), day.ceil(mouseDownAt.time, TZ_UTC)]);
       }
     }
   });
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 4b24849b723..3ba9b16f082 100644
--- a/web-console/src/components/segment-timeline/segment-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/segment-bar-chart.tsx
@@ -24,69 +24,17 @@ import { useMemo } from 'react';
 import type { Capabilities } from '../../helpers';
 import { useQueryManager } from '../../hooks';
 import { Api } from '../../singletons';
-import {
-  ceilDay,
-  ceilHour,
-  ceilMonth,
-  ceilYear,
-  filterMap,
-  floorDay,
-  floorHour,
-  floorMonth,
-  floorYear,
-  groupBy,
-  queryDruidSql,
-} from '../../utils';
+import { Duration, filterMap, groupBy, queryDruidSql, TZ_UTC } from '../../utils';
 import type { Stage } from '../../utils/stage';
 import { Loader } from '../loader/loader';
 
-import type { SegmentBar, SegmentRow, SegmentStat } from './common';
+import type { IntervalRow, SegmentBar, 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 floorToDuration(date: Date, duration: TrimDuration): Date {
-  switch (duration) {
-    case 'PT1H':
-      return floorHour(date);
-
-    case 'P1D':
-      return floorDay(date);
-
-    case 'P1M':
-      return floorMonth(date);
-
-    case 'P1Y':
-      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[] {
+function stackIntervalRows(segmentRows: IntervalRow[]): SegmentBar[] {
   const sorted = segmentRows.sort((a, b) => {
     const diff = b.durationSeconds - a.durationSeconds;
     if (diff) return diff;
@@ -99,7 +47,7 @@ function stackSegmentRows(segmentRows: SegmentRow[]): SegmentBar[] {
     segmentRow = normalizedSegmentRow(segmentRow);
     const startMs = segmentRow.start.valueOf();
     const endMs = segmentRow.end.valueOf();
-    const segmentRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as SegmentRow[];
+    const segmentRowsBelow = intervalTree.search([startMs + 1, startMs + 2]) as IntervalRow[];
     intervalTree.insert([startMs, endMs], segmentRow);
     return {
       ...segmentRow,
@@ -113,7 +61,6 @@ interface SegmentBarChartProps {
   stage: Stage;
   dateRange: NonNullDateRange;
   changeDateRange(newDateRange: NonNullDateRange): void;
-  breakByDataSource: boolean;
   shownSegmentStat: SegmentStat;
   changeActiveDatasource: (datasource: string | undefined) => void;
 }
@@ -123,22 +70,18 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
     capabilities,
     dateRange,
     changeDateRange,
-    breakByDataSource,
     stage,
     shownSegmentStat,
     changeActiveDatasource,
   } = props;
 
-  const intervalsQuery = useMemo(
-    () => ({ capabilities, dateRange, breakByDataSource }),
-    [capabilities, dateRange, breakByDataSource],
-  );
+  const intervalsQuery = useMemo(() => ({ capabilities, dateRange }), [capabilities, dateRange]);
 
-  const [segmentBarsState] = useQueryManager({
+  const [intervalBarsState] = useQueryManager({
     query: intervalsQuery,
-    processQuery: async ({ capabilities, dateRange, breakByDataSource }, cancelToken) => {
-      const trimDuration: TrimDuration = 'PT1H';
-      let segmentRows: SegmentRow[];
+    processQuery: async ({ capabilities, dateRange }, cancelToken) => {
+      const trimDuration = new Duration('PT1H');
+      let intervalRows: IntervalRow[];
       if (capabilities.hasSql()) {
         const query = SqlQuery.from(N('sys').table('segments'))
           .changeWhereExpression(
@@ -146,14 +89,14 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
           )
           .addSelect(C('start'), { addToGroupBy: 'end' })
           .addSelect(C('end'), { addToGroupBy: 'end' })
-          .applyIf(breakByDataSource, q => q.addSelect(C('datasource'), { addToGroupBy: 'end' }))
+          .addSelect(C('datasource'), { addToGroupBy: 'end' })
           .addSelect(F.count().as('count'))
           .addSelect(F.sum(C('size')).as('size'))
           .addSelect(F.sum(C('num_rows')).as('rows'));
 
-        segmentRows = (await queryDruidSql({ query: query.toString() })).map(sr => {
-          const start = floorToDuration(new Date(sr.start), trimDuration);
-          const end = ceilToDuration(new Date(sr.end), trimDuration);
+        intervalRows = (await queryDruidSql({ query: query.toString() })).map(sr => {
+          const start = trimDuration.floor(new Date(sr.start), TZ_UTC);
+          const end = trimDuration.ceil(new Date(sr.end), TZ_UTC);
           return {
             ...sr,
             start,
@@ -165,7 +108,7 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
         const datasources: string[] = (
           await Api.instance.get(`/druid/coordinator/v1/datasources`, { cancelToken })
         ).data;
-        segmentRows = (
+        intervalRows = (
           await Promise.all(
             datasources.map(async datasource => {
               const intervalMap = (
@@ -180,14 +123,14 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
               return filterMap(Object.entries(intervalMap), ([interval, v]) => {
                 // ToDo: Filter on start end
                 const [startStr, endStr] = interval.split('/');
-                const start = floorToDuration(new Date(startStr), trimDuration);
-                const end = ceilToDuration(new Date(endStr), trimDuration);
+                const start = trimDuration.floor(new Date(startStr), TZ_UTC);
+                const end = trimDuration.ceil(new Date(endStr), TZ_UTC);
                 const { count, size, rows } = v as any;
                 return {
                   start,
                   end,
                   durationSeconds: (end.valueOf() - start.valueOf()) / 1000,
-                  datasource: breakByDataSource ? datasource : undefined,
+                  datasource,
                   count,
                   size,
                   rows,
@@ -199,38 +142,38 @@ export const SegmentBarChart = function SegmentBarChart(props: SegmentBarChartPr
       }
 
       const fullyGroupedSegmentRows = groupBy(
-        segmentRows,
-        segmentRow =>
+        intervalRows,
+        intervalRow =>
           [
-            segmentRow.start.toISOString(),
-            segmentRow.end.toISOString(),
-            segmentRow.datasource || '',
+            intervalRow.start.toISOString(),
+            intervalRow.end.toISOString(),
+            intervalRow.datasource || '',
           ].join('/'),
-        (segmentRows): SegmentRow => {
+        (intervalRows): IntervalRow => {
           return {
-            ...segmentRows[0],
-            ...aggregateSegmentStats(segmentRows),
+            ...intervalRows[0],
+            ...aggregateSegmentStats(intervalRows),
           };
         },
       );
 
-      return stackSegmentRows(fullyGroupedSegmentRows);
+      return stackIntervalRows(fullyGroupedSegmentRows);
     },
   });
 
-  if (segmentBarsState.loading) {
+  if (intervalBarsState.loading) {
     return <Loader />;
   }
 
-  if (segmentBarsState.error) {
+  if (intervalBarsState.error) {
     return (
       <div className="empty-placeholder">
-        <span className="no-data-text">{`Error when loading data: ${segmentBarsState.getErrorMessage()}`}</span>
+        <span className="no-data-text">{`Error when loading data: ${intervalBarsState.getErrorMessage()}`}</span>
       </div>
     );
   }
 
-  const segmentBars = segmentBarsState.data;
+  const segmentBars = intervalBarsState.data;
   if (!segmentBars) return null;
 
   return (
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 10118c30f9d..db6653c912b 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -27,9 +27,11 @@ import { useState } from 'react';
 
 import type { Capabilities } from '../../helpers';
 import {
-  ceilToUtcDay,
+  day,
   isNonNullRange,
   localToUtcDateRange,
+  month,
+  TZ_UTC,
   utcToLocalDateRange,
 } from '../../utils';
 import { Stage } from '../../utils/stage';
@@ -47,10 +49,8 @@ interface SegmentTimelineProps {
 const DEFAULT_TIME_SPAN_MONTHS = 3;
 
 function getDefaultDateRange(): NonNullDateRange {
-  const start = ceilToUtcDay(new Date());
-  const end = new Date(start.valueOf());
-  start.setUTCMonth(start.getUTCMonth() - DEFAULT_TIME_SPAN_MONTHS);
-  return [start, end];
+  const end = day.ceil(new Date(), TZ_UTC);
+  return [month.shift(end, TZ_UTC, -DEFAULT_TIME_SPAN_MONTHS), end];
 }
 
 export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelineProps) {
@@ -60,7 +60,7 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
   const [activeDatasource, setActiveDatasource] = useState<string | undefined>();
   const [dateRange, setDateRange] = useState<NonNullDateRange>(getDefaultDateRange);
 
-  const datasources: string[] = ['wiki', 'kttm'];
+  const datasources: string[] = ['wiki', 'kttm']; // ToDo
 
   const DatasourceSelect: React.FC = () => {
     const showAll = 'Show all';
@@ -128,7 +128,6 @@ export const SegmentTimeline = function SegmentTimeline(props: SegmentTimelinePr
               dateRange={dateRange}
               changeDateRange={setDateRange}
               shownSegmentStat={activeSegmentStat}
-              breakByDataSource
               changeActiveDatasource={(datasource: string | undefined) =>
                 setActiveDatasource(activeDatasource ? undefined : datasource)
               }
diff --git a/web-console/src/utils/date.spec.ts b/web-console/src/utils/date.spec.ts
index 843c144244e..b219ee17af0 100644
--- a/web-console/src/utils/date.spec.ts
+++ b/web-console/src/utils/date.spec.ts
@@ -17,7 +17,6 @@
  */
 
 import {
-  ceilToUtcDay,
   dateToIsoDateString,
   intervalToLocalDateRange,
   localDateRangeToInterval,
@@ -60,12 +59,4 @@ describe('date', () => {
       expect(localDateRangeToInterval(intervalToLocalDateRange(interval))).toEqual(interval);
     });
   });
-
-  describe('ceilToUtcDay', () => {
-    it('works', () => {
-      expect(ceilToUtcDay(new Date('2021-02-03T12:03:02.001Z'))).toEqual(
-        new Date('2021-02-04T00:00:00Z'),
-      );
-    });
-  });
 });
diff --git a/web-console/src/utils/date.ts b/web-console/src/utils/date.ts
index 897618c030b..e59c01a74df 100644
--- a/web-console/src/utils/date.ts
+++ b/web-console/src/utils/date.ts
@@ -98,92 +98,3 @@ export function localDateRangeToInterval(localRange: DateRange): string {
     localEndDate ? localToUtcDate(localEndDate).toISOString().slice(0, 19) : ''
   }`;
 }
-
-export function ceilToUtcDay(date: Date): Date {
-  date = new Date(date.valueOf());
-  date.setUTCHours(0, 0, 0, 0);
-  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/duration/duration.ts b/web-console/src/utils/duration/duration.ts
index b93dab175ad..e582556b3f1 100755
--- a/web-console/src/utils/duration/duration.ts
+++ b/web-console/src/utils/duration/duration.ts
@@ -19,6 +19,8 @@
 import { second, shifters } from '../date-floor-shift-ceil/date-floor-shift-ceil';
 import { capitalizeFirst, pluralIfNeeded } from '../general';
 
+export const TZ_UTC = 'Etc/UTC';
+
 const SPANS_WITH_WEEK = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
 const SPANS_WITHOUT_WEEK = ['year', 'month', 'day', 'hour', 'minute', 'second'];
 const SPANS_WITHOUT_WEEK_OR_MONTH = ['year', 'day', 'hour', 'minute', 'second'];
@@ -139,11 +141,6 @@ export class Duration {
   public readonly singleSpan?: string;
   public readonly spans: Readonly<DurationValue>;
 
-  static parse(durationStr: string): Duration {
-    if (typeof durationStr !== 'string') throw new TypeError('Duration JS must be a string');
-    return new Duration(getSpansFromString(durationStr));
-  }
-
   static fromCanonicalLength(length: number, skipMonths = false): Duration {
     if (length <= 0) throw new Error('length must be positive');
     let spans = fitIntoSpans(length, skipMonths ? SPANS_WITHOUT_WEEK_OR_MONTH : SPANS_WITHOUT_WEEK);
@@ -168,6 +165,17 @@ export class Duration {
     return new Duration(getSpansFromStartEnd(start, end, timezone));
   }
 
+  static pickSmallestGranularityThatFits(
+    granularities: Duration[],
+    span: number,
+    maxEntities: number,
+  ): Duration {
+    for (const granularity of granularities) {
+      if (span / granularity.getCanonicalLength() < maxEntities) return granularity;
+    }
+    return granularities[granularities.length - 1];
+  }
+
   constructor(spans: DurationValue | string) {
     const effectiveSpans: DurationValue =
       typeof spans === 'string' ? getSpansFromString(spans) : removeZeros(spans);
diff --git a/web-console/src/views/explore-view/utils/get-auto-granularity.ts b/web-console/src/views/explore-view/utils/get-auto-granularity.ts
index 21cc2743820..34c3f27b4a6 100644
--- a/web-console/src/views/explore-view/utils/get-auto-granularity.ts
+++ b/web-console/src/views/explore-view/utils/get-auto-granularity.ts
@@ -17,65 +17,47 @@
  */
 
 import type { SqlExpression } from '@druid-toolkit/query';
-import { fitFilterPattern, SqlMulti } from '@druid-toolkit/query';
+import { fitFilterPatterns } from '@druid-toolkit/query';
 
-import { day, Duration, hour } from '../../../utils';
+import { Duration } from '../../../utils';
 
-function getCanonicalDuration(
+function getTimeSpanInExpression(
   expression: SqlExpression,
   timeColumnName: string,
 ): number | undefined {
-  const pattern = fitFilterPattern(expression);
-  if ('column' in pattern && pattern.column !== timeColumnName) return undefined;
-
-  switch (pattern.type) {
-    case 'timeInterval':
+  const patterns = fitFilterPatterns(expression);
+  for (const pattern of patterns) {
+    if (pattern.type === 'timeInterval' && pattern.column === timeColumnName) {
       return pattern.end.valueOf() - pattern.start.valueOf();
-
-    case 'timeRelative':
+    } else if (pattern.type === 'timeRelative' && pattern.column === timeColumnName) {
       return new Duration(pattern.rangeDuration).getCanonicalLength();
-
-    case 'custom':
-      if (pattern.expression instanceof SqlMulti) {
-        for (const value of pattern.expression.args.values) {
-          const canonicalDuration = getCanonicalDuration(value, timeColumnName);
-          if (canonicalDuration !== undefined) return canonicalDuration;
-        }
-      }
-
-      break;
-
-    default:
-      break;
+    }
   }
 
-  return undefined;
+  return;
 }
 
-const DEFAULT_GRANULARITY = 'PT1H';
-
-/**
- * Computes the granularity string from a where clause. If the where clause is TRUE, the default
- * granularity is returned (PT1H). Otherwise, the granularity is computed from the time column and the
- * duration of the where clause.
- *
- * @param where the where SQLExpression to read from
- * @param timeColumnName the name of the time column (any other time column will be ignored)
- * @returns the granularity string (default is PT1H)
- */
 export function getAutoGranularity(where: SqlExpression, timeColumnName: string): string {
-  if (where.toString() === 'TRUE') return DEFAULT_GRANULARITY;
-
-  const canonicalDuration = getCanonicalDuration(where, timeColumnName);
-
-  if (canonicalDuration) {
-    if (canonicalDuration > day.canonicalLength * 95) return 'P1W';
-    if (canonicalDuration > day.canonicalLength * 8) return 'P1D';
-    if (canonicalDuration > hour.canonicalLength * 8) return 'PT1H';
-    if (canonicalDuration > hour.canonicalLength * 3) return 'PT5M';
-
-    return 'PT1M';
-  }
-
-  return DEFAULT_GRANULARITY;
+  const timeSpan = getTimeSpanInExpression(where, timeColumnName);
+  if (!timeSpan) return 'P1D';
+  return Duration.pickSmallestGranularityThatFits(
+    [
+      'PT1S',
+      'PT5S',
+      'PT20S',
+      'PT1M',
+      'PT5M',
+      'PT20M',
+      'PT1H',
+      'PT3H',
+      'PT6H',
+      'P1D',
+      'P1W',
+      'P1M',
+      'P3M',
+      'P1Y',
+    ].map(s => new Duration(s)),
+    timeSpan,
+    200,
+  ).toString();
 }
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 11b3a67a3cb..f4af22ebaf5 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,17 +23,7 @@ import { C, F, SqlExpression } from '@druid-toolkit/query';
 import type { JSX } from 'react';
 import React from 'react';
 
-import {
-  floorDay,
-  floorHour,
-  floorMonth,
-  floorYear,
-  nextDay,
-  nextHour,
-  nextMonth,
-  nextYear,
-  prettyPrintSql,
-} from '../../../../../utils';
+import { day, hour, month, prettyPrintSql, TZ_UTC, year } from '../../../../../utils';
 
 const LATEST_HOUR: SqlExpression = SqlExpression.parse(
   `? >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR`,
@@ -87,10 +77,10 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
     }
 
     const now = new Date();
-    const hourStart = floorHour(now);
-    const dayStart = floorDay(now);
-    const monthStart = floorMonth(now);
-    const yearStart = floorYear(now);
+    const hourStart = hour.floor(now, TZ_UTC);
+    const dayStart = day.floor(now, TZ_UTC);
+    const monthStart = month.floor(now, TZ_UTC);
+    const yearStart = year.floor(now, TZ_UTC);
     return (
       <MenuItem icon={IconNames.FILTER} text="Filter">
         {filterMenuItem(`Latest hour`, fillWithColumn(LATEST_HOUR, columnName))}
@@ -101,19 +91,19 @@ export const TimeMenuItems = React.memo(function TimeMenuItems(props: TimeMenuIt
         <MenuDivider />
         {filterMenuItem(
           `Current hour`,
-          fillWithColumnStartEnd(columnName, hourStart, nextHour(hourStart)),
+          fillWithColumnStartEnd(columnName, hourStart, hour.shift(hourStart, TZ_UTC, 1)),
         )}
         {filterMenuItem(
           `Current day`,
-          fillWithColumnStartEnd(columnName, dayStart, nextDay(dayStart)),
+          fillWithColumnStartEnd(columnName, dayStart, day.shift(dayStart, TZ_UTC, 1)),
         )}
         {filterMenuItem(
           `Current month`,
-          fillWithColumnStartEnd(columnName, monthStart, nextMonth(monthStart)),
+          fillWithColumnStartEnd(columnName, monthStart, month.shift(monthStart, TZ_UTC, 1)),
         )}
         {filterMenuItem(
           `Current year`,
-          fillWithColumnStartEnd(columnName, yearStart, nextYear(yearStart)),
+          fillWithColumnStartEnd(columnName, yearStart, year.shift(yearStart, TZ_UTC, 1)),
         )}
       </MenuItem>
     );


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org