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.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 (
-
- );
- };
+ 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.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 (
-