mirror of https://github.com/linkerd/linkerd2.git
Remove the ScatterPlot code from /web (#1485)
Remove old unused graphs from the web code (scatter plot and line graph) and their associated css Files removed: web/app/css/line-graph.css web/app/css/list.css web/app/css/scatterplot.css web/app/css/version.css web/app/js/components/LineGraph.jsx web/app/js/components/ScatterPlot.jsx
This commit is contained in:
parent
1bf280b105
commit
3e73c096e9
|
@ -1,27 +0,0 @@
|
|||
@import 'styles.css';
|
||||
@import 'scatterplot.css';
|
||||
|
||||
.chart-line {
|
||||
fill: none;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.line {
|
||||
stroke: steelblue;
|
||||
}
|
||||
|
||||
.line-p50 {
|
||||
stroke: var(--latency-p50);
|
||||
}
|
||||
|
||||
.line-p95 {
|
||||
stroke: var(--latency-p95);
|
||||
}
|
||||
|
||||
.line-p99 {
|
||||
stroke: var(--latency-p99);
|
||||
}
|
||||
|
||||
.flash {
|
||||
fill: var(--pictonblue);
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
@import 'styles.css';
|
||||
|
||||
.summary-stat {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.line-graph {
|
||||
margin-top: var(--base-width);
|
||||
}
|
||||
|
||||
.scatterplot-display {
|
||||
font-size: 12px;
|
||||
|
||||
& .title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-top: var(--base-width);
|
||||
}
|
||||
|
||||
& .extremal-latencies {
|
||||
padding-bottom: var(--base-width);
|
||||
border-bottom: 1px solid #BDBDBD;
|
||||
}
|
||||
}
|
||||
|
||||
& .border-container {
|
||||
height: 151px; /* override height */
|
||||
}
|
||||
|
||||
& .border-container-content {
|
||||
height: 135px; /* override height */
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
.grid .tick, .x-axis .tick, .y-axis .tick, .axis-label {
|
||||
opacity: 0.5;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* horizontal grid lines */
|
||||
.tick line {
|
||||
stroke-width: 1px;
|
||||
stroke: #777;
|
||||
}
|
||||
|
||||
.y-axis-label {
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
/* hide the axis line */
|
||||
.x-axis path, .y-axis path {
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
circle.dot {
|
||||
stroke-width: 2px;
|
||||
fill-opacity: 0.7;
|
||||
stroke-opacity: 0.9;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
fill: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.overlay-tooltip {
|
||||
fill: #777;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.vertical-highlight {
|
||||
fill: steelblue;
|
||||
opacity: 0.1;
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import './../../css/line-graph.css';
|
||||
|
||||
const defaultSvgWidth = 238;
|
||||
const defaultSvgHeight = 72;
|
||||
const margin = { top: 6, right: 6, bottom: 6, left: 0 };
|
||||
|
||||
export default class LineGraph extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.getChartDimensions();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.initializeScales();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.svg = d3.select("." + this.props.containerClassName)
|
||||
.append("svg")
|
||||
.attr("width", this.state.svgWidth)
|
||||
.attr("height", this.state.svgHeight)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + this.state.margin.left + "," + this.state.margin.top + ")");
|
||||
this.xAxis = this.svg.append("g")
|
||||
.attr("transform", "translate(0," + this.state.height + ")");
|
||||
this.yAxis = this.svg.append("g");
|
||||
|
||||
this.loadingMessage = this.svg
|
||||
.append("text")
|
||||
.attr("transform",
|
||||
"translate(" + (this.state.width / 2 - 30) + "," + (this.state.height / 2) + ")");
|
||||
|
||||
this.updateScales();
|
||||
this.initializeGraph();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.lastUpdated === this.props.lastUpdated) {
|
||||
// control whether react re-renders the component
|
||||
// only rerender if the input data has changed
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateScales();
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
getChartDimensions() {
|
||||
let svgWidth = this.props.width || defaultSvgWidth;
|
||||
let svgHeight = this.props.height || defaultSvgHeight;
|
||||
|
||||
let width = svgWidth - margin.left - margin.right;
|
||||
let height = svgHeight - margin.top - margin.bottom;
|
||||
|
||||
return {
|
||||
svgWidth: svgWidth,
|
||||
svgHeight: svgHeight,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin
|
||||
};
|
||||
}
|
||||
|
||||
updateScales() {
|
||||
let data = this.props.data;
|
||||
let ymax = d3.max(data, d => parseFloat(d.value));
|
||||
let padding = 0;
|
||||
if (this.state.svgHeight) {
|
||||
padding = ymax / this.state.svgHeight;
|
||||
}
|
||||
this.xScale.domain(d3.extent(data, d => parseInt(d.timestamp)));
|
||||
this.yScale.domain([0-padding, ymax+padding]);
|
||||
}
|
||||
|
||||
initializeScales() {
|
||||
this.xScale = d3.scaleLinear().range([0, this.state.width]);
|
||||
this.yScale = d3.scaleLinear().range([this.state.height, 0]);
|
||||
|
||||
let x = this.xScale;
|
||||
let y = this.yScale;
|
||||
|
||||
// define the line
|
||||
this.line = d3.line()
|
||||
.x(d => x(d.timestamp))
|
||||
.y(d => y(d.value));
|
||||
}
|
||||
|
||||
initializeGraph() {
|
||||
if (_.isEmpty(this.props.data)) {
|
||||
this.loadingMessage.text("---");
|
||||
}
|
||||
|
||||
this.svg.select("path").remove();
|
||||
|
||||
let lineChart = this.svg.append("path")
|
||||
.attr("class", "chart-line line");
|
||||
|
||||
lineChart
|
||||
.attr("d", this.line(this.props.data));
|
||||
|
||||
this.svg.append("circle")
|
||||
.attr("class", "flash")
|
||||
.attr("flashing", "off")
|
||||
.style("opacity", 0)
|
||||
.attr("r", 6);
|
||||
|
||||
this.updateAxes();
|
||||
this.flashLatestDataPoint();
|
||||
}
|
||||
|
||||
updateGraph() {
|
||||
if (_.isEmpty(this.props.data)) {
|
||||
this.loadingMessage.style("opacity", 1);
|
||||
} else {
|
||||
this.loadingMessage.style("opacity", 0);
|
||||
}
|
||||
|
||||
this.svg.select(".line")
|
||||
.transition()
|
||||
.duration(450)
|
||||
.attr("d", this.line(this.props.data));
|
||||
|
||||
this.updateAxes();
|
||||
this.flashLatestDataPoint();
|
||||
}
|
||||
|
||||
updateAxes() {
|
||||
if (this.props.showAxes) {
|
||||
this.xAxis
|
||||
.call(d3.axisBottom(this.xScale)); // add x axis labels
|
||||
|
||||
this.yAxis
|
||||
.call(d3.axisLeft(this.yScale)); // add y axis labels
|
||||
}
|
||||
}
|
||||
|
||||
flashLatestDataPoint() {
|
||||
if (!this.props.flashLastDatapoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
let circle = this.svg.select("circle");
|
||||
if (_.isEmpty(this.props.data)) {
|
||||
circle.attr("flashing", "off").interrupt().style("opacity", 0);
|
||||
} else {
|
||||
let circleData = _.last(this.props.data);
|
||||
if (circle.attr("flashing") === "off") {
|
||||
circle
|
||||
.attr("flashing", "on")
|
||||
.transition()
|
||||
.on("start", function repeat() {
|
||||
d3.active(this)
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.style("opacity", 0.6)
|
||||
.transition()
|
||||
.duration(1000)
|
||||
.style("opacity", 0)
|
||||
.transition()
|
||||
.on("start", repeat);
|
||||
});
|
||||
}
|
||||
circle
|
||||
.attr("cx", () => this.xScale(circleData.timestamp))
|
||||
.attr("cy", () => this.yScale(circleData.value));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`line-graph ${this.props.containerClassName}`} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import './../../css/list.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
class Namespaces extends React.Component {
|
||||
|
|
|
@ -9,7 +9,6 @@ import { singularResource } from './util/Utils.js';
|
|||
import { Spin } from 'antd';
|
||||
import withREST from './util/withREST.jsx';
|
||||
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.js';
|
||||
import './../../css/list.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
const getResourceFromUrl = (match, pathPrefix) => {
|
||||
|
|
|
@ -9,7 +9,6 @@ import React from 'react';
|
|||
import { Spin } from 'antd';
|
||||
import withREST from './util/withREST.jsx';
|
||||
import { metricsPropType, processSingleResourceRollup } from './util/MetricUtils.js';
|
||||
import './../../css/list.css';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
export class ResourceListBase extends React.Component {
|
||||
|
|
|
@ -1,323 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { metricToFormatter } from './util/Utils.js';
|
||||
import React from 'react';
|
||||
import * as d3 from 'd3';
|
||||
import './../../css/scatterplot.css';
|
||||
|
||||
const defaultSvgWidth = 574;
|
||||
const defaultSvgHeight = 375;
|
||||
const margin = { top: 0, right: 0, bottom: 10, left: 0 };
|
||||
const baseWidth = 8;
|
||||
const circleRadius = 2 * baseWidth;
|
||||
const graphPadding = 3 * circleRadius;
|
||||
const highlightBarWidth = 3 * circleRadius;
|
||||
const successRateColorScale = d3.scaleQuantize()
|
||||
.domain([0, 1])
|
||||
.range(["#8B0000", "#FF6347", "#FF4500", "#FFA500","#008000"]);
|
||||
|
||||
export default class ScatterPlot extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.renderNodeTooltipDatum = this.renderNodeTooltipDatum.bind(this);
|
||||
this.state = this.getChartDimensions();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.initializeScales();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.svg = d3.select("." + this.props.containerClassName)
|
||||
.append("svg")
|
||||
.attr("class", "scatterplot")
|
||||
.attr("width", defaultSvgWidth)
|
||||
.attr("height", defaultSvgHeight)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
this.xAxis = this.svg.append("g")
|
||||
.attr("class", "x-axis")
|
||||
.attr("transform", "translate(0," + (this.state.height - graphPadding) + ")");
|
||||
|
||||
this.yAxis = this.svg.append("g")
|
||||
.attr("class", "y-axis")
|
||||
.attr("transform", "translate(" + this.state.width + ",0)");
|
||||
|
||||
this.sidebar = d3.select(".scatterplot-display")
|
||||
.append("div").attr("class", "sidebar-tooltip");
|
||||
|
||||
this.initializeVerticalHighlight();
|
||||
this.renderAxisLabels();
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.lastUpdated === this.props.lastUpdated) {
|
||||
// control whether react re-renders the component
|
||||
// only rerender if the input data has changed
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateGraph();
|
||||
}
|
||||
|
||||
initializeVerticalHighlight() {
|
||||
this.updateScales(this.props.data);
|
||||
|
||||
// highlight bar to show x position
|
||||
this.verticalHighlight = this.svg.append("rect")
|
||||
.attr("class", "vertical-highlight")
|
||||
.attr("width", highlightBarWidth)
|
||||
.attr("height", this.state.height);
|
||||
|
||||
// overlay on which to attach mouse events
|
||||
// attach this after all other items are attached otherwise they block mouse events
|
||||
this.overlay = this.svg.append("rect")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
.attr("class", "overlay")
|
||||
.attr("width", this.state.width)
|
||||
.attr("height", this.state.height);
|
||||
this.overlayNode = d3.select(".overlay").node();
|
||||
|
||||
this.overlayTooltip = this.svg.append("g")
|
||||
.attr("class", "overlay-tooltip")
|
||||
.attr("height", this.state.height);
|
||||
|
||||
this.highlightFirstDatapoint();
|
||||
}
|
||||
|
||||
highlightFirstDatapoint() {
|
||||
// when graph is initially loaded / reloaded, set highlight and sidebar to first datapoint
|
||||
let firstDatapoint = _.first(this.props.data);
|
||||
if (firstDatapoint) {
|
||||
let firstLatency = _.get(firstDatapoint, ["latency", "P99"]);
|
||||
let firstLatencyX = this.xScale(firstLatency);
|
||||
let nearestDatapoints = this.getNearbyDatapoints(firstLatencyX, this.props.data);
|
||||
|
||||
this.verticalHighlight
|
||||
.attr("transform", "translate(" + (firstLatencyX - (highlightBarWidth/2)) + ", 0)");
|
||||
this.renderSidebarTooltip(nearestDatapoints);
|
||||
this.overlayTooltip.text('');
|
||||
}
|
||||
}
|
||||
|
||||
getChartDimensions() {
|
||||
let svgWidth = this.props.width || defaultSvgWidth;
|
||||
let svgHeight = this.props.height || defaultSvgHeight;
|
||||
|
||||
let width = svgWidth - margin.left - margin.right;
|
||||
let height = svgHeight - margin.top - margin.bottom;
|
||||
|
||||
return {
|
||||
svgWidth: svgWidth,
|
||||
svgHeight: svgHeight,
|
||||
width: width,
|
||||
height: height,
|
||||
margin: margin
|
||||
};
|
||||
}
|
||||
|
||||
initializeScales() {
|
||||
this.xScale = d3.scaleLinear().range([graphPadding, this.state.width - graphPadding]);
|
||||
this.yScale = d3.scaleLinear().range([this.state.height - graphPadding, graphPadding]);
|
||||
}
|
||||
|
||||
updateScales(data) {
|
||||
this.xScale.domain(d3.extent(data, d => d.latency.P99));
|
||||
this.yScale.domain([0, 1]);
|
||||
|
||||
this.updateAxes();
|
||||
}
|
||||
|
||||
updateAxes() {
|
||||
let xAxis = d3.axisBottom(this.xScale)
|
||||
.ticks(5)
|
||||
.tickSize(5);
|
||||
this.xAxis.call(xAxis);
|
||||
|
||||
let yAxis = d3.axisLeft(this.yScale)
|
||||
.ticks(4)
|
||||
.tickSize(this.state.width)
|
||||
.tickFormat(metricToFormatter["SUCCESS_RATE"]);
|
||||
|
||||
// custom axis styling: https://bl.ocks.org/mbostock/3371592
|
||||
let customYAxis = g => {
|
||||
g.call(yAxis);
|
||||
g.select(".domain").remove();
|
||||
g.selectAll(".tick text")
|
||||
.attr("x", 4)
|
||||
.attr("dx", -10)
|
||||
.attr("dy", -4);
|
||||
};
|
||||
this.yAxis.call(customYAxis);
|
||||
}
|
||||
|
||||
renderAxisLabels() {
|
||||
// text label for the x axis
|
||||
this.svg.append("text")
|
||||
.attr("class", "axis-label x-axis-label")
|
||||
.attr("y", this.state.height - 12)
|
||||
.attr("x", 0)
|
||||
.text("p99 Latency (ms)");
|
||||
|
||||
// text label for the y axis
|
||||
this.svg.append("text")
|
||||
.attr("class", "axis-label y-axis-label")
|
||||
.attr("y", 20)
|
||||
.attr("x", this.state.width)
|
||||
.attr("dx", "-3em")
|
||||
.text("Success rate");
|
||||
}
|
||||
|
||||
getNearbyDatapoints(x, data) {
|
||||
// return nodes that have nearby x-coordinates
|
||||
let x0 = this.xScale.invert(x - highlightBarWidth);
|
||||
let x1 = this.xScale.invert(x + highlightBarWidth);
|
||||
|
||||
if (x0 === x1) {
|
||||
// handle case where all the x points are in one column
|
||||
let datapointsX = this.xScale(_.first(data).latency.P99);
|
||||
if (Math.abs(x - datapointsX < highlightBarWidth)) {
|
||||
return data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
return _(data).filter(d => {
|
||||
return d.latency.P99 <= x1 && d.latency.P99 >= x0;
|
||||
}).orderBy('successRate', 'desc').value();
|
||||
}
|
||||
}
|
||||
|
||||
updateGraph() {
|
||||
this.updateScales(this.props.data);
|
||||
|
||||
this.scatterPlot = this.svg.selectAll(".dot")
|
||||
.data(this.props.data);
|
||||
|
||||
this.scatterPlot.exit().remove();
|
||||
|
||||
let spNode = this.scatterPlot.node();
|
||||
|
||||
this.scatterPlot
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("class", "dot")
|
||||
.attr("r", circleRadius)
|
||||
.merge(this.scatterPlot) // newfangled d3 'update' selection
|
||||
.attr("cx", d => this.xScale(d.latency.P99))
|
||||
.attr("cy", d => this.yScale(d.successRate))
|
||||
.style("fill", d => successRateColorScale(d.successRate))
|
||||
.style("stroke", d => successRateColorScale(d.successRate))
|
||||
.on("mousemove", () => {
|
||||
if (spNode) {
|
||||
let currXPos = d3.mouse(spNode)[0];
|
||||
this.positionOverlayHighlightAndTooltip(currXPos);
|
||||
}
|
||||
});
|
||||
|
||||
this.highlightFirstDatapoint();
|
||||
this.overlay
|
||||
.on("mousemove", () => {
|
||||
let currXPos = d3.mouse(this.overlayNode)[0];
|
||||
this.positionOverlayHighlightAndTooltip(currXPos);
|
||||
});
|
||||
}
|
||||
|
||||
positionOverlayHighlightAndTooltip(currXPos) {
|
||||
let nearestDatapoints = this.getNearbyDatapoints(currXPos, this.props.data);
|
||||
this.renderOverlayTooltip(nearestDatapoints);
|
||||
this.renderSidebarTooltip(nearestDatapoints);
|
||||
this.verticalHighlight.attr("transform", "translate(" + (currXPos - highlightBarWidth / 2) + ", 0)");
|
||||
|
||||
let bbox = this.overlayTooltip.node().getBBox();
|
||||
|
||||
let overlayTooltipYPos = 0;
|
||||
let firstLabelPosition = _.isEmpty(nearestDatapoints) ? null : this.getTooltipLabelY(nearestDatapoints[0]);
|
||||
if (firstLabelPosition + bbox.height > this.state.height - 50) {
|
||||
// if there are a bunch of nodes at 0, the labels could extend below the chart
|
||||
// translate upward if this is the case
|
||||
overlayTooltipYPos -= bbox.height;
|
||||
|
||||
// re-render tooltip labels, squished together
|
||||
this.renderOverlayTooltip(nearestDatapoints, true);
|
||||
}
|
||||
|
||||
let overlayTooltipXPos = currXPos + highlightBarWidth / 2 + baseWidth;
|
||||
if (currXPos > defaultSvgWidth / 2) {
|
||||
// display tooltip to the left if we're on the RH side of the graph
|
||||
overlayTooltipXPos = currXPos - bbox.width - baseWidth - highlightBarWidth / 2;
|
||||
}
|
||||
|
||||
this.overlayTooltip
|
||||
.attr("transform", "translate(" + overlayTooltipXPos + ", " + overlayTooltipYPos + ")")
|
||||
.raise();
|
||||
}
|
||||
|
||||
renderSidebarTooltip(data) {
|
||||
let innerHtml = _.map(data, d => this.renderNodeTooltipDatum(d));
|
||||
this.sidebar.html(innerHtml.join(''));
|
||||
}
|
||||
|
||||
renderOverlayTooltip(data, ignoreNodeSpacing = false) {
|
||||
this.overlayTooltip.text('');
|
||||
let labelWithPosition = this.computeTooltipLabelPositions(data, ignoreNodeSpacing);
|
||||
|
||||
_.each(labelWithPosition, d => {
|
||||
this.overlayTooltip
|
||||
.append("text").text(d.name)
|
||||
.attr("x", 0)
|
||||
.attr("y", d.computedY);
|
||||
});
|
||||
}
|
||||
|
||||
getTooltipLabelY(datum) {
|
||||
// position the tooltip label roughly aligned with the center of the node
|
||||
return this.yScale(datum.successRate) + circleRadius / 2 - 5;
|
||||
}
|
||||
|
||||
computeTooltipLabelPositions(data, ignoreNodeSpacing = false) {
|
||||
// in the case that there are multiple nodes in the highlighted area,
|
||||
// try to position each label next to its corresponding node
|
||||
// if the nodes are too close together, simply list the node labels
|
||||
let positions = _.map(data, d => {
|
||||
return {
|
||||
name: d.name,
|
||||
computedY: this.getTooltipLabelY(d)
|
||||
};
|
||||
});
|
||||
|
||||
if (_.size(positions) > 1) {
|
||||
_.each(positions, (_d, i) => {
|
||||
if (i > 0) {
|
||||
if (positions[i].computedY - positions[i - 1].computedY < 10) {
|
||||
// labels are too close together, don't label at the node Y values
|
||||
ignoreNodeSpacing = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (ignoreNodeSpacing) {
|
||||
let basePos = positions[0].computedY;
|
||||
_.each(positions, (d, i) => {
|
||||
d.computedY = basePos + i * 15;
|
||||
});
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
renderNodeTooltipDatum(d) {
|
||||
let latency = metricToFormatter["LATENCY"](d.latency.P99);
|
||||
let sr = metricToFormatter["SUCCESS_RATE"](d.successRate);
|
||||
return `<div class="title">${d.name}</div><div>${latency}, ${sr}</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
// d3 selects the passed in container from this.props.containerClassName
|
||||
return _.isEmpty(this.props.data) ?
|
||||
<div className="clearfix no-data-msg">No data</div> : null;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ import { Link } from 'react-router-dom';
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withContext } from './util/AppContext.jsx';
|
||||
import './../../css/version.css';
|
||||
|
||||
class Version extends React.Component {
|
||||
static defaultProps = {
|
||||
|
|
Loading…
Reference in New Issue