mirror of https://github.com/apache/druid.git
Web console: segment timeline (#8202)
* Add segment timeline chart * fix file * Fix bugs: no data handling & scaling problems * resolve conflict * changed package-lock * do not show by default * trust the interop * stricter type fixes * fix sasslint
This commit is contained in:
parent
ddec5ea82e
commit
645fca53d8
File diff suppressed because it is too large
Load Diff
|
@ -58,6 +58,7 @@
|
|||
"classnames": "^2.2.6",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"core-js": "^3.1.4",
|
||||
"d3": "^5.9.7",
|
||||
"d3-array": "^2.2.0",
|
||||
"druid-console": "^0.0.2",
|
||||
"file-saver": "^2.0.2",
|
||||
|
@ -82,6 +83,7 @@
|
|||
"@babel/preset-env": "^7.5.5",
|
||||
"@testing-library/react": "^8.0.7",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/d3": "^5.7.2",
|
||||
"@types/d3-array": "^2.0.0",
|
||||
"@types/enzyme": "^3.10.3",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.5",
|
||||
|
|
|
@ -35,3 +35,4 @@ export * from './view-control-bar/view-control-bar';
|
|||
export * from './clearable-input/clearable-input';
|
||||
export * from './refresh-button/refresh-button';
|
||||
export * from './timed-button/timed-button';
|
||||
export * from './segment-timeline/segment-timeline';
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.segment-timeline {
|
||||
display: grid;
|
||||
grid-template-columns: 85% 15%;
|
||||
height: 100%;
|
||||
margin-top: 10px;
|
||||
|
||||
.loader {
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.loading-error {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.bar-chart-tooltip {
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-data-text {
|
||||
position: absolute;
|
||||
left: 30vw;
|
||||
top: 15vh;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.side-control {
|
||||
padding-left: 1vw;
|
||||
padding-top: 5vh;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
/*
|
||||
* 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 { FormGroup, HTMLSelect, Radio, RadioGroup } from '@blueprintjs/core';
|
||||
import * as d3 from 'd3';
|
||||
import { AxisScale } from 'd3';
|
||||
import React from 'react';
|
||||
|
||||
import { formatBytes, queryDruidSql, QueryManager } from '../../utils/index';
|
||||
import { StackedBarChart } from '../../visualization/stacked-bar-chart';
|
||||
import { Loader } from '../loader/loader';
|
||||
|
||||
import './segment-timeline.scss';
|
||||
|
||||
interface SegmentTimelineProps extends React.Props<any> {
|
||||
chartHeight: number;
|
||||
chartWidth: number;
|
||||
}
|
||||
|
||||
interface SegmentTimelineState {
|
||||
data?: Record<string, any>;
|
||||
datasources: string[];
|
||||
stackedData?: Record<string, BarUnitData[]>;
|
||||
singleDatasourceData?: Record<string, Record<string, BarUnitData[]>>;
|
||||
activeDatasource: string | null;
|
||||
activeDataType: string; // "countData" || "sizeData"
|
||||
dataToRender: BarUnitData[];
|
||||
timeSpan: number; // by months
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
xScale: AxisScale<Date> | null;
|
||||
yScale: AxisScale<number> | null;
|
||||
dStart: Date;
|
||||
dEnd: Date;
|
||||
}
|
||||
|
||||
interface BarChartScales {
|
||||
xScale: AxisScale<Date>;
|
||||
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;
|
||||
}
|
||||
|
||||
export class SegmentTimeline extends React.Component<SegmentTimelineProps, SegmentTimelineState> {
|
||||
private dataQueryManager: QueryManager<null, any>;
|
||||
private datasourceQueryManager: QueryManager<null, any>;
|
||||
private colors = [
|
||||
'#b33040',
|
||||
'#d25c4d',
|
||||
'#f2b447',
|
||||
'#d9d574',
|
||||
'#4FAA7E',
|
||||
'#57ceff',
|
||||
'#789113',
|
||||
'#098777',
|
||||
'#b33040',
|
||||
'#d2757b',
|
||||
'#f29063',
|
||||
'#d9a241',
|
||||
'#80aa61',
|
||||
'#c4ff9e',
|
||||
'#915412',
|
||||
'#87606c',
|
||||
];
|
||||
private chartMargin = { top: 20, right: 10, bottom: 20, left: 10 };
|
||||
|
||||
constructor(props: SegmentTimelineProps) {
|
||||
super(props);
|
||||
const dStart = new Date();
|
||||
const dEnd = new Date();
|
||||
dStart.setMonth(dStart.getMonth() - 3);
|
||||
this.state = {
|
||||
data: {},
|
||||
datasources: [],
|
||||
stackedData: {},
|
||||
singleDatasourceData: {},
|
||||
dataToRender: [],
|
||||
activeDatasource: null,
|
||||
activeDataType: 'countData',
|
||||
timeSpan: 3,
|
||||
loading: true,
|
||||
xScale: null,
|
||||
yScale: null,
|
||||
dEnd: dEnd,
|
||||
dStart: dStart,
|
||||
};
|
||||
|
||||
this.dataQueryManager = new QueryManager({
|
||||
processQuery: async () => {
|
||||
const { timeSpan } = this.state;
|
||||
const query = `SELECT "start", "end", "datasource", COUNT(*) AS "count", sum(size) as "total_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`;
|
||||
const resp: any[] = await queryDruidSql({ query });
|
||||
const data = this.processRawData(resp);
|
||||
const stackedData = this.calculateStackedData(data);
|
||||
const singleDatasourceData = this.calculateSingleDatasourceData(data);
|
||||
return { data, stackedData, singleDatasourceData };
|
||||
},
|
||||
onStateChange: ({ result, loading, error }) => {
|
||||
this.setState({
|
||||
data: result ? result.data : undefined,
|
||||
stackedData: result ? result.stackedData : undefined,
|
||||
singleDatasourceData: result ? result.singleDatasourceData : undefined,
|
||||
loading,
|
||||
error,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.datasourceQueryManager = new QueryManager({
|
||||
processQuery: async () => {
|
||||
const query = `SELECT DISTINCT "datasource" FROM sys.segments`;
|
||||
const resp: any[] = await queryDruidSql({ query });
|
||||
const data = resp.map((r: any) => r.datasource);
|
||||
return data;
|
||||
},
|
||||
onStateChange: ({ result }) => {
|
||||
if (result == null) result = [];
|
||||
this.setState({
|
||||
datasources: result,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.dataQueryManager.runQuery(null);
|
||||
this.datasourceQueryManager.runQuery(null);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.dataQueryManager.terminate();
|
||||
this.datasourceQueryManager.terminate();
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
const scales: BarChartScales | undefined = this.calculateScales();
|
||||
let dataToRender: BarUnitData[] | undefined;
|
||||
dataToRender = activeDatasource
|
||||
? singleDatasourceData
|
||||
? singleDatasourceData[activeDataType][activeDatasource]
|
||||
: undefined
|
||||
: stackedData
|
||||
? stackedData[activeDataType]
|
||||
: undefined;
|
||||
|
||||
if (scales && dataToRender) {
|
||||
this.setState({
|
||||
dataToRender,
|
||||
xScale: scales.xScale,
|
||||
yScale: scales.yScale,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processRawData(data: any) {
|
||||
if (data === null) return [];
|
||||
const countData: Record<string, any> = {};
|
||||
const sizeData: Record<string, any> = {};
|
||||
data.forEach((entry: any) => {
|
||||
const start = entry.start;
|
||||
const day = start.split('T')[0];
|
||||
const datasource = entry.datasource;
|
||||
const count = entry.count;
|
||||
const segmentSize = entry['total_size'];
|
||||
if (countData[day] === undefined) {
|
||||
countData[day] = {
|
||||
day,
|
||||
[datasource]: count,
|
||||
total: count,
|
||||
};
|
||||
sizeData[day] = {
|
||||
day,
|
||||
[datasource]: segmentSize,
|
||||
total: segmentSize,
|
||||
};
|
||||
} else {
|
||||
const countDataEntry = countData[day][datasource];
|
||||
countData[day][datasource] = count + (countDataEntry === undefined ? 0 : countDataEntry);
|
||||
const sizeDataEntry = sizeData[day][datasource];
|
||||
sizeData[day][datasource] = segmentSize + (sizeDataEntry === undefined ? 0 : sizeDataEntry);
|
||||
countData[day].total += count;
|
||||
sizeData[day].total += segmentSize;
|
||||
}
|
||||
});
|
||||
const countDataArray = Object.keys(countData)
|
||||
.reverse()
|
||||
.map((time: any) => {
|
||||
return countData[time];
|
||||
});
|
||||
const sizeDataArray = Object.keys(sizeData)
|
||||
.reverse()
|
||||
.map((time: any) => {
|
||||
return sizeData[time];
|
||||
});
|
||||
return { countData: countDataArray, sizeData: sizeDataArray };
|
||||
}
|
||||
|
||||
private calculateStackedData(data: Record<string, any>): Record<string, BarUnitData[]> {
|
||||
const { datasources } = this.state;
|
||||
const newStackedData: Record<string, BarUnitData[]> = {};
|
||||
Object.keys(data).forEach((type: any) => {
|
||||
const stackedData: any = data[type].map((d: any) => {
|
||||
let y0 = 0;
|
||||
return datasources.map((datasource: string, i) => {
|
||||
const barUnitData = {
|
||||
x: d.day,
|
||||
y: d[datasource] === undefined ? 0 : d[datasource],
|
||||
y0,
|
||||
datasource,
|
||||
color: this.colors[i],
|
||||
};
|
||||
y0 += d[datasource] === undefined ? 0 : d[datasource];
|
||||
return barUnitData;
|
||||
});
|
||||
});
|
||||
newStackedData[type] = stackedData.flat();
|
||||
});
|
||||
return newStackedData;
|
||||
}
|
||||
|
||||
private calculateSingleDatasourceData(
|
||||
data: Record<string, any>,
|
||||
): Record<string, Record<string, BarUnitData[]>> {
|
||||
const { datasources } = this.state;
|
||||
const singleDatasourceData: Record<string, Record<string, BarUnitData[]>> = {};
|
||||
Object.keys(data).forEach(dataType => {
|
||||
singleDatasourceData[dataType] = {};
|
||||
datasources.forEach((datasource, i) => {
|
||||
const currentData = data[dataType];
|
||||
if (currentData.length === 0) return;
|
||||
const dataResult = currentData.map((d: any) => {
|
||||
let y = 0;
|
||||
if (d[datasource] !== undefined) {
|
||||
y = d[datasource];
|
||||
}
|
||||
return {
|
||||
x: d.day,
|
||||
y,
|
||||
datasource,
|
||||
color: this.colors[i],
|
||||
};
|
||||
});
|
||||
if (!dataResult.every((d: any) => d.y === 0)) {
|
||||
singleDatasourceData[dataType][datasource] = dataResult;
|
||||
}
|
||||
});
|
||||
});
|
||||
return singleDatasourceData;
|
||||
}
|
||||
|
||||
private calculateScales(): BarChartScales | undefined {
|
||||
const { chartWidth, chartHeight } = this.props;
|
||||
const {
|
||||
data,
|
||||
activeDataType,
|
||||
activeDatasource,
|
||||
singleDatasourceData,
|
||||
dStart,
|
||||
dEnd,
|
||||
} = 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
|
||||
? 0
|
||||
: activeData.reduce((max: any, d: any) => (max.total > d.total ? max : d)).total,
|
||||
];
|
||||
|
||||
if (
|
||||
activeDatasource !== null &&
|
||||
singleDatasourceData![activeDataType][activeDatasource] !== undefined
|
||||
) {
|
||||
yDomain = [
|
||||
0,
|
||||
singleDatasourceData![activeDataType][activeDatasource].reduce((max: any, d: any) =>
|
||||
max.y > d.y ? max : d,
|
||||
).y,
|
||||
];
|
||||
}
|
||||
|
||||
const xScale: AxisScale<Date> = d3
|
||||
.scaleTime()
|
||||
.domain(xDomain)
|
||||
.range([0, chartWidth - this.chartMargin.left - this.chartMargin.right]);
|
||||
|
||||
const yScale: AxisScale<number> = d3
|
||||
.scaleLinear()
|
||||
.rangeRound([chartHeight - this.chartMargin.top - this.chartMargin.bottom, 0])
|
||||
.domain(yDomain);
|
||||
|
||||
return {
|
||||
xScale,
|
||||
yScale,
|
||||
};
|
||||
}
|
||||
|
||||
onTimeSpanChange = (e: any) => {
|
||||
const dStart = new Date();
|
||||
const dEnd = new Date();
|
||||
dStart.setMonth(dStart.getMonth() - e);
|
||||
this.setState({
|
||||
timeSpan: e,
|
||||
loading: true,
|
||||
dStart,
|
||||
dEnd,
|
||||
});
|
||||
this.dataQueryManager.rerunLastQuery();
|
||||
};
|
||||
|
||||
formatTick = (n: number) => {
|
||||
const { activeDataType } = this.state;
|
||||
if (activeDataType === 'countData') {
|
||||
return n.toString();
|
||||
} else {
|
||||
return formatBytes(n);
|
||||
}
|
||||
};
|
||||
|
||||
renderStackedBarChart() {
|
||||
const { chartWidth, chartHeight } = this.props;
|
||||
const {
|
||||
loading,
|
||||
dataToRender,
|
||||
activeDataType,
|
||||
error,
|
||||
xScale,
|
||||
yScale,
|
||||
data,
|
||||
activeDatasource,
|
||||
dStart,
|
||||
dEnd,
|
||||
} = this.state;
|
||||
if (loading) {
|
||||
return (
|
||||
<div>
|
||||
<Loader loading={loading} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<span className={'no-data-text'}>Error when loading data: {error}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (xScale === null || yScale === null) {
|
||||
return (
|
||||
<div>
|
||||
<span className={'no-data-text'}>Error when calculating scales</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (data![activeDataType].length === 0) {
|
||||
return (
|
||||
<div>
|
||||
<span className={'no-data-text'}>No data available for the time span selected</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
activeDatasource !== null &&
|
||||
data![activeDataType].every((d: any) => d[activeDatasource] === undefined)
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
<span className={'no-data-text'}>
|
||||
No data available for <i>{activeDatasource}</i>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const millisecondsPerDay = 24 * 60 * 60 * 1000;
|
||||
const barCounts = (dEnd.getTime() - dStart.getTime()) / millisecondsPerDay;
|
||||
const barWidth = (chartWidth - this.chartMargin.left - this.chartMargin.right) / barCounts;
|
||||
return (
|
||||
<StackedBarChart
|
||||
dataToRender={dataToRender}
|
||||
svgHeight={chartHeight}
|
||||
svgWidth={chartWidth}
|
||||
margin={this.chartMargin}
|
||||
changeActiveDatasource={(e: string) => this.setState({ activeDatasource: e })}
|
||||
activeDataType={activeDataType}
|
||||
formatTick={(n: number) => this.formatTick(n)}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
barWidth={barWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { datasources, activeDataType, activeDatasource, timeSpan } = this.state;
|
||||
|
||||
return (
|
||||
<div className={'segment-timeline app-view'}>
|
||||
{this.renderStackedBarChart()}
|
||||
<div className={'side-control'}>
|
||||
<FormGroup>
|
||||
<RadioGroup
|
||||
onChange={(e: any) => this.setState({ activeDataType: e.target.value })}
|
||||
selectedValue={activeDataType}
|
||||
>
|
||||
<Radio label={'Segment count'} value={'countData'} />
|
||||
<Radio label={'Total size'} value={'sizeData'} />
|
||||
</RadioGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label={'Datasource:'}>
|
||||
<HTMLSelect
|
||||
onChange={(e: any) =>
|
||||
this.setState({
|
||||
activeDatasource: e.target.value === 'all' ? null : e.target.value,
|
||||
})
|
||||
}
|
||||
value={activeDatasource == null ? 'all' : activeDatasource}
|
||||
fill
|
||||
>
|
||||
<option value={'all'}>Show all</option>
|
||||
{datasources.map(d => {
|
||||
return (
|
||||
<option key={d} value={d}>
|
||||
{d}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,11 @@ exports[`data source view matches snapshot 1`] = `
|
|||
onClick={[Function]}
|
||||
text="Go to SQL"
|
||||
/>
|
||||
<Blueprint3.Switch
|
||||
checked={false}
|
||||
label="Show segment timeline"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
<Blueprint3.Switch
|
||||
checked={false}
|
||||
label="Show disabled"
|
||||
|
@ -58,7 +63,7 @@ exports[`data source view matches snapshot 1`] = `
|
|||
TrComponent={[Function]}
|
||||
TrGroupComponent={[Function]}
|
||||
aggregatedKey="_aggregated"
|
||||
className=""
|
||||
className="-striped -highlight full-height"
|
||||
collapseOnDataChange={true}
|
||||
collapseOnPageChange={true}
|
||||
collapseOnSortingChange={true}
|
||||
|
|
|
@ -21,15 +21,25 @@
|
|||
.data-sources-view {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
.clickable-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 40vh;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ReactTable {
|
||||
position: absolute;
|
||||
top: $view-control-bar-height + $standard-padding;
|
||||
top: 50%; //$view-control-bar-height + $standard-padding;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
&.full-height {
|
||||
top: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
import { Button, FormGroup, InputGroup, Intent, Switch } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import axios from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import ReactTable, { Filter } from 'react-table';
|
||||
|
||||
|
@ -30,6 +31,7 @@ import {
|
|||
ViewControlBar,
|
||||
} from '../../components';
|
||||
import { ActionIcon } from '../../components/action-icon/action-icon';
|
||||
import { SegmentTimeline } from '../../components/segment-timeline/segment-timeline';
|
||||
import { AsyncActionDialog, CompactionDialog, RetentionDialog } from '../../dialogs';
|
||||
import { AppToaster } from '../../singletons/toaster';
|
||||
import {
|
||||
|
@ -123,6 +125,9 @@ export interface DatasourcesViewState {
|
|||
dropReloadAction: 'drop' | 'reload';
|
||||
dropReloadInterval: string;
|
||||
hiddenColumns: LocalStorageBackedArray<string>;
|
||||
showChart: boolean;
|
||||
chartWidth: number;
|
||||
chartHeight: number;
|
||||
}
|
||||
|
||||
export class DatasourcesView extends React.PureComponent<
|
||||
|
@ -175,6 +180,9 @@ GROUP BY 1`;
|
|||
hiddenColumns: new LocalStorageBackedArray<string>(
|
||||
LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
|
||||
),
|
||||
showChart: false,
|
||||
chartWidth: window.innerWidth * 0.85,
|
||||
chartHeight: window.innerHeight * 0.4,
|
||||
};
|
||||
|
||||
this.datasourceQueryManager = new QueryManager({
|
||||
|
@ -252,9 +260,31 @@ GROUP BY 1`;
|
|||
});
|
||||
}
|
||||
|
||||
private handleResize = () => {
|
||||
this.setState({
|
||||
chartWidth: window.innerWidth * 0.85,
|
||||
chartHeight: window.innerHeight * 0.4,
|
||||
});
|
||||
};
|
||||
|
||||
private refresh = (auto: any): void => {
|
||||
this.datasourceQueryManager.rerunLastQuery(auto);
|
||||
// this looks ugly, but it forces the chart to re-render when refresh is clicked
|
||||
this.setState(
|
||||
{
|
||||
showChart: !this.state.showChart,
|
||||
},
|
||||
() =>
|
||||
this.setState({
|
||||
showChart: !this.state.showChart,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
const { noSqlMode } = this.props;
|
||||
this.datasourceQueryManager.runQuery(noSqlMode);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
|
@ -572,6 +602,7 @@ GROUP BY 1`;
|
|||
datasourcesFilter,
|
||||
showDisabled,
|
||||
hiddenColumns,
|
||||
showChart,
|
||||
} = this.state;
|
||||
let data = datasources || [];
|
||||
if (!showDisabled) {
|
||||
|
@ -790,6 +821,7 @@ GROUP BY 1`;
|
|||
},
|
||||
]}
|
||||
defaultPageSize={50}
|
||||
className={classNames(`-striped -highlight`, showChart ? '' : 'full-height')}
|
||||
/>
|
||||
{this.renderDropDataAction()}
|
||||
{this.renderEnableAction()}
|
||||
|
@ -803,13 +835,15 @@ GROUP BY 1`;
|
|||
|
||||
render(): JSX.Element {
|
||||
const { goToQuery, noSqlMode } = this.props;
|
||||
const { showDisabled, hiddenColumns } = this.state;
|
||||
const { showDisabled, hiddenColumns, showChart, chartHeight, chartWidth } = this.state;
|
||||
|
||||
return (
|
||||
<div className="data-sources-view app-view">
|
||||
<ViewControlBar label="Datasources">
|
||||
<RefreshButton
|
||||
onRefresh={auto => this.datasourceQueryManager.rerunLastQuery(auto)}
|
||||
onRefresh={auto => {
|
||||
this.refresh(auto);
|
||||
}}
|
||||
localStorageKey={LocalStorageKeys.DATASOURCES_REFRESH_RATE}
|
||||
/>
|
||||
{!noSqlMode && (
|
||||
|
@ -819,6 +853,11 @@ GROUP BY 1`;
|
|||
onClick={() => goToQuery(DatasourcesView.DATASOURCE_SQL)}
|
||||
/>
|
||||
)}
|
||||
<Switch
|
||||
checked={showChart}
|
||||
label="Show segment timeline"
|
||||
onChange={() => this.setState({ showChart: !showChart })}
|
||||
/>
|
||||
<Switch
|
||||
checked={showDisabled}
|
||||
label="Show disabled"
|
||||
|
@ -830,6 +869,11 @@ GROUP BY 1`;
|
|||
tableColumnsHidden={hiddenColumns.storedArray}
|
||||
/>
|
||||
</ViewControlBar>
|
||||
{showChart && (
|
||||
<div className={'chart-container'}>
|
||||
<SegmentTimeline chartHeight={chartHeight} chartWidth={chartWidth} />
|
||||
</div>
|
||||
)}
|
||||
{this.renderDatasourceTable()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { 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';
|
||||
|
||||
interface BarGroupProps extends React.Props<any> {
|
||||
dataToRender: BarUnitData[];
|
||||
changeActiveDatasource: (e: string) => void;
|
||||
formatTick: (e: number) => string;
|
||||
xScale: AxisScale<Date>;
|
||||
yScale: AxisScale<number>;
|
||||
barWidth: number;
|
||||
onHoverBar?: (e: any) => void;
|
||||
offHoverBar?: () => void;
|
||||
hoverOn?: HoveredBarInfo | null;
|
||||
}
|
||||
|
||||
interface BarGroupState {}
|
||||
|
||||
export class BarGroup extends React.Component<BarGroupProps, BarGroupState> {
|
||||
constructor(props: BarGroupProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: BarGroupProps, nextState: BarGroupState): boolean {
|
||||
if (nextState === null) console.log(nextState);
|
||||
return nextProps.hoverOn === this.props.hoverOn;
|
||||
}
|
||||
|
||||
render(): JSX.Element[] | null {
|
||||
const {
|
||||
dataToRender,
|
||||
changeActiveDatasource,
|
||||
xScale,
|
||||
yScale,
|
||||
onHoverBar,
|
||||
barWidth,
|
||||
} = this.props;
|
||||
if (dataToRender === undefined) return null;
|
||||
|
||||
return dataToRender.map((entry: BarUnitData, i: number) => {
|
||||
const y0 = yScale(entry.y0 || 0) || 0;
|
||||
const x = xScale(new Date(entry.x));
|
||||
const y = yScale((entry.y0 || 0) + entry.y) || 0;
|
||||
const height = y0 - y;
|
||||
const barInfo: HoveredBarInfo = {
|
||||
xCoordinate: x,
|
||||
yCoordinate: y,
|
||||
height,
|
||||
datasource: entry.datasource,
|
||||
xValue: entry.x,
|
||||
yValue: entry.y,
|
||||
};
|
||||
return (
|
||||
<BarUnit
|
||||
key={i}
|
||||
x={x}
|
||||
y={y}
|
||||
width={barWidth}
|
||||
height={height}
|
||||
style={{ fill: entry.color }}
|
||||
onClick={() => changeActiveDatasource(entry.datasource)}
|
||||
onHover={() => onHoverBar && onHoverBar(barInfo)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.bar-chart-unit {
|
||||
transform: translateX(65px);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
|
||||
import './bar-unit.scss';
|
||||
|
||||
interface BarChartUnitProps extends React.Props<any> {
|
||||
x: number | undefined;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
style?: any;
|
||||
onClick?: () => void;
|
||||
onHover?: () => void;
|
||||
offHover?: () => void;
|
||||
}
|
||||
|
||||
interface BarChartUnitState {}
|
||||
|
||||
export class BarUnit extends React.Component<BarChartUnitProps, BarChartUnitState> {
|
||||
constructor(props: BarChartUnitProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { x, y, width, height, style, onClick, onHover, offHover } = this.props;
|
||||
return (
|
||||
<g
|
||||
className={`bar-chart-unit`}
|
||||
onClick={onClick}
|
||||
onMouseOver={onHover}
|
||||
onMouseLeave={offHover}
|
||||
>
|
||||
<rect x={x} y={y} width={width} height={height} style={style} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 * as d3 from 'd3';
|
||||
import React from 'react';
|
||||
|
||||
interface ChartAxisProps extends React.Props<any> {
|
||||
transform: string;
|
||||
scale: any;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ChartAxisState {}
|
||||
|
||||
export class ChartAxis extends React.Component<ChartAxisProps, ChartAxisState> {
|
||||
constructor(props: ChartAxisProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { transform, scale, className } = this.props;
|
||||
return (
|
||||
<g
|
||||
className={`axis ${className}`}
|
||||
transform={transform}
|
||||
ref={node => d3.select(node).call(scale)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.bar-chart {
|
||||
.hovered-bar {
|
||||
fill: transparent;
|
||||
stroke: #ffffff;
|
||||
stroke-width: 1.5px;
|
||||
transform: translateX(65px);
|
||||
}
|
||||
|
||||
.gridline-x {
|
||||
line {
|
||||
stroke-dasharray: 5, 5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 * as d3 from 'd3';
|
||||
import { AxisScale } from 'd3';
|
||||
import React 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 extends React.Props<any> {
|
||||
svgWidth: number;
|
||||
svgHeight: number;
|
||||
margin: BarChartMargin;
|
||||
activeDataType?: string;
|
||||
dataToRender: BarUnitData[];
|
||||
changeActiveDatasource: (e: string) => void;
|
||||
formatTick: (e: number) => string;
|
||||
xScale: AxisScale<Date>;
|
||||
yScale: AxisScale<number>;
|
||||
barWidth: number;
|
||||
}
|
||||
|
||||
interface StackedBarChartState {
|
||||
width: number;
|
||||
height: number;
|
||||
hoverOn?: HoveredBarInfo | null;
|
||||
}
|
||||
|
||||
export interface HoveredBarInfo {
|
||||
xCoordinate?: number;
|
||||
yCoordinate?: number;
|
||||
height?: number;
|
||||
width?: number;
|
||||
datasource?: string;
|
||||
xValue?: number;
|
||||
yValue?: number;
|
||||
}
|
||||
|
||||
export class StackedBarChart extends React.Component<StackedBarChartProps, StackedBarChartState> {
|
||||
constructor(props: StackedBarChartProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
width: this.props.svgWidth - this.props.margin.left - this.props.margin.right,
|
||||
height: this.props.svgHeight - this.props.margin.bottom - this.props.margin.top,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: StackedBarChartProps): void {
|
||||
if (nextProps !== this.props) {
|
||||
this.setState({
|
||||
width: nextProps.svgWidth - this.props.margin.left - this.props.margin.right,
|
||||
height: nextProps.svgHeight - this.props.margin.bottom - this.props.margin.top,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderBarChart() {
|
||||
const {
|
||||
svgWidth,
|
||||
svgHeight,
|
||||
formatTick,
|
||||
xScale,
|
||||
yScale,
|
||||
dataToRender,
|
||||
changeActiveDatasource,
|
||||
barWidth,
|
||||
} = this.props;
|
||||
const { width, height, hoverOn } = this.state;
|
||||
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={d3
|
||||
.axisLeft(yScale)
|
||||
.ticks(5)
|
||||
.tickSize(-width)
|
||||
.tickFormat(() => '')
|
||||
.tickSizeOuter(0)}
|
||||
/>
|
||||
<ChartAxis
|
||||
className={'axis--x'}
|
||||
transform={`translate(65, ${height})`}
|
||||
scale={d3.axisBottom(xScale)}
|
||||
/>
|
||||
<ChartAxis
|
||||
className={'axis--y'}
|
||||
transform={'translate(60, 0)'}
|
||||
scale={d3
|
||||
.axisLeft(yScale)
|
||||
.ticks(5)
|
||||
.tickFormat((e: number) => formatTick(e))}
|
||||
/>
|
||||
<g className="bars-group" onMouseLeave={() => this.setState({ hoverOn: null })}>
|
||||
<BarGroup
|
||||
dataToRender={dataToRender}
|
||||
changeActiveDatasource={changeActiveDatasource}
|
||||
formatTick={formatTick}
|
||||
xScale={xScale}
|
||||
yScale={yScale}
|
||||
onHoverBar={(e: HoveredBarInfo) => this.setState({ hoverOn: e })}
|
||||
hoverOn={hoverOn}
|
||||
barWidth={barWidth}
|
||||
/>
|
||||
{hoverOn && (
|
||||
<g
|
||||
className={'hovered-bar'}
|
||||
onClick={() => {
|
||||
this.setState({ hoverOn: null });
|
||||
changeActiveDatasource(hoverOn.datasource as string);
|
||||
}}
|
||||
>
|
||||
<rect
|
||||
x={hoverOn.xCoordinate}
|
||||
y={hoverOn.yCoordinate}
|
||||
width={barWidth}
|
||||
height={hoverOn.height}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { activeDataType, formatTick } = this.props;
|
||||
const { hoverOn } = this.state;
|
||||
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 as number) : ''
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
{this.renderBarChart()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue