diff --git a/src/core_plugins/status_page/index.js b/src/core_plugins/status_page/index.js
index d8027587bc1cf..1bf964d3b1400 100644
--- a/src/core_plugins/status_page/index.js
+++ b/src/core_plugins/status_page/index.js
@@ -24,7 +24,8 @@ export default function (kibana) {
title: 'Server Status',
main: 'plugins/status_page/status_page',
hidden: true,
- url: '/status'
+ url: '/status',
+ styleSheetPath: `${__dirname}/public/index.scss`
}
}
});
diff --git a/src/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap b/src/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap
new file mode 100644
index 0000000000000..ce24ff89776c8
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`byte metric 1`] = `
+
+`;
+
+exports[`float metric 1`] = `
+
+`;
+
+exports[`general metric 1`] = `
+
+`;
+
+exports[`millisecond metric 1`] = `
+
+`;
diff --git a/src/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap b/src/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap
new file mode 100644
index 0000000000000..b65d517bb3738
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render 1`] = `
+
+
+
+
+ Kibana status is
+
+ Green
+
+
+
+
+
+
+
+ My Computer
+
+
+
+
+`;
diff --git a/src/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap b/src/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap
new file mode 100644
index 0000000000000..cc9cdd6af1f39
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render 1`] = `
+
+`;
diff --git a/src/core_plugins/status_page/public/components/metric_tiles.js b/src/core_plugins/status_page/public/components/metric_tiles.js
new file mode 100644
index 0000000000000..32c2ee7cb896c
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/metric_tiles.js
@@ -0,0 +1,82 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import formatNumber from '../lib/format_number';
+import React, { Component } from 'react';
+import { Metric as MetricPropType } from '../lib/prop_types';
+import PropTypes from 'prop-types';
+import {
+ EuiFlexGrid,
+ EuiFlexItem,
+ EuiCard,
+} from '@elastic/eui';
+
+
+/*
+Displays a metric with the correct format.
+*/
+export class MetricTile extends Component {
+ static propTypes = {
+ metric: MetricPropType.isRequired
+ };
+
+ formattedMetric() {
+ const { value, type } = this.props.metric;
+
+ const metrics = [].concat(value);
+ return metrics.map(function (metric) {
+ return formatNumber(metric, type);
+ }).join(', ');
+ }
+
+ render() {
+ const { name } = this.props.metric;
+
+ return (
+
+ );
+ }
+}
+
+/*
+Wrapper component that simply maps each metric to MetricTile inside a FlexGroup
+*/
+const MetricTiles = ({
+ metrics
+}) => (
+
+ {
+ metrics.map(metric => (
+
+
+
+ ))
+ }
+
+);
+
+MetricTiles.propTypes = {
+ metrics: PropTypes.arrayOf(MetricPropType).isRequired
+};
+
+export default MetricTiles;
diff --git a/src/core_plugins/status_page/public/components/metric_tiles.test.js b/src/core_plugins/status_page/public/components/metric_tiles.test.js
new file mode 100644
index 0000000000000..67ad1145e423c
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/metric_tiles.test.js
@@ -0,0 +1,79 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { MetricTile } from './metric_tiles';
+
+const GENERAL_METRIC = {
+ name: 'A metric',
+ value: 1.8
+ // no type specified
+};
+
+const BYTE_METRIC = {
+ name: 'Heap Total',
+ value: 1501560832,
+ type: 'byte'
+};
+
+const FLOAT_METRIC = {
+ name: 'Load',
+ type: 'float',
+ value: [
+ 4.0537109375,
+ 3.36669921875,
+ 3.1220703125
+ ]
+};
+
+const MS_METRIC = {
+ name: 'Response Time Max',
+ type: 'ms',
+ value: 1234
+};
+
+test('general metric', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot(); // eslint-disable-line
+});
+
+test('byte metric', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot(); // eslint-disable-line
+});
+
+test('float metric', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot(); // eslint-disable-line
+});
+
+test('millisecond metric', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot(); // eslint-disable-line
+});
+
diff --git a/src/core_plugins/status_page/public/components/server_status.js b/src/core_plugins/status_page/public/components/server_status.js
new file mode 100644
index 0000000000000..7fffb612dcfc0
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/server_status.js
@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { State as StatePropType } from '../lib/prop_types';
+import {
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiBadge,
+} from '@elastic/eui';
+
+const ServerState = ({
+ name,
+ serverState
+}) => (
+
+
+
+
+ {'Kibana status is '}
+
+ {serverState.title }
+
+
+
+
+
+
+
+ {name}
+
+
+
+
+);
+
+ServerState.propTypes = {
+ name: PropTypes.string.isRequired,
+ serverState: StatePropType.isRequired
+};
+
+export default ServerState;
\ No newline at end of file
diff --git a/src/core_plugins/status_page/public/status_page_metric.js b/src/core_plugins/status_page/public/components/server_status.test.js
similarity index 55%
rename from src/core_plugins/status_page/public/status_page_metric.js
rename to src/core_plugins/status_page/public/components/server_status.test.js
index f1c38421c9bc1..8cec5252ac7a6 100644
--- a/src/core_plugins/status_page/public/status_page_metric.js
+++ b/src/core_plugins/status_page/public/components/server_status.test.js
@@ -17,27 +17,20 @@
* under the License.
*/
-import formatNumber from './lib/format_number';
-import { uiModules } from 'ui/modules';
-import statusPageMetricTemplate from './status_page_metric.html';
+import React from 'react';
+import { shallow } from 'enzyme';
+import ServerStatus from './server_status';
-uiModules
- .get('kibana', [])
- .filter('statusMetric', function () {
- return function (input, type) {
- const metrics = [].concat(input);
- return metrics.map(function (metric) {
- return formatNumber(metric, type);
- }).join(', ');
- };
- })
- .directive('statusPageMetric', function () {
- return {
- restrict: 'E',
- template: statusPageMetricTemplate,
- scope: {
- metric: '=',
- },
- controllerAs: 'metric'
- };
- });
+const STATE = {
+ id: 'green',
+ title: 'Green',
+ uiColor: 'secondary'
+};
+
+test('render', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot(); // eslint-disable-line
+});
diff --git a/src/core_plugins/status_page/public/components/status_app.js b/src/core_plugins/status_page/public/components/status_app.js
new file mode 100644
index 0000000000000..162af4918091c
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/status_app.js
@@ -0,0 +1,137 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import loadStatus from '../lib/load_status';
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import {
+ EuiLoadingSpinner,
+ EuiText,
+ EuiTitle,
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+import MetricTiles from './metric_tiles';
+import StatusTable from './status_table';
+import ServerStatus from './server_status';
+
+class StatusApp extends Component {
+ static propTypes = {
+ buildNum: PropTypes.number.isRequired,
+ buildSha: PropTypes.string.isRequired,
+ };
+
+ constructor() {
+ super();
+ this.state = {
+ loading: true,
+ fetchError: false,
+ data: null
+ };
+ }
+
+ componentDidMount = async function () {
+ const data = await loadStatus();
+
+ if (data) {
+ this.setState({ loading: false, data: data });
+ } else {
+ this.setState({ fetchError: true, loading: false });
+ }
+ }
+
+ render() {
+ const { buildNum, buildSha } = this.props;
+ const { loading, fetchError, data } = this.state;
+
+ // If we're still loading, return early with a spinner
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (fetchError) {
+ return (
+ An error occurred loading the status
+ );
+ }
+
+ // Extract the items needed to render each component
+ const { metrics, statuses, serverState, name } = data;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugin status
+
+
+
+
+
+
+
+ BUILD { buildNum }
+
+
+
+
+
+
+ COMMIT { buildSha }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default StatusApp;
diff --git a/src/core_plugins/status_page/public/components/status_table.js b/src/core_plugins/status_page/public/components/status_table.js
new file mode 100644
index 0000000000000..4b343ab510173
--- /dev/null
+++ b/src/core_plugins/status_page/public/components/status_table.js
@@ -0,0 +1,75 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { State as StatePropType } from '../lib/prop_types';
+import {
+ EuiBasicTable,
+ EuiIcon,
+} from '@elastic/eui';
+
+
+class StatusTable extends Component {
+ static propTypes = {
+ statuses: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.string.isRequired, // plugin id
+ state: StatePropType.isRequired // state of the plugin
+ })) // can be null
+ };
+
+ static columns = [{
+ field: 'state',
+ name: '',
+ render: state => ,
+ width: '32px'
+ }, {
+ field: 'id',
+ name: 'ID',
+ }, {
+ field: 'state',
+ name: 'Status',
+ render: state => { state.message }
+ }];
+
+ static getRowProps = ({ state }) => {
+ return {
+ className: `status-table-row-${state.uiColor}`
+ };
+ };
+
+ render() {
+ const { statuses } = this.props;
+
+ if (!statuses) {
+ return null;
+ }
+
+ return (
+
+ );
+ }
+}
+
+export default StatusTable;
diff --git a/src/core_plugins/status_page/public/lib/make_chart_options.js b/src/core_plugins/status_page/public/components/status_table.test.js
similarity index 53%
rename from src/core_plugins/status_page/public/lib/make_chart_options.js
rename to src/core_plugins/status_page/public/components/status_table.test.js
index bdb3fa4760433..94ebf010d0ff3 100644
--- a/src/core_plugins/status_page/public/lib/make_chart_options.js
+++ b/src/core_plugins/status_page/public/components/status_table.test.js
@@ -17,31 +17,29 @@
* under the License.
*/
+import React from 'react';
+import { shallow } from 'enzyme';
+import StatusTable from './status_table';
-import formatNumber from './format_number';
-export default function makeChartOptions(type) {
- return {
- chart: {
- type: 'lineChart',
- height: 200,
- showLegend: false,
- showXAxis: false,
- showYAxis: false,
- useInteractiveGuideline: true,
- tooltips: true,
- pointSize: 0,
- color: ['#444', '#777', '#aaa'],
- margin: {
- top: 10,
- left: 0,
- right: 0,
- bottom: 20
- },
- xAxis: { tickFormat: function (d) { return formatNumber(d, 'time'); } },
- yAxis: { tickFormat: function (d) { return formatNumber(d, type); }, },
- y: function (d) { return d.y; },
- x: function (d) { return d.x; }
- }
- };
-}
+const STATE = {
+ id: 'green',
+ uiColor: 'secondary',
+ message: 'Ready'
+};
+
+
+test('render', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot(); // eslint-disable-line
+});
+
+
+test('render empty', () => {
+ const component = shallow();
+ expect(component.isEmptyRender()).toBe(true); // eslint-disable-line
+});
diff --git a/src/core_plugins/status_page/public/index.scss b/src/core_plugins/status_page/public/index.scss
new file mode 100644
index 0000000000000..bef40abe1a8cf
--- /dev/null
+++ b/src/core_plugins/status_page/public/index.scss
@@ -0,0 +1,6 @@
+@import '../../../ui/public/styles/styling_constants';
+
+// SASSTODO: Remove when K7 applies background color to body
+.stsPage {
+ min-height: 100vh;
+}
\ No newline at end of file
diff --git a/src/core_plugins/status_page/public/lib/format_number.js b/src/core_plugins/status_page/public/lib/format_number.js
index 6abbf85cef19f..c846d3c15353a 100644
--- a/src/core_plugins/status_page/public/lib/format_number.js
+++ b/src/core_plugins/status_page/public/lib/format_number.js
@@ -18,15 +18,12 @@
*/
-import moment from 'moment';
import numeral from 'numeral';
export default function formatNumber(num, which) {
let format = '0.00';
let postfix = '';
switch (which) {
- case 'time':
- return moment(num).format('HH:mm:ss');
case 'byte':
format += ' b';
break;
@@ -37,5 +34,6 @@ export default function formatNumber(num, which) {
format = '0';
break;
}
+
return numeral(num).format(format) + postfix;
}
diff --git a/src/core_plugins/status_page/public/lib/format_number.test.js b/src/core_plugins/status_page/public/lib/format_number.test.js
new file mode 100644
index 0000000000000..78f17ffa76f39
--- /dev/null
+++ b/src/core_plugins/status_page/public/lib/format_number.test.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import formatNumber from './format_number';
+
+describe('format byte', () => {
+ test('zero', () => {
+ expect(formatNumber(0, 'byte')).toEqual('0.00 B');
+ });
+
+ test('mb', () => {
+ expect(formatNumber(181142512, 'byte')).toEqual('181.14 MB');
+ });
+
+ test('gb', () => {
+ expect(formatNumber(273727485000, 'byte')).toEqual('273.73 GB');
+ });
+});
+
+describe('format ms', () => {
+ test('zero', () => {
+ expect(formatNumber(0, 'ms')).toEqual('0.00 ms');
+ });
+
+ test('sub ms', () => {
+ expect(formatNumber(0.128, 'ms')).toEqual('0.13 ms');
+ });
+
+ test('many ms', () => {
+ expect(formatNumber(3030.284, 'ms')).toEqual('3030.28 ms');
+ });
+});
+
+describe('format integer', () => {
+ test('zero', () => {
+ expect(formatNumber(0, 'integer')).toEqual('0');
+ });
+
+ test('sub integer', () => {
+ expect(formatNumber(0.728, 'integer')).toEqual('1');
+ });
+
+ test('many integer', () => {
+ expect(formatNumber(3030.284, 'integer')).toEqual('3030');
+ });
+});
diff --git a/src/core_plugins/status_page/public/lib/load_status.js b/src/core_plugins/status_page/public/lib/load_status.js
new file mode 100644
index 0000000000000..1bb4935921928
--- /dev/null
+++ b/src/core_plugins/status_page/public/lib/load_status.js
@@ -0,0 +1,116 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import _ from 'lodash';
+
+import chrome from 'ui/chrome';
+import { notify } from 'ui/notify';
+
+// Module-level error returned by notify.error
+let errorNotif;
+
+/*
+Returns an object of any keys that should be included for metrics.
+*/
+function formatMetrics(data) {
+ if (!data.metrics) {
+ return null;
+ }
+
+ return [
+ {
+ name: 'Heap total',
+ value: _.get(data.metrics, 'process.memory.heap.size_limit'),
+ type: 'byte'
+ }, {
+ name: 'Heap used',
+ value: _.get(data.metrics, 'process.memory.heap.used_in_bytes'),
+ type: 'byte'
+ }, {
+ name: 'Load',
+ value: [
+ _.get(data.metrics, 'os.load.1m'),
+ _.get(data.metrics, 'os.load.5m'),
+ _.get(data.metrics, 'os.load.15m')
+ ],
+ type: 'float'
+ }, {
+ name: 'Response time avg',
+ value: _.get(data.metrics, 'response_times.avg_in_millis'),
+ type: 'ms'
+ }, {
+ name: 'Response time max',
+ value: _.get(data.metrics, 'response_times.max_in_millis'),
+ type: 'ms'
+ }, {
+ name: 'Requests per second',
+ value: _.get(data.metrics, 'requests.total') * 1000 / _.get(data.metrics, 'collection_interval_in_millis')
+ }
+ ];
+}
+
+async function fetchData() {
+ return fetch(
+ chrome.addBasePath('/api/status'),
+ {
+ method: 'get',
+ credentials: 'same-origin'
+ }
+ );
+}
+
+/*
+Get the status from the server API and format it for display.
+
+`fetchFn` can be injected for testing, defaults to the implementation above.
+*/
+async function loadStatus(fetchFn = fetchData) {
+ // Clear any existing error banner.
+ if (errorNotif) {
+ errorNotif.clear();
+ errorNotif = null;
+ }
+
+ let response;
+
+ try {
+ response = await fetchFn();
+ } catch (e) {
+ // If the fetch failed to connect, display an error and bail.
+ errorNotif = notify.error('Failed to request server status. Perhaps your server is down?');
+ return e;
+ }
+
+ if (response.status >= 400) {
+ // If the server does not respond with a successful status, display an error and bail.
+ errorNotif = notify.error(`Failed to request server status with status code ${response.status}`);
+ return;
+ }
+
+ const data = await response.json();
+
+ return {
+ name: data.name,
+ statuses: data.status.statuses,
+ serverState: data.status.overall.state,
+ metrics: formatMetrics(data),
+ };
+}
+
+export default loadStatus;
\ No newline at end of file
diff --git a/src/core_plugins/status_page/public/lib/load_status.test.js b/src/core_plugins/status_page/public/lib/load_status.test.js
new file mode 100644
index 0000000000000..9a66d146a087c
--- /dev/null
+++ b/src/core_plugins/status_page/public/lib/load_status.test.js
@@ -0,0 +1,110 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import loadStatus from './load_status';
+
+// Make importing the ui/notify module work in jest
+jest.mock('ui/metadata', () => ({
+ metadata: {
+ branch: 'my-metadata-branch',
+ version: 'my-metadata-version'
+ }
+}));
+
+// A faked response to the `fetch` call
+const mockFetch = async () => ({
+ status: 200,
+ json: async () => ({
+ name: 'My computer',
+ status: {
+ overall: {
+ state: { id: 'yellow', title: 'Yellow' }
+ },
+ statuses: [
+ { id: 'plugin:1', state: { id: 'green' } },
+ { id: 'plugin:2', state: { id: 'yellow' } }
+ ],
+ },
+ metrics: {
+ collection_interval_in_millis: 1000,
+ os: { load: {
+ '1m': 4.1,
+ '5m': 2.1,
+ '15m': 0.1,
+ } },
+
+ process: { memory: { heap: {
+ size_limit: 1000000,
+ used_in_bytes: 100
+ } } },
+
+ response_times: {
+ avg_in_millis: 4000,
+ max_in_millis: 8000
+ },
+
+ requests: {
+ total: 400
+ }
+ }
+ })
+});
+
+describe('response processing', () => {
+ test('includes the name', async () => {
+ const data = await loadStatus(mockFetch);
+ expect(data.name).toEqual('My computer');
+ });
+
+ test('includes the plugin statuses', async () => {
+ const data = await loadStatus(mockFetch);
+ expect(data.statuses).toEqual([
+ { id: 'plugin:1', state: { id: 'green' } },
+ { id: 'plugin:2', state: { id: 'yellow' } }
+ ]);
+ });
+
+ test('includes the serverState', async () => {
+ const data = await loadStatus(mockFetch);
+ expect(data.serverState).toEqual({ id: 'yellow', title: 'Yellow' });
+ });
+
+ test('builds the metrics', async () => {
+ const data = await loadStatus(mockFetch);
+ const names = data.metrics.map(m => m.name);
+ expect(names).toEqual([
+ 'Heap total',
+ 'Heap used',
+ 'Load',
+ 'Response time avg',
+ 'Response time max',
+ 'Requests per second'
+ ]);
+
+ const values = data.metrics.map(m => m.value);
+ expect(values).toEqual([
+ 1000000,
+ 100,
+ [4.1, 2.1, 0.1],
+ 4000,
+ 8000,
+ 400
+ ]);
+ });
+});
diff --git a/src/core_plugins/status_page/public/lib/prop_types.js b/src/core_plugins/status_page/public/lib/prop_types.js
new file mode 100644
index 0000000000000..ad07af7dc03e4
--- /dev/null
+++ b/src/core_plugins/status_page/public/lib/prop_types.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import PropTypes from 'prop-types';
+
+export const State = PropTypes.shape({
+ id: PropTypes.string.isRequired,
+ message: PropTypes.string, // optional
+ title: PropTypes.string, // optional
+ uiColor: PropTypes.string.isRequired,
+});
+
+export const Metric = PropTypes.shape({
+ name: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.number),
+ PropTypes.number
+ ]).isRequired,
+ type: PropTypes.string // optional
+});
diff --git a/src/core_plugins/status_page/public/lib/read_stat_data.js b/src/core_plugins/status_page/public/lib/read_stat_data.js
deleted file mode 100644
index 8f8c27c79068b..0000000000000
--- a/src/core_plugins/status_page/public/lib/read_stat_data.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import _ from 'lodash';
-
-export default function readStatData(data, seriesNames) {
- // Metric Values format
- // metric: [[xValue, yValue], ...]
- // LoadMetric:
- // metric: [[xValue, [yValue, yValue2, yValue3]], ...]
- // return [
- // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue}, ...]},
- // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue1}, ...]},
- // {type: 'line', key: name, yAxis: 1, values: [{x: xValue, y: yValue2}, ...]}]
- //
- // Go through all of the metric values and split the values out.
- // returns an array of all of the averages
-
- const metricList = [];
- seriesNames = seriesNames || [];
- data.forEach(function (vector) {
- vector = _.flatten(vector);
- const x = vector.shift();
- vector.forEach(function (yValue, i) {
- const series = seriesNames[i] || '';
-
- if (!metricList[i]) {
- metricList[i] = {
- key: series,
- values: []
- };
- }
- // unshift to make sure they're in the correct order
- metricList[i].values.unshift({
- x: x,
- y: yValue
- });
- });
- });
-
- return metricList;
-}
diff --git a/src/core_plugins/status_page/public/status_page.html b/src/core_plugins/status_page/public/status_page.html
index f5acbbf7dd9ae..af5bd1dd84eaa 100644
--- a/src/core_plugins/status_page/public/status_page.html
+++ b/src/core_plugins/status_page/public/status_page.html
@@ -1,52 +1 @@
-
-
-
- Status: {{ ui.serverStateMessage }}
-
-
- {{ ui.name }}
-
-
-
-
-
-
-
-
Status Breakdown
-
-
-
-
-
-
- No status information available
-
-
-
-
- ID |
- Status |
-
-
-
- {{status.id}} |
-
-
- {{status.message}}
- |
-
-
-
-
-
-
+
diff --git a/src/core_plugins/status_page/public/status_page.js b/src/core_plugins/status_page/public/status_page.js
index 6fe67264c66ff..dd5d7275c6004 100644
--- a/src/core_plugins/status_page/public/status_page.js
+++ b/src/core_plugins/status_page/public/status_page.js
@@ -17,91 +17,26 @@
* under the License.
*/
-import _ from 'lodash';
-import { notify } from 'ui/notify';
import 'ui/autoload/styles';
-import './status_page_metric';
-import './status_page.less';
import { uiModules } from 'ui/modules';
+import chrome from 'ui/chrome';
+import StatusApp from './components/status_app';
-const chrome = require('ui/chrome')
+const app = uiModules.get('apps/status', []);
+app.directive('statusApp', function (reactDirective) {
+ return reactDirective(StatusApp);
+});
+
+chrome
.setRootTemplate(require('plugins/status_page/status_page.html'))
.setRootController('ui', function ($http, buildNum, buildSha) {
const ui = this;
- ui.loading = false;
ui.buildInfo = {
num: buildNum,
sha: buildSha.substr(0, 8)
};
-
- ui.refresh = function () {
- ui.loading = true;
-
- // go ahead and get the info you want
- return $http
- .get(chrome.addBasePath('/api/status'))
- .then(function (resp) {
-
- if (ui.fetchError) {
- ui.fetchError.clear();
- ui.fetchError = null;
- }
-
- const data = resp.data;
- const metrics = data.metrics;
- if (metrics) {
- ui.metrics = [{
- name: 'Heap Total',
- value: _.get(metrics, 'process.memory.heap.size_limit'),
- type: 'byte'
- }, {
- name: 'Heap Used',
- value: _.get(metrics, 'process.memory.heap.used_in_bytes'),
- type: 'byte'
- }, {
- name: 'Load',
- value: [
- _.get(metrics, 'os.load.1m'),
- _.get(metrics, 'os.load.5m'),
- _.get(metrics, 'os.load.15m')
- ],
- type: 'float'
- }, {
- name: 'Response Time Avg',
- value: _.get(metrics, 'response_times.avg_in_millis'),
- type: 'ms'
- }, {
- name: 'Response Time Max',
- value: _.get(metrics, 'response_times.max_in_millis'),
- type: 'ms'
- }, {
- name: 'Requests Per Second',
- value: _.get(metrics, 'requests.total') * 1000 / _.get(metrics, 'collection_interval_in_millis')
- }];
- }
-
- ui.name = data.name;
- ui.statuses = data.status.statuses;
-
- const overall = data.status.overall;
- if (!ui.serverState || (ui.serverState !== overall.state)) {
- ui.serverState = overall.state;
- ui.serverStateMessage = overall.title;
- }
- })
- .catch(function () {
- if (ui.fetchError) return;
- ui.fetchError = notify.error('Failed to request server ui. Perhaps your server is down?');
- ui.metrics = ui.statuses = ui.overall = null;
- })
- .then(function () {
- ui.loading = false;
- });
- };
-
- ui.refresh();
});
uiModules.get('kibana')
diff --git a/src/core_plugins/status_page/public/status_page.less b/src/core_plugins/status_page/public/status_page.less
deleted file mode 100644
index 99894ccf1badd..0000000000000
--- a/src/core_plugins/status_page/public/status_page.less
+++ /dev/null
@@ -1,184 +0,0 @@
-@import "~font-awesome/less/font-awesome";
-
-@status-bg: #eff0f2;
-@status-metric-bg: #fff;
-@status-metric-border: #aaa;
-@status-metric-title-color: #666;
-
-@status-statuses-bg: #fff;
-@status-statuses-border: #bbb;
-@status-statuses-headings-color: #666;
-
-@status-default: #7c7c7c;
-@status-green: #94c63d;
-@status-yellow: #edb800;
-@status-red: #da1e04;
-
-@icon-default: @fa-var-clock-o;
-@icon-green: @fa-var-check;
-@icon-yellow: @fa-var-exclamation-circle;
-@icon-red: @fa-var-exclamation-triangle;
-
-// background of main page
-.content {
- background-color: @status-bg;
-}
-
-.section {
- margin-bottom:15px;
-}
-
-// metrics section
-.metrics_wrapper {
- margin-top: 25px;
- .status_metric_wrapper {
- padding: 10px;
- border: 0;
-
- .content {
- display: block;
- text-align: right;
- padding: 15px;
- padding-right: 20px;
- background-color: @status-metric-bg;
- border-top: 2px solid;
- border-top-color: @status-metric-border;
-
- .title {
- color: @status-metric-title-color;
- margin: 0 0 5px 0;
- }
-
- .average {
- font-size: 42px;
- line-height:45px;
- font-weight: normal;
- margin:0;
- }
- }
- }
-}
-
-// status status table section
-.statuses_wrapper {
- margin-top: 25px;
- margin-left: -5px;
- margin-right: -5px;
- border-top:2px solid;
- background-color: @status-statuses-bg;
- padding: 10px;
-
- h3 {
- margin-top: 3px;
- margin-bottom: 3px;
- }
-
- .statuses_loading,
- .statuses_missing {
- padding: 20px;
- text-align: center;
- }
-
- .statuses {
- margin-left: 0;
- margin-right: 0;
- margin-bottom: 30px;
-
- .status {
- height:30px;
- line-height:30px;
- border-bottom:1px solid;
- border-bottom-color: @status-statuses-border;
- }
-
- th {
- color:@status-statuses-headings-color;
- font-weight: normal;
- height:25px;
- line-height:25px;
- border-bottom:1px solid;
- border-bottom-color: @status-statuses-border;
- }
-
- .status_id {
- padding:0px 5px;
- border-left: 2px solid;
- }
-
- .status_message {
- padding:0;
- padding-left:15px;
- border-right: 2px solid;
- }
- }
-}
-
-//status state
-.status_state(@color, @icon) {
- .status_state_color {
- color: @color;
- }
-
- .status_state_icon:before {
- content: @icon;
- }
-
- .status_id {
- border-left-color: @color !important;
- }
-
- .status_message {
- border-right-color: @color !important;
- }
-}
-
-.status_state_default {
- .status_state(@status-default, @icon-default);
-}
-
-.status_state_green {
- .status_state(@status-green, @icon-green);
-}
-
-.status_state_yellow {
- .status_state(@status-yellow, @icon-yellow);
-}
-
-.status_state_red {
- .status_state(@status-red, @icon-red);
-}
-
-//server state
-.state(@color, @icon) {
- .overall_state_color {
- color: @color;
- }
-
- .overall_state_icon:before {
- content: @icon;
- }
-
- .statuses_wrapper {
- border-top-color: @color;
- }
-}
-
-.overall_state_default {
- .state(@status-default, @icon-default);
-}
-
-.overall_state_green {
- .state(@status-green, @icon-green);
-}
-
-.overall_state_yellow {
- .state(@status-yellow, @icon-yellow);
-}
-
-.overall_state_red {
- .state(@status-red, @icon-red);
-}
-
-.build-info {
- color: #555;
-}
diff --git a/src/core_plugins/status_page/public/status_page_metric.html b/src/core_plugins/status_page/public/status_page_metric.html
deleted file mode 100644
index 7d5a99d90a0d1..0000000000000
--- a/src/core_plugins/status_page/public/status_page_metric.html
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
{{ metric.name }}
- {{ metric.value | statusMetric: metric.type}}
-
-
diff --git a/src/server/status/lib/get_kibana_info_for_stats.js b/src/server/status/lib/get_kibana_info_for_stats.js
index bac800c03b2b1..e9ce57477cbcf 100644
--- a/src/server/status/lib/get_kibana_info_for_stats.js
+++ b/src/server/status/lib/get_kibana_info_for_stats.js
@@ -41,6 +41,6 @@ export function getKibanaInfoForStats(server, kbnServer) {
transport_address: `${config.get('server.host')}:${config.get('server.port')}`,
version: kbnServer.version.replace(snapshotRegex, ''),
snapshot: snapshotRegex.test(kbnServer.version),
- status: get(status, 'overall.state')
+ status: get(status, 'overall.id')
};
}
diff --git a/src/server/status/server_status.js b/src/server/status/server_status.js
index 8092d455b428d..37cfc852f0e92 100644
--- a/src/server/status/server_status.js
+++ b/src/server/status/server_status.js
@@ -86,16 +86,18 @@ export default class ServerStatus {
const since = _.get(_.sortBy(statuses, 'since'), [0, 'since']);
return {
- state: state.id,
- title: state.title,
- nickname: _.sample(state.nicknames),
- icon: state.icon,
+ id: state.id,
+ state: {
+ title: state.title,
+ uiColor: states.get(state.id).uiColor,
+ nickname: _.sample(state.nicknames),
+ },
since: since,
};
}
isGreen() {
- return (this.overall().state === 'green');
+ return (this.overall().id === 'green');
}
notGreen() {
diff --git a/src/server/status/server_status.test.js b/src/server/status/server_status.test.js
index 8b680d08c3512..80624b23f75fa 100644
--- a/src/server/status/server_status.test.js
+++ b/src/server/status/server_status.test.js
@@ -24,6 +24,7 @@ import * as states from './states';
import Status from './status';
import ServerStatus from './server_status';
+
describe('ServerStatus class', function () {
const plugin = { id: 'name', version: '1.2.3' };
@@ -93,13 +94,13 @@ describe('ServerStatus class', function () {
it('considers each status to produce a summary', function () {
const status = serverStatus.createForPlugin(plugin);
- expect(serverStatus.overall().state).toBe('uninitialized');
+ expect(serverStatus.overall().id).toBe('uninitialized');
const match = function (overall, state) {
- expect(overall).toHaveProperty('state', state.id);
- expect(overall).toHaveProperty('title', state.title);
- expect(overall).toHaveProperty('icon', state.icon);
- expect(state.nicknames).toContain(overall.nickname);
+ expect(overall).toHaveProperty('id', state.id);
+ expect(overall).toHaveProperty('state.title', state.title);
+ expect(overall).toHaveProperty('state.uiColor', state.uiColor);
+ expect(state.nicknames).toContain(overall.state.nickname);
};
status.green();
@@ -133,9 +134,12 @@ describe('ServerStatus class', function () {
expect(json.statuses).toHaveLength(3);
const out = status => find(json.statuses, { id: status.id });
- expect(out(service)).toHaveProperty('state', 'green');
- expect(out(p1)).toHaveProperty('state', 'yellow');
- expect(out(p2)).toHaveProperty('state', 'red');
+ expect(out(service)).toHaveProperty('state.message', 'Green');
+ expect(out(service)).toHaveProperty('state.uiColor', 'secondary');
+ expect(out(p1)).toHaveProperty('state.message', 'Yellow');
+ expect(out(p1)).toHaveProperty('state.uiColor', 'warning');
+ expect(out(p2)).toHaveProperty('state.message', 'Red');
+ expect(out(p2)).toHaveProperty('state.uiColor', 'danger');
});
});
diff --git a/src/server/status/states.js b/src/server/status/states.js
index 2534d04198781..30c5bc3034a72 100644
--- a/src/server/status/states.js
+++ b/src/server/status/states.js
@@ -23,7 +23,7 @@ export const all = [
{
id: 'red',
title: 'Red',
- icon: 'danger',
+ uiColor: 'danger',
severity: 1000,
nicknames: [
'Danger Will Robinson! Danger!'
@@ -32,7 +32,7 @@ export const all = [
{
id: 'uninitialized',
title: 'Uninitialized',
- icon: 'spinner',
+ uiColor: 'default',
severity: 900,
nicknames: [
'Initializing'
@@ -41,7 +41,7 @@ export const all = [
{
id: 'yellow',
title: 'Yellow',
- icon: 'warning',
+ uiColor: 'warning',
severity: 800,
nicknames: [
'S.N.A.F.U',
@@ -52,7 +52,7 @@ export const all = [
{
id: 'green',
title: 'Green',
- icon: 'success',
+ uiColor: 'secondary',
severity: 0,
nicknames: [
'Looking good'
@@ -62,7 +62,7 @@ export const all = [
id: 'disabled',
title: 'Disabled',
severity: -1,
- icon: 'toggle-off',
+ uiColor: 'default',
nicknames: [
'Am I even a thing?'
]
diff --git a/src/server/status/status.js b/src/server/status/status.js
index 28132eebba706..64b9347967510 100644
--- a/src/server/status/status.js
+++ b/src/server/status/status.js
@@ -55,9 +55,11 @@ export default class Status extends EventEmitter {
toJSON() {
return {
id: this.id,
- state: this.state,
- icon: states.get(this.state).icon,
- message: this.message,
+ state: {
+ id: this.state,
+ message: this.message,
+ uiColor: states.get(this.state).uiColor,
+ },
since: this.since
};
}
diff --git a/src/server/status/status.test.js b/src/server/status/status.test.js
index c6bc967ca0766..1e7632dbc66d6 100644
--- a/src/server/status/status.test.js
+++ b/src/server/status/status.test.js
@@ -74,8 +74,8 @@ describe('Status class', function () {
const json = status.toJSON();
expect(json.id).toEqual(status.id);
- expect(json.state).toEqual('green');
- expect(json.message).toEqual('Ready');
+ expect(json.state.id).toEqual('green');
+ expect(json.state.message).toEqual('Ready');
});
it('should call on handler if status is already matched', function (done) {
diff --git a/test/api_integration/apis/status/status.js b/test/api_integration/apis/status/status.js
index 08ab6fc7667a1..65e1e82ebe1a3 100644
--- a/test/api_integration/apis/status/status.js
+++ b/test/api_integration/apis/status/status.js
@@ -36,13 +36,16 @@ export default function ({ getService }) {
expect(body.version.build_number).to.be.a('number');
expect(body.status.overall).to.be.an('object');
- expect(body.status.overall.state).to.be('green');
+ expect(body.status.overall.id).to.be('green');
+ expect(body.status.overall.state).to.be.an('object');
+ expect(body.status.overall.state.title).to.be('Green');
expect(body.status.statuses).to.be.an('array');
const kibanaPlugin = body.status.statuses.find(s => {
return s.id.indexOf('plugin:kibana') === 0;
});
- expect(kibanaPlugin.state).to.be('green');
+ expect(kibanaPlugin.state).to.be.an('object');
+ expect(kibanaPlugin.state.id).to.be('green');
expect(body.metrics.collection_interval_in_millis).to.be.a('number');
diff --git a/test/common/services/kibana_server/status.js b/test/common/services/kibana_server/status.js
index 19af8c4d6f258..c765cfbe84de3 100644
--- a/test/common/services/kibana_server/status.js
+++ b/test/common/services/kibana_server/status.js
@@ -39,6 +39,6 @@ export class KibanaServerStatus {
async getOverallState() {
const status = await this.get();
- return status.status.overall.state;
+ return status.status.overall.id;
}
}