feat(cp-frontend): display metrics in service list

This commit is contained in:
Sérgio Ramos 2017-08-30 01:22:35 +01:00
parent 7f22dea0b8
commit b89d1ad686
15 changed files with 314 additions and 275 deletions

View File

@ -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>

View File

@ -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
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) // 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 => (
<Card key={key} headed active>
<CardHeader>
<CardTitle>{key}</CardTitle>
</CardHeader>
<MetricView>
<MetricGraph <MetricGraph
key={key} key={key}
metricsData={metricsData[key]} metricsData={metricsData[key]}
width={954}
height={292}
graphDurationSeconds={graphDurationSeconds} graphDurationSeconds={graphDurationSeconds}
displayY
displayX
/> />
</MetricView>
</Card>
)); ));
// This needs layout!!! // This needs layout!!!
return <div>{metricGraphs}</div>; return <div>{metricGraphs}</div>;
}; };

View File

@ -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>
<StatusContainer>
{isChild && title} {isChild && title}
{isChild && subtitle}
<CardDescription>
<Status service={service} /> <Status service={service} />
{healthyInfo} <HealthInfoContainer>{healthyInfo}</HealthInfoContainer>
</CardDescription> </StatusContainer>
</CardView> <GraphsContainer>{metrics}</GraphsContainer>
</ServiceView>
); );
return ( return (

View File

@ -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`

View File

@ -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
}; };
} }
}); });

View File

@ -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); */

View File

@ -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,16 +168,12 @@ 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 }) => ({
}
};
},
props: ({ data: { deploymentGroup, loading, error } }) => ({
deploymentGroup, deploymentGroup,
services: deploymentGroup services: deploymentGroup
? processServices(deploymentGroup.services, null) ? processServices(deploymentGroup.services, null)
@ -160,12 +181,8 @@ const ServicesGql = graphql(ServicesQuery, {
loading, loading,
error error
}) })
}); }),
withServiceMetricsPolling({ pollingInterval: 1000 }),
const ServiceListWithData = compose(
ServicesGql,
UiConnect, UiConnect,
withNotFound([GqlPaths.DEPLOYMENT_GROUP]) withNotFound([GqlPaths.DEPLOYMENT_GROUP])
)(ServiceList); )(ServiceList);
export default ServiceListWithData;

View File

@ -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
}
}
} }
} }
} }

View File

@ -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
}; };

View File

@ -163,7 +163,7 @@ const StyledAnchor = A.extend`
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
display: inline-block; display: inline-block;
${style} ${style};
`; `;
/** /**

View File

@ -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 }) => {

View File

@ -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>
); );
}; };

View File

@ -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>
);
}; };

View File

@ -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
} }
] ]
} }

View File

@ -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)};
} }