You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2021/06/17 23:10:29 UTC

[druid] branch master updated: Web console: Make segment timeline work over all time intervals (#11359)

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

cwylie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new 2e98d3c  Web console: Make segment timeline work over all time intervals (#11359)
2e98d3c is described below

commit 2e98d3c1ad95a3ba756d0d6409947dc3dea9066a
Author: Vadim Ogievetsky <va...@ogievetsky.com>
AuthorDate: Thu Jun 17 16:10:06 2021 -0700

    Web console: Make segment timeline work over all time intervals (#11359)
    
    * tidy up
    
    * add to segments view
    
    * add unit tests for date
    
    * better util export
    
    * fix ds view
    
    * fix tests
    
    * fix test in time
    
    * unset untermediate state
---
 .../date-range-selector/date-range-selector.scss}  |  10 +-
 .../date-range-selector/date-range-selector.tsx    |  68 +++++++
 .../components/interval-input/interval-input.tsx   |  40 +---
 .../__snapshots__/bar-unit.spec.tsx.snap           |  13 ++
 .../__snapshots__/segment-timeline.spec.tsx.snap   |  91 +++++----
 .../segment-timeline}/bar-group.tsx                |   8 +-
 .../segment-timeline/bar-unit.spec.tsx}            |  15 +-
 .../segment-timeline}/bar-unit.tsx                 |   4 +-
 .../segment-timeline}/chart-axis.tsx               |   2 +-
 .../segment-timeline/segment-timeline.scss         |  15 +-
 .../segment-timeline/segment-timeline.spec.tsx     |  83 ++------
 .../segment-timeline/segment-timeline.tsx          | 213 +++++++++++----------
 .../segment-timeline}/stacked-bar-chart.scss       |  37 +++-
 .../segment-timeline}/stacked-bar-chart.tsx        | 150 ++++++++-------
 web-console/src/utils/date.spec.ts                 |  71 +++++++
 web-console/src/utils/date.ts                      |  65 +++++++
 web-console/src/utils/index.tsx                    |   1 +
 .../__snapshots__/datasource-view.spec.tsx.snap    |   6 +-
 .../src/views/datasource-view/datasource-view.scss |  11 +-
 .../src/views/datasource-view/datasource-view.tsx  |  46 ++---
 .../__snapshots__/segments-view.spec.tsx.snap      |   6 +
 .../src/views/segments-view/segments-view.scss     |  11 ++
 .../src/views/segments-view/segments-view.tsx      |  20 +-
 .../__snapshots__/visualization.spec.tsx.snap      |  22 ---
 24 files changed, 573 insertions(+), 435 deletions(-)

diff --git a/web-console/src/visualization/bar-unit.scss b/web-console/src/components/date-range-selector/date-range-selector.scss
similarity index 88%
rename from web-console/src/visualization/bar-unit.scss
rename to web-console/src/components/date-range-selector/date-range-selector.scss
index 5767d68..a30d622 100644
--- a/web-console/src/visualization/bar-unit.scss
+++ b/web-console/src/components/date-range-selector/date-range-selector.scss
@@ -16,6 +16,12 @@
  * limitations under the License.
  */
 
-.bar-chart-unit {
-  transform: translateX(65px);
+.date-range-selector {
+  .bp3-popover-target {
+    display: block;
+  }
+
+  * {
+    cursor: pointer;
+  }
 }
diff --git a/web-console/src/components/date-range-selector/date-range-selector.tsx b/web-console/src/components/date-range-selector/date-range-selector.tsx
new file mode 100644
index 0000000..b58daec
--- /dev/null
+++ b/web-console/src/components/date-range-selector/date-range-selector.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { Button, InputGroup, Popover, Position } from '@blueprintjs/core';
+import { DateRange, DateRangePicker } from '@blueprintjs/datetime';
+import { IconNames } from '@blueprintjs/icons';
+import React, { useState } from 'react';
+
+import { dateToIsoDateString, localToUtcDate, utcToLocalDate } from '../../utils';
+
+import './date-range-selector.scss';
+
+interface DateRangeSelectorProps {
+  startDate: Date;
+  endDate: Date;
+  onChange: (startDate: Date, endDate: Date) => void;
+}
+
+export const DateRangeSelector = React.memo(function DateRangeSelector(
+  props: DateRangeSelectorProps,
+) {
+  const { startDate, endDate, onChange } = props;
+  const [intermediateDateRange, setIntermediateDateRange] = useState<DateRange | undefined>();
+
+  return (
+    <Popover
+      className="date-range-selector"
+      content={
+        <DateRangePicker
+          value={intermediateDateRange || [utcToLocalDate(startDate), utcToLocalDate(endDate)]}
+          contiguousCalendarMonths={false}
+          reverseMonthAndYearMenus
+          onChange={(selectedRange: DateRange) => {
+            const [startDate, endDate] = selectedRange;
+            if (!startDate || !endDate) {
+              setIntermediateDateRange(selectedRange);
+            } else {
+              setIntermediateDateRange(undefined);
+              onChange(localToUtcDate(startDate), localToUtcDate(endDate));
+            }
+          }}
+        />
+      }
+      position={Position.BOTTOM_RIGHT}
+    >
+      <InputGroup
+        value={`${dateToIsoDateString(startDate)} ➔ ${dateToIsoDateString(endDate)}`}
+        readOnly
+        rightElement={<Button rightIcon={IconNames.CALENDAR} minimal />}
+      />
+    </Popover>
+  );
+});
diff --git a/web-console/src/components/interval-input/interval-input.tsx b/web-console/src/components/interval-input/interval-input.tsx
index 64866cf..138c60d 100644
--- a/web-console/src/components/interval-input/interval-input.tsx
+++ b/web-console/src/components/interval-input/interval-input.tsx
@@ -17,40 +17,13 @@
  */
 
 import { Button, InputGroup, Intent, Popover, Position } from '@blueprintjs/core';
-import { DateRange, DateRangePicker } from '@blueprintjs/datetime';
+import { DateRange, DateRangePicker, TimePrecision } from '@blueprintjs/datetime';
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
-import './interval-input.scss';
-
-const CURRENT_YEAR = new Date().getUTCFullYear();
-
-function removeLocalTimezone(localDate: Date): Date {
-  // Function removes the local timezone of the date and displays it in UTC
-  return new Date(localDate.getTime() - localDate.getTimezoneOffset() * 60000);
-}
+import { intervalToLocalDateRange, localDateRangeToInterval } from '../../utils';
 
-function parseInterval(interval: string): DateRange {
-  const dates = interval.split('/');
-  if (dates.length !== 2) {
-    return [null, null];
-  }
-  const startDate = Date.parse(dates[0]) ? new Date(dates[0]) : null;
-  const endDate = Date.parse(dates[1]) ? new Date(dates[1]) : null;
-  // Must check if the start and end dates are within range
-  return [
-    startDate && startDate.getFullYear() < CURRENT_YEAR - 20 ? null : startDate,
-    endDate && endDate.getFullYear() > CURRENT_YEAR ? null : endDate,
-  ];
-}
-function stringifyDateRange(localRange: DateRange): string {
-  // This function takes in the dates selected from datepicker in local time, and displays them in UTC
-  // Shall Blueprint make any changes to the way dates are selected, this function will have to be reworked
-  const [localStartDate, localEndDate] = localRange;
-  return `${
-    localStartDate ? removeLocalTimezone(localStartDate).toISOString().substring(0, 19) : ''
-  }/${localEndDate ? removeLocalTimezone(localEndDate).toISOString().substring(0, 19) : ''}`;
-}
+import './interval-input.scss';
 
 export interface IntervalInputProps {
   interval: string;
@@ -72,11 +45,12 @@ export const IntervalInput = React.memo(function IntervalInput(props: IntervalIn
             popoverClassName="calendar"
             content={
               <DateRangePicker
-                timePrecision="second"
-                value={parseInterval(interval)}
+                timePrecision={TimePrecision.SECOND}
+                value={intervalToLocalDateRange(interval)}
                 contiguousCalendarMonths={false}
+                reverseMonthAndYearMenus
                 onChange={(selectedRange: DateRange) => {
-                  onValueChange(stringifyDateRange(selectedRange));
+                  onValueChange(localDateRangeToInterval(selectedRange));
                 }}
               />
             }
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
new file mode 100644
index 0000000..7d98145
--- /dev/null
+++ b/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
@@ -0,0 +1,13 @@
+// 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 c7a96a1..5f76670 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
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Segment Timeline matches snapshot 1`] = `
+exports[`SegmentTimeline matches snapshot 1`] = `
 <div
   class="segment-timeline app-view"
 >
@@ -85,7 +85,7 @@ exports[`Segment Timeline matches snapshot 1`] = `
       <label
         class="bp3-label"
       >
-        Datasource:
+        Datasource
          
         <span
           class="bp3-text-muted"
@@ -132,7 +132,7 @@ exports[`Segment Timeline matches snapshot 1`] = `
       <label
         class="bp3-label"
       >
-        Period:
+        Interval
          
         <span
           class="bp3-text-muted"
@@ -141,56 +141,53 @@ exports[`Segment Timeline matches snapshot 1`] = `
       <div
         class="bp3-form-content"
       >
-        <div
-          class="bp3-html-select bp3-fill"
+        <span
+          class="bp3-popover-wrapper date-range-selector"
         >
-          <select>
-            <option
-              value="1"
-            >
-              1 months
-            </option>
-            <option
-              value="3"
-            >
-              3 months
-            </option>
-            <option
-              value="6"
-            >
-              6 months
-            </option>
-            <option
-              value="9"
-            >
-              9 months
-            </option>
-            <option
-              value="12"
-            >
-              1 year
-            </option>
-          </select>
           <span
-            class="bp3-icon bp3-icon-double-caret-vertical"
-            icon="double-caret-vertical"
+            class="bp3-popover-target"
           >
-            <svg
-              data-icon="double-caret-vertical"
-              height="16"
-              viewBox="0 0 16 16"
-              width="16"
+            <div
+              class="bp3-input-group"
             >
-              <desc>
-                double-caret-vertical
-              </desc>
-              <path
-                d="M5 7h6a1.003 1.003 0 00.71-1.71l-3-3C8.53 2.11 8.28 2 8 2s-.53.11-.71.29l-3 3A1.003 1.003 0 005 7zm6 2H5a1.003 1.003 0 00-.71 1.71l3 3c.18.18.43.29.71.29s.53-.11.71-.29l3-3A1.003 1.003 0 0011 9z"
-                fill-rule="evenodd"
+              <input
+                class="bp3-input"
+                readonly=""
+                style="padding-right: 0px;"
+                type="text"
+                value="2021-03-09 ➔ 2021-06-09"
               />
-            </svg>
+              <span
+                class="bp3-input-action"
+              >
+                <button
+                  class="bp3-button bp3-minimal"
+                  type="button"
+                >
+                  <span
+                    class="bp3-icon bp3-icon-calendar"
+                    icon="calendar"
+                  >
+                    <svg
+                      data-icon="calendar"
+                      height="16"
+                      viewBox="0 0 16 16"
+                      width="16"
+                    >
+                      <desc>
+                        calendar
+                      </desc>
+                      <path
+                        d="M11 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1s-1 .4-1 1v1c0 .5.4 1 1 1zm3-2h-1v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H6v1c0 1.1-.9 2-2 2s-2-.9-2-2V1H1c-.6 0-1 .5-1 1v12c0 .6.4 1 1 1h13c.6 0 1-.4 1-1V2c0-.6-.5-1-1-1zM5 13H2v-3h3v3zm0-4H2V6h3v3zm4 4H6v-3h3v3zm0-4H6V6h3v3zm4 4h-3v-3h3v3zm0-4h-3V6h3v3zM4 3c.6 0 1-.5 1-1V1c0-.6-.4-1-1-1S3 .4 3 1v1c0 .5.4 1 1 1z"
+                        fill-rule="evenodd"
+                      />
+                    </svg>
+                  </span>
+                </button>
+              </span>
+            </div>
           </span>
-        </div>
+        </span>
       </div>
     </div>
   </div>
diff --git a/web-console/src/visualization/bar-group.tsx b/web-console/src/components/segment-timeline/bar-group.tsx
similarity index 91%
rename from web-console/src/visualization/bar-group.tsx
rename to web-console/src/components/segment-timeline/bar-group.tsx
index 1975c49..4bd75e6 100644
--- a/web-console/src/visualization/bar-group.tsx
+++ b/web-console/src/components/segment-timeline/bar-group.tsx
@@ -19,10 +19,8 @@
 import { AxisScale } from 'd3-axis';
 import React from 'react';
 
-import { BarUnitData } from '../components/segment-timeline/segment-timeline';
-
 import { BarUnit } from './bar-unit';
-import { HoveredBarInfo } from './stacked-bar-chart';
+import { BarUnitData, HoveredBarInfo } from './stacked-bar-chart';
 
 interface BarGroupProps {
   dataToRender: BarUnitData[];
@@ -54,9 +52,9 @@ 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));
+      const x = xScale(new Date(entry.x + 'Z'));
       const y = yScale((entry.y0 || 0) + entry.y) || 0;
-      const height = y0 - y;
+      const height = Math.max(y0 - y, 0);
       const barInfo: HoveredBarInfo = {
         xCoordinate: x,
         yCoordinate: y,
diff --git a/web-console/src/visualization/visualization.spec.tsx b/web-console/src/components/segment-timeline/bar-unit.spec.tsx
similarity index 76%
rename from web-console/src/visualization/visualization.spec.tsx
rename to web-console/src/components/segment-timeline/bar-unit.spec.tsx
index a879a73..e8e62b5 100644
--- a/web-console/src/visualization/visualization.spec.tsx
+++ b/web-console/src/components/segment-timeline/bar-unit.spec.tsx
@@ -20,10 +20,9 @@ import { render } from '@testing-library/react';
 import React from 'react';
 
 import { BarUnit } from './bar-unit';
-import { ChartAxis } from './chart-axis';
 
-describe('Visualization', () => {
-  it('BarUnit', () => {
+describe('BarUnit', () => {
+  it('matches snapshot', () => {
     const barGroup = (
       <svg>
         <BarUnit x={10} y={10} width={10} height={10} />
@@ -32,14 +31,4 @@ describe('Visualization', () => {
     const { container } = render(barGroup);
     expect(container.firstChild).toMatchSnapshot();
   });
-
-  it('action barGroup', () => {
-    const barGroup = (
-      <svg>
-        <ChartAxis transform="value" scale={() => null} />
-      </svg>
-    );
-    const { container } = render(barGroup);
-    expect(container.firstChild).toMatchSnapshot();
-  });
 });
diff --git a/web-console/src/visualization/bar-unit.tsx b/web-console/src/components/segment-timeline/bar-unit.tsx
similarity index 95%
rename from web-console/src/visualization/bar-unit.tsx
rename to web-console/src/components/segment-timeline/bar-unit.tsx
index 4cb1fd8..8d783f6 100644
--- a/web-console/src/visualization/bar-unit.tsx
+++ b/web-console/src/components/segment-timeline/bar-unit.tsx
@@ -18,8 +18,6 @@
 
 import React from 'react';
 
-import './bar-unit.scss';
-
 interface BarChartUnitProps {
   x: number | undefined;
   y: number;
@@ -35,7 +33,7 @@ export function BarUnit(props: BarChartUnitProps) {
   const { x, y, width, height, style, onClick, onHover, offHover } = props;
   return (
     <rect
-      className="bar-chart-unit"
+      className="bar-unit"
       x={x}
       y={y}
       width={width}
diff --git a/web-console/src/visualization/chart-axis.tsx b/web-console/src/components/segment-timeline/chart-axis.tsx
similarity index 98%
rename from web-console/src/visualization/chart-axis.tsx
rename to web-console/src/components/segment-timeline/chart-axis.tsx
index cc339f3..bc333d3 100644
--- a/web-console/src/visualization/chart-axis.tsx
+++ b/web-console/src/components/segment-timeline/chart-axis.tsx
@@ -20,7 +20,7 @@ import { select } from 'd3-selection';
 import React from 'react';
 
 interface ChartAxisProps {
-  transform: string;
+  transform?: string;
   scale: any;
   className?: string;
 }
diff --git a/web-console/src/components/segment-timeline/segment-timeline.scss b/web-console/src/components/segment-timeline/segment-timeline.scss
index 9a47f18..d77f37f 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.scss
+++ b/web-console/src/components/segment-timeline/segment-timeline.scss
@@ -18,8 +18,7 @@
 
 .segment-timeline {
   display: grid;
-  grid-template-columns: 85% 15%;
-  height: 100%;
+  grid-template-columns: 1fr 200px;
 
   .loader {
     width: 85%;
@@ -33,15 +32,6 @@
     transform: translate(-50%, -50%);
   }
 
-  .bar-chart-tooltip {
-    margin-left: 53px;
-
-    div {
-      display: inline-block;
-      width: 230px;
-    }
-  }
-
   .no-data-text {
     position: absolute;
     left: 30vw;
@@ -50,7 +40,6 @@
   }
 
   .side-control {
-    padding-left: 1vw;
-    padding-top: 5vh;
+    padding-top: 20px;
   }
 }
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 d5df902..5fba7eb 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx
@@ -18,23 +18,30 @@
 
 import { render } from '@testing-library/react';
 import { sane } from 'druid-query-toolkit/build/test-utils';
-import { mount } from 'enzyme';
 import React from 'react';
 
-import { Capabilities, QueryManager } from '../../utils';
+import { Capabilities } from '../../utils';
 
 import { SegmentTimeline } from './segment-timeline';
 
-describe('Segment Timeline', () => {
+jest.useFakeTimers('modern').setSystemTime(Date.parse('2021-06-08T12:34:56Z'));
+
+describe('SegmentTimeline', () => {
   it('.getSqlQuery', () => {
-    expect(SegmentTimeline.getSqlQuery(3)).toEqual(sane`
+    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
-        "start" > TIME_FORMAT(TIMESTAMPADD(MONTH, -3, CURRENT_TIMESTAMP), 'yyyy-MM-dd''T''hh:mm:ss.SSS') AND
+        '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
@@ -43,72 +50,8 @@ describe('Segment Timeline', () => {
   });
 
   it('matches snapshot', () => {
-    const segmentTimeline = (
-      <SegmentTimeline capabilities={Capabilities.FULL} chartHeight={100} chartWidth={100} />
-    );
+    const segmentTimeline = <SegmentTimeline capabilities={Capabilities.FULL} />;
     const { container } = render(segmentTimeline);
     expect(container.firstChild).toMatchSnapshot();
   });
-
-  it('queries 3 months of data by default', () => {
-    const dataQueryManager = new MockDataQueryManager();
-    const segmentTimeline = (
-      <SegmentTimeline
-        capabilities={Capabilities.FULL}
-        chartHeight={100}
-        chartWidth={100}
-        dataQueryManager={dataQueryManager}
-      />
-    );
-    render(segmentTimeline);
-
-    // Ideally, the test should verify the rendered bar graph to see if the bars
-    // cover the selected period. Since the unit test does not have a druid
-    // instance to query from, just verify the query has the correct time span.
-    expect(dataQueryManager.queryTimeSpan).toBe(3);
-  });
-
-  it('queries matching time span when new period is selected from dropdown', () => {
-    const dataQueryManager = new MockDataQueryManager();
-    const segmentTimeline = (
-      <SegmentTimeline
-        capabilities={Capabilities.FULL}
-        chartHeight={100}
-        chartWidth={100}
-        dataQueryManager={dataQueryManager}
-      />
-    );
-    const wrapper = mount(segmentTimeline);
-    const selects = wrapper.find('select');
-    expect(selects.length).toBe(2); // Datasource & Period
-    const periodSelect = selects.at(1);
-    const newTimeSpanMonths = 6;
-    periodSelect.simulate('change', { target: { value: newTimeSpanMonths } });
-
-    // Ideally, the test should verify the rendered bar graph to see if the bars
-    // cover the selected period. Since the unit test does not have a druid
-    // instance to query from, just verify the query has the correct time span.
-    expect(dataQueryManager.queryTimeSpan).toBe(newTimeSpanMonths);
-  });
 });
-
-/**
- * Mock the data query manager, since the unit test does not have a druid instance
- */
-class MockDataQueryManager extends QueryManager<
-  { capabilities: Capabilities; timeSpan: number },
-  any
-> {
-  queryTimeSpan?: number;
-
-  constructor() {
-    super({
-      // eslint-disable-next-line @typescript-eslint/require-await
-      processQuery: async ({ timeSpan }) => {
-        this.queryTimeSpan = timeSpan;
-      },
-      debounceIdle: 0,
-      debounceLoading: 0,
-    });
-  }
-}
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index ba48839..8b771d5 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -16,30 +16,49 @@
  * limitations under the License.
  */
 
-import { FormGroup, HTMLSelect, Radio, RadioGroup } from '@blueprintjs/core';
+import {
+  FormGroup,
+  HTMLSelect,
+  IResizeEntry,
+  Radio,
+  RadioGroup,
+  ResizeSensor,
+} from '@blueprintjs/core';
 import { AxisScale } from 'd3-axis';
-import { scaleLinear, scaleTime } from 'd3-scale';
+import { scaleLinear, scaleUtc } from 'd3-scale';
 import React from 'react';
 
 import { Api } from '../../singletons';
-import { Capabilities, formatBytes, queryDruidSql, QueryManager, uniq } from '../../utils';
-import { StackedBarChart } from '../../visualization/stacked-bar-chart';
+import {
+  Capabilities,
+  ceilToUtcDay,
+  formatBytes,
+  queryDruidSql,
+  QueryManager,
+  uniq,
+} from '../../utils';
+import { DateRangeSelector } from '../date-range-selector/date-range-selector';
 import { Loader } from '../loader/loader';
 
+import { BarUnitData, StackedBarChart } from './stacked-bar-chart';
+
 import './segment-timeline.scss';
 
 interface SegmentTimelineProps {
   capabilities: Capabilities;
-  chartHeight: number;
-  chartWidth: number;
 
   // For testing:
-  dataQueryManager?: QueryManager<{ capabilities: Capabilities; timeSpan: number }, any>;
+  dataQueryManager?: QueryManager<
+    { capabilities: Capabilities; startDate: Date; endDate: Date },
+    any
+  >;
 }
 
 type ActiveDataType = 'sizeData' | 'countData';
 
 interface SegmentTimelineState {
+  chartHeight: number;
+  chartWidth: number;
   data?: Record<string, any>;
   datasources: string[];
   stackedData?: Record<string, BarUnitData[]>;
@@ -47,13 +66,12 @@ interface SegmentTimelineState {
   activeDatasource: string | null;
   activeDataType: ActiveDataType;
   dataToRender: BarUnitData[];
-  timeSpan: number; // by months
   loading: boolean;
   error?: Error;
   xScale: AxisScale<Date> | null;
   yScale: AxisScale<number> | null;
-  dStart: Date;
-  dEnd: Date;
+  startDate: Date;
+  endDate: Date;
 }
 
 interface BarChartScales {
@@ -61,22 +79,6 @@ interface BarChartScales {
   yScale: AxisScale<number>;
 }
 
-export interface BarUnitData {
-  x: number;
-  y: number;
-  y0?: number;
-  width: number;
-  datasource: string;
-  color: string;
-}
-
-export interface BarChartMargin {
-  top: number;
-  right: number;
-  bottom: number;
-  left: number;
-}
-
 interface IntervalRow {
   start: string;
   end: string;
@@ -114,14 +116,15 @@ export class SegmentTimeline extends React.PureComponent<
     return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length];
   }
 
-  static getSqlQuery(timeSpan: number): string {
+  static getSqlQuery(startDate: Date, endDate: Date): string {
     return `SELECT
   "start", "end", "datasource",
   COUNT(*) AS "count",
   SUM("size") AS "size"
 FROM sys.segments
 WHERE
-  "start" > TIME_FORMAT(TIMESTAMPADD(MONTH, -${timeSpan}, CURRENT_TIMESTAMP), 'yyyy-MM-dd''T''hh:mm:ss.SSS') AND
+  '${startDate.toISOString()}' <= "start" AND
+  "end" <= '${endDate.toISOString()}' AND
   is_published = 1 AND
   is_overshadowed = 0
 GROUP BY 1, 2, 3
@@ -233,18 +236,21 @@ ORDER BY "start" DESC`;
   }
 
   private readonly dataQueryManager: QueryManager<
-    { capabilities: Capabilities; timeSpan: number },
+    { capabilities: Capabilities; startDate: Date; endDate: Date },
     any
   >;
 
-  private readonly chartMargin = { top: 20, right: 10, bottom: 20, left: 10 };
+  private readonly chartMargin = { top: 40, right: 15, bottom: 20, left: 60 };
 
   constructor(props: SegmentTimelineProps) {
     super(props);
-    const dStart = new Date();
-    const dEnd = new Date();
-    dStart.setMonth(dStart.getMonth() - DEFAULT_TIME_SPAN_MONTHS);
+    const startDate = ceilToUtcDay(new Date());
+    const endDate = new Date(startDate.valueOf());
+    startDate.setUTCMonth(startDate.getUTCMonth() - DEFAULT_TIME_SPAN_MONTHS);
+
     this.state = {
+      chartWidth: 1, // Dummy init values to be replaced
+      chartHeight: 1, // after first render
       data: {},
       datasources: [],
       stackedData: {},
@@ -252,27 +258,26 @@ ORDER BY "start" DESC`;
       dataToRender: [],
       activeDatasource: null,
       activeDataType: 'sizeData',
-      timeSpan: DEFAULT_TIME_SPAN_MONTHS,
       loading: true,
       xScale: null,
       yScale: null,
-      dEnd: dEnd,
-      dStart: dStart,
+      startDate,
+      endDate,
     };
 
     this.dataQueryManager =
       props.dataQueryManager ||
       new QueryManager({
-        processQuery: async ({ capabilities, timeSpan }) => {
+        processQuery: async ({ capabilities, startDate, endDate }) => {
           let intervals: IntervalRow[];
           let datasources: string[];
           if (capabilities.hasSql()) {
-            intervals = await queryDruidSql({ query: SegmentTimeline.getSqlQuery(timeSpan) });
+            intervals = await queryDruidSql({
+              query: SegmentTimeline.getSqlQuery(startDate, endDate),
+            });
             datasources = uniq(intervals.map(r => r.datasource));
           } else if (capabilities.hasCoordinatorAccess()) {
-            const before = new Date();
-            before.setMonth(before.getMonth() - timeSpan);
-            const beforeIso = before.toISOString();
+            const startIso = startDate.toISOString();
 
             datasources = (await Api.instance.get(`/druid/coordinator/v1/datasources`)).data;
             intervals = (
@@ -298,7 +303,7 @@ ORDER BY "start" DESC`;
                         size,
                       };
                     })
-                    .filter(a => beforeIso < a.start);
+                    .filter(a => startIso < a.start);
                 }),
               )
             )
@@ -331,23 +336,23 @@ ORDER BY "start" DESC`;
 
   componentDidMount(): void {
     const { capabilities } = this.props;
-    const { timeSpan } = this.state;
+    const { startDate, endDate } = this.state;
 
-    this.dataQueryManager.runQuery({ capabilities, timeSpan });
+    this.dataQueryManager.runQuery({ capabilities, startDate, endDate });
   }
 
   componentWillUnmount(): void {
     this.dataQueryManager.terminate();
   }
 
-  componentDidUpdate(prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void {
+  componentDidUpdate(_prevProps: SegmentTimelineProps, prevState: SegmentTimelineState): void {
     const { activeDatasource, activeDataType, singleDatasourceData, stackedData } = this.state;
     if (
       prevState.data !== this.state.data ||
       prevState.activeDataType !== this.state.activeDataType ||
       prevState.activeDatasource !== this.state.activeDatasource ||
-      prevProps.chartWidth !== this.props.chartWidth ||
-      prevProps.chartHeight !== this.props.chartHeight
+      prevState.chartWidth !== this.state.chartWidth ||
+      prevState.chartHeight !== this.state.chartHeight
     ) {
       const scales: BarChartScales | undefined = this.calculateScales();
       const dataToRender: BarUnitData[] | undefined = activeDatasource
@@ -369,18 +374,19 @@ ORDER BY "start" DESC`;
   }
 
   private calculateScales(): BarChartScales | undefined {
-    const { chartWidth, chartHeight } = this.props;
     const {
+      chartWidth,
+      chartHeight,
       data,
       activeDataType,
       activeDatasource,
       singleDatasourceData,
-      dStart,
-      dEnd,
+      startDate,
+      endDate,
     } = this.state;
     if (!data || !Object.keys(data).length) return;
     const activeData = data[activeDataType];
-    const xDomain: Date[] = [dStart, dEnd];
+
     let yDomain: number[] = [
       0,
       activeData.length === 0
@@ -400,8 +406,8 @@ ORDER BY "start" DESC`;
       ];
     }
 
-    const xScale: AxisScale<Date> = scaleTime()
-      .domain(xDomain)
+    const xScale: AxisScale<Date> = scaleUtc()
+      .domain([startDate, endDate])
       .range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]);
 
     const yScale: AxisScale<number> = scaleLinear()
@@ -414,22 +420,7 @@ ORDER BY "start" DESC`;
     };
   }
 
-  onTimeSpanChange = (e: any) => {
-    const dStart = new Date();
-    const dEnd = new Date();
-    const capabilities = this.props.capabilities;
-    const timeSpan = parseInt(e, 10) || DEFAULT_TIME_SPAN_MONTHS;
-    dStart.setMonth(dStart.getMonth() - timeSpan);
-    this.setState({
-      timeSpan: e,
-      loading: true,
-      dStart,
-      dEnd,
-    });
-    this.dataQueryManager.runQuery({ capabilities, timeSpan });
-  };
-
-  formatTick = (n: number) => {
+  private readonly formatTick = (n: number) => {
     const { activeDataType } = this.state;
     if (activeDataType === 'countData') {
       return n.toString();
@@ -438,9 +429,18 @@ ORDER BY "start" DESC`;
     }
   };
 
+  private readonly handleResize = (entries: IResizeEntry[]) => {
+    const chartRect = entries[0].contentRect;
+    this.setState({
+      chartWidth: chartRect.width,
+      chartHeight: chartRect.height,
+    });
+  };
+
   renderStackedBarChart() {
-    const { chartWidth, chartHeight } = this.props;
     const {
+      chartWidth,
+      chartHeight,
       loading,
       dataToRender,
       activeDataType,
@@ -449,9 +449,10 @@ ORDER BY "start" DESC`;
       yScale,
       data,
       activeDatasource,
-      dStart,
-      dEnd,
+      startDate,
+      endDate,
     } = this.state;
+
     if (loading) {
       return (
         <div>
@@ -498,30 +499,36 @@ ORDER BY "start" DESC`;
     }
 
     const millisecondsPerDay = 24 * 60 * 60 * 1000;
-    const barCounts = (dEnd.getTime() - dStart.getTime()) / millisecondsPerDay;
-    const barWidth = (chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts;
+    const barCounts = (endDate.getTime() - startDate.getTime()) / millisecondsPerDay;
+    const barWidth = Math.max(
+      0,
+      (chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts,
+    );
     return (
-      <StackedBarChart
-        dataToRender={dataToRender}
-        svgHeight={chartHeight}
-        svgWidth={chartWidth}
-        margin={this.chartMargin}
-        changeActiveDatasource={(datasource: string | null) =>
-          this.setState(prevState => ({
-            activeDatasource: prevState.activeDatasource ? null : datasource,
-          }))
-        }
-        activeDataType={activeDataType}
-        formatTick={(n: number) => this.formatTick(n)}
-        xScale={xScale}
-        yScale={yScale}
-        barWidth={barWidth}
-      />
+      <ResizeSensor onResize={this.handleResize}>
+        <StackedBarChart
+          dataToRender={dataToRender}
+          svgHeight={chartHeight}
+          svgWidth={chartWidth}
+          margin={this.chartMargin}
+          changeActiveDatasource={(datasource: string | null) =>
+            this.setState(prevState => ({
+              activeDatasource: prevState.activeDatasource ? null : datasource,
+            }))
+          }
+          activeDataType={activeDataType}
+          formatTick={(n: number) => this.formatTick(n)}
+          xScale={xScale}
+          yScale={yScale}
+          barWidth={barWidth}
+        />
+      </ResizeSensor>
     );
   }
 
   render(): JSX.Element {
-    const { datasources, activeDataType, activeDatasource, timeSpan } = this.state;
+    const { capabilities } = this.props;
+    const { datasources, activeDataType, activeDatasource, startDate, endDate } = this.state;
 
     return (
       <div className="segment-timeline app-view">
@@ -537,7 +544,7 @@ ORDER BY "start" DESC`;
             </RadioGroup>
           </FormGroup>
 
-          <FormGroup label="Datasource:">
+          <FormGroup label="Datasource">
             <HTMLSelect
               onChange={(e: any) =>
                 this.setState({
@@ -558,18 +565,16 @@ ORDER BY "start" DESC`;
             </HTMLSelect>
           </FormGroup>
 
-          <FormGroup label="Period:">
-            <HTMLSelect
-              onChange={(e: any) => this.onTimeSpanChange(e.target.value)}
-              value={timeSpan}
-              fill
-            >
-              <option value={1}>1 months</option>
-              <option value={3}>3 months</option>
-              <option value={6}>6 months</option>
-              <option value={9}>9 months</option>
-              <option value={12}>1 year</option>
-            </HTMLSelect>
+          <FormGroup label="Interval">
+            <DateRangeSelector
+              startDate={startDate}
+              endDate={endDate}
+              onChange={(startDate, endDate) => {
+                this.setState({ startDate, endDate }, () => {
+                  this.dataQueryManager.runQuery({ capabilities, startDate, endDate });
+                });
+              }}
+            />
           </FormGroup>
         </div>
       </div>
diff --git a/web-console/src/visualization/stacked-bar-chart.scss b/web-console/src/components/segment-timeline/stacked-bar-chart.scss
similarity index 65%
rename from web-console/src/visualization/stacked-bar-chart.scss
rename to web-console/src/components/segment-timeline/stacked-bar-chart.scss
index fed00ae..26e5f51 100644
--- a/web-console/src/visualization/stacked-bar-chart.scss
+++ b/web-console/src/components/segment-timeline/stacked-bar-chart.scss
@@ -16,18 +16,35 @@
  * limitations under the License.
  */
 
-.bar-chart {
-  .hovered-bar {
-    fill: transparent;
-    stroke: #ffffff;
-    stroke-width: 1.5px;
-    transform: translateX(65px);
+.stacked-bar-chart {
+  position: relative;
+  overflow: hidden;
+
+  .bar-chart-tooltip {
+    position: absolute;
+    left: 100px;
+    right: 0;
+
+    div {
+      display: inline-block;
+      width: 230px;
+    }
   }
 
-  .gridline-x {
-    line {
-      stroke-dasharray: 5, 5;
-      opacity: 0.5;
+  svg {
+    position: absolute;
+
+    .hovered-bar {
+      fill: transparent;
+      stroke: #ffffff;
+      stroke-width: 1.5px;
+    }
+
+    .gridline-x {
+      line {
+        stroke-dasharray: 5, 5;
+        opacity: 0.5;
+      }
     }
   }
 }
diff --git a/web-console/src/visualization/stacked-bar-chart.tsx b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
similarity index 54%
rename from web-console/src/visualization/stacked-bar-chart.tsx
rename to web-console/src/components/segment-timeline/stacked-bar-chart.tsx
index c511463..7c772ef 100644
--- a/web-console/src/visualization/stacked-bar-chart.tsx
+++ b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
@@ -19,13 +19,37 @@
 import { axisBottom, axisLeft, AxisScale } from 'd3-axis';
 import React, { useState } from 'react';
 
-import { BarChartMargin, BarUnitData } from '../components/segment-timeline/segment-timeline';
-
 import { BarGroup } from './bar-group';
 import { ChartAxis } from './chart-axis';
 
 import './stacked-bar-chart.scss';
 
+export interface BarUnitData {
+  x: number;
+  y: number;
+  y0?: number;
+  width: number;
+  datasource: string;
+  color: string;
+}
+
+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;
+}
+
 interface StackedBarChartProps {
   svgWidth: number;
   svgHeight: number;
@@ -39,21 +63,12 @@ interface StackedBarChartProps {
   barWidth: number;
 }
 
-export interface HoveredBarInfo {
-  xCoordinate?: number;
-  yCoordinate?: number;
-  height?: number;
-  width?: number;
-  datasource?: string;
-  xValue?: number;
-  yValue?: number;
-}
-
 export const StackedBarChart = React.memo(function StackedBarChart(props: StackedBarChartProps) {
   const {
     activeDataType,
     svgWidth,
     svgHeight,
+    margin,
     formatTick,
     xScale,
     yScale,
@@ -63,84 +78,87 @@ export const StackedBarChart = React.memo(function StackedBarChart(props: Stacke
   } = props;
   const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
 
-  const width = props.svgWidth - props.margin.left - props.margin.right;
-  const height = props.svgHeight - props.margin.bottom - props.margin.top;
+  const width = svgWidth - margin.left - margin.right;
+  const height = svgHeight - margin.top - margin.bottom;
 
   function renderBarChart() {
     return (
-      <div className="bar-chart-container">
-        <svg
-          width={width}
-          height={height}
-          viewBox={`0 0 ${svgWidth} ${svgHeight}`}
-          preserveAspectRatio="xMinYMin meet"
-          style={{ marginTop: '20px' }}
+      <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(60, 0)"
+            transform="translate(0, 0)"
             scale={axisLeft(yScale)
               .ticks(5)
               .tickSize(-width)
               .tickFormat(() => '')
               .tickSizeOuter(0)}
           />
+          <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(65, ${height})`}
+            className="axis-x"
+            transform={`translate(0, ${height})`}
             scale={axisBottom(xScale)}
           />
           <ChartAxis
-            className="axis--y"
-            transform="translate(60, 0)"
+            className="axis-y"
             scale={axisLeft(yScale)
               .ticks(5)
               .tickFormat((e: number) => formatTick(e))}
           />
-          <g className="bars-group" onMouseLeave={() => setHoverOn(undefined)}>
-            <BarGroup
-              dataToRender={dataToRender}
-              changeActiveDatasource={changeActiveDatasource}
-              formatTick={formatTick}
-              xScale={xScale}
-              yScale={yScale}
-              onHoverBar={(e: HoveredBarInfo) => setHoverOn(e)}
-              hoverOn={hoverOn}
-              barWidth={barWidth}
-            />
-            {hoverOn && (
-              <g
-                className="hovered-bar"
-                onClick={() => {
-                  setHoverOn(undefined);
-                  changeActiveDatasource(hoverOn.datasource ?? null);
-                }}
-              >
-                <rect
-                  x={hoverOn.xCoordinate}
-                  y={hoverOn.yCoordinate}
-                  width={barWidth}
-                  height={hoverOn.height}
-                />
-              </g>
-            )}
-          </g>
-        </svg>
-      </div>
+          {hoverOn && (
+            <g
+              className="hovered-bar"
+              onClick={() => {
+                setHoverOn(undefined);
+                changeActiveDatasource(hoverOn.datasource ?? null);
+              }}
+            >
+              <rect
+                x={hoverOn.xCoordinate}
+                y={hoverOn.yCoordinate}
+                width={barWidth}
+                height={hoverOn.height}
+              />
+            </g>
+          )}
+        </g>
+      </svg>
     );
   }
 
   return (
-    <div className="bar-chart">
-      <div className="bar-chart-tooltip">
-        <div>Datasource: {hoverOn ? hoverOn.datasource : ''}</div>
-        <div>Time: {hoverOn ? hoverOn.xValue : ''}</div>
-        <div>
-          {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${
-            hoverOn ? formatTick(hoverOn.yValue!) : ''
-          }`}
-        </div>
-      </div>
+    <div className="stacked-bar-chart">
+      {hoverOn && (
+        <>
+          <div className="bar-chart-tooltip">
+            <div>Datasource: {hoverOn.datasource}</div>
+            <div>Time: {hoverOn.xValue}</div>
+            <div>
+              {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${formatTick(
+                hoverOn.yValue!,
+              )}`}
+            </div>
+          </div>
+        </>
+      )}
       {renderBarChart()}
     </div>
   );
diff --git a/web-console/src/utils/date.spec.ts b/web-console/src/utils/date.spec.ts
new file mode 100644
index 0000000..843c144
--- /dev/null
+++ b/web-console/src/utils/date.spec.ts
@@ -0,0 +1,71 @@
+/*
+ * 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 {
+  ceilToUtcDay,
+  dateToIsoDateString,
+  intervalToLocalDateRange,
+  localDateRangeToInterval,
+  localToUtcDate,
+  utcToLocalDate,
+} from './date';
+
+describe('date', () => {
+  describe('dateToIsoDateString', () => {
+    it('works', () => {
+      expect(dateToIsoDateString(new Date('2021-02-03T12:00:00Z'))).toEqual('2021-02-03');
+    });
+  });
+
+  describe('utcToLocalDate / localToUtcDate', () => {
+    it('works', () => {
+      const date = new Date('2021-02-03T12:00:00Z');
+
+      expect(localToUtcDate(utcToLocalDate(date))).toEqual(date);
+      expect(utcToLocalDate(localToUtcDate(date))).toEqual(date);
+    });
+  });
+
+  describe('intervalToLocalDateRange / localDateRangeToInterval', () => {
+    it('works with full interval', () => {
+      const interval = '2021-02-03T12:00:00/2021-03-03T12:00:00';
+
+      expect(localDateRangeToInterval(intervalToLocalDateRange(interval))).toEqual(interval);
+    });
+
+    it('works with start only', () => {
+      const interval = '2021-02-03T12:00:00/';
+
+      expect(localDateRangeToInterval(intervalToLocalDateRange(interval))).toEqual(interval);
+    });
+
+    it('works with end only', () => {
+      const interval = '/2021-02-03T12:00:00';
+
+      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
new file mode 100644
index 0000000..be548f7
--- /dev/null
+++ b/web-console/src/utils/date.ts
@@ -0,0 +1,65 @@
+/*
+ * 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 { DateRange } from '@blueprintjs/datetime';
+
+const CURRENT_YEAR = new Date().getUTCFullYear();
+
+export function dateToIsoDateString(date: Date): string {
+  return date.toISOString().substr(0, 10);
+}
+
+export function utcToLocalDate(utcDate: Date): Date {
+  // Function removes the local timezone of the date and displays it in UTC
+  return new Date(utcDate.getTime() + utcDate.getTimezoneOffset() * 60000);
+}
+
+export function localToUtcDate(localDate: Date): Date {
+  // Function removes the local timezone of the date and displays it in UTC
+  return new Date(localDate.getTime() - localDate.getTimezoneOffset() * 60000);
+}
+
+export function intervalToLocalDateRange(interval: string): DateRange {
+  const dates = interval.split('/');
+  if (dates.length !== 2) return [null, null];
+
+  const startDate = Date.parse(dates[0]) ? new Date(dates[0]) : null;
+  const endDate = Date.parse(dates[1]) ? new Date(dates[1]) : null;
+
+  // Must check if the start and end dates are within range
+  return [
+    startDate && startDate.getFullYear() < CURRENT_YEAR - 20 ? null : startDate,
+    endDate && endDate.getFullYear() > CURRENT_YEAR ? null : endDate,
+  ];
+}
+
+export function localDateRangeToInterval(localRange: DateRange): string {
+  // This function takes in the dates selected from datepicker in local time, and displays them in UTC
+  // Shall Blueprint make any changes to the way dates are selected, this function will have to be reworked
+  const [localStartDate, localEndDate] = localRange;
+  return `${localStartDate ? localToUtcDate(localStartDate).toISOString().substring(0, 19) : ''}/${
+    localEndDate ? localToUtcDate(localEndDate).toISOString().substring(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;
+}
diff --git a/web-console/src/utils/index.tsx b/web-console/src/utils/index.tsx
index a47fc2c..0b40e73 100644
--- a/web-console/src/utils/index.tsx
+++ b/web-console/src/utils/index.tsx
@@ -18,6 +18,7 @@
 
 export * from './capabilities';
 export * from './column-metadata';
+export * from './date';
 export * from './druid-lookup';
 export * from './druid-query';
 export * from './general';
diff --git a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
index 866ea3d..2a1727c 100755
--- a/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
+++ b/web-console/src/views/datasource-view/__snapshots__/datasource-view.spec.tsx.snap
@@ -2,7 +2,7 @@
 
 exports[`data source view matches snapshot 1`] = `
 <div
-  className="datasource-view app-view no-chart"
+  className="datasource-view app-view"
 >
   <Memo(ViewControlBar)
     label="Datasources"
@@ -47,13 +47,13 @@ exports[`data source view matches snapshot 1`] = `
     <Blueprint3.Switch
       checked={false}
       disabled={false}
-      label="Show segment timeline"
+      label="Show unused"
       onChange={[Function]}
     />
     <Blueprint3.Switch
       checked={false}
       disabled={false}
-      label="Show unused"
+      label="Show segment timeline"
       onChange={[Function]}
     />
     <Memo(TableColumnSelector)
diff --git a/web-console/src/views/datasource-view/datasource-view.scss b/web-console/src/views/datasource-view/datasource-view.scss
index 41d53c9..b141d64 100644
--- a/web-console/src/views/datasource-view/datasource-view.scss
+++ b/web-console/src/views/datasource-view/datasource-view.scss
@@ -29,12 +29,13 @@
 
   .ReactTable {
     position: absolute;
+    top: $view-control-bar-height + $standard-padding;
     bottom: 0;
     width: 100%;
   }
 
-  &.show-chart {
-    .chart-container {
+  &.show-segment-timeline {
+    .segment-timeline {
       height: calc(50% - 55px);
       margin-top: 10px;
     }
@@ -43,10 +44,4 @@
       top: 50%;
     }
   }
-
-  &.no-chart {
-    .ReactTable {
-      top: $view-control-bar-height + $standard-padding;
-    }
-  }
 }
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx
index 1d4d27d..2901733 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -252,9 +252,7 @@ export interface DatasourcesViewState {
   useUnuseInterval: string;
   showForceCompact: boolean;
   hiddenColumns: LocalStorageBackedArray<string>;
-  showChart: boolean;
-  chartWidth: number;
-  chartHeight: number;
+  showSegmentTimeline: boolean;
 
   datasourceTableActionDialogId?: string;
   actions: BasicAction[];
@@ -356,9 +354,7 @@ ORDER BY 1`;
       hiddenColumns: new LocalStorageBackedArray<string>(
         LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
       ),
-      showChart: false,
-      chartWidth: window.innerWidth * 0.85,
-      chartHeight: window.innerHeight * 0.4,
+      showSegmentTimeline: false,
 
       actions: [],
     };
@@ -482,13 +478,6 @@ ORDER BY 1`;
     });
   }
 
-  private readonly handleResize = () => {
-    this.setState({
-      chartWidth: window.innerWidth * 0.85,
-      chartHeight: window.innerHeight * 0.4,
-    });
-  };
-
   private readonly refresh = (auto: any): void => {
     this.datasourceQueryManager.rerunLastQuery(auto);
     this.tiersQueryManager.rerunLastQuery(auto);
@@ -504,7 +493,6 @@ ORDER BY 1`;
     const { capabilities } = this.props;
     this.fetchDatasourceData();
     this.tiersQueryManager.runQuery(capabilities);
-    window.addEventListener('resize', this.handleResize);
   }
 
   componentWillUnmount(): void {
@@ -1399,16 +1387,16 @@ ORDER BY 1`;
     const {
       showUnused,
       hiddenColumns,
-      showChart,
-      chartHeight,
-      chartWidth,
+      showSegmentTimeline,
       datasourceTableActionDialogId,
       actions,
     } = this.state;
 
     return (
       <div
-        className={classNames('datasource-view app-view', showChart ? 'show-chart' : 'no-chart')}
+        className={classNames('datasource-view app-view', {
+          'show-segment-timeline': showSegmentTimeline,
+        })}
       >
         <ViewControlBar label="Datasources">
           <RefreshButton
@@ -1419,17 +1407,17 @@ ORDER BY 1`;
           />
           {this.renderBulkDatasourceActions()}
           <Switch
-            checked={showChart}
-            label="Show segment timeline"
-            onChange={() => this.setState({ showChart: !showChart })}
-            disabled={!capabilities.hasSqlOrCoordinatorAccess()}
-          />
-          <Switch
             checked={showUnused}
             label="Show unused"
             onChange={() => this.toggleUnused(showUnused)}
             disabled={!capabilities.hasCoordinatorAccess()}
           />
+          <Switch
+            checked={showSegmentTimeline}
+            label="Show segment timeline"
+            onChange={() => this.setState({ showSegmentTimeline: !showSegmentTimeline })}
+            disabled={!capabilities.hasSqlOrCoordinatorAccess()}
+          />
           <TableColumnSelector
             columns={tableColumns[capabilities.getMode()]}
             onChange={column =>
@@ -1444,15 +1432,7 @@ ORDER BY 1`;
             tableColumnsHidden={hiddenColumns.storedArray}
           />
         </ViewControlBar>
-        {showChart && (
-          <div className="chart-container">
-            <SegmentTimeline
-              capabilities={capabilities}
-              chartHeight={chartHeight}
-              chartWidth={chartWidth}
-            />
-          </div>
-        )}
+        {showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
         {this.renderDatasourceTable()}
         {datasourceTableActionDialogId && (
           <DatasourceTableActionDialog
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 7c3a51b..a33134d 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
@@ -40,6 +40,12 @@ exports[`segments-view matches snapshot 1`] = `
           text="View SQL query for table"
         />
       </Memo(MoreButton)>
+      <Blueprint3.Switch
+        checked={false}
+        disabled={false}
+        label="Show segment timeline"
+        onChange={[Function]}
+      />
       <Memo(TableColumnSelector)
         columns={
           Array [
diff --git a/web-console/src/views/segments-view/segments-view.scss b/web-console/src/views/segments-view/segments-view.scss
index c8b8814..d7b3100 100644
--- a/web-console/src/views/segments-view/segments-view.scss
+++ b/web-console/src/views/segments-view/segments-view.scss
@@ -32,4 +32,15 @@
       display: none;
     }
   }
+
+  &.show-segment-timeline {
+    .segment-timeline {
+      height: calc(50% - 55px);
+      margin-top: 10px;
+    }
+
+    .ReactTable {
+      top: 50%;
+    }
+  }
 }
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index a518fa0..33f03dd 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -16,8 +16,9 @@
  * limitations under the License.
  */
 
-import { Button, ButtonGroup, Intent, Label, MenuItem } from '@blueprintjs/core';
+import { Button, ButtonGroup, Intent, Label, MenuItem, Switch } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
+import classNames from 'classnames';
 import { SqlExpression, SqlRef } from 'druid-query-toolkit';
 import React from 'react';
 import ReactTable, { Filter } from 'react-table';
@@ -30,6 +31,7 @@ import {
   BracedText,
   MoreButton,
   RefreshButton,
+  SegmentTimeline,
   TableColumnSelector,
   ViewControlBar,
 } from '../../components';
@@ -160,6 +162,7 @@ export interface SegmentsViewState {
   terminateDatasourceId?: string;
   hiddenColumns: LocalStorageBackedArray<string>;
   groupByInterval: boolean;
+  showSegmentTimeline: boolean;
 }
 
 export class SegmentsView extends React.PureComponent<SegmentsViewProps, SegmentsViewState> {
@@ -251,6 +254,7 @@ END AS "partitioning"`,
         LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
       ),
       groupByInterval: false,
+      showSegmentTimeline: false,
     };
 
     this.segmentsQueryManager = new QueryManager({
@@ -745,13 +749,18 @@ END AS "partitioning"`,
       datasourceTableActionDialogId,
       actions,
       hiddenColumns,
+      showSegmentTimeline,
     } = this.state;
     const { capabilities } = this.props;
     const { groupByInterval } = this.state;
 
     return (
       <>
-        <div className="segments-view app-view">
+        <div
+          className={classNames('segments-view app-view', {
+            'show-segment-timeline': showSegmentTimeline,
+          })}
+        >
           <ViewControlBar label="Segments">
             <RefreshButton
               onRefresh={auto => this.segmentsQueryManager.rerunLastQuery(auto)}
@@ -779,6 +788,12 @@ END AS "partitioning"`,
               </Button>
             </ButtonGroup>
             {this.renderBulkSegmentsActions()}
+            <Switch
+              checked={showSegmentTimeline}
+              label="Show segment timeline"
+              onChange={() => this.setState({ showSegmentTimeline: !showSegmentTimeline })}
+              disabled={!capabilities.hasSqlOrCoordinatorAccess()}
+            />
             <TableColumnSelector
               columns={tableColumns[capabilities.getMode()]}
               onChange={column =>
@@ -793,6 +808,7 @@ END AS "partitioning"`,
               tableColumnsHidden={hiddenColumns.storedArray}
             />
           </ViewControlBar>
+          {showSegmentTimeline && <SegmentTimeline capabilities={capabilities} />}
           {this.renderSegmentsTable()}
         </div>
         {this.renderTerminateSegmentAction()}
diff --git a/web-console/src/visualization/__snapshots__/visualization.spec.tsx.snap b/web-console/src/visualization/__snapshots__/visualization.spec.tsx.snap
deleted file mode 100644
index 6883e41..0000000
--- a/web-console/src/visualization/__snapshots__/visualization.spec.tsx.snap
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Visualization BarUnit 1`] = `
-<svg>
-  <rect
-    class="bar-chart-unit"
-    height="10"
-    width="10"
-    x="10"
-    y="10"
-  />
-</svg>
-`;
-
-exports[`Visualization action barGroup 1`] = `
-<svg>
-  <g
-    class="chart-axis undefined"
-    transform="value"
-  />
-</svg>
-`;

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