From b89d1ad686ee0b00d20041dfe76b297872b50c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=CC=81rgio=20Ramos?= Date: Wed, 30 Aug 2017 01:22:35 +0100 Subject: [PATCH] feat(cp-frontend): display metrics in service list --- .../src/components/instances/list-item.js | 32 ++- .../src/components/service/metrics.js | 49 ++++- .../src/components/services/list-item.js | 108 ++++++++-- .../src/components/services/status.js | 7 +- .../containers/metrics/metrics-data-hoc.js | 31 +-- .../src/containers/service/metrics.js | 194 ++---------------- .../src/containers/services/list.js | 73 ++++--- packages/cp-frontend/src/graphql/Services.gql | 27 ++- packages/cp-frontend/src/state/selectors.js | 15 +- packages/ui-toolkit/src/button/index.js | 2 +- packages/ui-toolkit/src/card/description.js | 2 + packages/ui-toolkit/src/card/info.js | 23 ++- packages/ui-toolkit/src/form/label.js | 6 +- packages/ui-toolkit/src/metrics/graph.js | 7 +- packages/ui-toolkit/src/tooltip/tooltip.js | 13 +- 15 files changed, 314 insertions(+), 275 deletions(-) diff --git a/packages/cp-frontend/src/components/instances/list-item.js b/packages/cp-frontend/src/components/instances/list-item.js index 4e9cc92c..bae278ab 100644 --- a/packages/cp-frontend/src/components/instances/list-item.js +++ b/packages/cp-frontend/src/components/instances/list-item.js @@ -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 = ({ instance, onHealthMouseOver = () => {}, @@ -117,22 +126,25 @@ const InstanceCard = ({ {instance.name} -
- -
+
-
+ -
+
diff --git a/packages/cp-frontend/src/components/service/metrics.js b/packages/cp-frontend/src/components/service/metrics.js index 3b3e2a6d..e29b4100 100644 --- a/packages/cp-frontend/src/components/service/metrics.js +++ b/packages/cp-frontend/src/components/service/metrics.js @@ -1,19 +1,50 @@ import React, { Component } from 'react'; 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 }) => { // 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 => ( - // should also have a header, w metric name and number of instances (omit everything else from design for copilot) - + + + {key} + + + + + )); + // This needs layout!!! return
{metricGraphs}
; }; diff --git a/packages/cp-frontend/src/components/services/list-item.js b/packages/cp-frontend/src/components/services/list-item.js index 67bbe9d2..ab27c929 100644 --- a/packages/cp-frontend/src/components/services/list-item.js +++ b/packages/cp-frontend/src/components/services/list-item.js @@ -4,11 +4,15 @@ import styled from 'styled-components'; import forceArray from 'force-array'; import sortBy from 'lodash.sortby'; import { isNot } from 'styled-is'; +import { Col, Row } from 'react-styled-flexboxgrid'; +import remcalc from 'remcalc'; import { InstancesIcon, HealthyIcon } from 'joyent-ui-toolkit'; import Status from './status'; import { + Small, + MetricGraph, Card, CardView, 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 = ({ onQuickActionsClick = () => {}, deploymentGroup = '', @@ -68,7 +139,7 @@ const ServiceListItem = ({ : null; const title = isChild ? ( - {service.name} + {service.name} ) : ( @@ -79,13 +150,6 @@ const ServiceListItem = ({ ); - const subtitle = ( - - {service.instances.length}{' '} - {service.instances.length > 1 ? 'instances' : 'instance'} - - ); - const header = !isChild ? ( {title} @@ -115,17 +179,31 @@ const ServiceListItem = ({ ); } + const metrics = !children.length + ? Object.keys(service.metrics).map(key => ( + + + {key} + + + )) + : null; + const view = children.length ? ( {childrenItems} ) : ( - - {isChild && title} - {isChild && subtitle} - + + + {isChild && title} - {healthyInfo} - - + {healthyInfo} + + {metrics} + ); return ( diff --git a/packages/cp-frontend/src/components/services/status.js b/packages/cp-frontend/src/components/services/status.js index 8de95345..3aed4b49 100644 --- a/packages/cp-frontend/src/components/services/status.js +++ b/packages/cp-frontend/src/components/services/status.js @@ -6,9 +6,10 @@ import { StatusLoader, P } from 'joyent-ui-toolkit'; const StyledStatusContainer = styled.div` display: inline-block; - margin: 0 0 ${remcalc(15)} 0; - height: ${remcalc(54)}; - width: ${remcalc(200)}; + margin: 0; + + flex: 1 1 auto; + align-self: stretch; `; const StyledStatus = P.extend` diff --git a/packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js b/packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js index 73ae5e2b..81ece34d 100644 --- a/packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js +++ b/packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { compose, graphql } from 'react-apollo'; +import get from 'lodash.get'; import moment from 'moment'; export const MetricNames = [ @@ -18,7 +19,13 @@ export const withServiceMetricsPolling = ({ const { loading, error, service, fetchMoreMetrics } = this.props; 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); } }, 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 = ({ gqlQuery, graphDurationSeconds, - updateIntervalSeconds + updateIntervalSeconds, + variables = () => ({}), + props = () => ({}) }) => { const getPreviousMetrics = ( previousResult, @@ -81,7 +90,6 @@ export const withServiceMetricsGql = ({ 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(); @@ -93,16 +101,14 @@ export const withServiceMetricsGql = ({ return { variables: { deploymentGroupSlug, - serviceSlug, metricNames: MetricNames, start: start.utc().format(), - end: end.utc().format() + end: end.utc().format(), + ...variables(props) } }; }, - props: ({ - data: { deploymentGroup, loading, error, variables, fetchMore } - }) => { + props: ({ data: { variables, fetchMore, ...rest } }) => { const fetchMoreMetrics = previousEnd => { fetchMore({ variables: { @@ -120,13 +126,10 @@ export const withServiceMetricsGql = ({ } }); }; + return { - deploymentGroup, - service: - !loading && deploymentGroup ? deploymentGroup.services[0] : null, - loading, - error, - fetchMoreMetrics + fetchMoreMetrics, + ...props(rest) }; } }); diff --git a/packages/cp-frontend/src/containers/service/metrics.js b/packages/cp-frontend/src/containers/service/metrics.js index 47662938..2b1d2d22 100644 --- a/packages/cp-frontend/src/containers/service/metrics.js +++ b/packages/cp-frontend/src/containers/service/metrics.js @@ -6,9 +6,12 @@ import moment from 'moment'; import ServiceMetricsQuery from '@graphql/ServiceMetrics.gql'; import { withNotFound, GqlPaths } from '@containers/navigation'; import { LayoutContainer } from '@components/layout'; +import { Title } from '@components/navigation'; import { ServiceMetrics as ServiceMetricsComponent } from '@components/service'; import { Button } from 'joyent-ui-toolkit'; import { Loader, ErrorMessage } from '@components/messaging'; +import { processInstancesMetrics } from '@state/selectors'; +import get from 'lodash.get'; import { withServiceMetricsPolling, @@ -20,9 +23,12 @@ import { const GraphDurationSeconds = 90; const ServiceMetrics = ({ service, loading, error }) => { + const _title = Metrics; + if (loading || !service) { return ( + {_title} ); @@ -31,6 +37,7 @@ const ServiceMetrics = ({ service, loading, error }) => { if (error) { return ( + {_title} { ); } - // 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 ( + {_title} @@ -65,171 +61,15 @@ export default compose( withServiceMetricsGql({ gqlQuery: ServiceMetricsQuery, 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 }), 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 ( - - - - ); - } - - if (error) { - return ( - - - - ); - } - - // 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 ( - - - - ); - } -}; - -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); */ diff --git a/packages/cp-frontend/src/containers/services/list.js b/packages/cp-frontend/src/containers/services/list.js index 0c20b339..8e248024 100644 --- a/packages/cp-frontend/src/containers/services/list.js +++ b/packages/cp-frontend/src/containers/services/list.js @@ -8,13 +8,25 @@ import sortBy from 'lodash.sortby'; 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 { withNotFound, GqlPaths } from '@containers/navigation'; import { LayoutContainer } from '@components/layout'; import { Loader, ErrorMessage } from '@components/messaging'; 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` position: relative; `; @@ -107,16 +119,29 @@ export class ServiceList extends Component { ); } - const serviceList = sortBy(services, ['slug']).map(service => { - return ( + const serviceList = sortBy(services, ['slug']) + .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 => ( - ); - }); + )); return ( @@ -143,29 +168,21 @@ const mapDispatchToProps = dispatch => ({ const UiConnect = connect(mapStateToProps, mapDispatchToProps); -const ServicesGql = graphql(ServicesQuery, { - options(props) { - return { - pollInterval: 1000, - variables: { - deploymentGroupSlug: props.match.params.deploymentGroup - } - }; - }, - props: ({ data: { deploymentGroup, loading, error } }) => ({ - deploymentGroup, - services: deploymentGroup - ? processServices(deploymentGroup.services, null) - : null, - loading, - error - }) -}); - -const ServiceListWithData = compose( - ServicesGql, +export default compose( + withServiceMetricsGql({ + gqlQuery: ServicesQuery, + graphDurationSeconds: GraphDurationSeconds, + updateIntervalSeconds: 15, + props: ({ deploymentGroup, loading, error }) => ({ + deploymentGroup, + services: deploymentGroup + ? processServices(deploymentGroup.services, null) + : null, + loading, + error + }) + }), + withServiceMetricsPolling({ pollingInterval: 1000 }), UiConnect, withNotFound([GqlPaths.DEPLOYMENT_GROUP]) )(ServiceList); - -export default ServiceListWithData; diff --git a/packages/cp-frontend/src/graphql/Services.gql b/packages/cp-frontend/src/graphql/Services.gql index 5bc7d44f..26bc504a 100644 --- a/packages/cp-frontend/src/graphql/Services.gql +++ b/packages/cp-frontend/src/graphql/Services.gql @@ -1,7 +1,12 @@ #import "./DeploymentGroupInfo.gql" #import "./ServiceInfo.gql" -query Services($deploymentGroupSlug: String!) { +query Services( + $deploymentGroupSlug: String! + $metricNames: [MetricName]! + $start: String! + $end: String! +) { deploymentGroup(slug: $deploymentGroupSlug) { ...DeploymentGroupInfo services { @@ -13,6 +18,16 @@ query Services($deploymentGroupSlug: String!) { id status healthy + metrics(names: $metricNames, start: $start, end: $end) { + instance + name + start + end + metrics { + time + value + } + } } } connections @@ -20,6 +35,16 @@ query Services($deploymentGroupSlug: String!) { id status healthy + metrics(names: $metricNames, start: $start, end: $end) { + instance + name + start + end + metrics { + time + value + } + } } } } diff --git a/packages/cp-frontend/src/state/selectors.js b/packages/cp-frontend/src/state/selectors.js index c57d7974..6db78d89 100644 --- a/packages/cp-frontend/src/state/selectors.js +++ b/packages/cp-frontend/src/state/selectors.js @@ -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 */ export { @@ -184,5 +196,6 @@ export { getInstancesHealthy, getService, processServices, - processServicesForTopology + processServicesForTopology, + processInstancesMetrics }; diff --git a/packages/ui-toolkit/src/button/index.js b/packages/ui-toolkit/src/button/index.js index dc5639dc..349ee37a 100644 --- a/packages/ui-toolkit/src/button/index.js +++ b/packages/ui-toolkit/src/button/index.js @@ -163,7 +163,7 @@ const StyledAnchor = A.extend` const StyledLink = styled(Link)` display: inline-block; - ${style} + ${style}; `; /** diff --git a/packages/ui-toolkit/src/card/description.js b/packages/ui-toolkit/src/card/description.js index 88c9f651..89e0eeaa 100644 --- a/packages/ui-toolkit/src/card/description.js +++ b/packages/ui-toolkit/src/card/description.js @@ -22,6 +22,8 @@ const StyledTitle = Title.extend` const InnerDescription = styled.div` justify-content: flex-start; + height: 100%; + position: relative; `; const Description = ({ children, ...rest }) => { diff --git a/packages/ui-toolkit/src/card/info.js b/packages/ui-toolkit/src/card/info.js index dbd29f9c..ef457189 100644 --- a/packages/ui-toolkit/src/card/info.js +++ b/packages/ui-toolkit/src/card/info.js @@ -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 ( -
+ {icon} {label} -
+ ); }; diff --git a/packages/ui-toolkit/src/form/label.js b/packages/ui-toolkit/src/form/label.js index 38d7fc86..8142d304 100644 --- a/packages/ui-toolkit/src/form/label.js +++ b/packages/ui-toolkit/src/form/label.js @@ -13,9 +13,5 @@ export default props => { return ; }; - return ( - - {render} - - ); + return {render}; }; diff --git a/packages/ui-toolkit/src/metrics/graph.js b/packages/ui-toolkit/src/metrics/graph.js index 3518f8c3..1e0962d1 100644 --- a/packages/ui-toolkit/src/metrics/graph.js +++ b/packages/ui-toolkit/src/metrics/graph.js @@ -19,12 +19,13 @@ const chartColors = [ class MetricGraph extends Component { componentDidMount() { const { xMin, xMax, datasets } = this.processProps(this.props); + const { displayX = false, displayY = false } = this.props; const config = { type: 'line', data: { datasets }, options: { - responsive: false, // this needs to be played with + responsive: true, // this needs to be played with legend: { display: false }, @@ -34,7 +35,7 @@ class MetricGraph extends Component { scales: { xAxes: [ { - display: true, // config for mini should be false + display: displayX, // config for mini should be false type: 'time', distribution: 'linear', time: { @@ -46,7 +47,7 @@ class MetricGraph extends Component { ], yAxes: [ { - display: true // needs min / max and measurement + display: displayY // needs min / max and measurement } ] } diff --git a/packages/ui-toolkit/src/tooltip/tooltip.js b/packages/ui-toolkit/src/tooltip/tooltip.js index fd3231cd..be120df6 100644 --- a/packages/ui-toolkit/src/tooltip/tooltip.js +++ b/packages/ui-toolkit/src/tooltip/tooltip.js @@ -25,15 +25,16 @@ const StyledInnerContainer = styled.div` left: -50%; margin: 0; padding: ${unitcalc(2)} 0; - background-color: ${props => props.secondary ? props.theme.secondary : props.theme.white}; - border: ${props => props.secondary ? border.secondary : border.unchecked}; + background-color: ${props => + props.secondary ? props.theme.secondary : props.theme.white}; + border: ${props => (props.secondary ? border.secondary : border.unchecked)}; box-shadow: ${tooltipShadow}; border-radius: ${borderRadius}; z-index: 1000; &:after, &:before { - content: ""; + content: ''; position: absolute; bottom: 100%; left: 50%; @@ -43,13 +44,15 @@ const StyledInnerContainer = styled.div` } &: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)}; margin-left: ${remcalc(-3)}; } &: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)}; margin-left: ${remcalc(-5)}; }