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 5767d681883c..a30d62287619 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 000000000000..b58daecb3075 --- /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(); + + return ( + { + const [startDate, endDate] = selectedRange; + if (!startDate || !endDate) { + setIntermediateDateRange(selectedRange); + } else { + setIntermediateDateRange(undefined); + onChange(localToUtcDate(startDate), localToUtcDate(endDate)); + } + }} + /> + } + position={Position.BOTTOM_RIGHT} + > + } + /> + + ); +}); diff --git a/web-console/src/components/interval-input/interval-input.tsx b/web-console/src/components/interval-input/interval-input.tsx index 64866cf82190..138c60d57a9e 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={ { - 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 000000000000..7d98145d4b5e --- /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`] = ` + + + +`; 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 c7a96a16d9ff..5f76670f14e0 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`] = `
@@ -85,7 +85,7 @@ exports[`Segment Timeline matches snapshot 1`] = `
+ 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 1975c49e1f36..4bd75e6d117a 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 { 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 a879a73922c8..e8e62b5616b2 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 = ( @@ -32,14 +31,4 @@ describe('Visualization', () => { const { container } = render(barGroup); expect(container.firstChild).toMatchSnapshot(); }); - - it('action barGroup', () => { - const barGroup = ( - - null} /> - - ); - 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 4cb1fd8c6b19..8d783f67555a 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 ( { +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 = ( - - ); + const segmentTimeline = ; const { container } = render(segmentTimeline); expect(container.firstChild).toMatchSnapshot(); }); - - it('queries 3 months of data by default', () => { - const dataQueryManager = new MockDataQueryManager(); - const segmentTimeline = ( - - ); - 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 = ( - - ); - 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 ba48839f1bb3..8b771d585a8e 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; datasources: string[]; stackedData?: Record; @@ -47,13 +66,12 @@ interface SegmentTimelineState { activeDatasource: string | null; activeDataType: ActiveDataType; dataToRender: BarUnitData[]; - timeSpan: number; // by months loading: boolean; error?: Error; xScale: AxisScale | null; yScale: AxisScale | null; - dStart: Date; - dEnd: Date; + startDate: Date; + endDate: Date; } interface BarChartScales { @@ -61,22 +79,6 @@ interface BarChartScales { yScale: AxisScale; } -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 = scaleTime() - .domain(xDomain) + const xScale: AxisScale = scaleUtc() + .domain([startDate, endDate]) .range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]); const yScale: AxisScale = 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 (
@@ -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 ( - - this.setState(prevState => ({ - activeDatasource: prevState.activeDatasource ? null : datasource, - })) - } - activeDataType={activeDataType} - formatTick={(n: number) => this.formatTick(n)} - xScale={xScale} - yScale={yScale} - barWidth={barWidth} - /> + + + this.setState(prevState => ({ + activeDatasource: prevState.activeDatasource ? null : datasource, + })) + } + activeDataType={activeDataType} + formatTick={(n: number) => this.formatTick(n)} + xScale={xScale} + yScale={yScale} + barWidth={barWidth} + /> + ); } render(): JSX.Element { - const { datasources, activeDataType, activeDatasource, timeSpan } = this.state; + const { capabilities } = this.props; + const { datasources, activeDataType, activeDatasource, startDate, endDate } = this.state; return (
@@ -537,7 +544,7 @@ ORDER BY "start" DESC`; - + this.setState({ @@ -558,18 +565,16 @@ ORDER BY "start" DESC`; - - this.onTimeSpanChange(e.target.value)} - value={timeSpan} - fill - > - - - - - - + + { + this.setState({ startDate, endDate }, () => { + this.dataQueryManager.runQuery({ capabilities, startDate, endDate }); + }); + }} + />
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 fed00ae31dd5..26e5f5186b5f 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 c51146322317..7c772ef993fb 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(); - 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 ( -
- + setHoverOn(undefined)} > '') .tickSizeOuter(0)} /> + setHoverOn(e)} + hoverOn={hoverOn} + barWidth={barWidth} + /> formatTick(e))} /> - setHoverOn(undefined)}> - setHoverOn(e)} - hoverOn={hoverOn} - barWidth={barWidth} - /> - {hoverOn && ( - { - setHoverOn(undefined); - changeActiveDatasource(hoverOn.datasource ?? null); - }} - > - - - )} - - -
+ {hoverOn && ( + { + setHoverOn(undefined); + changeActiveDatasource(hoverOn.datasource ?? null); + }} + > + + + )} + + ); } return ( -
-
-
Datasource: {hoverOn ? hoverOn.datasource : ''}
-
Time: {hoverOn ? hoverOn.xValue : ''}
-
- {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${ - hoverOn ? formatTick(hoverOn.yValue!) : '' - }`} -
-
+
+ {hoverOn && ( + <> +
+
Datasource: {hoverOn.datasource}
+
Time: {hoverOn.xValue}
+
+ {`${activeDataType === 'countData' ? 'Count:' : 'Size:'} ${formatTick( + hoverOn.yValue!, + )}`} +
+
+ + )} {renderBarChart()}
); diff --git a/web-console/src/utils/date.spec.ts b/web-console/src/utils/date.spec.ts new file mode 100644 index 000000000000..843c144244ef --- /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 000000000000..be548f7d6560 --- /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 a47fc2c59e5a..0b40e734d9f6 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 866ea3dd750d..2a1727c6ad94 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`] = `
; - showChart: boolean; - chartWidth: number; - chartHeight: number; + showSegmentTimeline: boolean; datasourceTableActionDialogId?: string; actions: BasicAction[]; @@ -356,9 +354,7 @@ ORDER BY 1`; hiddenColumns: new LocalStorageBackedArray( 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 (
{this.renderBulkDatasourceActions()} - this.setState({ showChart: !showChart })} - disabled={!capabilities.hasSqlOrCoordinatorAccess()} - /> this.toggleUnused(showUnused)} disabled={!capabilities.hasCoordinatorAccess()} /> + this.setState({ showSegmentTimeline: !showSegmentTimeline })} + disabled={!capabilities.hasSqlOrCoordinatorAccess()} + /> @@ -1444,15 +1432,7 @@ ORDER BY 1`; tableColumnsHidden={hiddenColumns.storedArray} /> - {showChart && ( -
- -
- )} + {showSegmentTimeline && } {this.renderDatasourceTable()} {datasourceTableActionDialogId && ( + ; groupByInterval: boolean; + showSegmentTimeline: boolean; } export class SegmentsView extends React.PureComponent { @@ -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 ( <> -
+
this.segmentsQueryManager.rerunLastQuery(auto)} @@ -779,6 +788,12 @@ END AS "partitioning"`, {this.renderBulkSegmentsActions()} + this.setState({ showSegmentTimeline: !showSegmentTimeline })} + disabled={!capabilities.hasSqlOrCoordinatorAccess()} + /> @@ -793,6 +808,7 @@ END AS "partitioning"`, tableColumnsHidden={hiddenColumns.storedArray} /> + {showSegmentTimeline && } {this.renderSegmentsTable()}
{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 6883e412a649..000000000000 --- 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`] = ` - - - -`; - -exports[`Visualization action barGroup 1`] = ` - - - -`;