Convert status page to EUI (#21491)

* Convert the status_page plugin to EUI

* Fix uiColor for disabled state
This commit is contained in:
Josh Dover 2018-08-09 12:26:17 -05:00 committed by GitHub
parent 6af72b7d58
commit 33c6ade756
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 989 additions and 453 deletions

View file

@ -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`
}
}
});

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`byte metric 1`] = `
<EuiCard
description="Heap Total"
layout="horizontal"
textAlign="center"
title="1.50 GB"
titleElement="span"
/>
`;
exports[`float metric 1`] = `
<EuiCard
description="Load"
layout="horizontal"
textAlign="center"
title="4.05, 3.37, 3.12"
titleElement="span"
/>
`;
exports[`general metric 1`] = `
<EuiCard
description="A metric"
layout="horizontal"
textAlign="center"
title="1.80"
titleElement="span"
/>
`;
exports[`millisecond metric 1`] = `
<EuiCard
description="Response Time Max"
layout="horizontal"
textAlign="center"
title="1234.00 ms"
titleElement="span"
/>
`;

View file

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiFlexGroup
alignItems="center"
component="div"
direction="row"
gutterSize="l"
justifyContent="spaceBetween"
responsive={true}
style={
Object {
"flexGrow": 0,
}
}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiTitle
size="m"
>
<h2>
Kibana status is
<EuiBadge
color="secondary"
iconSide="left"
>
Green
</EuiBadge>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiText
grow={true}
>
<p>
My Computer
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render 1`] = `
<EuiBasicTable
columns={
Array [
Object {
"field": "state",
"name": "",
"render": [Function],
"width": "32px",
},
Object {
"field": "id",
"name": "ID",
},
Object {
"field": "state",
"name": "Status",
"render": [Function],
},
]
}
data-test-subj="statusBreakdown"
items={
Array [
Object {
"id": "plugin:1",
"state": Object {
"id": "green",
"message": "Ready",
"uiColor": "secondary",
},
},
]
}
noItemsMessage="No items found"
responsive={true}
rowProps={[Function]}
/>
`;

View file

@ -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 (
<EuiCard
layout="horizontal"
title={this.formattedMetric()}
description={name}
/>
);
}
}
/*
Wrapper component that simply maps each metric to MetricTile inside a FlexGroup
*/
const MetricTiles = ({
metrics
}) => (
<EuiFlexGrid columns={3}>
{
metrics.map(metric => (
<EuiFlexItem key={metric.name}>
<MetricTile metric={metric} />
</EuiFlexItem>
))
}
</EuiFlexGrid>
);
MetricTiles.propTypes = {
metrics: PropTypes.arrayOf(MetricPropType).isRequired
};
export default MetricTiles;

View file

@ -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(<MetricTile
metric={GENERAL_METRIC}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('byte metric', () => {
const component = shallow(<MetricTile
metric={BYTE_METRIC}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('float metric', () => {
const component = shallow(<MetricTile
metric={FLOAT_METRIC}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('millisecond metric', () => {
const component = shallow(<MetricTile
metric={MS_METRIC}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});

View file

@ -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
}) => (
<EuiFlexGroup
alignItems="center"
justifyContent="spaceBetween"
style={{ flexGrow: 0 }}
>
<EuiFlexItem grow={false}>
<EuiTitle>
<h2>
{'Kibana status is '}
<EuiBadge color={serverState.uiColor}>
{serverState.title }
</EuiBadge>
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<p>
{name}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
ServerState.propTypes = {
name: PropTypes.string.isRequired,
serverState: StatePropType.isRequired
};
export default ServerState;

View file

@ -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(<ServerStatus
serverState={STATE}
name="My Computer"
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});

View file

@ -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 (
<EuiLoadingSpinner size="l" />
);
}
if (fetchError) {
return (
<EuiText color="danger">An error occurred loading the status</EuiText>
);
}
// Extract the items needed to render each component
const { metrics, statuses, serverState, name } = data;
return (
<EuiPage className="stsPage">
<EuiPageBody restrictWidth>
<ServerStatus
name={name}
serverState={serverState}
/>
<EuiSpacer />
<MetricTiles metrics={metrics} />
<EuiSpacer />
<EuiPageContent grow={0}>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>Plugin status</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>
BUILD <strong>{ buildNum }</strong>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">
<p>
COMMIT <strong>{ buildSha }</strong>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<StatusTable statuses={statuses} />
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}
export default StatusApp;

View file

@ -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 => <EuiIcon type="dot" ariaabel="" color={state.uiColor} />,
width: '32px'
}, {
field: 'id',
name: 'ID',
}, {
field: 'state',
name: 'Status',
render: state => <span>{ state.message }</span>
}];
static getRowProps = ({ state }) => {
return {
className: `status-table-row-${state.uiColor}`
};
};
render() {
const { statuses } = this.props;
if (!statuses) {
return null;
}
return (
<EuiBasicTable
columns={StatusTable.columns}
items={statuses}
rowProps={StatusTable.getRowProps}
data-test-subj="statusBreakdown"
/>
);
}
}
export default StatusTable;

View file

@ -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(<StatusTable
statuses={[
{ id: 'plugin:1', state: STATE }
]}
/>);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('render empty', () => {
const component = shallow(<StatusTable />);
expect(component.isEmptyRender()).toBe(true); // eslint-disable-line
});

View file

@ -0,0 +1,6 @@
@import '../../../ui/public/styles/styling_constants';
// SASSTODO: Remove when K7 applies background color to body
.stsPage {
min-height: 100vh;
}

View file

@ -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;
}

View file

@ -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');
});
});

View file

@ -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;

View file

@ -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
]);
});
});

View file

@ -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
});

View file

@ -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;
}

View file

@ -1,52 +1 @@
<div data-test-subj="statusPageContainer" class="container overall_state_default overall_state_{{ui.serverState}}">
<header>
<h1>
Status: <span class="overall_state_color">{{ ui.serverStateMessage }}</span>
<i class="fa overall_state_color state_icon" />
<span class="pull-right">
{{ ui.name }}
</span>
</h1>
</header>
<div class="row metrics_wrapper">
<div ng-repeat="metric in ui.metrics">
<status-page-metric metric="metric"></status-page-metric>
</div>
</div>
<div class="row statuses_wrapper">
<h3>Status Breakdown</h3>
<div ng-if="!ui.statuses && ui.loading" class="statuses_loading">
<span class="spinner"></span>
</div>
<h4 ng-if="!ui.statuses && !ui.loading" class="statuses_missing">
No status information available
</h4>
<table class="statuses" data-test-subj="statusBreakdown" ng-if="ui.statuses">
<tr class="row">
<th class="col-xs-4" scope="col">ID</th>
<th class="col-xs-8" scope="col">Status</th>
</tr>
<tr
ng-repeat="status in ui.statuses"
class="status status_state_default status_state_{{status.state}} row">
<td class="col-xs-4 status_id">{{status.id}}</td>
<td class="col-xs-8 status_message">
<i class="fa status_state_color status_state_icon" />
{{status.message}}
</td>
</tr>
</table>
</div>
<footer class="row">
<div class="col-xs-12 text-right build-info">
Build {{::ui.buildInfo.num}}, Commit SHA {{::ui.buildInfo.sha}}
</div>
</footer>
</div>
<status-app build-num="{{ui.buildInfo.num}}" build-sha="'{{ui.buildInfo.sha}}'" />

View file

@ -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')

View file

@ -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;
}

View file

@ -1,6 +0,0 @@
<div class="status_metric_wrapper col-md-4">
<div class="content">
<h3 class="title">{{ metric.name }}</h3>
<h4 class="average">{{ metric.value | statusMetric: metric.type}}</h4>
</div>
</div>

View file

@ -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')
};
}

View file

@ -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() {

View file

@ -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');
});
});

View file

@ -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?'
]

View file

@ -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
};
}

View file

@ -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) {

View file

@ -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');

View file

@ -39,6 +39,6 @@ export class KibanaServerStatus {
async getOverallState() {
const status = await this.get();
return status.status.overall.state;
return status.status.overall.id;
}
}