mirror of
https://github.com/yldio/copilot.git
synced 2024-11-28 14:10:04 +02:00
feat(cp-frontend): display metrics in service list
This commit is contained in:
parent
7f22dea0b8
commit
b89d1ad686
@ -83,6 +83,15 @@ const StyledCard = Card.extend`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StatusContainer = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
const InstanceCard = ({
|
const InstanceCard = ({
|
||||||
instance,
|
instance,
|
||||||
onHealthMouseOver = () => {},
|
onHealthMouseOver = () => {},
|
||||||
@ -117,22 +126,25 @@ const InstanceCard = ({
|
|||||||
<CardView>
|
<CardView>
|
||||||
<CardTitle>{instance.name}</CardTitle>
|
<CardTitle>{instance.name}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<div onMouseOver={handleHealthMouseOver} onMouseOut={handleMouseOut}>
|
<CardInfo
|
||||||
<CardInfo
|
icon={icon}
|
||||||
icon={icon}
|
iconPosition="left"
|
||||||
iconPosition="left"
|
label={label}
|
||||||
label={label}
|
color="dark"
|
||||||
color="dark"
|
onMouseOver={handleHealthMouseOver}
|
||||||
/>
|
onMouseOut={handleMouseOut}
|
||||||
</div>
|
/>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<div onMouseOver={handleStatusMouseOver} onMouseOut={handleMouseOut}>
|
<StatusContainer
|
||||||
|
onMouseOver={handleStatusMouseOver}
|
||||||
|
onMouseOut={handleMouseOut}
|
||||||
|
>
|
||||||
<Label>
|
<Label>
|
||||||
<Dot {...statusProps} />
|
<Dot {...statusProps} />
|
||||||
{titleCase(instance.status)}
|
{titleCase(instance.status)}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</StatusContainer>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardView>
|
</CardView>
|
||||||
</StyledCard>
|
</StyledCard>
|
||||||
|
@ -1,19 +1,50 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { MetricGraph } from 'joyent-ui-toolkit';
|
import styled from 'styled-components';
|
||||||
|
import remcalc from 'remcalc';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MetricGraph,
|
||||||
|
Card,
|
||||||
|
CardView,
|
||||||
|
CardTitle,
|
||||||
|
CardSubTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardGroupView,
|
||||||
|
CardOptions,
|
||||||
|
CardHeader,
|
||||||
|
CardInfo,
|
||||||
|
Anchor
|
||||||
|
} from 'joyent-ui-toolkit';
|
||||||
|
|
||||||
|
const MetricView = styled(CardView)`
|
||||||
|
padding-top: ${remcalc(48)};
|
||||||
|
|
||||||
|
& canvas {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const ServiceMetrics = ({ metricsData, graphDurationSeconds }) => {
|
const ServiceMetrics = ({ metricsData, graphDurationSeconds }) => {
|
||||||
// metricsData should prob be an array rather than an object
|
// metricsData should prob be an array rather than an object
|
||||||
|
// should also have a header, w metric name and number of instances (omit everything else from design for copilot)
|
||||||
const metricGraphs = Object.keys(metricsData).map(key => (
|
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)
|
<Card key={key} headed active>
|
||||||
<MetricGraph
|
<CardHeader>
|
||||||
key={key}
|
<CardTitle>{key}</CardTitle>
|
||||||
metricsData={metricsData[key]}
|
</CardHeader>
|
||||||
width={954}
|
<MetricView>
|
||||||
height={292}
|
<MetricGraph
|
||||||
graphDurationSeconds={graphDurationSeconds}
|
key={key}
|
||||||
/>
|
metricsData={metricsData[key]}
|
||||||
|
graphDurationSeconds={graphDurationSeconds}
|
||||||
|
displayY
|
||||||
|
displayX
|
||||||
|
/>
|
||||||
|
</MetricView>
|
||||||
|
</Card>
|
||||||
));
|
));
|
||||||
|
|
||||||
// This needs layout!!!
|
// This needs layout!!!
|
||||||
return <div>{metricGraphs}</div>;
|
return <div>{metricGraphs}</div>;
|
||||||
};
|
};
|
||||||
|
@ -4,11 +4,15 @@ import styled from 'styled-components';
|
|||||||
import forceArray from 'force-array';
|
import forceArray from 'force-array';
|
||||||
import sortBy from 'lodash.sortby';
|
import sortBy from 'lodash.sortby';
|
||||||
import { isNot } from 'styled-is';
|
import { isNot } from 'styled-is';
|
||||||
|
import { Col, Row } from 'react-styled-flexboxgrid';
|
||||||
|
import remcalc from 'remcalc';
|
||||||
|
|
||||||
import { InstancesIcon, HealthyIcon } from 'joyent-ui-toolkit';
|
import { InstancesIcon, HealthyIcon } from 'joyent-ui-toolkit';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Small,
|
||||||
|
MetricGraph,
|
||||||
Card,
|
Card,
|
||||||
CardView,
|
CardView,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
@ -38,6 +42,73 @@ const StyledAnchor = styled(Anchor)`
|
|||||||
`};
|
`};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const GraphsContainer = styled(Row)`
|
||||||
|
background: #f6f7fe;
|
||||||
|
width: 50%;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphContainer = styled(Col)`
|
||||||
|
position: relative;
|
||||||
|
border-left: ${remcalc(1)} solid #d8d8d8;
|
||||||
|
padding-top: ${remcalc(20)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphLeftShaddow = styled.div`
|
||||||
|
z-index: 99;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: ${remcalc(-8)};
|
||||||
|
margin-top: ${remcalc(-20)};
|
||||||
|
width: ${remcalc(12)};
|
||||||
|
height: 100%;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(213, 216, 231, 0.8),
|
||||||
|
rgba(243, 244, 249, 0)
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const GraphTitle = Small.extend`
|
||||||
|
z-index: 99;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: ${remcalc(20)};
|
||||||
|
border-bottom: ${remcalc(1)} solid #d8d8d8;
|
||||||
|
|
||||||
|
font-size: ${remcalc(13)};
|
||||||
|
text-align: center;
|
||||||
|
color: #494949;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ChildTitle = styled(CardTitle)`
|
||||||
|
padding: 0;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ServiceView = styled(CardView)`
|
||||||
|
height: ${remcalc(120)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StatusContainer = styled(CardDescription)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HealthInfoContainer = styled.div`
|
||||||
|
flex: 0 1 auto;
|
||||||
|
align-self: flex-end;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
const ServiceListItem = ({
|
const ServiceListItem = ({
|
||||||
onQuickActionsClick = () => {},
|
onQuickActionsClick = () => {},
|
||||||
deploymentGroup = '',
|
deploymentGroup = '',
|
||||||
@ -68,7 +139,7 @@ const ServiceListItem = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const title = isChild ? (
|
const title = isChild ? (
|
||||||
<CardTitle>{service.name}</CardTitle>
|
<ChildTitle>{service.name}</ChildTitle>
|
||||||
) : (
|
) : (
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<TitleInnerContainer>
|
<TitleInnerContainer>
|
||||||
@ -79,13 +150,6 @@ const ServiceListItem = ({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
);
|
);
|
||||||
|
|
||||||
const subtitle = (
|
|
||||||
<CardSubTitle>
|
|
||||||
{service.instances.length}{' '}
|
|
||||||
{service.instances.length > 1 ? 'instances' : 'instance'}
|
|
||||||
</CardSubTitle>
|
|
||||||
);
|
|
||||||
|
|
||||||
const header = !isChild ? (
|
const header = !isChild ? (
|
||||||
<StyledCardHeader>
|
<StyledCardHeader>
|
||||||
{title}
|
{title}
|
||||||
@ -115,17 +179,31 @@ const ServiceListItem = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metrics = !children.length
|
||||||
|
? Object.keys(service.metrics).map(key => (
|
||||||
|
<GraphContainer xs={4}>
|
||||||
|
<GraphLeftShaddow />
|
||||||
|
<GraphTitle>{key}</GraphTitle>
|
||||||
|
<MetricGraph
|
||||||
|
key={key}
|
||||||
|
metricsData={service.metrics[key]}
|
||||||
|
graphDurationSeconds={90}
|
||||||
|
/>
|
||||||
|
</GraphContainer>
|
||||||
|
))
|
||||||
|
: null;
|
||||||
|
|
||||||
const view = children.length ? (
|
const view = children.length ? (
|
||||||
<CardGroupView>{childrenItems}</CardGroupView>
|
<CardGroupView>{childrenItems}</CardGroupView>
|
||||||
) : (
|
) : (
|
||||||
<CardView>
|
<ServiceView>
|
||||||
{isChild && title}
|
<StatusContainer>
|
||||||
{isChild && subtitle}
|
{isChild && title}
|
||||||
<CardDescription>
|
|
||||||
<Status service={service} />
|
<Status service={service} />
|
||||||
{healthyInfo}
|
<HealthInfoContainer>{healthyInfo}</HealthInfoContainer>
|
||||||
</CardDescription>
|
</StatusContainer>
|
||||||
</CardView>
|
<GraphsContainer>{metrics}</GraphsContainer>
|
||||||
|
</ServiceView>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -6,9 +6,10 @@ import { StatusLoader, P } from 'joyent-ui-toolkit';
|
|||||||
|
|
||||||
const StyledStatusContainer = styled.div`
|
const StyledStatusContainer = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 0 ${remcalc(15)} 0;
|
margin: 0;
|
||||||
height: ${remcalc(54)};
|
|
||||||
width: ${remcalc(200)};
|
flex: 1 1 auto;
|
||||||
|
align-self: stretch;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledStatus = P.extend`
|
const StyledStatus = P.extend`
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { compose, graphql } from 'react-apollo';
|
import { compose, graphql } from 'react-apollo';
|
||||||
|
import get from 'lodash.get';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
export const MetricNames = [
|
export const MetricNames = [
|
||||||
@ -18,7 +19,13 @@ export const withServiceMetricsPolling = ({
|
|||||||
const { loading, error, service, fetchMoreMetrics } = this.props;
|
const { loading, error, service, fetchMoreMetrics } = this.props;
|
||||||
|
|
||||||
if (!loading && !error && service) {
|
if (!loading && !error && service) {
|
||||||
const previousEnd = service.instances[0].metrics[0].end;
|
const previousEnd = get(
|
||||||
|
service,
|
||||||
|
'instances[0].metrics[0].end',
|
||||||
|
moment()
|
||||||
|
.utc()
|
||||||
|
.format()
|
||||||
|
);
|
||||||
fetchMoreMetrics(previousEnd);
|
fetchMoreMetrics(previousEnd);
|
||||||
}
|
}
|
||||||
}, pollingInterval); // TODO this is the polling interval - think about amount is the todo I guess...
|
}, pollingInterval); // TODO this is the polling interval - think about amount is the todo I guess...
|
||||||
@ -38,7 +45,9 @@ export const withServiceMetricsPolling = ({
|
|||||||
export const withServiceMetricsGql = ({
|
export const withServiceMetricsGql = ({
|
||||||
gqlQuery,
|
gqlQuery,
|
||||||
graphDurationSeconds,
|
graphDurationSeconds,
|
||||||
updateIntervalSeconds
|
updateIntervalSeconds,
|
||||||
|
variables = () => ({}),
|
||||||
|
props = () => ({})
|
||||||
}) => {
|
}) => {
|
||||||
const getPreviousMetrics = (
|
const getPreviousMetrics = (
|
||||||
previousResult,
|
previousResult,
|
||||||
@ -81,7 +90,6 @@ export const withServiceMetricsGql = ({
|
|||||||
options(props) {
|
options(props) {
|
||||||
const params = props.match.params;
|
const params = props.match.params;
|
||||||
const deploymentGroupSlug = params.deploymentGroup;
|
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
|
// 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 end = moment();
|
||||||
@ -93,16 +101,14 @@ export const withServiceMetricsGql = ({
|
|||||||
return {
|
return {
|
||||||
variables: {
|
variables: {
|
||||||
deploymentGroupSlug,
|
deploymentGroupSlug,
|
||||||
serviceSlug,
|
|
||||||
metricNames: MetricNames,
|
metricNames: MetricNames,
|
||||||
start: start.utc().format(),
|
start: start.utc().format(),
|
||||||
end: end.utc().format()
|
end: end.utc().format(),
|
||||||
|
...variables(props)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: ({
|
props: ({ data: { variables, fetchMore, ...rest } }) => {
|
||||||
data: { deploymentGroup, loading, error, variables, fetchMore }
|
|
||||||
}) => {
|
|
||||||
const fetchMoreMetrics = previousEnd => {
|
const fetchMoreMetrics = previousEnd => {
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: {
|
variables: {
|
||||||
@ -120,13 +126,10 @@ export const withServiceMetricsGql = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deploymentGroup,
|
fetchMoreMetrics,
|
||||||
service:
|
...props(rest)
|
||||||
!loading && deploymentGroup ? deploymentGroup.services[0] : null,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
fetchMoreMetrics
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -6,9 +6,12 @@ import moment from 'moment';
|
|||||||
import ServiceMetricsQuery from '@graphql/ServiceMetrics.gql';
|
import ServiceMetricsQuery from '@graphql/ServiceMetrics.gql';
|
||||||
import { withNotFound, GqlPaths } from '@containers/navigation';
|
import { withNotFound, GqlPaths } from '@containers/navigation';
|
||||||
import { LayoutContainer } from '@components/layout';
|
import { LayoutContainer } from '@components/layout';
|
||||||
|
import { Title } from '@components/navigation';
|
||||||
import { ServiceMetrics as ServiceMetricsComponent } from '@components/service';
|
import { ServiceMetrics as ServiceMetricsComponent } from '@components/service';
|
||||||
import { Button } from 'joyent-ui-toolkit';
|
import { Button } from 'joyent-ui-toolkit';
|
||||||
import { Loader, ErrorMessage } from '@components/messaging';
|
import { Loader, ErrorMessage } from '@components/messaging';
|
||||||
|
import { processInstancesMetrics } from '@state/selectors';
|
||||||
|
import get from 'lodash.get';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
withServiceMetricsPolling,
|
withServiceMetricsPolling,
|
||||||
@ -20,9 +23,12 @@ import {
|
|||||||
const GraphDurationSeconds = 90;
|
const GraphDurationSeconds = 90;
|
||||||
|
|
||||||
const ServiceMetrics = ({ service, loading, error }) => {
|
const ServiceMetrics = ({ service, loading, error }) => {
|
||||||
|
const _title = <Title>Metrics</Title>;
|
||||||
|
|
||||||
if (loading || !service) {
|
if (loading || !service) {
|
||||||
return (
|
return (
|
||||||
<LayoutContainer center>
|
<LayoutContainer center>
|
||||||
|
{_title}
|
||||||
<Loader />
|
<Loader />
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
);
|
);
|
||||||
@ -31,6 +37,7 @@ const ServiceMetrics = ({ service, loading, error }) => {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
|
{_title}
|
||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
title="Ooops!"
|
title="Ooops!"
|
||||||
message="An error occurred while loading your metrics."
|
message="An error occurred while loading your metrics."
|
||||||
@ -39,22 +46,11 @@ const ServiceMetrics = ({ service, loading, error }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
|
{_title}
|
||||||
<ServiceMetricsComponent
|
<ServiceMetricsComponent
|
||||||
metricsData={metricsData}
|
metricsData={processInstancesMetrics(service.instances)}
|
||||||
graphDurationSeconds={GraphDurationSeconds}
|
graphDurationSeconds={GraphDurationSeconds}
|
||||||
/>
|
/>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
@ -65,171 +61,15 @@ export default compose(
|
|||||||
withServiceMetricsGql({
|
withServiceMetricsGql({
|
||||||
gqlQuery: ServiceMetricsQuery,
|
gqlQuery: ServiceMetricsQuery,
|
||||||
graphDurationSeconds: GraphDurationSeconds,
|
graphDurationSeconds: GraphDurationSeconds,
|
||||||
updateIntervalSeconds: 15
|
updateIntervalSeconds: 15,
|
||||||
|
variables: ({ match }) => ({ serviceSlug: match.params.service }),
|
||||||
|
props: ({ deploymentGroup, loading, error }) => ({
|
||||||
|
deploymentGroup,
|
||||||
|
service: get(deploymentGroup || {}, 'services', [])[0],
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
withServiceMetricsPolling({ pollingInterval: 1000 }),
|
withServiceMetricsPolling({ pollingInterval: 1000 }),
|
||||||
withNotFound([GqlPaths.DEPLOYMENT_GROUP, GqlPaths.SERVICES])
|
withNotFound([GqlPaths.DEPLOYMENT_GROUP, GqlPaths.SERVICES])
|
||||||
)(ServiceMetrics);
|
)(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); */
|
|
||||||
|
@ -8,13 +8,25 @@ import sortBy from 'lodash.sortby';
|
|||||||
|
|
||||||
import ServicesQuery from '@graphql/Services.gql';
|
import ServicesQuery from '@graphql/Services.gql';
|
||||||
|
|
||||||
import { processServices } from '@root/state/selectors';
|
import {
|
||||||
|
processServices,
|
||||||
|
processInstancesMetrics
|
||||||
|
} from '@root/state/selectors';
|
||||||
import { toggleServicesQuickActions } from '@root/state/actions';
|
import { toggleServicesQuickActions } from '@root/state/actions';
|
||||||
import { withNotFound, GqlPaths } from '@containers/navigation';
|
import { withNotFound, GqlPaths } from '@containers/navigation';
|
||||||
import { LayoutContainer } from '@components/layout';
|
import { LayoutContainer } from '@components/layout';
|
||||||
import { Loader, ErrorMessage } from '@components/messaging';
|
import { Loader, ErrorMessage } from '@components/messaging';
|
||||||
import { ServiceListItem } from '@components/services';
|
import { ServiceListItem } from '@components/services';
|
||||||
|
|
||||||
|
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 StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
@ -107,16 +119,29 @@ export class ServiceList extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceList = sortBy(services, ['slug']).map(service => {
|
const serviceList = sortBy(services, ['slug'])
|
||||||
return (
|
.map(service =>
|
||||||
|
Object.assign(service, {
|
||||||
|
metrics: !service.children
|
||||||
|
? processInstancesMetrics(service.instances)
|
||||||
|
: null,
|
||||||
|
children: service.children
|
||||||
|
? service.children.map(children =>
|
||||||
|
Object.assign(children, {
|
||||||
|
metrics: processInstancesMetrics(children.instances)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.map(service => (
|
||||||
<ServiceListItem
|
<ServiceListItem
|
||||||
key={service.id}
|
key={service.id}
|
||||||
deploymentGroup={deploymentGroup.slug}
|
deploymentGroup={deploymentGroup.slug}
|
||||||
service={service}
|
service={service}
|
||||||
onQuickActionsClick={handleQuickActionsClick}
|
onQuickActionsClick={handleQuickActionsClick}
|
||||||
/>
|
/>
|
||||||
);
|
));
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
@ -143,29 +168,21 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
|
|
||||||
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
|
const UiConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
const ServicesGql = graphql(ServicesQuery, {
|
export default compose(
|
||||||
options(props) {
|
withServiceMetricsGql({
|
||||||
return {
|
gqlQuery: ServicesQuery,
|
||||||
pollInterval: 1000,
|
graphDurationSeconds: GraphDurationSeconds,
|
||||||
variables: {
|
updateIntervalSeconds: 15,
|
||||||
deploymentGroupSlug: props.match.params.deploymentGroup
|
props: ({ deploymentGroup, loading, error }) => ({
|
||||||
}
|
deploymentGroup,
|
||||||
};
|
services: deploymentGroup
|
||||||
},
|
? processServices(deploymentGroup.services, null)
|
||||||
props: ({ data: { deploymentGroup, loading, error } }) => ({
|
: null,
|
||||||
deploymentGroup,
|
loading,
|
||||||
services: deploymentGroup
|
error
|
||||||
? processServices(deploymentGroup.services, null)
|
})
|
||||||
: null,
|
}),
|
||||||
loading,
|
withServiceMetricsPolling({ pollingInterval: 1000 }),
|
||||||
error
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const ServiceListWithData = compose(
|
|
||||||
ServicesGql,
|
|
||||||
UiConnect,
|
UiConnect,
|
||||||
withNotFound([GqlPaths.DEPLOYMENT_GROUP])
|
withNotFound([GqlPaths.DEPLOYMENT_GROUP])
|
||||||
)(ServiceList);
|
)(ServiceList);
|
||||||
|
|
||||||
export default ServiceListWithData;
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
#import "./DeploymentGroupInfo.gql"
|
#import "./DeploymentGroupInfo.gql"
|
||||||
#import "./ServiceInfo.gql"
|
#import "./ServiceInfo.gql"
|
||||||
|
|
||||||
query Services($deploymentGroupSlug: String!) {
|
query Services(
|
||||||
|
$deploymentGroupSlug: String!
|
||||||
|
$metricNames: [MetricName]!
|
||||||
|
$start: String!
|
||||||
|
$end: String!
|
||||||
|
) {
|
||||||
deploymentGroup(slug: $deploymentGroupSlug) {
|
deploymentGroup(slug: $deploymentGroupSlug) {
|
||||||
...DeploymentGroupInfo
|
...DeploymentGroupInfo
|
||||||
services {
|
services {
|
||||||
@ -13,6 +18,16 @@ query Services($deploymentGroupSlug: String!) {
|
|||||||
id
|
id
|
||||||
status
|
status
|
||||||
healthy
|
healthy
|
||||||
|
metrics(names: $metricNames, start: $start, end: $end) {
|
||||||
|
instance
|
||||||
|
name
|
||||||
|
start
|
||||||
|
end
|
||||||
|
metrics {
|
||||||
|
time
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
connections
|
connections
|
||||||
@ -20,6 +35,16 @@ query Services($deploymentGroupSlug: String!) {
|
|||||||
id
|
id
|
||||||
status
|
status
|
||||||
healthy
|
healthy
|
||||||
|
metrics(names: $metricNames, start: $start, end: $end) {
|
||||||
|
instance
|
||||||
|
name
|
||||||
|
start
|
||||||
|
end
|
||||||
|
metrics {
|
||||||
|
time
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,18 @@ const processServicesForTopology = services => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// metricsData should prob be an array rather than an object
|
||||||
|
const processInstancesMetrics = instances =>
|
||||||
|
forceArray(instances).reduce((metrics, instance) => {
|
||||||
|
instance.metrics.forEach(instanceMetrics => {
|
||||||
|
metrics[instanceMetrics.name] = forceArray(
|
||||||
|
metrics[instanceMetrics.name]
|
||||||
|
).concat([instanceMetrics]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}, {});
|
||||||
|
|
||||||
/* ,
|
/* ,
|
||||||
instancesByServiceId */
|
instancesByServiceId */
|
||||||
export {
|
export {
|
||||||
@ -184,5 +196,6 @@ export {
|
|||||||
getInstancesHealthy,
|
getInstancesHealthy,
|
||||||
getService,
|
getService,
|
||||||
processServices,
|
processServices,
|
||||||
processServicesForTopology
|
processServicesForTopology,
|
||||||
|
processInstancesMetrics
|
||||||
};
|
};
|
||||||
|
@ -163,7 +163,7 @@ const StyledAnchor = A.extend`
|
|||||||
|
|
||||||
const StyledLink = styled(Link)`
|
const StyledLink = styled(Link)`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
${style}
|
${style};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +22,8 @@ const StyledTitle = Title.extend`
|
|||||||
|
|
||||||
const InnerDescription = styled.div`
|
const InnerDescription = styled.div`
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Description = ({ children, ...rest }) => {
|
const Description = ({ children, ...rest }) => {
|
||||||
|
@ -23,16 +23,33 @@ const StyledIconContainer = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CardInfo = ({ label, icon, iconPosition = 'left', color = 'light' }) => {
|
const CardInfoContainer = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
float: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CardInfo = ({
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
iconPosition = 'left',
|
||||||
|
color = 'light',
|
||||||
|
onMouseOver,
|
||||||
|
onMouseOut
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<CardInfoContainer onMouseOver={onMouseOver} onMouseOut={onMouseOver}>
|
||||||
<StyledIconContainer iconPosition={iconPosition} color={color}>
|
<StyledIconContainer iconPosition={iconPosition} color={color}>
|
||||||
{icon}
|
{icon}
|
||||||
</StyledIconContainer>
|
</StyledIconContainer>
|
||||||
<StyledLabel iconPosition={iconPosition} color={color}>
|
<StyledLabel iconPosition={iconPosition} color={color}>
|
||||||
{label}
|
{label}
|
||||||
</StyledLabel>
|
</StyledLabel>
|
||||||
</div>
|
</CardInfoContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,9 +13,5 @@ export default props => {
|
|||||||
return <StyledLabel {...props} htmlFor={id} />;
|
return <StyledLabel {...props} htmlFor={id} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <Subscriber channel="input-group">{render}</Subscriber>;
|
||||||
<Subscriber channel="input-group">
|
|
||||||
{render}
|
|
||||||
</Subscriber>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@ -19,12 +19,13 @@ const chartColors = [
|
|||||||
class MetricGraph extends Component {
|
class MetricGraph extends Component {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { xMin, xMax, datasets } = this.processProps(this.props);
|
const { xMin, xMax, datasets } = this.processProps(this.props);
|
||||||
|
const { displayX = false, displayY = false } = this.props;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: { datasets },
|
data: { datasets },
|
||||||
options: {
|
options: {
|
||||||
responsive: false, // this needs to be played with
|
responsive: true, // this needs to be played with
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
@ -34,7 +35,7 @@ class MetricGraph extends Component {
|
|||||||
scales: {
|
scales: {
|
||||||
xAxes: [
|
xAxes: [
|
||||||
{
|
{
|
||||||
display: true, // config for mini should be false
|
display: displayX, // config for mini should be false
|
||||||
type: 'time',
|
type: 'time',
|
||||||
distribution: 'linear',
|
distribution: 'linear',
|
||||||
time: {
|
time: {
|
||||||
@ -46,7 +47,7 @@ class MetricGraph extends Component {
|
|||||||
],
|
],
|
||||||
yAxes: [
|
yAxes: [
|
||||||
{
|
{
|
||||||
display: true // needs min / max and measurement
|
display: displayY // needs min / max and measurement
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -25,15 +25,16 @@ const StyledInnerContainer = styled.div`
|
|||||||
left: -50%;
|
left: -50%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: ${unitcalc(2)} 0;
|
padding: ${unitcalc(2)} 0;
|
||||||
background-color: ${props => props.secondary ? props.theme.secondary : props.theme.white};
|
background-color: ${props =>
|
||||||
border: ${props => props.secondary ? border.secondary : border.unchecked};
|
props.secondary ? props.theme.secondary : props.theme.white};
|
||||||
|
border: ${props => (props.secondary ? border.secondary : border.unchecked)};
|
||||||
box-shadow: ${tooltipShadow};
|
box-shadow: ${tooltipShadow};
|
||||||
border-radius: ${borderRadius};
|
border-radius: ${borderRadius};
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
&:after,
|
&:after,
|
||||||
&:before {
|
&:before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -43,13 +44,15 @@ const StyledInnerContainer = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
border-bottom-color: ${props => props.secondary ? props.theme.secondary : theme.white};
|
border-bottom-color: ${props =>
|
||||||
|
props.secondary ? props.theme.secondary : theme.white};
|
||||||
border-width: ${remcalc(3)};
|
border-width: ${remcalc(3)};
|
||||||
margin-left: ${remcalc(-3)};
|
margin-left: ${remcalc(-3)};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
border-bottom-color: ${props => props.secondary ? props.theme.secondaryActive : theme.grey};
|
border-bottom-color: ${props =>
|
||||||
|
props.secondary ? props.theme.secondaryActive : theme.grey};
|
||||||
border-width: ${remcalc(5)};
|
border-width: ${remcalc(5)};
|
||||||
margin-left: ${remcalc(-5)};
|
margin-left: ${remcalc(-5)};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user