diff --git a/frontend/locales/en-us.json b/frontend/locales/en-us.json index 8262cd3b..b74f12f2 100644 --- a/frontend/locales/en-us.json +++ b/frontend/locales/en-us.json @@ -18,5 +18,24 @@ "mem-res-set-size": "Memory resident set size", "mem-res-set-size-sm": "Process memory that is actually stored in the RAM", "apache-http-reqs": "Apache HTTP requests", - "apache-http-reqs-sm": "Number of website requests to apache if it is used" -} \ No newline at end of file + "apache-http-reqs-sm": "Number of website requests to apache if it is used", + "metrics": { + "add": { + "add-label": "Add", + "added-label": "Added", + "link-label": "Learn more" + }, + "cpu_agg_usage": { + "title": "Aggregated CPU usage", + "description": "CPU usages accross all of the CPU cores." + }, + "cpu_wait_time": { + "title": "Memory resident set size", + "description": "Process memory that is actually stored in the RAM." + }, + "zfs_used": { + "title": "Apache HTTP requests", + "description": "Number of website requests to apache if it is used." + } + } +} diff --git a/frontend/scripts/build-locales.js b/frontend/scripts/build-locales.js index 599b7e0d..8a949229 100644 --- a/frontend/scripts/build-locales.js +++ b/frontend/scripts/build-locales.js @@ -22,13 +22,31 @@ const source = ({ })(); `; +const flattenMessages = (nestedMessages, prefix='') => { + return Object.keys(nestedMessages).reduce((messages, key) => { + const value = nestedMessages[key]; + const prefixedKey = prefix ? `${prefix}.${key}` : key; + + if(typeof value === 'string') { + messages[prefixedKey] = value; + } + else { + Object.assign(messages, flattenMessages(value, prefixedKey)); + } + + return messages; + }, {}); +}; + const compile = async () => { const files = await readdir(root); const jsons = files.filter(filename => path.extname(filename) === '.json'); const locales = jsons.reduce((res, filename) => { const name = path.parse(filename).name; - const json = JSON.stringify(require(path.join(root, filename))); + const messages = require(path.join(root, filename)); + const flattenedMessages = flattenMessages(messages); + const json = JSON.stringify(flattenedMessages); const lang = name.split(/\-/)[0]; return { diff --git a/frontend/src/components/empty/instances.js b/frontend/src/components/empty/instances.js new file mode 100644 index 00000000..739406c1 --- /dev/null +++ b/frontend/src/components/empty/instances.js @@ -0,0 +1,19 @@ +const React = require('react'); +const ReactIntl = require('react-intl'); + +const Column = require('@ui/components/column'); +const Row = require('@ui/components/row'); + +const { + FormattedMessage +} = ReactIntl; + +module.exports = () => ( + + +

+ +

+
+
+); 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 new file mode 100644 index 00000000..3fe83741 --- /dev/null +++ b/frontend/src/components/service-item/index.js @@ -0,0 +1,115 @@ +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 { + Link +} = ReactRouter; + +const { + ListItem, + ListItemView, + ListItemMeta, + ListItemTitle, + ListItemSubTitle, + ListItemDescription, + ListItemGroupView, + ListItemOutlet, + ListItemOptions, + ListItemHeader +} = List; + +const ServiceItem = ({ + org = '', + project = '', + service = {} +}) => { + const isChild = !!service.parent; + + const childs = forceArray(service.services).map((service) => ( + + )); + + const to = `/${org}/projects/${project}/services/${service.id}`; + + 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 ? ( + + {childs} + + ) : ( + + + {isChild && title} + {isChild && subtitle} + {description} + + + + + + ); + + return ( + 1)} + > + {header} + {view} + + ); +}; + +ServiceItem.propTypes = { + org: React.PropTypes.string, + project: React.PropTypes.string, + service: PropTypes.service +}; + +module.exports = ServiceItem; diff --git a/frontend/src/containers/metrics/add-metrics.js b/frontend/src/containers/metrics/add-metrics.js new file mode 100644 index 00000000..c1091361 --- /dev/null +++ b/frontend/src/containers/metrics/add-metrics.js @@ -0,0 +1,65 @@ +const React = require('react'); +const PropTypes = require('@root/prop-types'); +const AddMetric = require('@ui/components/add-metric'); +const ReactIntl = require('react-intl'); + +const { + FormattedMessage +} = ReactIntl; + +const { + AddMetricButton, + AddMetricDescription, + AddMetricLink, + AddMetricTile, + AddMetricTitle +} = AddMetric; + +const AddMetrics = ({ + metricTypes, + metrics, + onAddMetric +}) => { + + const added = (metric) => + Boolean(metrics.filter((m) => m.id === metric).length); + const addButton = (metric) => ( + + + + ); + const addedButton = ( + + + + ); + + const metricList = metricTypes.map((metric) => ( + + + + + + + + + + + { added(metric) ? addedButton : addButton(metric) } + + )); + + return ( +
+ {metricList} +
+ ); +}; + +AddMetrics.propTypes = { + metricTypes: PropTypes.metricTypes.isRequired, + metrics: React.PropTypes.arrayOf(PropTypes.metric).isRequired, + onAddMetric: React.PropTypes.func.isRequired, +}; + +module.exports = AddMetrics; diff --git a/frontend/src/containers/metrics/index.js b/frontend/src/containers/metrics/index.js new file mode 100644 index 00000000..e69de29b diff --git a/frontend/src/containers/service/instances.js b/frontend/src/containers/service/instances.js index 84bda49e..6572c529 100644 --- a/frontend/src/containers/service/instances.js +++ b/frontend/src/containers/service/instances.js @@ -1,5 +1,84 @@ const React = require('react'); +const ReactRedux = require('react-redux'); -module.exports = () => ( -

instances

-); +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; + +const { + instancesByServiceIdSelector +} = selectors; + +const { + ListItem, + ListItemView, + ListItemMeta, + ListItemTitle, + ListItemOptions, + ListItemOutlet +} = List; + +const Instances = ({ + instances = [], + toggleCollapsed = () => null +}) => { + const onClick = (uuid) => () => toggleCollapsed(uuid); + + const empty = instances.length ? null : ( + + ); + + const instanceList = instances.map((instance) => ( + + + + {instance.name} + + + + + + + … + + + )); + + return ( +
+ {empty} + {instanceList} +
+ ); +}; + +Instances.propTypes = { + instances: React.PropTypes.arrayOf(PropTypes.instance), + toggleCollapsed: React.PropTypes.func +}; + +const mapStateToProps = (state, { + params = {} +}) => ({ + instances: instancesByServiceIdSelector(params.serviceId)(state) +}); + +const mapDispatchToProps = (dispatch) => ({ + toggleCollapsed: (uuid) => dispatch(toggleInstanceCollapsed(uuid)) +}); + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(Instances); diff --git a/frontend/src/containers/service/metrics.js b/frontend/src/containers/service/metrics.js index 66890823..c7ffa76e 100644 --- a/frontend/src/containers/service/metrics.js +++ b/frontend/src/containers/service/metrics.js @@ -1,5 +1,71 @@ const React = require('react'); +const ReactRedux = require('react-redux'); +const PropTypes = require('@root/prop-types'); +const selectors = require('@state/selectors'); +const AddMetrics = require('../metrics/add-metrics'); +const actions = require('@state/actions'); -module.exports = () => ( -

metrics

-); +const { + connect +} = ReactRedux; + +const { + metricsByServiceIdSelector, + serviceByIdSelector +} = selectors; + +const { + addMetric +} = actions; + +const Metrics = ({ + addMetric, + metrics, + metricTypes, + service +}) => { + + const onAddMetric = (metric) => { + addMetric({ + id: metric, + service: service.uuid + }); + }; + + return ( +
+

metrics

+
+ +
+
+ ); +}; + +Metrics.propTypes = { + addMetric: React.PropTypes.func.isRequired, + metricTypes: PropTypes.metricTypes, + metrics: React.PropTypes.arrayOf(PropTypes.metric), + service: PropTypes.service +}; + +const mapStateToProps = (state, { + params = {} +}) => ({ + metrics: metricsByServiceIdSelector(params.serviceId)(state), + metricTypes: state.metrics.ui.types, + service: serviceByIdSelector(params.serviceId)(state) +}); + +const mapDispatchToProps = (dispatch) => ({ + addMetric: (payload) => dispatch(addMetric(payload)) +}); + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(Metrics); diff --git a/frontend/src/containers/services/index.js b/frontend/src/containers/services/index.js index 244cae8f..cc0c1c78 100644 --- a/frontend/src/containers/services/index.js +++ b/frontend/src/containers/services/index.js @@ -1,10 +1,9 @@ const React = require('react'); const ReactRedux = require('react-redux'); -const ReactRouter = require('react-router'); const EmptyServices = require('@components/empty/services'); const PropTypes = require('@root/prop-types'); -const Row = require('@ui/components/row'); +const ServiceItem = require('@components/service-item'); const selectors = require('@state/selectors'); const { @@ -17,10 +16,6 @@ const { servicesByProjectIdSelector } = selectors; -const { - Link -} = ReactRouter; - const Services = ({ org = {}, project = {}, @@ -30,37 +25,19 @@ const Services = ({ ); - const serviceList = (services) => { - if (!services || !services.length) { - return null; - } - - const list = services.map((service) => { - const to = `/${org.id}/projects/${project.id}/services/${service.id}`; - - return ( -
  • - - {service.name} - - {serviceList(service.services)} -
  • - ); - }); - - return ( -
      - {list} -
    - ); - }; + const serviceList = services.map((service) => ( + + )); return (
    {empty} - - {serviceList(services)} - + {serviceList}
    ); }; diff --git a/frontend/src/mock-state.json b/frontend/src/mock-state.json index b1a729c4..48be8b2d 100644 --- a/frontend/src/mock-state.json +++ b/frontend/src/mock-state.json @@ -47,16 +47,147 @@ }] }, "metrics": { - "data": [{ - "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" - }] + "ui": { + "types": [ + "cpu_agg_usage", + "cpu_wait_time", + "zfs_used", + "zfs_available", + "load_average", + "mem_agg_usage", + "mem_limit", + "mem_swap", + "mem_swap_limit", + "net_agg_packets_in", + "net_agg_packets_out", + "net_agg_bytes_in", + "net_agg_bytes_out", + "time_of_day" + ] + }, + "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": { @@ -142,6 +273,7 @@ }, "services": { "ui": { + "collapsed": [], "sections": [ "summary", "instances", @@ -157,113 +289,219 @@ "uuid": "081a792c-47e0-4439-924b-2efa9788ae9e", "id": "nginx", "name": "Nginx", - "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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" + "project": "e0ea0c02-55cc-45fe-8064-3e5176a59401", + "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 1a5b45ef..c3422655 100644 --- a/frontend/src/prop-types.js +++ b/frontend/src/prop-types.js @@ -28,15 +28,50 @@ const Service = React.PropTypes.shape({ ...BaseObject }); +const Instance = React.PropTypes.shape({ + ...BaseObject, + datacenter: React.PropTypes.string, + service: React.PropTypes.string, + project: React.PropTypes.string +}); + +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 ); +// consinder renaming this to 'Types' as it could be used for any +const MetricTypes = React.PropTypes.arrayOf( + React.PropTypes.string +); + module.exports = { account: Account, link: Link, org: Org, project: Project, sections: Sections, - service: Service + service: Service, + instance: Instance, + metric: Metric, + // consinder renaming this to 'Types' as it could be used for any + metricTypes: MetricTypes, + dataset: Dataset }; diff --git a/frontend/src/state/actions.js b/frontend/src/state/actions.js index 3f7edba6..e0811e7c 100644 --- a/frontend/src/state/actions.js +++ b/frontend/src/state/actions.js @@ -10,5 +10,8 @@ const APP = constantCase(process.env['APP_NAME']); module.exports = { ...require('@state/thunks'), updateRouter: createAction(`${APP}/APP/UPDATE_ROUTER`), - toggleHeaderTooltip: createAction(`${APP}/APP/TOGGLE_HEADER_TOOLTIP`) + toggleHeaderTooltip: createAction(`${APP}/APP/TOGGLE_HEADER_TOOLTIP`), + toggleServiceCollapsed: createAction(`${APP}/APP/TOGGLE_SERVICE_COLLAPSED`), + 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/index.js b/frontend/src/state/reducers/index.js index 8085e6d8..9f428409 100644 --- a/frontend/src/state/reducers/index.js +++ b/frontend/src/state/reducers/index.js @@ -8,7 +8,9 @@ module.exports = () => { return combineReducers({ account: require('@state/reducers/account'), app: require('@state/reducers/app'), + instances: require('@state/reducers/instances'), intl: require('@state/reducers/intl'), + metrics: require('@state/reducers/metrics'), orgs: require('@state/reducers/orgs'), projects: require('@state/reducers/projects'), services: require('@state/reducers/services') diff --git a/frontend/src/state/reducers/instances.js b/frontend/src/state/reducers/instances.js new file mode 100644 index 00000000..4f04e2f4 --- /dev/null +++ b/frontend/src/state/reducers/instances.js @@ -0,0 +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({ + [toggleInstanceCollapsed.toString()]: toggleCollapsed +}, {}); diff --git a/frontend/src/state/reducers/metrics.js b/frontend/src/state/reducers/metrics.js new file mode 100644 index 00000000..52c27dff --- /dev/null +++ b/frontend/src/state/reducers/metrics.js @@ -0,0 +1,30 @@ +const ReduxActions = require('redux-actions'); + +const actions = require('@state/actions'); + +const { + handleActions +} = ReduxActions; + +const { + addMetric +} = actions; + +// This will need to be handled by an async action +// to update on the server too +module.exports = handleActions({ + [addMetric.toString()]: (state, action) => { + return ({ + ...state, + 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 7fec06fb..d67ffb59 100644 --- a/frontend/src/state/reducers/services.js +++ b/frontend/src/state/reducers/services.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 { + toggleServiceCollapsed +} = actions; + +const { + toggleCollapsed +} = common; + module.exports = handleActions({ - 'x': (state) => state // somehow handleActions needs at least one reducer + [toggleServiceCollapsed.toString()]: toggleCollapsed }, {}); diff --git a/frontend/src/state/selectors.js b/frontend/src/state/selectors.js index 2f267da0..27aa0201 100644 --- a/frontend/src/state/selectors.js +++ b/frontend/src/state/selectors.js @@ -15,6 +15,11 @@ const serviceUiSections = (state) => get(state, 'services.ui.sections', []); 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 metricTypes = (state) => get(state, 'metrics.data.types', []); +const metricDatasets = (state) => get(state, 'metrics.data.datasets', []); const projectById = (projectId) => createSelector( projects, @@ -43,17 +48,52 @@ 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)], - (services, project) => + [services, projectById(projectId), collapsedServices, metricDatasets], + (services, project, collapsed, metrics) => services.filter((s) => s.project === project.uuid) .map((service) => ({ ...service, services: services.filter((s) => s.parent === service.uuid) })) .filter((s) => !s.parent) + .map((service) => ({ + ...service, + metrics: datasets(metrics, service.metrics), + collapsed: isCollapsed(collapsed, service.uuid), + services: service.services.map((service) => ({ + ...service, + metrics: datasets(metrics, service.metrics), + collapsed: isCollapsed(collapsed, service.uuid) + })) + })) ); +const instancesByServiceId = (serviceId) => createSelector( + [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( + [metricTypes, serviceById(serviceId)], + (metricTypes, service) => + metricTypes.filter((i) => i.service === service.uuid) +); + + module.exports = { accountSelector: account, accountUISelector: accountUi, @@ -66,5 +106,7 @@ module.exports = { serviceSectionsSelector: serviceUiSections, projectsByOrgIdSelector: projectsByOrgId, projectByIdSelector: projectById, - servicesByProjectIdSelector: servicesByProjectId + servicesByProjectIdSelector: servicesByProjectId, + instancesByServiceIdSelector: instancesByServiceId, + metricsByServiceIdSelector: metricsByServiceId }; diff --git a/ui/.storybook/config.js b/ui/.storybook/config.js index c00d9483..bb888c3a 100644 --- a/ui/.storybook/config.js +++ b/ui/.storybook/config.js @@ -1,6 +1,15 @@ const { configure } = require('@kadira/storybook'); +const req = require.context('../src/components', true, /story.js$/) + function loadStories() { + 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/package.json b/ui/package.json index 19693a4b..76e31d92 100644 --- a/ui/package.json +++ b/ui/package.json @@ -8,7 +8,7 @@ "lint": "make lint", "test": "make test", "build": "make compile", - "storybook": "start-storybook -p 6006", + "storybook": "start-storybook -s ./src/shared/assets -p 6006", "build-storybook": "build-storybook" }, "dependencies": { @@ -31,7 +31,8 @@ "react-faux-dom": "^3.0.0", "react-select": "^1.0.0-rc.2", "reduce-css-calc": "^1.3.0", - "styled-components": "^1.2.1" + "styled-components": "^1.2.1", + "transform-props-with": "^2.1.0" }, "devDependencies": { "@kadira/storybook": "^2.35.2", @@ -53,6 +54,7 @@ "eslint-plugin-promise": "^3.4.0", "eslint-plugin-react": "^6.8.0", "eslint-plugin-standard": "^2.0.1", + "jsdom": "^9.9.1", "memory-fs": "^0.4.1", "nyc": "^10.0.0", "pre-commit": "^1.2.2", diff --git a/ui/src/components/add-metric/button.js b/ui/src/components/add-metric/button.js new file mode 100644 index 00000000..80282ad5 --- /dev/null +++ b/ui/src/components/add-metric/button.js @@ -0,0 +1,53 @@ +const React = require('react'); +const Styled = require('styled-components'); +const fns = require('../../shared/functions'); +const Button = require('../button'); + +const { + default: styled +} = Styled; + +const { + remcalc +} = fns; + +const StyledButton = styled(Button)` + position: absolute; + left: ${remcalc(24)}; + bottom: ${remcalc(24)}; +`; + +const AddMetricButton = ({ + children, + disabled, + metric, + onClick +}) => { + const onButtonClick = (e) => onClick(metric); + return disabled ? + ( + + {children} + + ) : ( + + {children} + + ); +}; + +AddMetricButton.propTypes = { + children: React.PropTypes.node, + disabled: React.PropTypes.bool, + metric: React.PropTypes.string, + onClick: React.PropTypes.func, +}; + +module.exports = AddMetricButton; diff --git a/ui/src/components/add-metric/description.js b/ui/src/components/add-metric/description.js new file mode 100644 index 00000000..d47fe2ad --- /dev/null +++ b/ui/src/components/add-metric/description.js @@ -0,0 +1,28 @@ +const React = require('react'); +const Styled = require('styled-components'); +const constants = require('../../shared/constants'); + +const { + colors +} = constants; + +const { + default: styled +} = Styled; + +const StyledDescription = styled.p` + margin: 0; + color: ${colors.regular}; +`; + +const Description = (props) => ( + + {props.children} + +); + +Description.propTypes = { + children: React.PropTypes.node +}; + +module.exports = Description; diff --git a/ui/src/components/add-metric/index.js b/ui/src/components/add-metric/index.js new file mode 100644 index 00000000..43176783 --- /dev/null +++ b/ui/src/components/add-metric/index.js @@ -0,0 +1,7 @@ +module.exports = { + AddMetricButton: require('./button'), + AddMetricDescription: require('./description'), + AddMetricLink: require('./link'), + AddMetricTile: require('./tile'), + AddMetricTitle: require('./title'), +}; diff --git a/ui/src/components/add-metric/link.js b/ui/src/components/add-metric/link.js new file mode 100644 index 00000000..d689593b --- /dev/null +++ b/ui/src/components/add-metric/link.js @@ -0,0 +1,26 @@ +const React = require('react'); +const Styled = require('styled-components'); + +const { + default: styled +} = Styled; + +const StyledLink = styled.a` + text-decoration: underline !important; +`; + +const Link = ({ + children, + href +}) => ( + + {children} + +); + +Link.propTypes = { + children: React.PropTypes.node, + href: React.PropTypes.string.isRequired +}; + +module.exports = Link; diff --git a/ui/src/components/add-metric/readme.md b/ui/src/components/add-metric/readme.md new file mode 100644 index 00000000..a079e0ce --- /dev/null +++ b/ui/src/components/add-metric/readme.md @@ -0,0 +1 @@ +# `` diff --git a/ui/src/components/add-metric/story.js b/ui/src/components/add-metric/story.js new file mode 100644 index 00000000..ece22fbd --- /dev/null +++ b/ui/src/components/add-metric/story.js @@ -0,0 +1,40 @@ +const React = require('react'); +const Base = require('../base'); + +const { + storiesOf +} = require('@kadira/storybook'); + +const { + AddMetricButton, + AddMetricDescription, + AddMetricLink, + AddMetricTile, + AddMetricTitle +} = require('./'); + +storiesOf('Add Metric', module) + .add('Add Metric', () => ( + + + Aggregated CPU usage + + CPU usages accross all of the CPU cores. + + Learn more + Add + + + )) + .add('Added Metric', () => ( + + + Aggregated CPU usage + + CPU usages accross all of the CPU cores. + + Learn more + Added + + + )); diff --git a/ui/src/components/add-metric/tile.js b/ui/src/components/add-metric/tile.js new file mode 100644 index 00000000..66e8e16d --- /dev/null +++ b/ui/src/components/add-metric/tile.js @@ -0,0 +1,64 @@ +const React = require('react'); +const Styled = require('styled-components'); +const constants = require('../../shared/constants'); +const fns = require('../../shared/functions'); + +const { + boxes, + breakpoints, + colors +} = constants; + +const { + remcalc +} = fns; + +const { + default: styled +} = Styled; + +const spacing = remcalc(24); + +const StyledTile = styled.div` + position: relative; + display: inline-block; + box-sizing: border-box; + margin: 0 ${spacing} ${spacing} 0; + padding: ${spacing}; + width: ${remcalc(300)}; + height: ${remcalc(247)}; + box-shadow: ${boxes.bottomShaddow}; + border: 1px solid ${colors.borderSecondary}; + background-color: ${colors.brandSecondary}; + + ${breakpoints.small` + width: ${remcalc(300)}; + height: ${remcalc(247)}; + `} + + ${breakpoints.medium` + width: ${remcalc(300)}; + height: ${remcalc(247)}; + `} + + ${breakpoints.large` + width: ${remcalc(300)}; + height: ${remcalc(247)}; + `} +`; + +const Tile = ({ + children +}) => { + return ( + + {children} + + ); +}; + +Tile.propTypes = { + children: React.PropTypes.node +}; + +module.exports = Tile; diff --git a/ui/src/components/add-metric/title.js b/ui/src/components/add-metric/title.js new file mode 100644 index 00000000..dedda96e --- /dev/null +++ b/ui/src/components/add-metric/title.js @@ -0,0 +1,30 @@ +const React = require('react'); +const Styled = require('styled-components'); +const constants = require('../../shared/constants'); + +const { + colors +} = constants; + +const { + default: styled +} = Styled; + +const StyledTitle = styled.h4` + margin: 0; + color: ${colors.semibold}; +`; + +const Title = ({ + children +}) => ( + + {children} + +); + +Title.propTypes = { + children: React.PropTypes.node +}; + +module.exports = Title; diff --git a/ui/src/components/anchor/index.js b/ui/src/components/anchor/index.js new file mode 100644 index 00000000..758ea827 --- /dev/null +++ b/ui/src/components/anchor/index.js @@ -0,0 +1,26 @@ +const constants = require('../../shared/constants'); +const React = require('react'); +const Styled = require('styled-components'); + +const { + colors +} = constants; + +const { + default: styled +} = Styled; + +const color = (props) => props.secondary + ? colors.brandSecondaryLink + : colors.brandPrimaryLink; + +const Anchor = styled.a` + color: ${color} !important; +`; + +module.exports = Anchor; + +module.exports.fn = (element) => (props) => React.cloneElement(element, { + ...element.props, + ...props +}, element.props.children); diff --git a/ui/src/components/avatar/story.js b/ui/src/components/avatar/story.js new file mode 100644 index 00000000..d17e80d5 --- /dev/null +++ b/ui/src/components/avatar/story.js @@ -0,0 +1,40 @@ +const React = require('react'); +const fakeData = require('../../shared/fake-data'); +const Base = require('../base'); + +const { + profile +} = fakeData; + +const { + storiesOf +} = require('@kadira/storybook'); + +const Avatar = require('./'); + +storiesOf('Avatar', module) + .add('Avatar Picture', () => ( + + + + )) + .add('Avatar Text', () => ( + + + + + + )); \ No newline at end of file diff --git a/ui/src/components/base/index.js b/ui/src/components/base/index.js index b999bc8e..efcc93e9 100644 --- a/ui/src/components/base/index.js +++ b/ui/src/components/base/index.js @@ -1,28 +1,42 @@ const constants = require('../../shared/constants'); +const fncs = require('../../shared/functions'); + const Styled = require('styled-components'); const { forms, - links, tables, - typography + typography, + colors } = constants; const { - default: styled + default: styled, } = Styled; +const { + generateFonts +} = fncs; + + +// The name that will be used in the 'font-family' property +const fontFamilies = [ + 'LibreFranklin' +]; + +// The name the font file without the extension +const fontFilenames = [ + 'librefranklin-webfont' +]; + module.exports = styled.div` - @font-face { - font-family: 'LibreFranklin'; - src: url('../../shared/fonts/LibreFranklin.ttf') format('truetype') - } + ${generateFonts(fontFamilies, fontFilenames)}; font-family: 'LibreFranklin', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 1rem; line-height: 1.5; - color: #373A3C; + color: ${colors.fonts.regular}; background-color: #FFFFFF; /************************************************************************** @@ -329,13 +343,12 @@ module.exports = styled.div` */ & a { - color: ${links.color}; - text-decoration: ${links.decoration}; + color: ${colors.brandPrimaryLink}; + text-decoration: underline; &:focus, &:hover { - color: ${links.hoverColor}; - text-decoration: ${links.hoverDecoration}; + text-decoration: none; } &:focus { @@ -350,8 +363,7 @@ module.exports = styled.div` &:focus, &:hover { - color: ${links.hoverColor}; - text-decoration: ${links.hoverDecoration}; + text-decoration: none; } &:focus { diff --git a/ui/src/components/button/story.js b/ui/src/components/button/story.js new file mode 100644 index 00000000..08e4d381 --- /dev/null +++ b/ui/src/components/button/story.js @@ -0,0 +1,27 @@ +const React = require('react'); +const { + storiesOf +} = require('@kadira/storybook'); + +const Button = require('./'); + +storiesOf('Button', module) + .add('With text', () => ( + + )).add('Secondary', () => ( + + )).add('Disabled', () => ( + + )).add('Anchor', () => ( +
    + +
    + )); \ No newline at end of file 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/index.js b/ui/src/components/close/index.js new file mode 100644 index 00000000..bc955fe9 --- /dev/null +++ b/ui/src/components/close/index.js @@ -0,0 +1,44 @@ +const React = require('react'); +const Styled = require('styled-components'); + +const fns = require('../../shared/functions'); + +const { + default: styled +} = Styled; + +const { + remcalc +} = fns; + +const StyledButton = styled.button` + background: none; + border: none; + position: absolute; + top: ${remcalc(16)}; + right: ${remcalc(16)}; +`; + +const Close = ({ + style, + onClick +}) => { + return ( + + Close + + ); +}; + +Close.propTypes = { + onClick: React.PropTypes.func, + style: React.PropTypes.object +}; + +module.exports = Close; 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 4e6c6958..81b89968 100644 --- a/ui/src/components/input/index.js +++ b/ui/src/components/input/index.js @@ -11,7 +11,7 @@ const { } = constants; const { - remcalc + remcalc, } = fns; const { @@ -19,37 +19,57 @@ const { } = composers; const { - default: styled + default: styled, + css } = Styled; +const successBakcground = css` + background-color: ${colors.brandSecondary}; + background-image: url("./input-confirm.svg"); + background-repeat: no-repeat; + background-position: 98% 20px; +`; + +const defaultBackground = css` + background-color: ${colors.brandSecondary}; +`; + const Label = styled.label` - color: #464646; + color: ${props => props.error ? colors.alert : colors.fonts.regular} `; const InputField = styled.input` - background: ${colors.brandSecondary}; + ${baseBox()}; + + ${props => props.success ? successBakcground : defaultBackground } + + border-color: ${props => props.error ? colors.alert : 'auto'} + color: ${props => props.error ? colors.alert : colors.fonts.semibold} display: block; font-size: 16px; - height: ${remcalc(50)}; - padding-left: ${remcalc(15)}; - padding-right: ${remcalc(15)}; + padding: ${remcalc('15 18')}; visibility: visible; width: 100%; - - ${baseBox()} - + &:focus { border-color: ${boxes.border.checked}; outline: none; } `; +const Error = styled.span` + float: right; + color: ${colors.alert}; + font-size: ${remcalc(14)}; +`; + const Input = ({ autoComplete, autoFocus, children, className, disabled = false, + error, form, id, inputMode, @@ -65,23 +85,30 @@ const Input = ({ selectionDirection, spellCheck, style, + success, tabIndex, type, value }) => { const _label = label || children; const _children = label && children ? children : null; + const _error = error ? ({error}) : null; return (
    -