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
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,41 +17,14 @@
*/
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 { intervalToLocalDateRange, localDateRangeToInterval } from '../../utils';
+
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);
-}
-
-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) : ''}`;
-}
-
export interface IntervalInputProps {
interval: string;
placeholder: string | undefined;
@@ -72,11 +45,12 @@
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 @@
<label
class="bp3-label"
>
- Datasource:
+ Datasource
<span
class="bp3-text-muted"
@@ -132,7 +132,7 @@
<label
class="bp3-label"
>
- Period:
+ Interval
<span
class="bp3-text-muted"
@@ -141,56 +141,53 @@
<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 @@
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 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 @@
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 @@
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 97%
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 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 @@
});
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 @@
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 @@
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 @@
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 @@
}
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 @@
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 @@
size,
};
})
- .filter(a => beforeIso < a.start);
+ .filter(a => startIso < a.start);
}),
)
)
@@ -331,23 +336,23 @@
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 @@
}
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 @@
];
}
- 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 @@
};
}
- 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 @@
}
};
+ 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 @@
yScale,
data,
activeDatasource,
- dStart,
- dEnd,
+ startDate,
+ endDate,
} = this.state;
+
if (loading) {
return (
<div>
@@ -498,30 +499,36 @@
}
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 @@
</RadioGroup>
</FormGroup>
- <FormGroup label="Datasource:">
+ <FormGroup label="Datasource">
<HTMLSelect
onChange={(e: any) =>
this.setState({
@@ -558,18 +565,16 @@
</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/components/segment-timeline/stacked-bar-chart.tsx b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
new file mode 100644
index 0000000..7c772ef
--- /dev/null
+++ b/web-console/src/components/segment-timeline/stacked-bar-chart.tsx
@@ -0,0 +1,165 @@
+/*
+ * 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 { axisBottom, axisLeft, AxisScale } from 'd3-axis';
+import React, { useState } from 'react';
+
+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;
+ margin: BarChartMargin;
+ activeDataType?: string;
+ dataToRender: BarUnitData[];
+ changeActiveDatasource: (e: string | null) => void;
+ formatTick: (e: number) => string;
+ xScale: AxisScale<Date>;
+ yScale: AxisScale<number>;
+ barWidth: number;
+}
+
+export const StackedBarChart = React.memo(function StackedBarChart(props: StackedBarChartProps) {
+ const {
+ activeDataType,
+ svgWidth,
+ svgHeight,
+ margin,
+ formatTick,
+ xScale,
+ yScale,
+ dataToRender,
+ changeActiveDatasource,
+ barWidth,
+ } = props;
+ const [hoverOn, setHoverOn] = useState<HoveredBarInfo>();
+
+ const width = svgWidth - margin.left - margin.right;
+ const height = svgHeight - margin.top - margin.bottom;
+
+ function renderBarChart() {
+ return (
+ <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)"
+ 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(0, ${height})`}
+ scale={axisBottom(xScale)}
+ />
+ <ChartAxis
+ className="axis-y"
+ scale={axisLeft(yScale)
+ .ticks(5)
+ .tickFormat((e: number) => formatTick(e))}
+ />
+ {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="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 @@
<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 @@
useUnuseInterval: string;
showForceCompact: boolean;
hiddenColumns: LocalStorageBackedArray<string>;
- showChart: boolean;
- chartWidth: number;
- chartHeight: number;
+ showSegmentTimeline: boolean;
datasourceTableActionDialogId?: string;
actions: BasicAction[];
@@ -356,9 +354,7 @@
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 @@
});
}
- 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 @@
const { capabilities } = this.props;
this.fetchDatasourceData();
this.tiersQueryManager.runQuery(capabilities);
- window.addEventListener('resize', this.handleResize);
}
componentWillUnmount(): void {
@@ -1399,16 +1387,16 @@
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 @@
/>
{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 @@
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 @@
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 @@
BracedText,
MoreButton,
RefreshButton,
+ SegmentTimeline,
TableColumnSelector,
ViewControlBar,
} from '../../components';
@@ -160,6 +162,7 @@
terminateDatasourceId?: string;
hiddenColumns: LocalStorageBackedArray<string>;
groupByInterval: boolean;
+ showSegmentTimeline: boolean;
}
export class SegmentsView extends React.PureComponent<SegmentsViewProps, SegmentsViewState> {
@@ -251,6 +254,7 @@
LocalStorageKeys.SEGMENT_TABLE_COLUMN_SELECTION,
),
groupByInterval: false,
+ showSegmentTimeline: false,
};
this.segmentsQueryManager = new QueryManager({
@@ -745,13 +749,18 @@
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 @@
</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 @@
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>
-`;
diff --git a/web-console/src/visualization/stacked-bar-chart.tsx b/web-console/src/visualization/stacked-bar-chart.tsx
deleted file mode 100644
index c511463..0000000
--- a/web-console/src/visualization/stacked-bar-chart.tsx
+++ /dev/null
@@ -1,147 +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 { 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';
-
-interface StackedBarChartProps {
- svgWidth: number;
- svgHeight: number;
- margin: BarChartMargin;
- activeDataType?: string;
- dataToRender: BarUnitData[];
- changeActiveDatasource: (e: string | null) => void;
- formatTick: (e: number) => string;
- xScale: AxisScale<Date>;
- yScale: AxisScale<number>;
- 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,
- formatTick,
- xScale,
- yScale,
- dataToRender,
- changeActiveDatasource,
- barWidth,
- } = 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;
-
- function renderBarChart() {
- return (
- <div className="bar-chart-container">
- <svg
- width={width}
- height={height}
- viewBox={`0 0 ${svgWidth} ${svgHeight}`}
- preserveAspectRatio="xMinYMin meet"
- style={{ marginTop: '20px' }}
- >
- <ChartAxis
- className="gridline-x"
- transform="translate(60, 0)"
- scale={axisLeft(yScale)
- .ticks(5)
- .tickSize(-width)
- .tickFormat(() => '')
- .tickSizeOuter(0)}
- />
- <ChartAxis
- className="axis--x"
- transform={`translate(65, ${height})`}
- scale={axisBottom(xScale)}
- />
- <ChartAxis
- className="axis--y"
- transform="translate(60, 0)"
- 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>
- );
- }
-
- 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>
- {renderBarChart()}
- </div>
- );
-});