diff --git a/frontend/src/components/metrics-row/index.js b/frontend/src/components/metrics-row/index.js new file mode 100644 index 00000000..ff842e52 --- /dev/null +++ b/frontend/src/components/metrics-row/index.js @@ -0,0 +1,56 @@ +const React = require('react'); +const Styled = require('styled-components'); + +const Column = require('@ui/components/column'); +const MiniMetric = require('@ui/components/mini-metric'); +const PropTypes = require('@root/prop-types'); +const Row = require('@ui/components/row'); + +const { + default: styled +} = Styled; + +const { + MiniMetricGraph, + MiniMetricMeta, + MiniMetricTitle, + MiniMetricSubtitle, + MiniMetricView +} = MiniMetric; + +const StyledRow = styled(Row)` + margin: 0; + + & > div { + padding-left: 0; + padding-right: 0; + } +`; + +const MetricsRow = ({ + datasets = [] +}) => { + const _datasets = datasets.map((metric, i) => ( + + + + Memory: 54% + (1280/3000 MB) + + + + + )); + + return ( + + {_datasets} + + ); +}; + +MetricsRow.propTypes = { + datasets: React.PropTypes.arrayOf(PropTypes.dataset) +}; + +module.exports = MetricsRow; diff --git a/frontend/src/components/service-item/index.js b/frontend/src/components/service-item/index.js index 2680f894..3fe83741 100644 --- a/frontend/src/components/service-item/index.js +++ b/frontend/src/components/service-item/index.js @@ -1,8 +1,10 @@ +const forceArray = require('force-array'); const React = require('react'); const ReactRouter = require('react-router'); const Anchor = require('@ui/components/anchor'); const List = require('@ui/components/list'); +const MetricsRow = require('@components/metrics-row'); const PropTypes = require('@root/prop-types'); const { @@ -27,26 +29,51 @@ const ServiceItem = ({ project = '', service = {} }) => { + const isChild = !!service.parent; + + const childs = forceArray(service.services).map((service) => ( + + )); + const to = `/${org}/projects/${project}/services/${service.id}`; - const childs = service.services.map((service) => ( - 1} - > - - - {service.name} - {service.instances} instances - - - Metrics - - - - )); + const title = isChild ? ( + {service.name} + ) : ( + + + {Anchor.fn( + + {service.name} + + )} + + + ); + + const subtitle = ( + {service.instances} instances + ); + + const description = ( + Flags + ); + + const header = isChild ? null : ( + + + {title} + {subtitle} + {description} + + + + ); const view = childs.length ? ( @@ -55,10 +82,12 @@ const ServiceItem = ({ ) : ( - Flags + {isChild && title} + {isChild && subtitle} + {description} - Metrics + ); @@ -66,23 +95,12 @@ const ServiceItem = ({ return ( 1)} > - - - - - {Anchor.fn( - - {service.name} - - )} - - - {service.instances} instance - - - + {header} {view} ); diff --git a/frontend/src/containers/metrics/add-metrics.js b/frontend/src/containers/metrics/add-metrics.js index c9e37e8e..c1091361 100644 --- a/frontend/src/containers/metrics/add-metrics.js +++ b/frontend/src/containers/metrics/add-metrics.js @@ -24,7 +24,7 @@ const AddMetrics = ({ const added = (metric) => Boolean(metrics.filter((m) => m.id === metric).length); const addButton = (metric) => ( - + ); diff --git a/frontend/src/containers/service/instances.js b/frontend/src/containers/service/instances.js index c062a507..6572c529 100644 --- a/frontend/src/containers/service/instances.js +++ b/frontend/src/containers/service/instances.js @@ -1,11 +1,17 @@ const React = require('react'); const ReactRedux = require('react-redux'); +const actions = require('@state/actions'); const EmptyInstances = require('@components/empty/instances'); const PropTypes = require('@root/prop-types'); const List = require('@ui/components/list'); +const DatasetsRow = require('@components/metrics-row'); const selectors = require('@state/selectors'); +const { + toggleInstanceCollapsed +} = actions; + const { connect } = ReactRedux; @@ -19,22 +25,29 @@ const { ListItemView, ListItemMeta, ListItemTitle, - ListItemOptions + ListItemOptions, + ListItemOutlet } = List; const Instances = ({ - instances = [] + instances = [], + toggleCollapsed = () => null }) => { + const onClick = (uuid) => () => toggleCollapsed(uuid); + const empty = instances.length ? null : ( ); - const instanceList = instances.map((service) => ( - + const instanceList = instances.map((instance) => ( + - - {service.name} + + {instance.name} + + + … @@ -51,7 +64,8 @@ const Instances = ({ }; Instances.propTypes = { - instances: React.PropTypes.arrayOf(PropTypes.instance) + instances: React.PropTypes.arrayOf(PropTypes.instance), + toggleCollapsed: React.PropTypes.func }; const mapStateToProps = (state, { @@ -60,6 +74,11 @@ const mapStateToProps = (state, { instances: instancesByServiceIdSelector(params.serviceId)(state) }); +const mapDispatchToProps = (dispatch) => ({ + toggleCollapsed: (uuid) => dispatch(toggleInstanceCollapsed(uuid)) +}); + module.exports = connect( - mapStateToProps + mapStateToProps, + mapDispatchToProps )(Instances); diff --git a/frontend/src/mock-state.json b/frontend/src/mock-state.json index 365dffc0..48be8b2d 100644 --- a/frontend/src/mock-state.json +++ b/frontend/src/mock-state.json @@ -65,19 +65,129 @@ "time_of_day" ] }, - "data": [{ - "uuid": "dca08514-72e5-46ce-ad91-e68b3b0914d4", - "id": "cpu_agg_usage", - "service": "081a792c-47e0-4439-924b-2efa9788ae9e" - }, { - "uuid": "9e77b50e-42d7-425d-8daf-c0e98e2bdd6a", - "id": "zfs_used", - "service": "081a792c-47e0-4439-924b-2efa9788ae9e" - }, { - "uuid": "347dbdc7-15e3-4e12-8dfb-865d38526e14", - "id": "mem_limit", - "service": "081a792c-47e0-4439-924b-2efa9788ae9e" - }] + "data": { + "types": [{ + "uuid": "dca08514-72e5-46ce-ad91-e68b3b0914d4", + "id": "agg-cpu-usage" + }, { + "uuid": "9e77b50e-42d7-425d-8daf-c0e98e2bdd6a", + "id": "mem-res-set-size" + }, { + "uuid": "347dbdc7-15e3-4e12-8dfb-865d38526e14", + "id": "apache-http-reqs" + }, { + "uuid": "2aaa237d-42b3-442f-9094-a17aa470014b", + "name": "Memory", + "id": "memory" + }], + "datasets": [{ + "type": "2aaa237d-42b3-442f-9094-a17aa470014b", + "uuid": "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "data": [{ + "firstQuartile": 15, + "thirdQuartile": 15, + "median": 15, + "max": 15, + "min": 15 + }, { + "firstQuartile": 26, + "thirdQuartile": 26, + "median": 26, + "max": 26, + "min": 26 + }, { + "firstQuartile": 17, + "thirdQuartile": 17, + "median": 17, + "max": 17, + "min": 17 + }, { + "firstQuartile": 15, + "thirdQuartile": 25, + "median": 19, + "max": 19, + "min": 20 + }, { + "firstQuartile": 19, + "thirdQuartile": 25, + "median": 21, + "max": 20, + "min": 25 + }, { + "firstQuartile": 24, + "thirdQuartile": 30, + "median": 25, + "max": 26, + "min": 27 + }, { + "firstQuartile": 28, + "thirdQuartile": 34, + "median": 30, + "max": 30, + "min": 30 + }, { + "firstQuartile": 30, + "thirdQuartile": 45, + "median": 35, + "max": 40, + "min": 40 + }, { + "firstQuartile": 20, + "thirdQuartile": 55, + "median": 45, + "max": 44, + "min": 44 + }, { + "firstQuartile": 55, + "thirdQuartile": 55, + "median": 55, + "max": 55, + "min": 55 + }, { + "firstQuartile": 57, + "thirdQuartile": 56, + "median": 57, + "max": 58, + "min": 57 + }, { + "firstQuartile": 57, + "thirdQuartile": 56, + "median": 56, + "max": 56, + "min": 56 + }, { + "firstQuartile": 60, + "thirdQuartile": 56, + "median": 60, + "max": 60, + "min": 60 + }, { + "firstQuartile": 57, + "thirdQuartile": 57, + "median": 57, + "max": 57, + "min": 57 + }, { + "firstQuartile": 57, + "thirdQuartile": 55, + "median": 55, + "max": 55, + "min": 55 + }, { + "firstQuartile": 20, + "thirdQuartile": 45, + "median": 45, + "max": 45, + "min": 45 + }, { + "firstQuartile": 15, + "thirdQuartile": 40, + "median": 30, + "max": 49, + "min": 30 + }] + }] + } }, "orgs": { "ui": { @@ -180,120 +290,218 @@ "id": "nginx", "name": "Nginx", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 1 + "instances": 1, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "be227788-74f1-4e5b-a85f-b5c71cbae8d8", "id": "wordpress", "name": "Wordpress", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 1 + "instances": 1, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "6a0eee76-c019-413b-9d5f-44712b55b993", "id": "nfs", "name": "NFS", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 1 + "instances": 1, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "6d31aff4-de1e-4042-a983-fbd23d5c530c", "id": "memcached", "name": "Memcached", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 1 + "instances": 5, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "4ee4103e-1a52-4099-a48e-01588f597c70", "id": "percona", "name": "Percona", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 5 + "instances": 5, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "9572d367-c4ae-4fb1-8ad5-f5e3830e7034", "id": "primary", "name": "Primary", "parent": "4ee4103e-1a52-4099-a48e-01588f597c70", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 1 + "instances": 1, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "c8411ef0-ab39-42cb-a704-d20b170eff31", "id": "secondaries", "name": "Secondaries", "parent": "4ee4103e-1a52-4099-a48e-01588f597c70", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 4 + "instances": 4, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "97c68055-db88-45c9-ad49-f26da4264777", "id": "consul", "name": "Consul", "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", - "instances": 1 + "instances": 1, + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }] }, "instances": { + "ui": { + "collapsed": [] + }, "data": [{ "uuid": "309ecd9f-ac03-474b-aff7-4bd2e743296c", "name": "wordpress_01", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "be227788-74f1-4e5b-a85f-b5c71cbae8d8", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "0db6db53-de6f-4378-839e-5d5b452fbaf2", "name": "nfs_01", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "6a0eee76-c019-413b-9d5f-44712b55b993", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "250c8a6c-7d02-49a9-8abd-e1c22773041d", "name": "consul", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "97c68055-db88-45c9-ad49-f26da4264777", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "2c921f3a-8bc3-4f57-9cd7-789ebae72061", "name": "memcache_01", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "6d31aff4-de1e-4042-a983-fbd23d5c530c", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "68d3046e-8e34-4f5d-a0e5-db3795a250fd", "name": "memcache_02", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "6d31aff4-de1e-4042-a983-fbd23d5c530c", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "2ea99763-3b44-4179-8393-d66d94961051", "name": "memcache_03", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "6d31aff4-de1e-4042-a983-fbd23d5c530c", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "25f6bc62-63b8-4959-908e-1f6d7ff6341d", "name": "memcache_04", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "6d31aff4-de1e-4042-a983-fbd23d5c530c", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "8be01042-0281-4a77-a357-25979e87bf3d", "name": "memcache_05", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "6d31aff4-de1e-4042-a983-fbd23d5c530c", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "3d652e9d-73e8-4a6f-8171-84fa83740662", "name": "nginx", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", "service": "081a792c-47e0-4439-924b-2efa9788ae9e", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "c3ec7633-a02b-4615-86a0-9e6faeaae94b", "name": "percona-primary", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", - "service": "9572d367-c4ae-4fb1-8ad5-f5e3830e7034", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "service": "4ee4103e-1a52-4099-a48e-01588f597c70", + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }, { "uuid": "c2b5fec2-31e2-41a7-b7fc-cd0bb1822e76", "name": "percona-secundary", "datacenter": "f018da03-41c8-4619-a36a-ab8b706160cb", - "service": "c8411ef0-ab39-42cb-a704-d20b170eff31", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "service": "4ee4103e-1a52-4099-a48e-01588f597c70", + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "metrics": [ + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec", + "3e6ee79a-7453-4fc6-b9da-7ae1e41138ec" + ] }] } } diff --git a/frontend/src/prop-types.js b/frontend/src/prop-types.js index 611097f0..c3422655 100644 --- a/frontend/src/prop-types.js +++ b/frontend/src/prop-types.js @@ -39,6 +39,20 @@ const Metric = React.PropTypes.shape({ ...BaseObject }); +const Dataset = React.PropTypes.shape({ + uuid: React.PropTypes.string, + type: React.PropTypes.string, + data: React.PropTypes.arrayOf( + React.PropTypes.shape({ + firstQuartile: React.PropTypes.string, + thirdQuartile: React.PropTypes.string, + median: React.PropTypes.string, + max: React.PropTypes.string, + min: React.PropTypes.string + }) + ) +}); + const Sections = React.PropTypes.arrayOf( React.PropTypes.string ); @@ -58,5 +72,6 @@ module.exports = { instance: Instance, metric: Metric, // consinder renaming this to 'Types' as it could be used for any - metricTypes: MetricTypes + metricTypes: MetricTypes, + dataset: Dataset }; diff --git a/frontend/src/state/actions.js b/frontend/src/state/actions.js index 15308844..e0811e7c 100644 --- a/frontend/src/state/actions.js +++ b/frontend/src/state/actions.js @@ -12,5 +12,6 @@ module.exports = { updateRouter: createAction(`${APP}/APP/UPDATE_ROUTER`), toggleHeaderTooltip: createAction(`${APP}/APP/TOGGLE_HEADER_TOOLTIP`), toggleServiceCollapsed: createAction(`${APP}/APP/TOGGLE_SERVICE_COLLAPSED`), - addMetric: createAction(`${APP}/APP/ADD_METRIC`) + addMetric: createAction(`${APP}/APP/ADD_METRIC`), + toggleInstanceCollapsed: createAction(`${APP}/APP/TOGGLE_INSTANCE_COLLAPSED`) }; diff --git a/frontend/src/state/reducers/common.js b/frontend/src/state/reducers/common.js new file mode 100644 index 00000000..d192085c --- /dev/null +++ b/frontend/src/state/reducers/common.js @@ -0,0 +1,25 @@ +const toggleCollapsed = (state, action) => { + const { + ui + } = state; + + const { + collapsed = [] + } = ui; + + const _collapsed = collapsed.indexOf(action.payload) >= 0 + ? collapsed.filter((uuid) => uuid !== action.payload) + : [...collapsed, action.payload]; + + return { + ...state, + ui: { + ...ui, + collapsed: _collapsed + } + }; +}; + +module.exports = { + toggleCollapsed +}; diff --git a/frontend/src/state/reducers/instances.js b/frontend/src/state/reducers/instances.js index 7fec06fb..4f04e2f4 100644 --- a/frontend/src/state/reducers/instances.js +++ b/frontend/src/state/reducers/instances.js @@ -1,9 +1,20 @@ const ReduxActions = require('redux-actions'); +const actions = require('@state/actions'); +const common = require('@state/reducers/common'); + const { handleActions } = ReduxActions; +const { + toggleInstanceCollapsed +} = actions; + +const { + toggleCollapsed +} = common; + module.exports = handleActions({ - 'x': (state) => state // somehow handleActions needs at least one reducer + [toggleInstanceCollapsed.toString()]: toggleCollapsed }, {}); diff --git a/frontend/src/state/reducers/metrics.js b/frontend/src/state/reducers/metrics.js index 01549ac4..52c27dff 100644 --- a/frontend/src/state/reducers/metrics.js +++ b/frontend/src/state/reducers/metrics.js @@ -16,10 +16,15 @@ module.exports = handleActions({ [addMetric.toString()]: (state, action) => { return ({ ...state, - data: [ - ...state.data, - action.payload - ] + data: { + types: [ + ...state.data.types, + action.payload + ], + datasets: [ + ...state.data.datasets + ] + } }); } }, {}); diff --git a/frontend/src/state/reducers/services.js b/frontend/src/state/reducers/services.js index 4a6311db..d67ffb59 100644 --- a/frontend/src/state/reducers/services.js +++ b/frontend/src/state/reducers/services.js @@ -1,6 +1,7 @@ const ReduxActions = require('redux-actions'); const actions = require('@state/actions'); +const common = require('@state/reducers/common'); const { handleActions @@ -10,14 +11,10 @@ const { toggleServiceCollapsed } = actions; +const { + toggleCollapsed +} = common; + module.exports = handleActions({ - [toggleServiceCollapsed.toString()]: (state, action) => ({ - ...state, - ui: { - ...state.ui, - collapsed: state.ui.collapsed.indexOf(action.payload) >= 0 - ? state.ui.collapsed.filter((uuid) => uuid !== action.payload) - : [...state.ui.collapsed, action.payload] - } - }) + [toggleServiceCollapsed.toString()]: toggleCollapsed }, {}); diff --git a/frontend/src/state/selectors.js b/frontend/src/state/selectors.js index d8976164..27aa0201 100644 --- a/frontend/src/state/selectors.js +++ b/frontend/src/state/selectors.js @@ -16,8 +16,10 @@ const orgs = (state) => get(state, 'orgs.data', []); const projects = (state) => get(state, 'projects.data', []); const services = (state) => get(state, 'services.data', []); const collapsedServices = (state) => get(state, 'services.ui.collapsed', []); +const collapsedInstances = (state) => get(state, 'instances.ui.collapsed', []); const instances = (state) => get(state, 'instances.data', []); -const metrics = (state) => get(state, 'metrics.data', []); +const metricTypes = (state) => get(state, 'metrics.data.types', []); +const metricDatasets = (state) => get(state, 'metrics.data.datasets', []); const projectById = (projectId) => createSelector( projects, @@ -46,9 +48,16 @@ const orgSections = (orgId) => createSelector( ) ); +const isCollapsed = (collapsed, uuid) => collapsed.indexOf(uuid) >= 0; + +const datasets = (metrics, uuids) => uuids.map((uuid) => find(metrics, [ + 'uuid', + uuid +])); + const servicesByProjectId = (projectId) => createSelector( - [services, projectById(projectId), collapsedServices], - (services, project, collapsed) => + [services, projectById(projectId), collapsedServices, metricDatasets], + (services, project, collapsed, metrics) => services.filter((s) => s.project === project.uuid) .map((service) => ({ ...service, @@ -57,24 +66,31 @@ const servicesByProjectId = (projectId) => createSelector( .filter((s) => !s.parent) .map((service) => ({ ...service, - collapsed: collapsed.indexOf(service.uuid) >= 0, + metrics: datasets(metrics, service.metrics), + collapsed: isCollapsed(collapsed, service.uuid), services: service.services.map((service) => ({ ...service, - collapsed: collapsed.indexOf(service.uuid) >= 0 + metrics: datasets(metrics, service.metrics), + collapsed: isCollapsed(collapsed, service.uuid) })) })) ); const instancesByServiceId = (serviceId) => createSelector( - [instances, serviceById(serviceId)], - (instances, service) => + [instances, serviceById(serviceId), collapsedInstances, metricDatasets], + (instances, service, collapsed, metrics) => instances.filter((i) => i.service === service.uuid) + .map((instance) => ({ + ...instance, + metrics: datasets(metrics, instance.metrics), + collapsed: isCollapsed(collapsed, instance.uuid) + })) ); const metricsByServiceId = (serviceId) => createSelector( - [metrics, serviceById(serviceId)], - (metrics, service) => - metrics.filter((i) => i.service === service.uuid) + [metricTypes, serviceById(serviceId)], + (metricTypes, service) => + metricTypes.filter((i) => i.service === service.uuid) ); diff --git a/ui/.storybook/config.js b/ui/.storybook/config.js index 404a63a1..61c63a83 100644 --- a/ui/.storybook/config.js +++ b/ui/.storybook/config.js @@ -3,7 +3,13 @@ const { configure } = require('@kadira/storybook'); const req = require.context('../src/components', true, /story.js$/) function loadStories() { - req.keys().forEach((filename) => req(filename)); + let stories = req.keys(); + stories = stories.sort(); + + stories.forEach( story => req(story)); + + // Fallback to stories/index.js file for anything that + // hasn't been moved require('../stories'); } diff --git a/ui/src/components/add-metric/button.js b/ui/src/components/add-metric/button.js index d3b1275b..80282ad5 100644 --- a/ui/src/components/add-metric/button.js +++ b/ui/src/components/add-metric/button.js @@ -20,8 +20,10 @@ const StyledButton = styled(Button)` const AddMetricButton = ({ children, disabled, + metric, onClick }) => { + const onButtonClick = (e) => onClick(metric); return disabled ? ( {children} @@ -44,6 +46,7 @@ const AddMetricButton = ({ AddMetricButton.propTypes = { children: React.PropTypes.node, disabled: React.PropTypes.bool, + metric: React.PropTypes.string, onClick: React.PropTypes.func, }; diff --git a/ui/src/components/checkbox/story.js b/ui/src/components/checkbox/story.js new file mode 100644 index 00000000..bdbebdab --- /dev/null +++ b/ui/src/components/checkbox/story.js @@ -0,0 +1,18 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Checkbox = require('./'); + +storiesOf('Checkbox', module) + .add('Default', () => ( + + )) + .add('Checked', () => ( + + )) + .add('Disabled', () => ( + + )); \ No newline at end of file diff --git a/ui/src/components/close/story.js b/ui/src/components/close/story.js new file mode 100644 index 00000000..2776d6e2 --- /dev/null +++ b/ui/src/components/close/story.js @@ -0,0 +1,20 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base = require('../base'); +const Close = require('./'); + +storiesOf('Close', module) + .add('Default', () => ( + + + + )); \ No newline at end of file diff --git a/ui/src/components/input/index.js b/ui/src/components/input/index.js index 63fa5d72..0ced95fa 100644 --- a/ui/src/components/input/index.js +++ b/ui/src/components/input/index.js @@ -31,12 +31,11 @@ const InputField = styled.input` color: ${props => props.error ? colors.alert : colors.fonts.semibold} display: block; font-size: 16px; - height: ${remcalc(50)}; - padding: ${remcalc(16)}; + padding: ${remcalc('15 18')}; visibility: visible; width: 100%; ${baseBox()}; - border-color: ${props => props.error ? colors.alert : 'auto'}; + border-color: ${props => props.error ? colors.alert : ''}; &:focus { border-color: ${boxes.border.checked}; diff --git a/ui/src/components/input/story.js b/ui/src/components/input/story.js new file mode 100644 index 00000000..af8538db --- /dev/null +++ b/ui/src/components/input/story.js @@ -0,0 +1,35 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base= require('../base'); +const Input = require('./'); + +storiesOf('Input', module) + .add('Default', () => ( + + + + )) + .add('type=email', () => ( + + + We'll never share your email with anyone else. + + + )) + .add('Error', () => ( + + + + )); \ No newline at end of file diff --git a/ui/src/components/list/mini-metric-data.js b/ui/src/components/list/mini-metric-data.js new file mode 100644 index 00000000..c1cdb615 --- /dev/null +++ b/ui/src/components/list/mini-metric-data.js @@ -0,0 +1,103 @@ +module.exports = [{ + firstQuartile: 15, + thirdQuartile: 15, + median: 15, + max: 15, + min: 15, +}, { + firstQuartile: 26, + thirdQuartile: 26, + median: 26, + max: 26, + min: 26, +}, { + firstQuartile: 17, + thirdQuartile: 17, + median: 17, + max: 17, + min: 17, +}, { + firstQuartile: 15, + thirdQuartile: 25, + median: 19, + max: 19, + min: 20, +}, { + firstQuartile: 19, + thirdQuartile: 25, + median: 21, + max: 20, + min: 25, +}, { + firstQuartile: 24, + thirdQuartile: 30, + median: 25, + max: 26, + min: 27, +}, { + firstQuartile: 28, + thirdQuartile: 34, + median: 30, + max: 30, + min: 30, +}, { + firstQuartile: 30, + thirdQuartile: 45, + median: 35, + max: 40, + min: 40, +}, { + firstQuartile: 20, + thirdQuartile: 55, + median: 45, + max: 44, + min: 44, +}, { + firstQuartile: 55, + thirdQuartile: 55, + median: 55, + max: 55, + min: 55, +}, { + firstQuartile: 57, + thirdQuartile: 56, + median: 57, + max: 58, + min: 57, +}, { + firstQuartile: 57, + thirdQuartile: 56, + median: 56, + max: 56, + min: 56, +}, { + firstQuartile: 60, + thirdQuartile: 56, + median: 60, + max: 60, + min: 60, +}, { + firstQuartile: 57, + thirdQuartile: 57, + median: 57, + max: 57, + min: 57, +}, { + firstQuartile: 57, + thirdQuartile: 55, + median: 55, + max: 55, + min: 55, +}, { + firstQuartile: 20, + thirdQuartile: 45, + median: 45, + max: 45, + min: 45, +}, { + firstQuartile: 15, + thirdQuartile: 40, + median: 30, + max: 49, + min: 30, +}]; \ No newline at end of file diff --git a/ui/src/components/list/story.js b/ui/src/components/list/story.js new file mode 100644 index 00000000..49da758f --- /dev/null +++ b/ui/src/components/list/story.js @@ -0,0 +1,320 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base = require('../base'); +const Row = require('../row'); +const Column = require('../column'); +const MiniMetricData = require('./mini-metric-data'); + +const { + MiniMetricGraph, + MiniMetricMeta, + MiniMetricTitle, + MiniMetricSubtitle, + MiniMetricView +} = require('../mini-metric'); + +const { + ListItemDescription, + ListItemHeader, + ListItem, + ListItemMeta, + ListItemOptions, + ListItemOutlet, + ListItemSubTitle, + ListItemTitle, + ListItemView, + ListItemGroupView +} = require('./'); + +storiesOf('List Item', module) + .add('default', () => ( + + + + + Nginx 01 + 4 instances + Flags + + + Metrics + + + + … + + + + )) + .add('collapsed', () => ( + + + + + Nginx 01 + 4 instances + Flags + + + Metrics + + + + … + + + + )) + .add('headed', () => ( + + + + + Nginx 01 + 4 instances + Flags + + + … + + + + + Flags + + + Metrics + + + + + )) + .add('headed collapsed', () => ( + + + + + Nginx 01 + 4 instances + Flags + + + … + + + + + Flags + + + Metrics + + + + + )) + .add('stacked', () => ( + + + + + Nginx 01 + 4 instances + Flags + + + Metrics + + + + … + + + + )) + .add('view-group', () => ( + + + + + Percona + 5 instances + Flags + + + + + + + + percona_database + + + Metrics + + + + + + + percona_database + 5 instances + Flags + + + Metrics + + + + + + + percona_database + 5 instances + + + Metrics + + + + + + + )) + .add('view-group with metrics', () => ( + + + + + Percona + 5 instances + Flags + + + + + + + + percona_database + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + + + + + percona_database + 5 instances + Flags + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + + + + + percona_database + 5 instances + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + Memory: 54% + (1280/3000 MB) + + + + + + + + + + + + )); \ No newline at end of file diff --git a/ui/src/components/list/transfer-props.js b/ui/src/components/list/transfer-props.js index 0d8f4284..275dc6e3 100644 --- a/ui/src/components/list/transfer-props.js +++ b/ui/src/components/list/transfer-props.js @@ -4,7 +4,7 @@ const React = require('react'); const transfer = (parentProps, props) => { // eslint-disable-next-line react/prop-types return React.Children.map(props.children, (c) => { - return React.cloneElement(c, { + return c && React.cloneElement(c, { ...c.props, ...parentProps.reduce((sum, name) => ({ ...sum, diff --git a/ui/src/components/modal/story.js b/ui/src/components/modal/story.js new file mode 100644 index 00000000..74a142fc --- /dev/null +++ b/ui/src/components/modal/story.js @@ -0,0 +1,17 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base= require('../base'); +const Modal = require('./'); + +storiesOf('Modal', module) + .add('Default', () => ( + + +

This is the Modal

+
+ + )); \ No newline at end of file diff --git a/ui/src/components/notification/story.js b/ui/src/components/notification/story.js new file mode 100644 index 00000000..5c126b90 --- /dev/null +++ b/ui/src/components/notification/story.js @@ -0,0 +1,34 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base= require('../base'); +const Notificaton = require('./'); + +storiesOf('Notificaton', module) + .add('Default', () => ( + + + This is the default content + + + )) + .add('Success', () => ( + + + This is a success notification that is closable + + + )) + .add('Alert', () => ( + + + This is the alert content + + + )); \ No newline at end of file diff --git a/ui/src/components/pagination/story.js b/ui/src/components/pagination/story.js new file mode 100644 index 00000000..284fb49e --- /dev/null +++ b/ui/src/components/pagination/story.js @@ -0,0 +1,20 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Pagination = require('./'); + +storiesOf('Pagination', module) + .add('Default', () => ( + + + « + Previous + + 1 + 2 + 3 + + )); \ No newline at end of file diff --git a/ui/src/components/radio-group/story.js b/ui/src/components/radio-group/story.js new file mode 100644 index 00000000..09d7b34f --- /dev/null +++ b/ui/src/components/radio-group/story.js @@ -0,0 +1,27 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base= require('../base'); +const RadioGroup = require('./'); +const Radio = require('./item'); + + +storiesOf('Radio Group', module) + .add('Default', () => ( + + + + Video killed the radio star + + + Video killed the radio star + + + Video killed the radio star + + + + )); \ No newline at end of file diff --git a/ui/src/components/radio/story.js b/ui/src/components/radio/story.js new file mode 100644 index 00000000..ed9697ad --- /dev/null +++ b/ui/src/components/radio/story.js @@ -0,0 +1,20 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Radio = require('./'); + +storiesOf('Radio', module) + .add('Default', () => ( + + Video killed the radio star + + )) + .add('Checked', () => ( + + )) + .add('Disabled', () => ( + + )); \ No newline at end of file diff --git a/ui/src/components/range-slider/story.js b/ui/src/components/range-slider/story.js new file mode 100644 index 00000000..21e479f5 --- /dev/null +++ b/ui/src/components/range-slider/story.js @@ -0,0 +1,12 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const RangeSlider = require('./'); + +storiesOf('Range Slider', module) + .add('Default', () => ( + + )); \ No newline at end of file diff --git a/ui/src/components/select-custom/story.js b/ui/src/components/select-custom/story.js new file mode 100644 index 00000000..7763f7fa --- /dev/null +++ b/ui/src/components/select-custom/story.js @@ -0,0 +1,28 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const { + selectData +} = require('../../shared/fake-data'); + +const SelectCustom = require('./'); + +storiesOf('Select Custom', module) + .add('Default', () => ( + + )) + .add('Multiple', () => ( + + )); \ No newline at end of file diff --git a/ui/src/components/select/story.js b/ui/src/components/select/story.js new file mode 100644 index 00000000..871941c3 --- /dev/null +++ b/ui/src/components/select/story.js @@ -0,0 +1,26 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Select = require('./'); + +storiesOf('Select', module) + .add('Default', () => ( + + )) + .add('multiple', () => ( + + )); \ No newline at end of file diff --git a/ui/src/components/tabs/story.js b/ui/src/components/tabs/story.js new file mode 100644 index 00000000..f23bb076 --- /dev/null +++ b/ui/src/components/tabs/story.js @@ -0,0 +1,20 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Tabs = require('./'); +const Tab = require('./tab'); + +storiesOf('Tabs', module) + .add('Default', () => ( + + +

Containers

+
+ +

User

+
+
+ )); \ No newline at end of file diff --git a/ui/src/components/textarea/index.js b/ui/src/components/textarea/index.js new file mode 100644 index 00000000..b7b0f0ad --- /dev/null +++ b/ui/src/components/textarea/index.js @@ -0,0 +1,146 @@ +const React = require('react'); + +const composers = require('../../shared/composers'); +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); +const Styled = require('styled-components'); + +const { + boxes, + colors +} = constants; + +const { + remcalc +} = fns; + +const { + baseBox +} = composers; + +const { + default: styled +} = Styled; + +const Label = styled.label` + color: ${props => props.error ? colors.alert : colors.fonts.regular} +`; + +const InputField = styled.textarea` + background: ${colors.brandSecondary}; + color: ${props => props.error ? colors.alert : colors.fonts.semibold} + display: block; + font-size: 16px; + padding: ${remcalc('15 18')}; + visibility: visible; + width: 100%; + min-height: ${remcalc(96)}; + ${baseBox()}; + border-color: ${props => props.error ? colors.alert : ''}; + + &:focus { + border-color: ${boxes.border.checked}; + outline: none; + } +`; + +const Error = styled.span` + float: right; + color: ${colors.alert}; + font-size: ${remcalc(14)}; +`; + +const Textarea = ({ + autoComplete, + autoFocus, + children, + className, + disabled = false, + error, + form, + id, + inputMode, + label, + labelledby, + list, + name, + onChange, + pattern, + placeholder, + readOnly, + required, + selectionDirection, + spellCheck, + style, + tabIndex, + type, + value +}) => { + const _label = label || children; + const _children = label && children ? children : null; + const _error = error ? ({error}) : null; + + return ( +
+ + {_error} + + {_children} +
+ ); +}; + +Textarea.propTypes = { + autoComplete: React.PropTypes.string, + autoFocus: React.PropTypes.bool, + children: React.PropTypes.node, + className: React.PropTypes.string, + disabled: React.PropTypes.bool, + error: React.PropTypes.string, + form: React.PropTypes.string, + id: React.PropTypes.string, + inputMode: React.PropTypes.string, + label: React.PropTypes.string, + labelledby: React.PropTypes.string, + list: React.PropTypes.string, + name: React.PropTypes.string, + onChange: React.PropTypes.func, + pattern: React.PropTypes.string, + placeholder: React.PropTypes.string, + readOnly: React.PropTypes.bool, + required: React.PropTypes.bool, + selectionDirection: React.PropTypes.string, + spellCheck: React.PropTypes.bool, + style: React.PropTypes.object, + tabIndex: React.PropTypes.string, + type: React.PropTypes.string, + value: React.PropTypes.string +}; + +module.exports = Textarea; diff --git a/ui/src/components/textarea/readme.md b/ui/src/components/textarea/readme.md new file mode 100644 index 00000000..e199c638 --- /dev/null +++ b/ui/src/components/textarea/readme.md @@ -0,0 +1,60 @@ +# Input + +## demo + +```embed +const React = require('react'); +const ReactDOM = require('react-dom/server'); +const Base = require('../base'); +const Container = require('../container'); +const Row = require('../row'); +const Column = require('../column'); +const Input = require('./index.js'); + +nmodule.exports = ReactDOM.renderToString( + + + + + We'll never share your email with anyone else. + + + + + + + Password + + + + +); +``` + +## usage + +```js +const React = require('react'); +const Input = require('ui/input'); + +module.exports = () => { + return ( +
+ + We'll never share your email with anyone else. + + + Password + +
+ ); +} +``` diff --git a/ui/src/components/textarea/story.js b/ui/src/components/textarea/story.js new file mode 100644 index 00000000..d74a5ee3 --- /dev/null +++ b/ui/src/components/textarea/story.js @@ -0,0 +1,24 @@ +const React = require('react'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const Base= require('../base'); +const Textarea = require('./'); + +storiesOf('Textarea', module) + .add('Default', () => ( + +