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 -

- - - - - - - - - - - -
IDStatus
{{status.id}} - - {{status.message}} -
-
- -
-
- Build {{::ui.buildInfo.num}}, Commit SHA {{::ui.buildInfo.sha}} -
-
-
+ 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; } }