mirror of
https://github.com/yldio/copilot.git
synced 2024-12-29 05:10:05 +02:00
feat: initial metrics implementation
This commit is contained in:
parent
0d8a282248
commit
1eac90c79a
@ -21,6 +21,7 @@
|
||||
"dependencies": {
|
||||
"apollo": "^0.2.2",
|
||||
"apr-intercept": "^1.0.4",
|
||||
"chart.js": "^2.6.0",
|
||||
"constant-case": "^2.0.0",
|
||||
"force-array": "^3.1.0",
|
||||
"graphql-tag": "^2.4.0",
|
||||
|
@ -1,2 +1,3 @@
|
||||
export { default as ServiceScale } from './scale';
|
||||
export { default as ServiceDelete } from './delete';
|
||||
export { default as ServiceMetrics} from './metrics';
|
||||
|
35
packages/cp-frontend/src/components/service/metrics.js
Normal file
35
packages/cp-frontend/src/components/service/metrics.js
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MetricGraph } from 'joyent-ui-toolkit';
|
||||
|
||||
const ServiceMetrics = ({
|
||||
metricsData,
|
||||
graphDurationSeconds
|
||||
}) => {
|
||||
|
||||
// metricsData should prob be an array rather than an object
|
||||
const metricGraphs = Object.keys(metricsData).map((key) => (
|
||||
// should also have a header, w metric name and number of instances (omit everything else from design for copilot)
|
||||
<MetricGraph
|
||||
key={key}
|
||||
metricsData={metricsData[key]}
|
||||
width={954}
|
||||
height={292}
|
||||
graphDurationSeconds={graphDurationSeconds}
|
||||
/>
|
||||
))
|
||||
// This needs layout!!!
|
||||
return (
|
||||
<div>
|
||||
{metricGraphs}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ServiceMetrics.propTypes = {
|
||||
// metricsData should prob be an array rather than an object
|
||||
metricsData: PropTypes.object.isRequired,
|
||||
graphDurationSeconds: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
export default ServiceMetrics;
|
1
packages/cp-frontend/src/containers/metrics/index.js
Normal file
1
packages/cp-frontend/src/containers/metrics/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from './metrics-data-hoc';
|
128
packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js
Normal file
128
packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import moment from 'moment';
|
||||
|
||||
export const MetricNames = [
|
||||
'AVG_MEM_BYTES',
|
||||
'AVG_LOAD_PERCENT',
|
||||
'AGG_NETWORK_BYTES'
|
||||
];
|
||||
|
||||
export const withServiceMetricsPolling = ({
|
||||
pollingInterval = 1000 // in milliseconds
|
||||
}) => {
|
||||
return (WrappedComponent) => {
|
||||
|
||||
return class extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
this._poll = setInterval(() => {
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
service,
|
||||
fetchMoreMetrics
|
||||
} = this.props;
|
||||
|
||||
if(!loading && !error && service) {
|
||||
const previousEnd = service.instances[0].metrics[0].end;
|
||||
fetchMoreMetrics(previousEnd);
|
||||
}
|
||||
|
||||
}, pollingInterval); // TODO this is the polling interval - think about amount is the todo I guess...
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this._poll);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <WrappedComponent {...this.props} />
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const withServiceMetricsGql = ({
|
||||
gqlQuery,
|
||||
graphDurationSeconds,
|
||||
updateIntervalSeconds
|
||||
}) => {
|
||||
|
||||
const getPreviousMetrics = (previousResult, serviceId, instanceId, metricName) => {
|
||||
return previousResult.deploymentGroup.services
|
||||
.find(s => s.id === serviceId).instances
|
||||
.find(i => i.id === instanceId).metrics
|
||||
.find(m => m.name === metricName).metrics;
|
||||
}
|
||||
|
||||
const getNextResult = (previousResult, fetchNextResult) => {
|
||||
const deploymentGroup = fetchNextResult.deploymentGroup;
|
||||
const nextResult = {
|
||||
deploymentGroup: {
|
||||
...deploymentGroup,
|
||||
services: deploymentGroup.services.map(service => ({
|
||||
...service,
|
||||
instances: service.instances.map(instance => ({
|
||||
...instance,
|
||||
metrics: instance.metrics.map(metric => ({
|
||||
...metric,
|
||||
metrics: getPreviousMetrics(
|
||||
previousResult,
|
||||
service.id,
|
||||
instance.id,
|
||||
metric.name
|
||||
).concat(metric.metrics)
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}
|
||||
return nextResult;
|
||||
}
|
||||
|
||||
return graphql(gqlQuery, {
|
||||
options(props) {
|
||||
const params = props.match.params;
|
||||
const deploymentGroupSlug = params.deploymentGroup;
|
||||
const serviceSlug = params.service;
|
||||
|
||||
// this is potentially prone to overfetching if we already have data within timeframe and we leave the page then come back to it
|
||||
const end = moment();
|
||||
const start = moment(end).subtract(graphDurationSeconds + updateIntervalSeconds, 'seconds'); // TODO initial amount of data we wanna get - should be the same as what we display + 15 secs
|
||||
|
||||
return {
|
||||
variables: {
|
||||
deploymentGroupSlug,
|
||||
serviceSlug,
|
||||
metricNames: MetricNames,
|
||||
start: start.utc().format(),
|
||||
end: end.utc().format()
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ({ data: { deploymentGroup, loading, error, variables, fetchMore }}) => {
|
||||
|
||||
const fetchMoreMetrics = (previousEnd) => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
...variables,
|
||||
start: previousEnd,
|
||||
end: moment().utc().format()
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult, queryVariables }) => {
|
||||
return getNextResult(previousResult, fetchMoreResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ({
|
||||
deploymentGroup,
|
||||
service: !loading && deploymentGroup ? deploymentGroup.services[0] : null,
|
||||
loading,
|
||||
error,
|
||||
fetchMoreMetrics
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export { default as ServiceScale } from './scale';
|
||||
export { default as ServiceDelete } from './delete';
|
||||
export { default as ServiceMetrics } from './metrics';
|
||||
|
239
packages/cp-frontend/src/containers/service/metrics.js
Normal file
239
packages/cp-frontend/src/containers/service/metrics.js
Normal file
@ -0,0 +1,239 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, graphql } from 'react-apollo';
|
||||
import { connect } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import ServiceMetricsQuery from '@graphql/ServiceMetrics.gql';
|
||||
import { withNotFound, GqlPaths } from '@containers/navigation';
|
||||
import { LayoutContainer } from '@components/layout';
|
||||
import { ServiceMetrics as ServiceMetricsComponent } from '@components/service';
|
||||
import { Button } from 'joyent-ui-toolkit';
|
||||
import { Loader, ErrorMessage } from '@components/messaging';
|
||||
|
||||
import { withServiceMetricsPolling, withServiceMetricsGql } from '@containers/metrics';
|
||||
|
||||
// 'width' of graph, i.e. total duration of time it'll display and truncate data to
|
||||
// amount of data we'll need to initially fetch
|
||||
const GraphDurationSeconds = 90;
|
||||
|
||||
const ServiceMetrics = ({
|
||||
service,
|
||||
loading,
|
||||
error
|
||||
}) => {
|
||||
if (loading || !service) {
|
||||
return (
|
||||
<LayoutContainer center>
|
||||
<Loader />
|
||||
</LayoutContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<LayoutContainer>
|
||||
<ErrorMessage
|
||||
title="Ooops!"
|
||||
message="An error occurred while loading your metrics."
|
||||
/>
|
||||
</LayoutContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// metricsData should prob be an array rather than an object
|
||||
const metricsData = service.instances.reduce((metrics, instance) => {
|
||||
// gather metrics of instances according to type
|
||||
instance.metrics.forEach((instanceMetrics) => {
|
||||
if(!metrics[instanceMetrics.name]) {
|
||||
metrics[instanceMetrics.name] = [];
|
||||
}
|
||||
metrics[instanceMetrics.name].push(instanceMetrics);
|
||||
});
|
||||
return metrics;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<LayoutContainer>
|
||||
<ServiceMetricsComponent
|
||||
metricsData={metricsData}
|
||||
graphDurationSeconds={GraphDurationSeconds}
|
||||
/>
|
||||
</LayoutContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withServiceMetricsGql({
|
||||
gqlQuery: ServiceMetricsQuery,
|
||||
graphDurationSeconds: GraphDurationSeconds,
|
||||
updateIntervalSeconds: 15
|
||||
}),
|
||||
withServiceMetricsPolling({ pollingInterval: 1000 }),
|
||||
withNotFound([
|
||||
GqlPaths.DEPLOYMENT_GROUP,
|
||||
GqlPaths.SERVICES
|
||||
])
|
||||
)(ServiceMetrics);
|
||||
|
||||
/*
|
||||
const metricNames = [
|
||||
'AVG_MEM_BYTES',
|
||||
'AVG_LOAD_PERCENT',
|
||||
'AGG_NETWORK_BYTES'
|
||||
];
|
||||
|
||||
class ServiceMetrics extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
this._poll = setInterval(() => {
|
||||
const {
|
||||
loading,
|
||||
deploymentGroup,
|
||||
service,
|
||||
fetchMoreMetrics
|
||||
} = this.props;
|
||||
|
||||
if(!loading && service) {
|
||||
const previousEnd = service.instances[0].metrics[0].end;
|
||||
fetchMoreMetrics(previousEnd);
|
||||
}
|
||||
|
||||
}, 1000); // TODO this is the polling interval - think about amount is the todo I guess...
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this._poll);
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
const {
|
||||
service,
|
||||
loading,
|
||||
error,
|
||||
fetchMoreMetrics
|
||||
} = this.props;
|
||||
|
||||
if (loading || !service) {
|
||||
return (
|
||||
<LayoutContainer center>
|
||||
<Loader />
|
||||
</LayoutContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<LayoutContainer>
|
||||
<ErrorMessage
|
||||
title="Ooops!"
|
||||
message="An error occurred while loading your metrics."
|
||||
/>
|
||||
</LayoutContainer>
|
||||
);
|
||||
}
|
||||
|
||||
// metricsData should prob be an array rather than an object
|
||||
const metricsData = service.instances.reduce((metrics, instance) => {
|
||||
// gather metrics of instances according to type
|
||||
instance.metrics.forEach((instanceMetrics) => {
|
||||
if(!metrics[instanceMetrics.name]) {
|
||||
metrics[instanceMetrics.name] = [];
|
||||
}
|
||||
metrics[instanceMetrics.name].push(instanceMetrics);
|
||||
});
|
||||
return metrics;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<LayoutContainer>
|
||||
<ServiceMetricsComponent metricsData={metricsData} />
|
||||
</LayoutContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getPreviousMetrics = (previousResult, serviceId, instanceId, metricName) => {
|
||||
return previousResult.deploymentGroup.services
|
||||
.find(s => s.id === serviceId).instances
|
||||
.find(i => i.id === instanceId).metrics
|
||||
.find(m => m.name === metricName).metrics;
|
||||
}
|
||||
|
||||
const getNextResult = (previousResult, fetchNextResult) => {
|
||||
const deploymentGroup = fetchNextResult.deploymentGroup;
|
||||
const nextResult = {
|
||||
deploymentGroup: {
|
||||
...deploymentGroup,
|
||||
services: deploymentGroup.services.map(service => ({
|
||||
...service,
|
||||
instances: service.instances.map(instance => ({
|
||||
...instance,
|
||||
metrics: instance.metrics.map(metric => ({
|
||||
...metric,
|
||||
metrics: getPreviousMetrics(
|
||||
previousResult,
|
||||
service.id,
|
||||
instance.id,
|
||||
metric.name
|
||||
).concat(metric.metrics)
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}
|
||||
}
|
||||
return nextResult;
|
||||
}
|
||||
|
||||
const ServiceMetricsGql = graphql(ServiceMetricsQuery, {
|
||||
options(props) {
|
||||
const params = props.match.params;
|
||||
const deploymentGroupSlug = params.deploymentGroup;
|
||||
const serviceSlug = params.service;
|
||||
|
||||
// this is potentially prone to overfetching if we already have data within timeframe and we leave the page then come back to it
|
||||
const end = moment();
|
||||
const start = moment(end).subtract(105, 'seconds'); // TODO initial amount of data we wanna get - should be the same as what we display + 15 secs
|
||||
|
||||
return {
|
||||
variables: {
|
||||
deploymentGroupSlug,
|
||||
serviceSlug,
|
||||
metricNames,
|
||||
start: start.utc().format(),
|
||||
end: end.utc().format()
|
||||
}
|
||||
};
|
||||
},
|
||||
props: ({ data: { deploymentGroup, loading, error, variables, fetchMore }}) => {
|
||||
|
||||
const fetchMoreMetrics = (previousEnd) => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
...variables,
|
||||
start: previousEnd,
|
||||
end: moment().utc().format()
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult, queryVariables }) => {
|
||||
return getNextResult(previousResult, fetchMoreResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ({
|
||||
deploymentGroup,
|
||||
service: !loading && deploymentGroup ? deploymentGroup.services[0] : null,
|
||||
loading,
|
||||
error,
|
||||
fetchMoreMetrics
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
export default compose(
|
||||
ServiceMetricsGql,
|
||||
withNotFound([
|
||||
GqlPaths.DEPLOYMENT_GROUP,
|
||||
GqlPaths.SERVICES
|
||||
])
|
||||
)(ServiceMetrics); */
|
32
packages/cp-frontend/src/graphql/ServiceMetrics.gql
Normal file
32
packages/cp-frontend/src/graphql/ServiceMetrics.gql
Normal file
@ -0,0 +1,32 @@
|
||||
query Instances(
|
||||
$deploymentGroupSlug: String!,
|
||||
$serviceSlug: String!,
|
||||
$metricNames: [MetricName]!,
|
||||
$start: String!,
|
||||
$end: String!
|
||||
) {
|
||||
deploymentGroup(slug: $deploymentGroupSlug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
services(slug: $serviceSlug) {
|
||||
id
|
||||
name
|
||||
slug
|
||||
instances {
|
||||
id
|
||||
name
|
||||
metrics(names: $metricNames, start: $start, end: $end) {
|
||||
instance
|
||||
name
|
||||
start
|
||||
end
|
||||
metrics {
|
||||
time
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Header, Breadcrumb, Menu } from '@containers/navigation';
|
||||
import { ServiceScale, ServiceDelete } from '@containers/service';
|
||||
import Manifest from '@containers/manifest';
|
||||
import Environment from '@containers/environment';
|
||||
|
||||
@ -20,6 +19,12 @@ import {
|
||||
ServicesQuickActions
|
||||
} from '@containers/services';
|
||||
|
||||
import {
|
||||
ServiceScale,
|
||||
ServiceDelete,
|
||||
ServiceMetrics
|
||||
} from '@containers/service';
|
||||
|
||||
import {
|
||||
InstanceList,
|
||||
InstancesTooltip
|
||||
@ -167,6 +172,12 @@ const App = p =>
|
||||
component={InstanceList}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/deployment-groups/:deploymentGroup/services/:service/metrics"
|
||||
exact
|
||||
component={ServiceMetrics}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/deployment-groups/:deploymentGroup/services/:service"
|
||||
component={serviceRedirect}
|
||||
|
@ -23,6 +23,10 @@ const state = {
|
||||
{
|
||||
pathname: 'instances',
|
||||
name: 'Instances'
|
||||
},
|
||||
{
|
||||
pathname: 'metrics',
|
||||
name: 'Metrics'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -28,7 +28,10 @@ export const client = new ApolloClient({
|
||||
? o.uuid
|
||||
: o.timestamp
|
||||
? o.timestamp
|
||||
: o.name ? o.name : 'apollo-cache-key-not-defined';
|
||||
: o.name && o.instance
|
||||
? `${o.name}-${o.instance}`
|
||||
: o.name ? o.name : o.time && o.value
|
||||
? `${o.time}-${o.value}` : 'apollo-cache-key-not-defined';
|
||||
return `${o.__typename}:${id}`;
|
||||
},
|
||||
networkInterface: createNetworkInterface({
|
||||
|
34469
packages/cp-gql-mock-server/src/metric-datasets-0.json
Normal file
34469
packages/cp-gql-mock-server/src/metric-datasets-0.json
Normal file
File diff suppressed because it is too large
Load Diff
34493
packages/cp-gql-mock-server/src/metric-datasets-1.json
Normal file
34493
packages/cp-gql-mock-server/src/metric-datasets-1.json
Normal file
File diff suppressed because it is too large
Load Diff
22996
packages/cp-gql-mock-server/src/metric-datasets-2.json
Normal file
22996
packages/cp-gql-mock-server/src/metric-datasets-2.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,11 +11,16 @@ const uniq = require('lodash.uniq');
|
||||
const yaml = require('js-yaml');
|
||||
const hasha = require('hasha');
|
||||
const Boom = require('boom');
|
||||
const moment = require('moment');
|
||||
|
||||
const wpData = require('./wp-data.json');
|
||||
const cpData = require('./cp-data.json');
|
||||
const complexData = require('./complex-data.json');
|
||||
const metricData = require('./metric-data.json');
|
||||
const metricData = [
|
||||
require('./metric-datasets-0.json'),
|
||||
require('./metric-datasets-1.json'),
|
||||
require('./metric-datasets-2.json')
|
||||
];
|
||||
|
||||
const { datacenter, portal } = require('./data.json');
|
||||
|
||||
@ -40,16 +45,55 @@ const find = (query = {}) => item =>
|
||||
|
||||
const cleanQuery = (q = {}) => JSON.parse(JSON.stringify(q));
|
||||
|
||||
let metricDataIndex = 0;
|
||||
|
||||
const getMetrics = query => {
|
||||
const {
|
||||
names,
|
||||
start,
|
||||
end
|
||||
end,
|
||||
instanceId
|
||||
} = query;
|
||||
|
||||
const metrics = names.reduce((metrics, name) =>
|
||||
metrics.concat(metricData.filter(md =>
|
||||
md.name === name)), []);
|
||||
const metrics = names.reduce((metrics, name) => {
|
||||
|
||||
// pick one of the three metric data jsons, so there's variety
|
||||
const index = metricDataIndex%metricData.length;
|
||||
metricDataIndex++;
|
||||
|
||||
const md = metricData[index].find(md => md.name === name);
|
||||
const m = md.metrics;
|
||||
|
||||
const s = moment.utc(start);
|
||||
const e = moment.utc(end);
|
||||
|
||||
// how many records do we need?
|
||||
const duration = e.diff(s); // duration for which we need data
|
||||
const records = Math.floor(duration/15000); // new metric record every 15 secs
|
||||
|
||||
const requiredMetrics = [];
|
||||
let i = 0;
|
||||
const time = moment(s);
|
||||
// start at a random point within the dataset for variety
|
||||
const randomIndex = Math.round(Math.random() * m.length);
|
||||
while(i < records) {
|
||||
const index = (randomIndex + i)%m.length; // loop if not enough data
|
||||
const requiredMetric = m[index];
|
||||
requiredMetric.time = time.add(15, 'seconds').utc().format(); // we should have a new record every 15 secs
|
||||
requiredMetrics.push(requiredMetric);
|
||||
i++;
|
||||
}
|
||||
|
||||
const requiredMetricData = {
|
||||
instance: instanceId,
|
||||
name,
|
||||
start: s.utc().format(),
|
||||
end: time.utc().format(), // this will be used by the frontend for the next fetch
|
||||
metrics: requiredMetrics
|
||||
}
|
||||
metrics.push(requiredMetricData);
|
||||
return metrics;
|
||||
}, []);
|
||||
|
||||
return Promise.resolve(metrics);
|
||||
}
|
||||
|
@ -181,6 +181,8 @@ enum MetricName {
|
||||
type InstanceMetric {
|
||||
instance: String!
|
||||
name: MetricName!
|
||||
start: String!
|
||||
end: String!
|
||||
metrics: [Metric]
|
||||
}
|
||||
|
||||
|
@ -107,3 +107,7 @@ export {
|
||||
UnhealthyIcon,
|
||||
BinIcon
|
||||
} from './icons';
|
||||
|
||||
export {
|
||||
MetricGraph
|
||||
} from './metrics';
|
||||
|
153
packages/ui-toolkit/src/metrics/graph.js
Normal file
153
packages/ui-toolkit/src/metrics/graph.js
Normal file
@ -0,0 +1,153 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Chart from 'chart.js';
|
||||
import moment from 'moment';
|
||||
|
||||
// colours to come from theme
|
||||
// AJ to supply friendly colours - these were nicked from chartjs utils
|
||||
const chartColors = [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(255, 159, 64)',
|
||||
'rgb(255, 205, 86)',
|
||||
'rgb(75, 192, 192)',
|
||||
'rgb(54, 162, 235)',
|
||||
'rgb(153, 102, 255)',
|
||||
'rgb(201, 203, 207)'
|
||||
];
|
||||
|
||||
// TODO DISPLAY TIMES SHOULD NOT BE UTC
|
||||
class MetricGraph extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
const {
|
||||
xMin,
|
||||
xMax,
|
||||
datasets
|
||||
} = this.processProps(this.props);
|
||||
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: { datasets },
|
||||
options: {
|
||||
responsive: false, // this needs to be played with
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
display: false // this config doesn't seem to work???
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: true, // config for mini should be false
|
||||
type: 'time',
|
||||
distribution: 'linear',
|
||||
time: {
|
||||
unit: 'minute', // this also needs to be played with
|
||||
min: xMin,
|
||||
max: xMax
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
display: true // needs min / max and measurement
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ctx = this._refs.chart.getContext('2d');
|
||||
this._chart = new Chart(ctx, config);
|
||||
}
|
||||
|
||||
processProps(props) {
|
||||
|
||||
const {
|
||||
metricsData,
|
||||
graphDurationSeconds
|
||||
} = props;
|
||||
|
||||
const xMax = metricsData[0].end;
|
||||
const xMin = moment.utc(xMax).subtract(graphDurationSeconds, 'seconds').utc().format();
|
||||
|
||||
const datasets = metricsData.map((data, i) => ({
|
||||
fill: false,
|
||||
borderColor: chartColors[i],
|
||||
data: this.truncateAndConvertMetrics(data.metrics, xMin, xMax)
|
||||
}));
|
||||
|
||||
return {
|
||||
xMax,
|
||||
xMin,
|
||||
datasets
|
||||
};
|
||||
}
|
||||
|
||||
truncateAndConvertMetrics(metrics, xMin, xMax) {
|
||||
|
||||
const xMinMoment = moment.utc(xMin);
|
||||
|
||||
return metrics.reduce((metrics, metric) => {
|
||||
const diff = moment.utc(metric.time).diff(xMinMoment);
|
||||
if(diff > -10000) { // diff comparison is less than zero - otherwise no data for beginning of chart - bug or charjs weirdness?
|
||||
metrics.push({
|
||||
x: metric.time,
|
||||
y: metric.value // value should be converted here to a readable format
|
||||
})
|
||||
}
|
||||
return metrics;
|
||||
}, []);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
const {
|
||||
xMin,
|
||||
xMax,
|
||||
datasets
|
||||
} = this.processProps(nextProps);
|
||||
|
||||
this._chart.data.datasets = datasets;
|
||||
// these need to be set, but don't seem to truncate the data that's displayed
|
||||
this._chart.options.scales.xAxes[0].time.max = xMax;
|
||||
this._chart.options.scales.xAxes[0].time.min = xMin;
|
||||
this._chart.update(0);
|
||||
}
|
||||
|
||||
// should not rerender ever, we update only the canvas via chartjs
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
ref(name) {
|
||||
this._refs = this._refs || {};
|
||||
|
||||
return el => {
|
||||
this._refs[name] = el;
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
width,
|
||||
height
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={this.ref('chart')}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MetricGraph.propTypes = {
|
||||
metricsData: PropTypes.object.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
graphDurationSeconds: PropTypes.number.isRequired // 'width' of graph, i.e. total duration of time it'll display and truncate data to
|
||||
};
|
||||
|
||||
export default MetricGraph;
|
1
packages/ui-toolkit/src/metrics/index.js
Normal file
1
packages/ui-toolkit/src/metrics/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as MetricGraph } from './graph';
|
Loading…
Reference in New Issue
Block a user