From f1b14c587f09d651b0e9f9ccd604cb22643ca269 Mon Sep 17 00:00:00 2001 From: Will Xu <2bethere@gmail.com> Date: Sat, 30 May 2020 16:27:25 -0700 Subject: [PATCH 1/2] Segment timeline doesn't show results older than 3 months --- .../src/components/segment-timeline/segment-timeline.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx index 9da1c4c2ee31..303e8bb262c4 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -394,14 +394,16 @@ ORDER BY "start" DESC`; onTimeSpanChange = (e: any) => { const dStart = new Date(); const dEnd = new Date(); - dStart.setMonth(dStart.getMonth() - e); + const capabilities = this.props.capabilities; + const timeSpan = parseInt(e, 10) || 3; + dStart.setMonth(dStart.getMonth() - timeSpan); this.setState({ - timeSpan: e, + timeSpan: timeSpan, loading: true, dStart, dEnd, }); - this.dataQueryManager.rerunLastQuery(); + this.dataQueryManager.runQuery({ capabilities, timeSpan }); }; formatTick = (n: number) => { From c75db09d11d989bca399489f877b0ae12e59fee8 Mon Sep 17 00:00:00 2001 From: Will Xu <2bethere@gmail.com> Date: Sat, 27 Jun 2020 15:36:53 -0700 Subject: [PATCH 2/2] Adoption testing patch for web segment timeline view and also refactoring default time config --- .../segment-timeline.spec.tsx | 67 ++++++++- .../segment-timeline/segment-timeline.tsx | 136 +++++++++--------- 2 files changed, 137 insertions(+), 66 deletions(-) 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 9b18d67e68e2..83440d314613 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.spec.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.spec.tsx @@ -17,18 +17,81 @@ */ import { render } from '@testing-library/react'; +import { mount } from 'enzyme'; import React from 'react'; +import { QueryManager } from '../../utils'; import { Capabilities } from '../../utils/capabilities'; import { SegmentTimeline } from './segment-timeline'; describe('Segment Timeline', () => { it('matches snapshot', () => { - const tableColumn = ( + const segmentTimeline = ( ); - const { container } = render(tableColumn); + 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({ + 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 303e8bb262c4..e6e7502c9cc0 100644 --- a/web-console/src/components/segment-timeline/segment-timeline.tsx +++ b/web-console/src/components/segment-timeline/segment-timeline.tsx @@ -33,6 +33,9 @@ interface SegmentTimelineProps { capabilities: Capabilities; chartHeight: number; chartWidth: number; + + // For testing: + dataQueryManager?: QueryManager<{ capabilities: Capabilities; timeSpan: number }, any>; } interface SegmentTimelineState { @@ -81,6 +84,8 @@ interface IntervalRow { size: number; } +const DEFAULT_TIME_SPAN_MONTHS = 3; + export class SegmentTimeline extends React.PureComponent< SegmentTimelineProps, SegmentTimelineState @@ -219,7 +224,7 @@ export class SegmentTimeline extends React.PureComponent< super(props); const dStart = new Date(); const dEnd = new Date(); - dStart.setMonth(dStart.getMonth() - 3); + dStart.setMonth(dStart.getMonth() - DEFAULT_TIME_SPAN_MONTHS); this.state = { data: {}, datasources: [], @@ -228,7 +233,7 @@ export class SegmentTimeline extends React.PureComponent< dataToRender: [], activeDatasource: null, activeDataType: 'countData', - timeSpan: 3, + timeSpan: DEFAULT_TIME_SPAN_MONTHS, loading: true, xScale: null, yScale: null, @@ -236,73 +241,76 @@ export class SegmentTimeline extends React.PureComponent< dStart: dStart, }; - this.dataQueryManager = new QueryManager({ - processQuery: async ({ capabilities, timeSpan }) => { - let intervals: IntervalRow[]; - let datasources: string[]; - if (capabilities.hasSql()) { - const query = `SELECT + this.dataQueryManager = + props.dataQueryManager || + new QueryManager({ + processQuery: async ({ capabilities, timeSpan }) => { + let intervals: IntervalRow[]; + let datasources: string[]; + if (capabilities.hasSql()) { + const query = ` +SELECT "start", "end", "datasource", - COUNT(*) AS "count", SUM("size") as "size" +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') GROUP BY 1, 2, 3 ORDER BY "start" DESC`; - intervals = await queryDruidSql({ query }); - datasources = uniq(intervals.map(r => r.datasource)); - } else if (capabilities.hasCoordinatorAccess()) { - const before = new Date(); - before.setMonth(before.getMonth() - timeSpan); - const beforeIso = before.toISOString(); - - datasources = (await axios.get(`/druid/coordinator/v1/datasources`)).data; - intervals = (await Promise.all( - datasources.map(async datasource => { - const intervalMap = (await axios.get( - `/druid/coordinator/v1/datasources/${datasource}/intervals?simple`, - )).data; - - return Object.keys(intervalMap) - .map(interval => { - const [start, end] = interval.split('/'); - const { count, size } = intervalMap[interval]; - return { - start, - end, - datasource, - count, - size, - }; - }) - .filter(a => beforeIso < a.start); - }), - )) - .flat() - .sort((a, b) => b.start.localeCompare(a.start)); - } else { - throw new Error(`must have SQL or coordinator access`); - } + intervals = await queryDruidSql({ query }); + datasources = uniq(intervals.map(r => r.datasource)); + } else if (capabilities.hasCoordinatorAccess()) { + const before = new Date(); + before.setMonth(before.getMonth() - timeSpan); + const beforeIso = before.toISOString(); + + datasources = (await axios.get(`/druid/coordinator/v1/datasources`)).data; + intervals = (await Promise.all( + datasources.map(async datasource => { + const intervalMap = (await axios.get( + `/druid/coordinator/v1/datasources/${datasource}/intervals?simple`, + )).data; + + return Object.keys(intervalMap) + .map(interval => { + const [start, end] = interval.split('/'); + const { count, size } = intervalMap[interval]; + return { + start, + end, + datasource, + count, + size, + }; + }) + .filter(a => beforeIso < a.start); + }), + )) + .flat() + .sort((a, b) => b.start.localeCompare(a.start)); + } else { + throw new Error(`must have SQL or coordinator access`); + } - const data = SegmentTimeline.processRawData(intervals); - const stackedData = SegmentTimeline.calculateStackedData(data, datasources); - const singleDatasourceData = SegmentTimeline.calculateSingleDatasourceData( - data, - datasources, - ); - return { data, datasources, stackedData, singleDatasourceData }; - }, - onStateChange: ({ result, loading, error }) => { - this.setState({ - data: result ? result.data : undefined, - datasources: result ? result.datasources : [], - stackedData: result ? result.stackedData : undefined, - singleDatasourceData: result ? result.singleDatasourceData : undefined, - loading, - error, - }); - }, - }); + const data = SegmentTimeline.processRawData(intervals); + const stackedData = SegmentTimeline.calculateStackedData(data, datasources); + const singleDatasourceData = SegmentTimeline.calculateSingleDatasourceData( + data, + datasources, + ); + return { data, datasources, stackedData, singleDatasourceData }; + }, + onStateChange: ({ result, loading, error }) => { + this.setState({ + data: result ? result.data : undefined, + datasources: result ? result.datasources : [], + stackedData: result ? result.stackedData : undefined, + singleDatasourceData: result ? result.singleDatasourceData : undefined, + loading, + error, + }); + }, + }); } componentDidMount(): void { @@ -395,10 +403,10 @@ ORDER BY "start" DESC`; const dStart = new Date(); const dEnd = new Date(); const capabilities = this.props.capabilities; - const timeSpan = parseInt(e, 10) || 3; + const timeSpan = parseInt(e, 10) || DEFAULT_TIME_SPAN_MONTHS; dStart.setMonth(dStart.getMonth() - timeSpan); this.setState({ - timeSpan: timeSpan, + timeSpan: e, loading: true, dStart, dEnd,