diff --git a/app/controllers/hosts_controller.rb b/app/controllers/hosts_controller.rb
index 440654b0eb4..5e4d3fd7db2 100644
--- a/app/controllers/hosts_controller.rb
+++ b/app/controllers/hosts_controller.rb
@@ -309,15 +309,11 @@ def vm
end
def runtime
- render :partial => 'runtime'
- rescue ActionView::Template::Error => exception
- process_ajax_error exception, 'fetch runtime chart information'
+ render :json => helpers.runtime_chart(params[:range].empty? ? 7.days.ago : params[:range].to_i.days.ago)
end
def resources
- render :partial => 'resources'
- rescue ActionView::Template::Error => exception
- process_ajax_error exception, 'fetch resources chart information'
+ render :json => helpers.resources_chart(params[:range].empty? ? 7.days.ago : params[:range].to_i.days.ago)
end
def templates
diff --git a/app/helpers/hosts_helper.rb b/app/helpers/hosts_helper.rb
index 1223a368abc..cf4983d0727 100644
--- a/app/helpers/hosts_helper.rb
+++ b/app/helpers/hosts_helper.rb
@@ -216,11 +216,13 @@ def resources_chart(timerange = 1.day.ago)
failed_restarts << [r.reported_at.to_i * 1000, r.failed_restarts ]
skipped << [r.reported_at.to_i * 1000, r.skipped ]
end
- [{:label => _("Applied"), :data => applied, :color => '#89A54E'},
- {:label => _("Failed"), :data => failed, :color => '#AA4643'},
- {:label => _("Failed restarts"), :data => failed_restarts, :color => '#EC971F'},
- {:label => _("Skipped"), :data => skipped, :color => '#80699B'},
- {:label => _("Restarted"), :data => restarted, :color => '#4572A7'}]
+ {:results => [
+ {:label => _("Applied"), :data => applied, :color => '#89A54E'},
+ {:label => _("Failed"), :data => failed, :color => '#AA4643'},
+ {:label => _("Failed restarts"), :data => failed_restarts, :color => '#EC971F'},
+ {:label => _("Skipped"), :data => skipped, :color => '#80699B'},
+ {:label => _("Restarted"), :data => restarted, :color => '#4572A7'}
+ ]}
end
def runtime_chart(timerange = 1.day.ago)
@@ -229,7 +231,10 @@ def runtime_chart(timerange = 1.day.ago)
config << [r.reported_at.to_i * 1000, r.config_retrieval]
runtime << [r.reported_at.to_i * 1000, r.runtime]
end
- [{:label => _("Config Retrieval"), :data => config, :color => '#AA4643'}, {:label => _("Runtime"), :data => runtime, :color => '#4572A7'}]
+ {:results => [
+ {:label => _("Config Retrieval"), :data => config, :color => '#AA4643'},
+ {:label => _("Runtime"), :data => runtime, :color => '#4572A7'}
+ ]}
end
def reports_show
diff --git a/app/views/hosts/_resources.html.erb b/app/views/hosts/_resources.html.erb
deleted file mode 100644
index dc5c93d15ee..00000000000
--- a/app/views/hosts/_resources.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= flot_chart('resource_graph', '', _("Number of Events"), resources_chart(params["range"].to_i.days.ago)) %>
diff --git a/app/views/hosts/_runtime.html.erb b/app/views/hosts/_runtime.html.erb
deleted file mode 100644
index 4efcfe2ef6e..00000000000
--- a/app/views/hosts/_runtime.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-<%= flot_chart('runtime_graph','', _('Time in Seconds'), runtime_chart(params["range"].to_i.days.ago), :class=>"statistics-chart stack") %>
diff --git a/app/views/hosts/show.html.erb b/app/views/hosts/show.html.erb
index bbfbfb4ec17..1002f1016dc 100644
--- a/app/views/hosts/show.html.erb
+++ b/app/views/hosts/show.html.erb
@@ -75,16 +75,22 @@
<%= _("Runtime") %>
<%= n_("last %s day", "last %s days", @range) % @range %>
-
- <%= spinner(_('Loading runtime information ...')) %>
-
+
+ <%= mount_react_component('HostChart', '#host_runtime', {
+ :url => runtime_host_path(@host, :range => @range),
+ :name => 'runtime',
+ :type => 'area'
+ }.to_json) %>
<%= _("Resources") %>
<%= n_("last %s day", "last %s days", @range) % @range %>
-
- <%= spinner(_('Loading resources information ...')) %>
-
+
+ <%= mount_react_component('HostChart', '#host_resources', {
+ :url => resources_host_path(@host, :range => @range),
+ :name => 'resource',
+ :type => 'area'
+ }.to_json) %>
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/DonutChart.test.js b/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/DonutChart.test.js
index f6f4e2de88e..87079bc448e 100644
--- a/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/DonutChart.test.js
+++ b/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/DonutChart.test.js
@@ -1,22 +1,16 @@
-import { shallow } from 'enzyme';
-import toJson from 'enzyme-to-json';
-import React from 'react';
import { mockStoryData, emptyData } from './DonutChart.fixtures';
import DonutChart from './';
-import * as chartService from '../../../../../services/ChartService';
+import { testComponentSnapshotsWithFixtures } from '../../../../common/testHelpers';
jest.unmock('./');
-describe('renders DonutChart', () => {
- it('render donut chart', () => {
- chartService.getDonutChartConfig = jest.fn(() => mockStoryData);
- const wrapper = shallow();
-
- expect(toJson(wrapper)).toMatchSnapshot();
- });
- it('render empty state', () => {
- chartService.getDonutChartConfig = jest.fn(() => emptyData);
- const wrapper = shallow();
-
- expect(toJson(wrapper)).toMatchSnapshot();
- });
-});
+const fixtures = {
+ 'render donut chart': {
+ data: null,
+ getConfig: () => mockStoryData,
+ },
+ 'render empty donut chart': {
+ data: null,
+ getConfig: () => emptyData,
+ },
+};
+testComponentSnapshotsWithFixtures(DonutChart, fixtures);
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/__snapshots__/DonutChart.test.js.snap b/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/__snapshots__/DonutChart.test.js.snap
index 27017eb380f..5bcd02c0a87 100644
--- a/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/__snapshots__/DonutChart.test.js.snap
+++ b/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/__snapshots__/DonutChart.test.js.snap
@@ -1,16 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`renders DonutChart render donut chart 1`] = `
-
`;
-exports[`renders DonutChart render empty state 1`] = `
-
`;
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/index.js b/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/index.js
index 487b0fc69f5..126ed470fc4 100644
--- a/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/index.js
+++ b/webpack/assets/javascripts/react_app/components/common/charts/DonutChart/index.js
@@ -1,29 +1,21 @@
import React from 'react';
import { DonutChart as PfDonutChart } from 'patternfly-react';
import { getDonutChartConfig } from '../../../../../services/ChartService';
-import MessageBox from '../../MessageBox';
+import { chartWithNoDataMessage } from '../common/chartWithNoDataMessage';
+
+const DonutChartWithNoDataMsgBox = chartWithNoDataMessage(PfDonutChart);
const DonutChart = ({
data,
onclick,
config = 'regular',
- noDataMsg = __('No data available'),
title = { type: 'percent' },
unloadData = false,
-
-}) => {
- const chartConfig = getDonutChartConfig({ data, config, onclick });
-
- if (chartConfig.data.columns.length > 0) {
- return (
-
- );
- }
- return ;
-};
+ getConfig = getDonutChartConfig,
+}) => ;
export default DonutChart;
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/TimeseriesChart.test.js b/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/TimeseriesChart.test.js
new file mode 100644
index 00000000000..5b3d4f29f83
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/TimeseriesChart.test.js
@@ -0,0 +1,22 @@
+import TimeseriesChart from './';
+import { timeseriesChartConfig } from '../../../../../services/ChartService.consts';
+import { testComponentSnapshotsWithFixtures } from '../../../../common/testHelpers';
+
+jest.unmock('./');
+const fixtures = {
+ 'render timeseries chart': {
+ data: null,
+ getConfig: () => Object.assign({}, {
+ ...timeseriesChartConfig,
+ data: {
+ x: 'time',
+ columns: [['time', 15029999123], ['Runtime', 8], ['Config Retrieval', 35]],
+ },
+ }),
+ },
+ 'render empty timeseries chart': {
+ data: null,
+ getConfig: () => timeseriesChartConfig,
+ },
+};
+testComponentSnapshotsWithFixtures(TimeseriesChart, fixtures);
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/__snapshots__/TimeseriesChart.test.js.snap b/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/__snapshots__/TimeseriesChart.test.js.snap
new file mode 100644
index 00000000000..1d4a8f6b063
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/__snapshots__/TimeseriesChart.test.js.snap
@@ -0,0 +1,94 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render empty timeseries chart 1`] = `
+
+`;
+
+exports[`render timeseries chart 1`] = `
+
+`;
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/index.js b/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/index.js
new file mode 100644
index 00000000000..0823305b212
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/common/charts/TimeseriesChart/index.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { LineChart as PfLineChart } from 'patternfly-react';
+import { chartWithNoDataMessage } from '../common/chartWithNoDataMessage';
+import { getTimeseriesChartConfig } from '../../../../../services/ChartService';
+
+const TimeseriesChartWithNoDataMsgBox = chartWithNoDataMessage(PfLineChart);
+
+const TimeseriesChart = ({
+ data,
+ type = 'line',
+ unloadData = false,
+ getConfig = getTimeseriesChartConfig,
+}) => ;
+
+export default TimeseriesChart;
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/common/__snapshots__/chartWithNoDataMessage.test.js.snap b/webpack/assets/javascripts/react_app/components/common/charts/common/__snapshots__/chartWithNoDataMessage.test.js.snap
new file mode 100644
index 00000000000..f648c866738
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/common/charts/common/__snapshots__/chartWithNoDataMessage.test.js.snap
@@ -0,0 +1,163 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`common chart components render a messagebox if no data 1`] = `
+
+
+
+
+
+ No data available
+
+
+
+
+`;
+
+exports[`common chart components render a messagebox if no data 2`] = `
+
+
+
+
+
+ No data available
+
+
+
+
+`;
+
+exports[`common chart components render a messagebox if no data 3`] = `
+
+
+
+
+
+ No data available
+
+
+
+
+`;
+
+exports[`common chart components render a messagebox if no data 4`] = `
+
+
+
+
+
+ No data available
+
+
+
+
+`;
+
+exports[`common chart components render a messagebox if no data 5`] = `
+
+
+
+
+
+ No data available
+
+
+
+
+`;
+
+exports[`common chart components render chart if there is data 1`] = `
+
+
+
+ Chart
+
+
+
+`;
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/common/chartWithNoDataMessage.js b/webpack/assets/javascripts/react_app/components/common/charts/common/chartWithNoDataMessage.js
new file mode 100644
index 00000000000..880c61dd41a
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/common/charts/common/chartWithNoDataMessage.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import MessageBox from '../../MessageBox';
+
+export const chartWithNoDataMessage = (WrappedComp, noDataMsg = 'No data available') => (props) => {
+ const columns = props && props.data && props.data.columns ? props.data.columns : [];
+ if (columns.length > 0) {
+ return ();
+ }
+ return ;
+};
diff --git a/webpack/assets/javascripts/react_app/components/common/charts/common/chartWithNoDataMessage.test.js b/webpack/assets/javascripts/react_app/components/common/charts/common/chartWithNoDataMessage.test.js
new file mode 100644
index 00000000000..8a9896dbb01
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/common/charts/common/chartWithNoDataMessage.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import toJson from 'enzyme-to-json';
+
+import { chartWithNoDataMessage } from './chartWithNoDataMessage';
+
+describe('common chart components', () => {
+ const Chart = chartWithNoDataMessage(() => Chart
);
+ it('render a messagebox if no data', () => {
+ [undefined, null, {}, { data: {} }, { data: { columns: [] } }]
+ .forEach(props => expect(toJson(mount())).toMatchSnapshot());
+ });
+ it('render chart if there is data', () => {
+ expect(toJson(mount())).toMatchSnapshot();
+ });
+});
diff --git a/webpack/assets/javascripts/react_app/components/componentRegistry.js b/webpack/assets/javascripts/react_app/components/componentRegistry.js
index dc84d6709fa..897582d0dac 100644
--- a/webpack/assets/javascripts/react_app/components/componentRegistry.js
+++ b/webpack/assets/javascripts/react_app/components/componentRegistry.js
@@ -12,6 +12,7 @@ import BreadcrumbBar from './BreadcrumbBar';
import FactChart from './factCharts';
import Pagination from './Pagination/Pagination';
import AuditsList from './AuditsList';
+import HostChart from './hosts/chart/';
const componentRegistry = {
registry: {},
@@ -72,6 +73,7 @@ const coreComponets = [
{ name: 'FactChart', type: FactChart },
{ name: 'Pagination', type: Pagination },
{ name: 'AuditsList', type: AuditsList },
+ { name: 'HostChart', type: HostChart },
];
componentRegistry.registerMultiple(coreComponets);
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/HostChart.js b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChart.js
new file mode 100644
index 00000000000..0022bad1efa
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChart.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import MessageBox from '../../common/MessageBox';
+import { noop } from '../../../common/helpers';
+import Normalizer from './Normalizer';
+import Loader from '../../common/Loader';
+import { STATUS } from '../../../constants';
+import TimeseriesChart from '../../common/charts/TimeseriesChart';
+
+class HostChart extends React.Component {
+ componentDidMount() {
+ const { data: { name, url } } = this.props;
+ this.props.getChartData(url, name);
+ }
+ render() {
+ const {
+ charts, data: { name, type },
+ } = this.props;
+
+ const chartData = Object.keys(charts || {})
+ .filter(cName => cName === name)
+ .map(cName => charts[cName]);
+ const data = chartData.length > 0 ?
+ Object.assign({}, { ...chartData[0], results: Normalizer(chartData[0].results) }) :
+ { status: STATUS.PENDING, results: [], error: null };
+
+ return (
+
+ {[
+ ,
+ ,
+ ]}
+
+ );
+ }
+}
+
+HostChart.propTypes = {
+ data: PropTypes.shape({
+ url: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ type: PropTypes.oneOf(['area', 'line']),
+ }),
+ getChartData: PropTypes.func.isRequired,
+ isLoadingData: PropTypes.bool,
+ chartData: PropTypes.object,
+};
+
+HostChart.defaultProps = {
+ data: {
+ type: 'line',
+ },
+ getChartData: noop,
+};
+
+export default HostChart;
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/HostChart.stories.js b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChart.stories.js
new file mode 100644
index 00000000000..4dc70110e43
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChart.stories.js
@@ -0,0 +1,92 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import HostChart from './HostChart';
+import { STATUS } from '../../../constants';
+
+const getConfig = (hostname, status) => ({
+ charts: {
+ [hostname]: {
+ results: [],
+ status,
+ },
+ },
+ data: {
+ name: hostname,
+ url: `host/${hostname}/resources`,
+ },
+});
+
+storiesOf('HostChart', module)
+ .add('Loading', () => (
+
+ ))
+ .add('Without Data', () => (
+
+ ))
+ .add('Line Chart', () => (
+
+ ))
+ .add('Area Chart', () => (
+
+ ));
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartActions.js b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartActions.js
new file mode 100644
index 00000000000..e892419ebc0
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartActions.js
@@ -0,0 +1,13 @@
+import { ajaxRequestAction } from '../../../redux/actions/common';
+import * as tsConsts from './HostChartConsts';
+
+export const getChartData = (url, name) => (dispatch) => {
+ ajaxRequestAction({
+ dispatch,
+ requestAction: tsConsts.TIMESERIES_REQUEST,
+ successAction: tsConsts.TIMESERIES_SUCCESS,
+ failedAction: tsConsts.TIMESERIES_FAILURE,
+ url,
+ item: { name },
+ });
+};
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartConsts.js b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartConsts.js
new file mode 100644
index 00000000000..d4d066f0c6f
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartConsts.js
@@ -0,0 +1,3 @@
+export const TIMESERIES_REQUEST = 'TIMESERIES_REQUEST';
+export const TIMESERIES_SUCCESS = 'TIMESERIES_SUCCESS';
+export const TIMESERIES_FAILURE = 'TIMESERIES_FAILURE';
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartReducer.js b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartReducer.js
new file mode 100644
index 00000000000..c88a3d010bf
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/HostChartReducer.js
@@ -0,0 +1,31 @@
+import Immutable from 'seamless-immutable';
+import { TIMESERIES_REQUEST, TIMESERIES_FAILURE, TIMESERIES_SUCCESS } from './HostChartConsts';
+import { STATUS } from '../../../constants';
+
+const hostChartReducer = (state = Immutable({}), action) => {
+ const { payload } = action;
+ switch (action.type) {
+ case TIMESERIES_REQUEST:
+ return state.setIn(['charts', payload.name], {
+ error: null,
+ results: [],
+ status: STATUS.PENDING,
+ });
+ case TIMESERIES_FAILURE:
+ return state.setIn(['charts', payload.item.name], {
+ error: payload.error.message,
+ results: [],
+ status: STATUS.ERROR,
+ });
+ case TIMESERIES_SUCCESS:
+ return state.setIn(['charts', payload.name], {
+ error: null,
+ results: payload.results,
+ status: STATUS.RESOLVED,
+ });
+ default:
+ return state;
+ }
+};
+
+export default hostChartReducer;
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/Normalizer.js b/webpack/assets/javascripts/react_app/components/hosts/chart/Normalizer.js
new file mode 100644
index 00000000000..5d79a5632a6
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/Normalizer.js
@@ -0,0 +1,20 @@
+const Normalizer = (vector) => {
+ if (vector.length === 0) {
+ return { columns: [] };
+ }
+ const data = {
+ x: 'time',
+ };
+ const timeFrame = vector[0].data.map(ts => ts[0]);
+ const tsItems = vector.map(item => [item.label, ...item.data.map(ts => ts[1])]);
+ data.columns = [['time', ...timeFrame], ...tsItems];
+ const colorItems = vector.filter(item => Object.prototype.hasOwnProperty.call(item, 'color'));
+ if (colorItems.length === 0) {
+ return data;
+ }
+ const colorReducer = (acc, cur) => Object.assign({}, { ...acc, [cur.label]: cur.color });
+ data.colors = { ...colorItems.reduce(colorReducer, {}) };
+ return data;
+};
+
+export default Normalizer;
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChart.test.js b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChart.test.js
new file mode 100644
index 00000000000..2705733c00b
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChart.test.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import HostChart from '../HostChart';
+import { testComponentSnapshotsWithFixtures } from '../../../../common/testHelpers';
+import { STATUS } from '../../../../constants';
+
+jest.unmock('../../../../../services/ChartService');
+
+const fixtures = {
+ 'render empty state (before dispatching the REQUEST action)': {
+ data: {
+ url: 'something',
+ name: 'chartName',
+ },
+ charts: {},
+ },
+ 'render error mode': {
+ data: {
+ url: '/host/localhost/runtime',
+ name: 'runtime',
+ },
+ charts: {
+ runtime: {
+ status: STATUS.ERROR,
+ results: [],
+ error: 'bad news',
+ },
+ },
+ },
+ 'render loading mode': {
+ data: {
+ url: '#',
+ name: 'ts',
+ },
+ charts: {
+ ts: {
+ status: STATUS.PENDING,
+ results: [],
+ error: null,
+ },
+ },
+ },
+ 'render data resolved mode': {
+ data: {
+ url: 'host/domain1/resources',
+ name: 'resources',
+ },
+ charts: {
+ resources: {
+ status: STATUS.RESOLVED,
+ results: [{ label: 'data1', data: [[129321, 100]] }],
+ error: null,
+ },
+ },
+ },
+};
+testComponentSnapshotsWithFixtures(HostChart, fixtures);
+
+describe('HostChart', () => {
+ it('should call getChartData on mount', () => {
+ const url = 'url';
+ const name = 'name';
+ const getChartData = jest.fn();
+ shallow();
+ expect(getChartData).toBeCalledWith(url, name);
+ });
+});
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChartActions.test.js b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChartActions.test.js
new file mode 100644
index 00000000000..8185dc3ea07
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChartActions.test.js
@@ -0,0 +1,23 @@
+import { getChartData } from '../HostChartActions';
+import { ajaxRequestAction } from '../../../../redux/actions/common';
+import { TIMESERIES_REQUEST, TIMESERIES_SUCCESS, TIMESERIES_FAILURE } from '../HostChartConsts';
+
+jest.mock('../../../../redux/actions/common');
+
+describe('Timeseries chart actions', () => {
+ it('should call ajaxRequestAction on getChartData', () => {
+ const url = '/runtime/ts';
+ const name = 'runtime';
+ const dispatch = jest.fn();
+
+ getChartData(url, name)(dispatch);
+ expect(ajaxRequestAction).toBeCalledWith({
+ dispatch,
+ url,
+ item: { name },
+ requestAction: TIMESERIES_REQUEST,
+ successAction: TIMESERIES_SUCCESS,
+ failedAction: TIMESERIES_FAILURE,
+ });
+ });
+});
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChartReducer.test.js b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChartReducer.test.js
new file mode 100644
index 00000000000..43c3c765e50
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/HostChartReducer.test.js
@@ -0,0 +1,40 @@
+import { TIMESERIES_REQUEST, TIMESERIES_FAILURE, TIMESERIES_SUCCESS } from '../HostChartConsts';
+import reducer from '../HostChartReducer';
+import { testReducerSnapshotWithFixtures } from '../../../../common/testHelpers';
+
+const fixtures = {
+ 'should handle empty action': {},
+ 'should handle TIMESERIES_REQUEST': {
+ action: {
+ type: TIMESERIES_REQUEST,
+ payload: {
+ name: 'ts1',
+ results: [{ label: 'data', data: [[1, 1], [2, 2]] }],
+ },
+ },
+ },
+ 'should handle TIMESERIES_FAILURE': {
+ action: {
+ type: TIMESERIES_FAILURE,
+ payload: { error: new Error('Oops, something is wrong'), item: { name: 'ts1' } },
+ },
+ },
+ 'should handle TIMESERIES_SUCCESS': {
+ action: {
+ type: TIMESERIES_SUCCESS,
+ payload: {
+ name: 'ts1',
+ results: [{
+ label: 'data-1',
+ data: [[10, 20], [20, 20], [30, 0]],
+ color: 'blue',
+ }, {
+ label: 'data-2',
+ data: [[10, 0], [20, 0], [30, 0]],
+ }],
+ },
+ },
+ },
+};
+
+describe('TimeseriesChart reducer', () => testReducerSnapshotWithFixtures(reducer, fixtures));
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/Normalizer.test.js b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/Normalizer.test.js
new file mode 100644
index 00000000000..673405d98b6
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/Normalizer.test.js
@@ -0,0 +1,50 @@
+import Normalizer from '../Normalizer';
+
+describe('Normalize TimeseriesChart response', () => {
+ it('should normalize an empty response', () => {
+ expect(Normalizer([])).toEqual({
+ columns: [],
+ });
+ });
+ it('should normalize a response with single item', () => {
+ const resp = [{
+ label: 'item',
+ data: [[10, 20], [20, 20], [30, 10]],
+ }];
+ expect(Normalizer(resp)).toEqual({
+ x: 'time',
+ columns: [['time', 10, 20, 30], ['item', 20, 20, 10]],
+ });
+ });
+ it('should normalize a response with more than one item', () => {
+ const resp = [{
+ label: 'item1',
+ data: [[10, 20], [12, 44]],
+ },
+ {
+ label: 'item2',
+ data: [[10, 0], [12, 0]],
+ }];
+ expect(Normalizer(resp)).toEqual({
+ x: 'time',
+ columns: [['time', 10, 12], ['item1', 20, 44], ['item2', 0, 0]],
+ });
+ });
+ it('should normalize colors in a response', () => {
+ const resp = [{
+ label: 'item1',
+ data: [[10, 20]],
+ color: 'black',
+ },
+ {
+ label: 'item2',
+ data: [[10, 0]],
+ color: 'green',
+ }];
+ expect(Normalizer(resp)).toEqual({
+ x: 'time',
+ columns: [['time', 10], ['item1', 20], ['item2', 0]],
+ colors: { item1: 'black', item2: 'green' },
+ });
+ });
+});
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/__snapshots__/HostChart.test.js.snap b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/__snapshots__/HostChart.test.js.snap
new file mode 100644
index 00000000000..8297c3d25b4
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/__snapshots__/HostChart.test.js.snap
@@ -0,0 +1,87 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render data resolved mode 1`] = `
+
+
+
+
+`;
+
+exports[`render empty state (before dispatching the REQUEST action) 1`] = `
+
+
+
+
+`;
+
+exports[`render error mode 1`] = `
+
+
+
+
+`;
+
+exports[`render loading mode 1`] = `
+
+
+
+
+`;
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/__snapshots__/HostChartReducer.test.js.snap b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/__snapshots__/HostChartReducer.test.js.snap
new file mode 100644
index 00000000000..819a9f0c7e5
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/__tests__/__snapshots__/HostChartReducer.test.js.snap
@@ -0,0 +1,75 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TimeseriesChart reducer should handle TIMESERIES_FAILURE 1`] = `
+Object {
+ "charts": Object {
+ "ts1": Object {
+ "error": "Oops, something is wrong",
+ "results": Array [],
+ "status": "ERROR",
+ },
+ },
+}
+`;
+
+exports[`TimeseriesChart reducer should handle TIMESERIES_REQUEST 1`] = `
+Object {
+ "charts": Object {
+ "ts1": Object {
+ "error": null,
+ "results": Array [],
+ "status": "PENDING",
+ },
+ },
+}
+`;
+
+exports[`TimeseriesChart reducer should handle TIMESERIES_SUCCESS 1`] = `
+Object {
+ "charts": Object {
+ "ts1": Object {
+ "error": null,
+ "results": Array [
+ Object {
+ "color": "blue",
+ "data": Array [
+ Array [
+ 10,
+ 20,
+ ],
+ Array [
+ 20,
+ 20,
+ ],
+ Array [
+ 30,
+ 0,
+ ],
+ ],
+ "label": "data-1",
+ },
+ Object {
+ "data": Array [
+ Array [
+ 10,
+ 0,
+ ],
+ Array [
+ 20,
+ 0,
+ ],
+ Array [
+ 30,
+ 0,
+ ],
+ ],
+ "label": "data-2",
+ },
+ ],
+ "status": "RESOLVED",
+ },
+ },
+}
+`;
+
+exports[`TimeseriesChart reducer should handle empty action 1`] = `Object {}`;
diff --git a/webpack/assets/javascripts/react_app/components/hosts/chart/index.js b/webpack/assets/javascripts/react_app/components/hosts/chart/index.js
new file mode 100644
index 00000000000..c685c2ef4eb
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/hosts/chart/index.js
@@ -0,0 +1,12 @@
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+
+import * as actions from './HostChartActions';
+import reducer from './HostChartReducer';
+import HostChart from './HostChart';
+
+const mapStateToProps = ({ hostChart }) => ({ ...hostChart });
+const mapDispatchToProps = dispatch => bindActionCreators(actions, dispatch);
+
+export const reducers = { hostChart: reducer };
+export default connect(mapStateToProps, mapDispatchToProps)(HostChart);
diff --git a/webpack/assets/javascripts/react_app/redux/reducers/index.js b/webpack/assets/javascripts/react_app/redux/reducers/index.js
index c79a738a64e..cba4a278588 100644
--- a/webpack/assets/javascripts/react_app/redux/reducers/index.js
+++ b/webpack/assets/javascripts/react_app/redux/reducers/index.js
@@ -7,6 +7,7 @@ import notifications from './notifications/';
import toasts from './toasts';
import { reducers as passwordStrengthReducers } from '../../components/PasswordStrength';
import { reducers as breadcrumbBarReducers } from '../../components/BreadcrumbBar';
+import { reducers as hostChartReducers } from '../../components/hosts/chart';
import factChart from './factCharts/';
export function combineReducersAsync(asyncReducers) {
@@ -17,6 +18,7 @@ export function combineReducersAsync(asyncReducers) {
hosts,
notifications,
toasts,
+ ...hostChartReducers,
...passwordStrengthReducers,
...breadcrumbBarReducers,
...asyncReducers,
diff --git a/webpack/assets/javascripts/services/ChartService.consts.js b/webpack/assets/javascripts/services/ChartService.consts.js
index 5cda413b811..9c433cab2f0 100644
--- a/webpack/assets/javascripts/services/ChartService.consts.js
+++ b/webpack/assets/javascripts/services/ChartService.consts.js
@@ -33,6 +33,33 @@ export const donutChartConfig = {
size: enums.SIZE.REGULAR,
};
+export const timeseriesChartConfig = {
+ data: {
+ x: 'time',
+ columns: [],
+ },
+ axis: {
+ x: {
+ type: 'timeseries',
+ tick: {
+ fit: false,
+ },
+ },
+ },
+ zoom: {
+ enabled: true,
+ },
+ subchart: {
+ show: true,
+ },
+ tooltip: {
+ grouped: false,
+ format: {
+ title: d => new Date(d).toUTCString(),
+ },
+ },
+};
+
export const donutLargeChartConfig = {
...donutChartConfig,
size: enums.SIZE.LARGE,
diff --git a/webpack/assets/javascripts/services/ChartService.js b/webpack/assets/javascripts/services/ChartService.js
index 66427ea4f01..73a0541bbcc 100644
--- a/webpack/assets/javascripts/services/ChartService.js
+++ b/webpack/assets/javascripts/services/ChartService.js
@@ -1,6 +1,6 @@
import uuidV1 from 'uuid/v1';
import Immutable from 'seamless-immutable';
-import { donutChartConfig, donutLargeChartConfig } from './ChartService.consts';
+import { timeseriesChartConfig, donutChartConfig, donutLargeChartConfig } from './ChartService.consts';
const sizeConfig = {
regular: donutChartConfig,
@@ -107,3 +107,11 @@ export const navigateToSearch = (url, data) => {
}
window.location.href = setUrl;
};
+
+export const getTimeseriesChartConfig = ({
+ data, type = 'line', id = uuidV1(),
+}) => ({
+ ...timeseriesChartConfig,
+ id,
+ data: { ...(data || { columns: [] }), type },
+});
diff --git a/webpack/assets/javascripts/services/ChartServices.test.js b/webpack/assets/javascripts/services/ChartServices.test.js
index cec5ea9fd86..2deb5aa93ec 100644
--- a/webpack/assets/javascripts/services/ChartServices.test.js
+++ b/webpack/assets/javascripts/services/ChartServices.test.js
@@ -1,5 +1,5 @@
import Immutable from 'seamless-immutable';
-import { getDonutChartConfig } from './ChartService';
+import { getDonutChartConfig, getTimeseriesChartConfig } from './ChartService';
import { zeroedData, mixedData, dataWithLongLabels } from '../react_app/components/common/charts/DonutChart/DonutChart.fixtures';
jest.unmock('./ChartService');
@@ -37,3 +37,19 @@ describe('getDonutChartConfig', () => {
}))).toMatchSnapshot();
});
});
+
+describe('getTimeseriesChartConfig', () => {
+ it('should render an empty line chart by default', () => {
+ expect(getTimeseriesChartConfig({
+ data: null,
+ id: 'my-data',
+ })).toMatchSnapshot();
+ });
+
+ it('should render a line chart from data', () => {
+ expect(getTimeseriesChartConfig({
+ data: { columns: [['time', 1, 2, 3], ['data', 10, 20, 0]] },
+ id: 'my-data',
+ })).toMatchSnapshot();
+ });
+});
diff --git a/webpack/assets/javascripts/services/__snapshots__/ChartServices.test.js.snap b/webpack/assets/javascripts/services/__snapshots__/ChartServices.test.js.snap
index ca2b2eb82a9..9390b6c5f0a 100644
--- a/webpack/assets/javascripts/services/__snapshots__/ChartServices.test.js.snap
+++ b/webpack/assets/javascripts/services/__snapshots__/ChartServices.test.js.snap
@@ -202,3 +202,76 @@ Object {
},
}
`;
+
+exports[`getTimeseriesChartConfig should render a line chart from data 1`] = `
+Object {
+ "axis": Object {
+ "x": Object {
+ "tick": Object {
+ "fit": false,
+ },
+ "type": "timeseries",
+ },
+ },
+ "data": Object {
+ "columns": Array [
+ Array [
+ "time",
+ 1,
+ 2,
+ 3,
+ ],
+ Array [
+ "data",
+ 10,
+ 20,
+ 0,
+ ],
+ ],
+ "type": "line",
+ },
+ "id": "my-data",
+ "subchart": Object {
+ "show": true,
+ },
+ "tooltip": Object {
+ "format": Object {
+ "title": [Function],
+ },
+ "grouped": false,
+ },
+ "zoom": Object {
+ "enabled": true,
+ },
+}
+`;
+
+exports[`getTimeseriesChartConfig should render an empty line chart by default 1`] = `
+Object {
+ "axis": Object {
+ "x": Object {
+ "tick": Object {
+ "fit": false,
+ },
+ "type": "timeseries",
+ },
+ },
+ "data": Object {
+ "columns": Array [],
+ "type": "line",
+ },
+ "id": "my-data",
+ "subchart": Object {
+ "show": true,
+ },
+ "tooltip": Object {
+ "format": Object {
+ "title": [Function],
+ },
+ "grouped": false,
+ },
+ "zoom": Object {
+ "enabled": true,
+ },
+}
+`;