From 2178a14519796f2a6f9925a85422480c9ea65473 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 23 Jul 2020 12:15:03 +0200 Subject: [PATCH] Migrate status page app to core (#72017) * move http.anonymousPaths.register('/status'); logic into core, remove status_page plugin * move status_page to core & migrate lib * migrate the status_app components to TS/KP APIs * update rendering snapshots * use import type syntax * moves `/status` server-side route to core * fix route registration * update generated file * change statusPage i18n prefix * (temporary) try to restore legacy plugin to check behavior * add some FTR tests * do not import whole lodash * update snapshots * review comments * improve / clean component unit tests * change url for legacy status app * set status app as chromeless * fix FTR test due to chromeless app * fix typings * add unit test for CoreApp /status registration --- .../architecture/code-exploration.asciidoc | 5 - src/core/public/core_app/core_app.ts | 32 +++- .../__snapshots__/metric_tiles.test.tsx.snap | 37 ++++ .../__snapshots__/server_status.test.tsx.snap | 67 +++++++ .../__snapshots__/status_table.test.tsx.snap} | 3 +- .../core_app/status/components}/index.ts | 8 +- .../status/components/metric_tiles.test.tsx} | 43 ++--- .../status/components/metric_tiles.tsx} | 60 +++---- .../status/components/server_status.test.tsx | 58 +++++++ .../status/components/server_status.tsx} | 33 ++-- .../status/components/status_table.test.tsx} | 21 ++- .../status/components/status_table.tsx | 67 +++++++ .../public/core_app/status/index.ts} | 15 +- .../status/lib/format_number.test.ts} | 38 +++- .../core_app/status/lib/format_number.ts} | 16 +- src/core/public/core_app/status/lib/index.ts | 21 +++ .../core_app/status/lib/load_status.test.ts | 152 ++++++++++++++++ .../public/core_app/status/lib/load_status.ts | 153 ++++++++++++++++ .../public/core_app/status/render_app.tsx | 44 +++++ .../public/core_app/status/status_app.tsx} | 71 ++++---- src/core/public/core_system.ts | 4 +- .../injected_metadata_service.mock.ts | 2 + .../injected_metadata_service.ts | 6 + src/core/server/core_app/core_app.test.ts | 71 ++++++++ src/core/server/core_app/core_app.ts | 18 ++ .../http_resources_service.mock.ts | 2 +- src/core/server/mocks.ts | 5 +- src/core/server/rendering/__mocks__/params.ts | 3 + .../rendering_service.test.ts.snap | 10 ++ .../server/rendering/rendering_service.tsx | 2 + src/core/server/rendering/types.ts | 3 + src/core/server/server.ts | 67 +++---- src/core/server/status/index.ts | 1 + .../server/status/status_config.ts} | 30 ++-- src/core/server/status/status_service.mock.ts | 1 + src/core/server/status/status_service.test.ts | 22 ++- src/core/server/status/status_service.ts | 9 +- src/core/server/status/types.ts | 1 + .../public/plugin.ts => core/types/status.ts} | 43 +++-- src/legacy/core_plugins/status_page/index.js | 9 +- .../__snapshots__/metric_tiles.test.js.snap | 33 ---- .../__snapshots__/server_status.test.js.snap | 44 ----- .../status_page/public/components/render.js | 13 +- .../public/components/status_table.js | 82 --------- .../status_page/public/lib/load_status.js | 163 ------------------ .../public/lib/load_status.test.js | 115 ------------ .../status_page/public/lib/prop_types.js | 33 ---- src/legacy/server/status/index.js | 3 +- src/legacy/server/status/routes/index.js | 1 - .../status/routes/page/register_status.js | 53 ------ src/legacy/ui/ui_render/ui_render_mixin.js | 7 +- src/plugins/status_page/kibana.json | 6 - .../apps/status_page/{index.js => index.ts} | 26 ++- .../translations/translations/ja-JP.json | 30 ++-- .../translations/translations/zh-CN.json | 30 ++-- .../functional/page_objects/status_page.js | 2 +- 56 files changed, 1064 insertions(+), 830 deletions(-) create mode 100644 src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap create mode 100644 src/core/public/core_app/status/components/__snapshots__/server_status.test.tsx.snap rename src/{legacy/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap => core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap} (89%) rename src/{plugins/status_page/public => core/public/core_app/status/components}/index.ts (75%) rename src/{legacy/core_plugins/status_page/public/components/metric_tiles.test.js => core/public/core_app/status/components/metric_tiles.test.tsx} (58%) rename src/{legacy/core_plugins/status_page/public/components/metric_tiles.js => core/public/core_app/status/components/metric_tiles.tsx} (50%) create mode 100644 src/core/public/core_app/status/components/server_status.test.tsx rename src/{legacy/core_plugins/status_page/public/components/server_status.js => core/public/core_app/status/components/server_status.tsx} (66%) rename src/{legacy/core_plugins/status_page/public/components/status_table.test.js => core/public/core_app/status/components/status_table.test.tsx} (67%) create mode 100644 src/core/public/core_app/status/components/status_table.tsx rename src/{legacy/core_plugins/status_page/public/components/server_status.test.js => core/public/core_app/status/index.ts} (69%) rename src/{legacy/core_plugins/status_page/public/lib/format_number.test.js => core/public/core_app/status/lib/format_number.test.ts} (61%) rename src/{legacy/core_plugins/status_page/public/lib/format_number.js => core/public/core_app/status/lib/format_number.ts} (78%) create mode 100644 src/core/public/core_app/status/lib/index.ts create mode 100644 src/core/public/core_app/status/lib/load_status.test.ts create mode 100644 src/core/public/core_app/status/lib/load_status.ts create mode 100644 src/core/public/core_app/status/render_app.tsx rename src/{legacy/core_plugins/status_page/public/components/status_app.js => core/public/core_app/status/status_app.tsx} (67%) create mode 100644 src/core/server/core_app/core_app.test.ts rename src/{legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js => core/server/status/status_config.ts} (64%) rename src/{plugins/status_page/public/plugin.ts => core/types/status.ts} (56%) delete mode 100644 src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap delete mode 100644 src/legacy/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap delete mode 100644 src/legacy/core_plugins/status_page/public/components/status_table.js delete mode 100644 src/legacy/core_plugins/status_page/public/lib/load_status.js delete mode 100644 src/legacy/core_plugins/status_page/public/lib/load_status.test.js delete mode 100644 src/legacy/core_plugins/status_page/public/lib/prop_types.js delete mode 100644 src/legacy/server/status/routes/page/register_status.js delete mode 100644 src/plugins/status_page/kibana.json rename test/functional/apps/status_page/{index.js => index.ts} (56%) diff --git a/docs/developer/architecture/code-exploration.asciidoc b/docs/developer/architecture/code-exploration.asciidoc index 2f67ae002c916..f18a6c2f14926 100644 --- a/docs/developer/architecture/code-exploration.asciidoc +++ b/docs/developer/architecture/code-exploration.asciidoc @@ -186,11 +186,6 @@ WARNING: Missing README. Replaces the legacy ui/share module for registering share context menus. -- {kib-repo}blob/{branch}/src/plugins/status_page[statusPage] - -WARNING: Missing README. - - - {kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: diff --git a/src/core/public/core_app/core_app.ts b/src/core/public/core_app/core_app.ts index 04d58b7c3c65c..ef6ea0a0e1050 100644 --- a/src/core/public/core_app/core_app.ts +++ b/src/core/public/core_app/core_app.ts @@ -24,15 +24,19 @@ import { AppNavLinkStatus, AppMountParameters, } from '../application'; -import { HttpSetup, HttpStart } from '../http'; -import { CoreContext } from '../core_system'; -import { renderApp, setupUrlOverflowDetection } from './errors'; -import { NotificationsStart } from '../notifications'; -import { IUiSettingsClient } from '../ui_settings'; +import type { HttpSetup, HttpStart } from '../http'; +import type { CoreContext } from '../core_system'; +import type { NotificationsSetup, NotificationsStart } from '../notifications'; +import type { IUiSettingsClient } from '../ui_settings'; +import type { InjectedMetadataSetup } from '../injected_metadata'; +import { renderApp as renderErrorApp, setupUrlOverflowDetection } from './errors'; +import { renderApp as renderStatusApp } from './status'; interface SetupDeps { application: InternalApplicationSetup; http: HttpSetup; + injectedMetadata: InjectedMetadataSetup; + notifications: NotificationsSetup; } interface StartDeps { @@ -47,7 +51,7 @@ export class CoreApp { constructor(private readonly coreContext: CoreContext) {} - public setup({ http, application }: SetupDeps) { + public setup({ http, application, injectedMetadata, notifications }: SetupDeps) { application.register(this.coreContext.coreId, { id: 'error', title: 'App Error', @@ -56,7 +60,21 @@ export class CoreApp { // Do not use an async import here in order to ensure that network failures // cannot prevent the error UI from displaying. This UI is tiny so an async // import here is probably not useful anyways. - return renderApp(params, { basePath: http.basePath }); + return renderErrorApp(params, { basePath: http.basePath }); + }, + }); + + if (injectedMetadata.getAnonymousStatusPage()) { + http.anonymousPaths.register('/status'); + } + application.register(this.coreContext.coreId, { + id: 'status', + title: 'Server Status', + appRoute: '/status', + chromeless: true, + navLinkStatus: AppNavLinkStatus.hidden, + mount(params: AppMountParameters) { + return renderStatusApp(params, { http, notifications }); }, }); } diff --git a/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap new file mode 100644 index 0000000000000..2219e0d7609b8 --- /dev/null +++ b/src/core/public/core_app/status/components/__snapshots__/metric_tiles.test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MetricTile correct displays a byte metric 1`] = ` + +`; + +exports[`MetricTile correct displays a float metric 1`] = ` + +`; + +exports[`MetricTile correct displays a time metric 1`] = ` + +`; + +exports[`MetricTile correct displays an untyped metric 1`] = ` + +`; diff --git a/src/core/public/core_app/status/components/__snapshots__/server_status.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/server_status.test.tsx.snap new file mode 100644 index 0000000000000..0ed784ef680f7 --- /dev/null +++ b/src/core/public/core_app/status/components/__snapshots__/server_status.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ServerStatus renders correctly for green state 2`] = ` + + + + + + Green + + + + + +`; + +exports[`ServerStatus renders correctly for red state 2`] = ` + + + + + + Red + + + + + +`; diff --git a/src/legacy/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap similarity index 89% rename from src/legacy/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap rename to src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap index 3379d6cd649c4..f5d3b837ce718 100644 --- a/src/legacy/core_plugins/status_page/public/components/__snapshots__/status_table.test.js.snap +++ b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`render 1`] = ` +exports[`StatusTable renders when statuses is provided 1`] = ` = () => - new StatusPagePlugin(); +export { MetricTile, MetricTiles } from './metric_tiles'; +export { ServerStatus } from './server_status'; +export { StatusTable } from './status_table'; diff --git a/src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js b/src/core/public/core_app/status/components/metric_tiles.test.tsx similarity index 58% rename from src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js rename to src/core/public/core_app/status/components/metric_tiles.test.tsx index 13d0a61bbc96f..b22c5a494afe7 100644 --- a/src/legacy/core_plugins/status_page/public/components/metric_tiles.test.js +++ b/src/core/public/core_app/status/components/metric_tiles.test.tsx @@ -20,47 +20,50 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MetricTile } from './metric_tiles'; +import { Metric } from '../lib'; -const GENERAL_METRIC = { +const untypedMetric: Metric = { name: 'A metric', value: 1.8, // no type specified }; -const BYTE_METRIC = { +const byteMetric: Metric = { name: 'Heap Total', value: 1501560832, type: 'byte', }; -const FLOAT_METRIC = { +const floatMetric: Metric = { name: 'Load', type: 'float', value: [4.0537109375, 3.36669921875, 3.1220703125], }; -const MS_METRIC = { +const timeMetric: Metric = { name: 'Response Time Max', - type: 'ms', + type: 'time', value: 1234, }; -test('general metric', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); -}); +describe('MetricTile', () => { + it('correct displays an untyped metric', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); -test('byte metric', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); -}); + it('correct displays a byte metric', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); -test('float metric', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); -}); + it('correct displays a float metric', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); -test('millisecond metric', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); + it('correct displays a time metric', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); }); diff --git a/src/legacy/core_plugins/status_page/public/components/metric_tiles.js b/src/core/public/core_app/status/components/metric_tiles.tsx similarity index 50% rename from src/legacy/core_plugins/status_page/public/components/metric_tiles.js rename to src/core/public/core_app/status/components/metric_tiles.tsx index 6cde975875ad1..4b1b5fcbc633d 100644 --- a/src/legacy/core_plugins/status_page/public/components/metric_tiles.js +++ b/src/core/public/core_app/status/components/metric_tiles.tsx @@ -17,53 +17,43 @@ * 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 React, { FunctionComponent } from 'react'; import { EuiFlexGrid, EuiFlexItem, EuiCard } from '@elastic/eui'; +import { formatNumber, Metric } from '../lib'; /* -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 ; - } -} + * Displays a metric with the correct format. + */ +export const MetricTile: FunctionComponent<{ metric: Metric }> = ({ metric }) => { + const { name } = metric; + return ( + + ); +}; /* -Wrapper component that simply maps each metric to MetricTile inside a FlexGroup -*/ -const MetricTiles = ({ metrics }) => ( + * Wrapper component that simply maps each metric to MetricTile inside a FlexGroup + */ +export const MetricTiles: FunctionComponent<{ metrics: Metric[] }> = ({ metrics }) => ( {metrics.map((metric) => ( - + ))} ); -MetricTiles.propTypes = { - metrics: PropTypes.arrayOf(MetricPropType).isRequired, +const formatMetric = ({ value, type }: Metric) => { + const metrics = Array.isArray(value) ? value : [value]; + return metrics.map((metric) => formatNumber(metric, type)).join(', '); }; -export default MetricTiles; +const formatMetricId = ({ name }: Metric) => { + return name.toLowerCase().replace(/[ ]+/g, '-'); +}; diff --git a/src/core/public/core_app/status/components/server_status.test.tsx b/src/core/public/core_app/status/components/server_status.test.tsx new file mode 100644 index 0000000000000..a37697b6ab4e6 --- /dev/null +++ b/src/core/public/core_app/status/components/server_status.test.tsx @@ -0,0 +1,58 @@ +/* + * 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 { mount } from 'enzyme'; +import { ServerStatus } from './server_status'; +import { FormattedStatus } from '../lib'; + +const getStatus = (parts: Partial = {}): FormattedStatus['state'] => ({ + id: 'green', + title: 'Green', + uiColor: 'secondary', + message: '', + ...parts, +}); + +describe('ServerStatus', () => { + it('renders correctly for green state', () => { + const status = getStatus(); + const component = mount(); + expect(component.find('EuiTitle').text()).toMatchInlineSnapshot(`"Kibana status is Green"`); + expect(component.find('EuiBadge')).toMatchSnapshot(); + }); + + it('renders correctly for red state', () => { + const status = getStatus({ + id: 'red', + title: 'Red', + }); + const component = mount(); + expect(component.find('EuiTitle').text()).toMatchInlineSnapshot(`"Kibana status is Red"`); + expect(component.find('EuiBadge')).toMatchSnapshot(); + }); + + it('displays the correct `name`', () => { + let component = mount(); + expect(component.find('EuiText').text()).toMatchInlineSnapshot(`"Localhost"`); + + component = mount(); + expect(component.find('EuiText').text()).toMatchInlineSnapshot(`"Kibana"`); + }); +}); diff --git a/src/legacy/core_plugins/status_page/public/components/server_status.js b/src/core/public/core_app/status/components/server_status.tsx similarity index 66% rename from src/legacy/core_plugins/status_page/public/components/server_status.js rename to src/core/public/core_app/status/components/server_status.tsx index 0c32109b94645..5baa97cfabeda 100644 --- a/src/legacy/core_plugins/status_page/public/components/server_status.js +++ b/src/core/public/core_app/status/components/server_status.tsx @@ -17,22 +17,34 @@ * under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { State as StatePropType } from '../lib/prop_types'; +import React, { FunctionComponent } from 'react'; import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiBadge } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { FormattedStatus } from '../lib'; -const ServerState = ({ name, serverState }) => ( +interface ServerStateProps { + name: string; + serverState: FormattedStatus['state']; +} + +export const ServerStatus: FunctionComponent = ({ name, serverState }) => ( -

+

{serverState.title}, + kibanaStatus: ( + + {serverState.title} + + ), }} />

@@ -45,10 +57,3 @@ const ServerState = ({ name, serverState }) => (
); - -ServerState.propTypes = { - name: PropTypes.string.isRequired, - serverState: StatePropType.isRequired, -}; - -export default ServerState; diff --git a/src/legacy/core_plugins/status_page/public/components/status_table.test.js b/src/core/public/core_app/status/components/status_table.test.tsx similarity index 67% rename from src/legacy/core_plugins/status_page/public/components/status_table.test.js rename to src/core/public/core_app/status/components/status_table.test.tsx index 303be0d17c79d..4e25d274463ea 100644 --- a/src/legacy/core_plugins/status_page/public/components/status_table.test.js +++ b/src/core/public/core_app/status/components/status_table.test.tsx @@ -19,20 +19,23 @@ import React from 'react'; import { shallow } from 'enzyme'; -import StatusTable from './status_table'; +import { StatusTable } from './status_table'; -const STATE = { +const state = { id: 'green', uiColor: 'secondary', message: 'Ready', + title: 'green', }; -test('render', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line -}); +describe('StatusTable', () => { + it('renders when statuses is provided', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); -test('render empty', () => { - const component = shallow(); - expect(component.isEmptyRender()).toBe(true); // eslint-disable-line + it('renders when statuses is not provided', () => { + const component = shallow(); + expect(component.isEmptyRender()).toBe(true); + }); }); diff --git a/src/core/public/core_app/status/components/status_table.tsx b/src/core/public/core_app/status/components/status_table.tsx new file mode 100644 index 0000000000000..c1d66cc779ccd --- /dev/null +++ b/src/core/public/core_app/status/components/status_table.tsx @@ -0,0 +1,67 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiBasicTable, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import type { FormattedStatus } from '../lib'; + +interface StatusTableProps { + statuses?: FormattedStatus[]; +} + +const tableColumns = [ + { + field: 'state', + name: '', + render: (state: FormattedStatus['state']) => ( + + ), + width: '32px', + }, + { + field: 'id', + name: i18n.translate('core.statusPage.statusTable.columns.idHeader', { + defaultMessage: 'ID', + }), + }, + { + field: 'state', + name: i18n.translate('core.statusPage.statusTable.columns.statusHeader', { + defaultMessage: 'Status', + }), + render: (state: FormattedStatus['state']) => {state.message}, + }, +]; + +export const StatusTable: FunctionComponent = ({ statuses }) => { + if (!statuses) { + return null; + } + return ( + + columns={tableColumns} + items={statuses} + rowProps={({ state }) => ({ + className: `status-table-row-${state.uiColor}`, + })} + data-test-subj="statusBreakdown" + /> + ); +}; diff --git a/src/legacy/core_plugins/status_page/public/components/server_status.test.js b/src/core/public/core_app/status/index.ts similarity index 69% rename from src/legacy/core_plugins/status_page/public/components/server_status.test.js rename to src/core/public/core_app/status/index.ts index 79f217e18ecb5..938a037ae60e5 100644 --- a/src/legacy/core_plugins/status_page/public/components/server_status.test.js +++ b/src/core/public/core_app/status/index.ts @@ -17,17 +17,4 @@ * under the License. */ -import React from 'react'; -import { shallow } from 'enzyme'; -import ServerStatus from './server_status'; - -const STATE = { - id: 'green', - title: 'Green', - uiColor: 'secondary', -}; - -test('render', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); // eslint-disable-line -}); +export { renderApp } from './render_app'; diff --git a/src/legacy/core_plugins/status_page/public/lib/format_number.test.js b/src/core/public/core_app/status/lib/format_number.test.ts similarity index 61% rename from src/legacy/core_plugins/status_page/public/lib/format_number.test.js rename to src/core/public/core_app/status/lib/format_number.test.ts index f70377dcba241..a3b602d210b74 100644 --- a/src/legacy/core_plugins/status_page/public/lib/format_number.test.js +++ b/src/core/public/core_app/status/lib/format_number.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import formatNumber from './format_number'; +import { formatNumber } from './format_number'; describe('format byte', () => { test('zero', () => { @@ -33,17 +33,17 @@ describe('format byte', () => { }); }); -describe('format ms', () => { +describe('format time', () => { test('zero', () => { - expect(formatNumber(0, 'ms')).toMatchInlineSnapshot(`"0.00 ms"`); + expect(formatNumber(0, 'time')).toMatchInlineSnapshot(`"0.00 ms"`); }); test('sub ms', () => { - expect(formatNumber(0.128, 'ms')).toMatchInlineSnapshot(`"0.13 ms"`); + expect(formatNumber(0.128, 'time')).toMatchInlineSnapshot(`"0.13 ms"`); }); test('many ms', () => { - expect(formatNumber(3030.284, 'ms')).toMatchInlineSnapshot(`"3030.28 ms"`); + expect(formatNumber(3030.284, 'time')).toMatchInlineSnapshot(`"3030.28 ms"`); }); }); @@ -60,3 +60,31 @@ describe('format integer', () => { expect(formatNumber(3030.284, 'integer')).toMatchInlineSnapshot(`"3030"`); }); }); + +describe('format float', () => { + test('zero', () => { + expect(formatNumber(0, 'float')).toMatchInlineSnapshot(`"0.00"`); + }); + + test('sub integer', () => { + expect(formatNumber(0.728, 'float')).toMatchInlineSnapshot(`"0.73"`); + }); + + test('many integer', () => { + expect(formatNumber(3030.284, 'float')).toMatchInlineSnapshot(`"3030.28"`); + }); +}); + +describe('format default', () => { + test('zero', () => { + expect(formatNumber(0)).toMatchInlineSnapshot(`"0.00"`); + }); + + test('sub integer', () => { + expect(formatNumber(0.464)).toMatchInlineSnapshot(`"0.46"`); + }); + + test('many integer', () => { + expect(formatNumber(6237.291)).toMatchInlineSnapshot(`"6237.29"`); + }); +}); diff --git a/src/legacy/core_plugins/status_page/public/lib/format_number.js b/src/core/public/core_app/status/lib/format_number.ts similarity index 78% rename from src/legacy/core_plugins/status_page/public/lib/format_number.js rename to src/core/public/core_app/status/lib/format_number.ts index 4a8be4fc48a15..bfd5a4746b4d9 100644 --- a/src/legacy/core_plugins/status_page/public/lib/format_number.js +++ b/src/core/public/core_app/status/lib/format_number.ts @@ -19,19 +19,25 @@ import numeral from '@elastic/numeral'; -export default function formatNumber(num, which) { - let format = '0.00'; +export type DataType = 'byte' | 'float' | 'integer' | 'time'; + +export function formatNumber(num: number, type?: DataType) { + let format: string; let postfix = ''; - switch (which) { + switch (type) { case 'byte': - format += ' b'; + format = '0.00 b'; break; - case 'ms': + case 'time': + format = '0.00'; postfix = ' ms'; break; case 'integer': format = '0'; break; + case 'float': + default: + format = '0.00'; } return numeral(num).format(format) + postfix; diff --git a/src/core/public/core_app/status/lib/index.ts b/src/core/public/core_app/status/lib/index.ts new file mode 100644 index 0000000000000..eaa4e2ae4821f --- /dev/null +++ b/src/core/public/core_app/status/lib/index.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export { formatNumber, DataType } from './format_number'; +export { loadStatus, Metric, FormattedStatus, ProcessedServerResponse } from './load_status'; diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts new file mode 100644 index 0000000000000..3a444a4448467 --- /dev/null +++ b/src/core/public/core_app/status/lib/load_status.test.ts @@ -0,0 +1,152 @@ +/* + * 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 { StatusResponse } from '../../../../types/status'; +import { httpServiceMock } from '../../../http/http_service.mock'; +import { notificationServiceMock } from '../../../notifications/notifications_service.mock'; +import { loadStatus } from './load_status'; + +const mockedResponse: StatusResponse = { + name: 'My computer', + uuid: 'uuid', + version: { + number: '8.0.0', + build_hash: '9007199254740991', + build_number: '12', + build_snapshot: 'XXXXXXXX', + }, + status: { + overall: { + id: 'overall', + state: 'yellow', + title: 'Yellow', + message: 'yellow', + uiColor: 'secondary', + }, + statuses: [ + { + id: 'plugin:1', + state: 'green', + title: 'Green', + message: 'Ready', + uiColor: 'secondary', + }, + { + id: 'plugin:2', + state: 'yellow', + title: 'Yellow', + message: 'Something is weird', + uiColor: 'warning', + }, + ], + }, + metrics: { + collection_interval_in_millis: 1000, + os: { + platform: 'darwin' as const, + platformRelease: 'test', + memory: { total_in_bytes: 1, free_in_bytes: 1, used_in_bytes: 1 }, + uptime_in_millis: 1, + load: { + '1m': 4.1, + '5m': 2.1, + '15m': 0.1, + }, + }, + process: { + memory: { + heap: { + size_limit: 1000000, + used_in_bytes: 100, + total_in_bytes: 0, + }, + resident_set_size_in_bytes: 1, + }, + event_loop_delay: 1, + pid: 1, + uptime_in_millis: 1, + }, + response_times: { + avg_in_millis: 4000, + max_in_millis: 8000, + }, + requests: { + disconnects: 1, + total: 400, + statusCodes: {}, + }, + concurrent_connections: 1, + }, +}; + +describe('response processing', () => { + let http: ReturnType; + let notifications: ReturnType; + + beforeEach(() => { + http = httpServiceMock.createSetupContract(); + http.get.mockResolvedValue(mockedResponse); + notifications = notificationServiceMock.createSetupContract(); + }); + + test('includes the name', async () => { + const data = await loadStatus({ http, notifications }); + expect(data.name).toEqual('My computer'); + }); + + test('includes the plugin statuses', async () => { + const data = await loadStatus({ http, notifications }); + expect(data.statuses).toEqual([ + { + id: 'plugin:1', + state: { id: 'green', title: 'Green', message: 'Ready', uiColor: 'secondary' }, + }, + { + id: 'plugin:2', + state: { id: 'yellow', title: 'Yellow', message: 'Something is weird', uiColor: 'warning' }, + }, + ]); + }); + + test('includes the serverState', async () => { + const data = await loadStatus({ http, notifications }); + expect(data.serverState).toEqual({ + id: 'yellow', + title: 'Yellow', + message: 'yellow', + uiColor: 'secondary', + }); + }); + + test('builds the metrics', async () => { + const data = await loadStatus({ http, notifications }); + 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/public/core_app/status/lib/load_status.ts b/src/core/public/core_app/status/lib/load_status.ts new file mode 100644 index 0000000000000..95efa0bb87ae6 --- /dev/null +++ b/src/core/public/core_app/status/lib/load_status.ts @@ -0,0 +1,153 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { UnwrapPromise } from '@kbn/utility-types'; +import type { ServerStatus, StatusResponse } from '../../../../types/status'; +import type { HttpSetup } from '../../../http'; +import type { NotificationsSetup } from '../../../notifications'; +import type { DataType } from '../lib'; + +export interface Metric { + name: string; + value: number | number[]; + type?: DataType; +} + +export interface FormattedStatus { + id: string; + state: { + id: string; + title: string; + message: string; + uiColor: string; + }; +} + +/** + * Returns an object of any keys that should be included for metrics. + */ +function formatMetrics({ metrics }: StatusResponse): Metric[] { + if (!metrics) { + return []; + } + + return [ + { + name: i18n.translate('core.statusPage.metricsTiles.columns.heapTotalHeader', { + defaultMessage: 'Heap total', + }), + value: metrics.process.memory.heap.size_limit, + type: 'byte', + }, + { + name: i18n.translate('core.statusPage.metricsTiles.columns.heapUsedHeader', { + defaultMessage: 'Heap used', + }), + value: metrics.process.memory.heap.used_in_bytes, + type: 'byte', + }, + { + name: i18n.translate('core.statusPage.metricsTiles.columns.loadHeader', { + defaultMessage: 'Load', + }), + value: [metrics.os.load['1m'], metrics.os.load['5m'], metrics.os.load['15m']], + type: 'time', + }, + { + name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeAvgHeader', { + defaultMessage: 'Response time avg', + }), + value: metrics.response_times.avg_in_millis, + type: 'time', + }, + { + name: i18n.translate('core.statusPage.metricsTiles.columns.resTimeMaxHeader', { + defaultMessage: 'Response time max', + }), + value: metrics.response_times.max_in_millis, + type: 'time', + }, + { + name: i18n.translate('core.statusPage.metricsTiles.columns.requestsPerSecHeader', { + defaultMessage: 'Requests per second', + }), + value: (metrics.requests.total * 1000) / metrics.collection_interval_in_millis, + type: 'float', + }, + ]; +} + +/** + * Reformat the backend data to make the frontend views simpler. + */ +function formatStatus(status: ServerStatus): FormattedStatus { + return { + id: status.id, + state: { + id: status.state, + title: status.title, + message: status.message, + uiColor: status.uiColor, + }, + }; +} + +/** + * Get the status from the server API and format it for display. + */ +export async function loadStatus({ + http, + notifications, +}: { + http: HttpSetup; + notifications: NotificationsSetup; +}) { + let response: StatusResponse; + + try { + response = await http.get('/api/status'); + } catch (e) { + if ((e.response?.status ?? 0) >= 400) { + notifications.toasts.addDanger( + i18n.translate('core.statusPage.loadStatus.serverStatusCodeErrorMessage', { + defaultMessage: 'Failed to request server status with status code {responseStatus}', + values: { responseStatus: e.response?.status }, + }) + ); + } else { + notifications.toasts.addDanger( + i18n.translate('core.statusPage.loadStatus.serverIsDownErrorMessage', { + defaultMessage: 'Failed to request server status. Perhaps your server is down?', + }) + ); + } + throw e; + } + + return { + name: response.name, + version: response.version, + statuses: response.status.statuses.map(formatStatus), + serverState: formatStatus(response.status.overall).state, + metrics: formatMetrics(response), + }; +} + +export type ProcessedServerResponse = UnwrapPromise>; diff --git a/src/core/public/core_app/status/render_app.tsx b/src/core/public/core_app/status/render_app.tsx new file mode 100644 index 0000000000000..fdec85942b964 --- /dev/null +++ b/src/core/public/core_app/status/render_app.tsx @@ -0,0 +1,44 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import type { AppMountParameters } from '../../application'; +import type { HttpSetup } from '../../http'; +import type { NotificationsSetup } from '../../notifications'; +import { StatusApp } from './status_app'; + +interface Deps { + http: HttpSetup; + notifications: NotificationsSetup; +} + +export const renderApp = ({ element }: AppMountParameters, { http, notifications }: Deps) => { + ReactDOM.render( + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/src/legacy/core_plugins/status_page/public/components/status_app.js b/src/core/public/core_app/status/status_app.tsx similarity index 67% rename from src/legacy/core_plugins/status_page/public/components/status_app.js rename to src/core/public/core_app/status/status_app.tsx index a6b0321e53a8f..5ead0d39d72c2 100644 --- a/src/legacy/core_plugins/status_page/public/components/status_app.js +++ b/src/core/public/core_app/status/status_app.tsx @@ -17,10 +17,7 @@ * under the License. */ -import loadStatus from '../lib/load_status'; - import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiLoadingSpinner, EuiText, @@ -33,19 +30,25 @@ import { EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { HttpSetup } from '../../http'; +import { NotificationsSetup } from '../../notifications'; +import { loadStatus, ProcessedServerResponse } from './lib'; +import { MetricTiles, StatusTable, ServerStatus } from './components'; + +interface StatusAppProps { + http: HttpSetup; + notifications: NotificationsSetup; +} -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, - }; +interface StatusAppState { + loading: boolean; + fetchError: boolean; + data: ProcessedServerResponse | null; +} - constructor() { - super(); +export class StatusApp extends Component { + constructor(props: StatusAppProps) { + super(props); this.state = { loading: true, fetchError: false, @@ -53,18 +56,17 @@ class StatusApp extends Component { }; } - componentDidMount = async function () { - const data = await loadStatus(); - - if (data) { - this.setState({ loading: false, data: data }); - } else { - this.setState({ fetchError: true, loading: false }); + async componentDidMount() { + const { http, notifications } = this.props; + try { + const data = await loadStatus({ http, notifications }); + this.setState({ loading: false, fetchError: false, data }); + } catch (e) { + this.setState({ fetchError: true, loading: false, data: null }); } - }; + } render() { - const { buildNum, buildSha } = this.props; const { loading, fetchError, data } = this.state; // If we're still loading, return early with a spinner @@ -76,7 +78,7 @@ class StatusApp extends Component { return ( @@ -84,10 +86,11 @@ class StatusApp extends Component { } // Extract the items needed to render each component - const { metrics, statuses, serverState, name } = data; + const { metrics, statuses, serverState, name, version } = data!; + const { build_hash: buildHash, build_number: buildNumber } = version; return ( - + @@ -103,7 +106,7 @@ class StatusApp extends Component {

@@ -113,12 +116,12 @@ class StatusApp extends Component { -

+

{buildNum}, + buildNum: {buildNumber}, }} />

@@ -126,12 +129,12 @@ class StatusApp extends Component {
-

+

{buildSha}, + buildSha: {buildHash}, }} />

@@ -150,5 +153,3 @@ class StatusApp extends Component { ); } } - -export default StatusApp; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 00fabc2b6f2f1..e08841b0271d9 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -42,8 +42,8 @@ import { RenderingService } from './rendering'; import { SavedObjectsService } from './saved_objects'; import { ContextService } from './context'; import { IntegrationsService } from './integrations'; -import { InternalApplicationSetup, InternalApplicationStart } from './application/types'; import { CoreApp } from './core_app'; +import type { InternalApplicationSetup, InternalApplicationStart } from './application/types'; interface Params { rootDomElement: HTMLElement; @@ -180,7 +180,7 @@ export class CoreSystem { ]), }); const application = this.application.setup({ context, http, injectedMetadata }); - this.coreApp.setup({ application, http }); + this.coreApp.setup({ application, http, injectedMetadata, notifications }); const core: InternalCoreSetup = { application, diff --git a/src/core/public/injected_metadata/injected_metadata_service.mock.ts b/src/core/public/injected_metadata/injected_metadata_service.mock.ts index 5caa9830a643d..e6b1c440519bd 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.mock.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.mock.ts @@ -26,6 +26,7 @@ const createSetupContractMock = () => { getKibanaBranch: jest.fn(), getCspConfig: jest.fn(), getLegacyMode: jest.fn(), + getAnonymousStatusPage: jest.fn(), getLegacyMetadata: jest.fn(), getPlugins: jest.fn(), getInjectedVar: jest.fn(), @@ -35,6 +36,7 @@ const createSetupContractMock = () => { setupContract.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true }); setupContract.getKibanaVersion.mockReturnValue('kibanaVersion'); setupContract.getLegacyMode.mockReturnValue(true); + setupContract.getAnonymousStatusPage.mockReturnValue(false); setupContract.getLegacyMetadata.mockReturnValue({ app: { id: 'foo', diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 75abdd6d87d5a..db4bfdf415bcc 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -68,6 +68,7 @@ export interface InjectedMetadataParams { }; uiPlugins: InjectedPluginMetadata[]; legacyMode: boolean; + anonymousStatusPage: boolean; legacyMetadata: { app: { id: string; @@ -120,6 +121,10 @@ export class InjectedMetadataService { return this.state.serverBasePath; }, + getAnonymousStatusPage: () => { + return this.state.anonymousStatusPage; + }, + getKibanaVersion: () => { return this.state.version; }, @@ -179,6 +184,7 @@ export interface InjectedMetadataSetup { getPlugins: () => InjectedPluginMetadata[]; /** Indicates whether or not we are rendering a known legacy app. */ getLegacyMode: () => boolean; + getAnonymousStatusPage: () => boolean; getLegacyMetadata: () => { app: { id: string; diff --git a/src/core/server/core_app/core_app.test.ts b/src/core/server/core_app/core_app.test.ts new file mode 100644 index 0000000000000..841088c45833b --- /dev/null +++ b/src/core/server/core_app/core_app.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { mockCoreContext } from '../core_context.mock'; +import { coreMock } from '../mocks'; +import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; +import { CoreApp } from './core_app'; + +describe('CoreApp', () => { + let coreApp: CoreApp; + let internalCoreSetup: ReturnType; + let httpResourcesRegistrar: ReturnType; + + beforeEach(() => { + const coreContext = mockCoreContext.create(); + internalCoreSetup = coreMock.createInternalSetup(); + httpResourcesRegistrar = httpResourcesMock.createRegistrar(); + internalCoreSetup.httpResources.createRegistrar.mockReturnValue(httpResourcesRegistrar); + coreApp = new CoreApp(coreContext); + }); + + describe('`/status` route', () => { + it('is registered with `authRequired: false` is the status page is anonymous', () => { + internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(true); + coreApp.setup(internalCoreSetup); + + expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( + { + path: '/status', + validate: false, + options: { + authRequired: false, + }, + }, + expect.any(Function) + ); + }); + + it('is registered with `authRequired: true` is the status page is not anonymous', () => { + internalCoreSetup.status.isStatusPageAnonymous.mockReturnValue(false); + coreApp.setup(internalCoreSetup); + + expect(httpResourcesRegistrar.register).toHaveBeenCalledWith( + { + path: '/status', + validate: false, + options: { + authRequired: true, + }, + }, + expect.any(Function) + ); + }); + }); +}); diff --git a/src/core/server/core_app/core_app.ts b/src/core/server/core_app/core_app.ts index 5e1a3794632ee..508e69ea11170 100644 --- a/src/core/server/core_app/core_app.ts +++ b/src/core/server/core_app/core_app.ts @@ -52,6 +52,24 @@ export class CoreApp { router.get({ path: '/core', validate: false }, async (context, req, res) => res.ok({ body: { version: '0.0.1' } }) ); + + const anonymousStatusPage = coreSetup.status.isStatusPageAnonymous(); + coreSetup.httpResources.createRegistrar(router).register( + { + path: '/status', + validate: false, + options: { + authRequired: !anonymousStatusPage, + }, + }, + async (context, request, response) => { + if (anonymousStatusPage) { + return response.renderAnonymousCoreApp(); + } else { + return response.renderCoreApp(); + } + } + ); } private registerStaticDirs(coreSetup: InternalCoreSetup) { coreSetup.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets')); diff --git a/src/core/server/http_resources/http_resources_service.mock.ts b/src/core/server/http_resources/http_resources_service.mock.ts index 4536b0898cad9..9d7db3a5b7273 100644 --- a/src/core/server/http_resources/http_resources_service.mock.ts +++ b/src/core/server/http_resources/http_resources_service.mock.ts @@ -25,7 +25,7 @@ const createHttpResourcesMock = (): jest.Mocked => ({ function createInternalHttpResourcesSetup() { return { - createRegistrar: createHttpResourcesMock, + createRegistrar: jest.fn(() => createHttpResourcesMock()), }; } diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index a3dbb279d19eb..84e4b4741b717 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -31,7 +31,6 @@ import { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_object import { renderingMock } from './rendering/rendering_service.mock'; import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; -import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; import { metricsServiceMock } from './metrics/metrics_service.mock'; import { uuidServiceMock } from './uuid/uuid_service.mock'; @@ -157,7 +156,7 @@ function createCoreStartMock() { } function createInternalCoreSetupMock() { - const setupDeps: InternalCoreSetup = { + const setupDeps = { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createInternalSetup(), @@ -175,7 +174,7 @@ function createInternalCoreSetupMock() { } function createInternalCoreStartMock() { - const startDeps: InternalCoreStart = { + const startDeps = { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createInternalStart(), http: httpServiceMock.createInternalStartContract(), diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts index ce2eea119d1bb..0901cec768cd2 100644 --- a/src/core/server/rendering/__mocks__/params.ts +++ b/src/core/server/rendering/__mocks__/params.ts @@ -21,15 +21,18 @@ import { mockCoreContext } from '../../core_context.mock'; import { httpServiceMock } from '../../http/http_service.mock'; import { pluginServiceMock } from '../../plugins/plugins_service.mock'; import { legacyServiceMock } from '../../legacy/legacy_service.mock'; +import { statusServiceMock } from '../../status/status_service.mock'; const context = mockCoreContext.create(); const http = httpServiceMock.createInternalSetupContract(); const uiPlugins = pluginServiceMock.createUiPlugins(); const legacyPlugins = legacyServiceMock.createDiscoverPlugins(); +const status = statusServiceMock.createInternalSetupContract(); export const mockRenderingServiceParams = context; export const mockRenderingSetupDeps = { http, legacyPlugins, uiPlugins, + status, }; diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index 3b11313367d9c..95230b52c5c03 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -2,6 +2,7 @@ exports[`RenderingService setup() render() renders "core" from legacy request 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -74,6 +75,7 @@ Object { exports[`RenderingService setup() render() renders "core" page 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -146,6 +148,7 @@ Object { exports[`RenderingService setup() render() renders "core" page driven by settings 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -222,6 +225,7 @@ Object { exports[`RenderingService setup() render() renders "core" page for blank basepath 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "", "branch": Any, "buildNumber": Any, @@ -294,6 +298,7 @@ Object { exports[`RenderingService setup() render() renders "core" with excluded user settings 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -366,6 +371,7 @@ Object { exports[`RenderingService setup() render() renders "legacy" page 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -438,6 +444,7 @@ Object { exports[`RenderingService setup() render() renders "legacy" page for blank basepath 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "", "branch": Any, "buildNumber": Any, @@ -510,6 +517,7 @@ Object { exports[`RenderingService setup() render() renders "legacy" with custom vars 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -584,6 +592,7 @@ Object { exports[`RenderingService setup() render() renders "legacy" with excluded user settings 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, @@ -656,6 +665,7 @@ Object { exports[`RenderingService setup() render() renders "legacy" with excluded user settings and custom vars 1`] = ` Object { + "anonymousStatusPage": false, "basePath": "/mock-server-basepath", "branch": Any, "buildNumber": Any, diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index a02d85d22b2cb..8f87d62496891 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -42,6 +42,7 @@ export class RenderingService implements CoreService { @@ -79,6 +80,7 @@ export class RenderingService implements CoreService]> = [ - [pathConfig.path, pathConfig.schema], - [cspConfig.path, cspConfig.schema], - [elasticsearchConfig.path, elasticsearchConfig.schema], - [loggingConfig.path, loggingConfig.schema], - [httpConfig.path, httpConfig.schema], - [pluginsConfig.path, pluginsConfig.schema], - [devConfig.path, devConfig.schema], - [kibanaConfig.path, kibanaConfig.schema], - [savedObjectsConfig.path, savedObjectsConfig.schema], - [savedObjectsMigrationConfig.path, savedObjectsMigrationConfig.schema], - [uiSettingsConfig.path, uiSettingsConfig.schema], - [opsConfig.path, opsConfig.schema], + const configDescriptors: Array> = [ + pathConfig, + cspConfig, + elasticsearchConfig, + loggingConfig, + httpConfig, + pluginsConfig, + devConfig, + kibanaConfig, + savedObjectsConfig, + savedObjectsMigrationConfig, + uiSettingsConfig, + opsConfig, + statusConfig, ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); - this.configService.addDeprecationProvider( - elasticsearchConfig.path, - elasticsearchConfig.deprecations! - ); - this.configService.addDeprecationProvider( - uiSettingsConfig.path, - uiSettingsConfig.deprecations! - ); - - for (const [path, schema] of schemas) { - await this.configService.setSchema(path, schema); + for (const descriptor of configDescriptors) { + if (descriptor.deprecations) { + this.configService.addDeprecationProvider(descriptor.path, descriptor.deprecations); + } + await this.configService.setSchema(descriptor.path, descriptor.schema); } } } diff --git a/src/core/server/status/index.ts b/src/core/server/status/index.ts index c39115d55a682..79d62390b3d47 100644 --- a/src/core/server/status/index.ts +++ b/src/core/server/status/index.ts @@ -18,4 +18,5 @@ */ export { StatusService } from './status_service'; +export { config } from './status_config'; export * from './types'; diff --git a/src/legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js b/src/core/server/status/status_config.ts similarity index 64% rename from src/legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js rename to src/core/server/status/status_config.ts index ec633a429b2e0..34e61dc2bb1fb 100644 --- a/src/legacy/core_plugins/status_page/public/lib/load_status.test.mocks.js +++ b/src/core/server/status/status_config.ts @@ -17,22 +17,16 @@ * under the License. */ -import { - fatalErrorsServiceMock, - notificationServiceMock, - overlayServiceMock, -} from '../../../../../core/public/mocks'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { ServiceConfigDescriptor } from '../internal_types'; -jest.doMock('ui/new_platform', () => ({ - npSetup: { - core: { - fatalErrors: fatalErrorsServiceMock.createSetupContract(), - notifications: notificationServiceMock.createSetupContract(), - }, - }, - npStart: { - core: { - overlays: overlayServiceMock.createStartContract(), - }, - }, -})); +const statusConfigSchema = schema.object({ + allowAnonymous: schema.boolean({ defaultValue: false }), +}); + +export type StatusConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { + path: 'status', + schema: statusConfigSchema, +}; diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index d550c2f06750b..c6eb11be6967c 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -48,6 +48,7 @@ const createInternalSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), + isStatusPageAnonymous: jest.fn().mockReturnValue(false), }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index b692cf3161901..863fe34e8ecea 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -28,6 +28,12 @@ import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); describe('StatusService', () => { + let service: StatusService; + + beforeEach(() => { + service = new StatusService(mockCoreContext.create()); + }); + const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available', @@ -40,7 +46,7 @@ describe('StatusService', () => { describe('setup', () => { describe('core$', () => { it('rolls up core status observables into single observable', async () => { - const setup = new StatusService(mockCoreContext.create()).setup({ + const setup = await service.setup({ elasticsearch: { status$: of(available), }, @@ -55,7 +61,7 @@ describe('StatusService', () => { }); it('replays last event', async () => { - const setup = new StatusService(mockCoreContext.create()).setup({ + const setup = await service.setup({ elasticsearch: { status$: of(available), }, @@ -80,10 +86,10 @@ describe('StatusService', () => { }); }); - it('does not emit duplicate events', () => { + it('does not emit duplicate events', async () => { const elasticsearch$ = new BehaviorSubject(available); const savedObjects$ = new BehaviorSubject(degraded); - const setup = new StatusService(mockCoreContext.create()).setup({ + const setup = await service.setup({ elasticsearch: { status$: elasticsearch$, }, @@ -145,7 +151,7 @@ describe('StatusService', () => { describe('overall$', () => { it('exposes an overall summary', async () => { - const setup = new StatusService(mockCoreContext.create()).setup({ + const setup = await service.setup({ elasticsearch: { status$: of(degraded), }, @@ -160,7 +166,7 @@ describe('StatusService', () => { }); it('replays last event', async () => { - const setup = new StatusService(mockCoreContext.create()).setup({ + const setup = await service.setup({ elasticsearch: { status$: of(degraded), }, @@ -185,10 +191,10 @@ describe('StatusService', () => { }); }); - it('does not emit duplicate events', () => { + it('does not emit duplicate events', async () => { const elasticsearch$ = new BehaviorSubject(available); const savedObjects$ = new BehaviorSubject(degraded); - const setup = new StatusService(mockCoreContext.create()).setup({ + const setup = await service.setup({ elasticsearch: { status$: elasticsearch$, }, diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index ef7bed9587245..569b044a4fb27 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -20,7 +20,7 @@ /* eslint-disable max-classes-per-file */ import { Observable, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { CoreService } from '../../types'; @@ -29,6 +29,7 @@ import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; +import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; @@ -39,12 +40,15 @@ interface SetupDeps { export class StatusService implements CoreService { private readonly logger: Logger; + private readonly config$: Observable; constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); + this.config$ = coreContext.configService.atPath(config.path); } - public setup(core: SetupDeps) { + public async setup(core: SetupDeps) { + const statusConfig = await this.config$.pipe(take(1)).toPromise(); const core$ = this.setupCoreStatus(core); const overall$: Observable = core$.pipe( map((coreStatus) => { @@ -58,6 +62,7 @@ export class StatusService implements CoreService { return { core$, overall$, + isStatusPageAnonymous: () => statusConfig.allowAnonymous, }; } diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index 84a7356c66bbf..b04c25a1eee93 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -131,4 +131,5 @@ export interface InternalStatusServiceSetup extends StatusServiceSetup { * Overall system status used for HTTP API */ overall$: Observable; + isStatusPageAnonymous: () => boolean; } diff --git a/src/plugins/status_page/public/plugin.ts b/src/core/types/status.ts similarity index 56% rename from src/plugins/status_page/public/plugin.ts rename to src/core/types/status.ts index d072fd4a67c30..20b012e960a6a 100644 --- a/src/plugins/status_page/public/plugin.ts +++ b/src/core/types/status.ts @@ -17,23 +17,36 @@ * under the License. */ -import { Plugin, CoreSetup } from 'kibana/public'; +import type { OpsMetrics } from '../server/metrics'; -export class StatusPagePlugin implements Plugin { - public setup(core: CoreSetup) { - const isStatusPageAnonymous = core.injectedMetadata.getInjectedVar( - 'isStatusPageAnonymous' - ) as boolean; - - if (isStatusPageAnonymous) { - core.http.anonymousPaths.register('/status'); - } - } +export interface ServerStatus { + id: string; + title: string; + state: string; + message: string; + uiColor: string; + icon?: string; + since?: string; +} - public start() {} +export type ServerMetrics = OpsMetrics & { + collection_interval_in_millis: number; +}; - public stop() {} +export interface ServerVersion { + number: string; + build_hash: string; + build_number: string; + build_snapshot: string; } -export type StatusPagePluginSetup = ReturnType; -export type StatusPagePluginStart = ReturnType; +export interface StatusResponse { + name: string; + uuid: string; + version: ServerVersion; + status: { + overall: ServerStatus; + statuses: ServerStatus[]; + }; + metrics: ServerMetrics; +} diff --git a/src/legacy/core_plugins/status_page/index.js b/src/legacy/core_plugins/status_page/index.js index 01991d8439a04..5a94eb9c77160 100644 --- a/src/legacy/core_plugins/status_page/index.js +++ b/src/legacy/core_plugins/status_page/index.js @@ -21,15 +21,10 @@ export default function (kibana) { return new kibana.Plugin({ uiExports: { app: { - title: 'Server Status', + title: 'Legacy Server Status', main: 'plugins/status_page/status_page', hidden: true, - url: '/status', - }, - injectDefaultVars(server) { - return { - isStatusPageAnonymous: server.config().get('status.allowAnonymous'), - }; + url: '/__legacy__/status', }, }, }); diff --git a/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap b/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap deleted file mode 100644 index 7d4b245021c4c..0000000000000 --- a/src/legacy/core_plugins/status_page/public/components/__snapshots__/metric_tiles.test.js.snap +++ /dev/null @@ -1,33 +0,0 @@ -// 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/legacy/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap b/src/legacy/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap deleted file mode 100644 index 6ff046557afa3..0000000000000 --- a/src/legacy/core_plugins/status_page/public/components/__snapshots__/server_status.test.js.snap +++ /dev/null @@ -1,44 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render 1`] = ` - - - -

- - Green - , - } - } - /> -

-
-
- - -

- My Computer -

-
-
-
-`; diff --git a/src/legacy/core_plugins/status_page/public/components/render.js b/src/legacy/core_plugins/status_page/public/components/render.js index b9462bf21797c..dca79d783a29a 100644 --- a/src/legacy/core_plugins/status_page/public/components/render.js +++ b/src/legacy/core_plugins/status_page/public/components/render.js @@ -20,24 +20,19 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nContext } from 'ui/i18n'; - -import StatusApp from './status_app'; +// just to import eui into legacy +import '@elastic/eui'; const STATUS_PAGE_DOM_NODE_ID = 'createStatusPageReact'; -export function renderStatusPage(buildNum, buildSha) { +export function renderStatusPage() { const node = document.getElementById(STATUS_PAGE_DOM_NODE_ID); if (!node) { return; } - render( - - - , - node - ); + render(Foo, node); } export function destroyStatusPage() { diff --git a/src/legacy/core_plugins/status_page/public/components/status_table.js b/src/legacy/core_plugins/status_page/public/components/status_table.js deleted file mode 100644 index 68b93153951cb..0000000000000 --- a/src/legacy/core_plugins/status_page/public/components/status_table.js +++ /dev/null @@ -1,82 +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 React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { State as StatePropType } from '../lib/prop_types'; -import { EuiBasicTable, EuiIcon } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; - -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: i18n.translate('statusPage.statusTable.columns.idHeader', { - defaultMessage: 'ID', - }), - }, - { - field: 'state', - name: i18n.translate('statusPage.statusTable.columns.statusHeader', { - defaultMessage: '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/legacy/core_plugins/status_page/public/lib/load_status.js b/src/legacy/core_plugins/status_page/public/lib/load_status.js deleted file mode 100644 index d033e5f147d9d..0000000000000 --- a/src/legacy/core_plugins/status_page/public/lib/load_status.js +++ /dev/null @@ -1,163 +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'; - -import chrome from 'ui/chrome'; -import { toastNotifications } from 'ui/notify'; -import { i18n } from '@kbn/i18n'; - -// 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: i18n.translate('statusPage.metricsTiles.columns.heapTotalHeader', { - defaultMessage: 'Heap total', - }), - value: _.get(data.metrics, 'process.memory.heap.size_limit'), - type: 'byte', - }, - { - name: i18n.translate('statusPage.metricsTiles.columns.heapUsedHeader', { - defaultMessage: 'Heap used', - }), - value: _.get(data.metrics, 'process.memory.heap.used_in_bytes'), - type: 'byte', - }, - { - name: i18n.translate('statusPage.metricsTiles.columns.loadHeader', { - defaultMessage: 'Load', - }), - value: [ - _.get(data.metrics, 'os.load.1m'), - _.get(data.metrics, 'os.load.5m'), - _.get(data.metrics, 'os.load.15m'), - ], - type: 'float', - }, - { - name: i18n.translate('statusPage.metricsTiles.columns.resTimeAvgHeader', { - defaultMessage: 'Response time avg', - }), - value: _.get(data.metrics, 'response_times.avg_in_millis'), - type: 'ms', - }, - { - name: i18n.translate('statusPage.metricsTiles.columns.resTimeMaxHeader', { - defaultMessage: 'Response time max', - }), - value: _.get(data.metrics, 'response_times.max_in_millis'), - type: 'ms', - }, - { - name: i18n.translate('statusPage.metricsTiles.columns.requestsPerSecHeader', { - defaultMessage: 'Requests per second', - }), - value: - (_.get(data.metrics, 'requests.total') * 1000) / - _.get(data.metrics, 'collection_interval_in_millis'), - }, - ]; -} - -/** - * Reformat the backend data to make the frontend views simpler. - */ -function formatStatus(status) { - return { - id: status.id, - state: { - id: status.state, - title: status.title, - message: status.message, - uiColor: status.uiColor, - }, - }; -} - -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. - const serverIsDownErrorMessage = i18n.translate( - 'statusPage.loadStatus.serverIsDownErrorMessage', - { - defaultMessage: 'Failed to request server status. Perhaps your server is down?', - } - ); - - errorNotif = toastNotifications.addDanger(serverIsDownErrorMessage); - return e; - } - - if (response.status >= 400) { - // If the server does not respond with a successful status, display an error and bail. - const serverStatusCodeErrorMessage = i18n.translate( - 'statusPage.loadStatus.serverStatusCodeErrorMessage', - { - defaultMessage: 'Failed to request server status with status code {responseStatus}', - values: { responseStatus: response.status }, - } - ); - - errorNotif = toastNotifications.addDanger(serverStatusCodeErrorMessage); - return; - } - - const data = await response.json(); - - return { - name: data.name, - statuses: data.status.statuses.map(formatStatus), - serverState: formatStatus(data.status.overall).state, - metrics: formatMetrics(data), - }; -} - -export default loadStatus; diff --git a/src/legacy/core_plugins/status_page/public/lib/load_status.test.js b/src/legacy/core_plugins/status_page/public/lib/load_status.test.js deleted file mode 100644 index a0f1930ca7667..0000000000000 --- a/src/legacy/core_plugins/status_page/public/lib/load_status.test.js +++ /dev/null @@ -1,115 +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 './load_status.test.mocks'; -import loadStatus from './load_status'; - -// A faked response to the `fetch` call -const mockFetch = async () => ({ - status: 200, - json: async () => ({ - name: 'My computer', - status: { - overall: { - state: 'yellow', - title: 'Yellow', - }, - statuses: [ - { id: 'plugin:1', state: 'green', title: 'Green', message: 'Ready', uiColor: 'secondary' }, - { - id: 'plugin:2', - state: 'yellow', - title: 'Yellow', - message: 'Something is weird', - uiColor: 'warning', - }, - ], - }, - 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', title: 'Green', message: 'Ready', uiColor: 'secondary' }, - }, - { - id: 'plugin:2', - state: { id: 'yellow', title: 'Yellow', message: 'Something is weird', uiColor: 'warning' }, - }, - ]); - }); - - 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/legacy/core_plugins/status_page/public/lib/prop_types.js b/src/legacy/core_plugins/status_page/public/lib/prop_types.js deleted file mode 100644 index d1f665f97475b..0000000000000 --- a/src/legacy/core_plugins/status_page/public/lib/prop_types.js +++ /dev/null @@ -1,33 +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 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/legacy/server/status/index.js b/src/legacy/server/status/index.js index 377a5d74610a9..ab7ec471a67ff 100644 --- a/src/legacy/server/status/index.js +++ b/src/legacy/server/status/index.js @@ -19,7 +19,7 @@ import ServerStatus from './server_status'; import { Metrics } from './lib/metrics'; -import { registerStatusPage, registerStatusApi, registerStatsApi } from './routes'; +import { registerStatusApi, registerStatsApi } from './routes'; import Oppsy from 'oppsy'; import { cloneDeep } from 'lodash'; import { getOSInfo } from './lib/get_os_info'; @@ -53,7 +53,6 @@ export function statusMixin(kbnServer, server, config) { }); // init routes - registerStatusPage(kbnServer, server, config); registerStatusApi(kbnServer, server, config); registerStatsApi(usageCollection, server, config, kbnServer); diff --git a/src/legacy/server/status/routes/index.js b/src/legacy/server/status/routes/index.js index 720309c3fd749..12736a76d4915 100644 --- a/src/legacy/server/status/routes/index.js +++ b/src/legacy/server/status/routes/index.js @@ -17,6 +17,5 @@ * under the License. */ -export { registerStatusPage } from './page/register_status'; export { registerStatusApi } from './api/register_status'; export { registerStatsApi } from './api/register_stats'; diff --git a/src/legacy/server/status/routes/page/register_status.js b/src/legacy/server/status/routes/page/register_status.js deleted file mode 100644 index 47bd3c34eba59..0000000000000 --- a/src/legacy/server/status/routes/page/register_status.js +++ /dev/null @@ -1,53 +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 { wrapAuthConfig } from '../../wrap_auth_config'; - -export function registerStatusPage(kbnServer, server, config) { - const allowAnonymous = config.get('status.allowAnonymous'); - const wrapAuth = wrapAuthConfig(allowAnonymous); - - server.decorate('toolkit', 'renderStatusPage', async function () { - const app = server.getHiddenUiAppById('status_page'); - const h = this; - - let response; - // An unauthenticated (anonymous) user may not have access to the customized configuration. - // For this scenario, render with the default config. - if (app) { - response = allowAnonymous ? await h.renderAppWithDefaultConfig(app) : await h.renderApp(app); - } else { - h.response(kbnServer.status.toString()); - } - - if (response) { - return response.code(kbnServer.status.isGreen() ? 200 : 503); - } - }); - - server.route( - wrapAuth({ - method: 'GET', - path: '/status', - handler(request, h) { - return h.renderStatusPage(); - }, - }) - ); -} diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 7788aeaee72e5..12ae6390fdc22 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -204,13 +204,8 @@ export function uiRenderMixin(kbnServer, server, config) { async handler(req, h) { const id = req.params.id; const app = server.getUiAppById(id); - try { - if (kbnServer.status.isGreen()) { - return await h.renderApp(app); - } else { - return await h.renderStatusPage(); - } + return await h.renderApp(app); } catch (err) { throw Boom.boomify(err); } diff --git a/src/plugins/status_page/kibana.json b/src/plugins/status_page/kibana.json deleted file mode 100644 index 0d54f6a39e2b1..0000000000000 --- a/src/plugins/status_page/kibana.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "statusPage", - "version": "kibana", - "server": false, - "ui": true -} diff --git a/test/functional/apps/status_page/index.js b/test/functional/apps/status_page/index.ts similarity index 56% rename from test/functional/apps/status_page/index.js rename to test/functional/apps/status_page/index.ts index 34f2df287dd6b..65349aba93b9b 100644 --- a/test/functional/apps/status_page/index.js +++ b/test/functional/apps/status_page/index.ts @@ -18,8 +18,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common']); @@ -31,11 +32,32 @@ export default function ({ getService, getPageObjects }) { await PageObjects.common.navigateToApp('status_page'); }); - it('should show the kibana plugin as ready', async function () { + it('should show the kibana plugin as ready', async () => { await retry.tryForTime(6000, async () => { const text = await testSubjects.getVisibleText('statusBreakdown'); expect(text.indexOf('plugin:kibana')).to.be.above(-1); }); }); + + it('should show the build hash and number', async () => { + const buildNumberText = await testSubjects.getVisibleText('statusBuildNumber'); + expect(buildNumberText).to.contain('BUILD '); + + const hashText = await testSubjects.getVisibleText('statusBuildHash'); + expect(hashText).to.contain('COMMIT '); + }); + + it('should display the server metrics', async () => { + const metrics = await testSubjects.findAll('serverMetric'); + expect(metrics).to.have.length(6); + }); + + it('should display the server status', async () => { + const titleText = await testSubjects.getVisibleText('serverStatusTitle'); + expect(titleText).to.contain('Kibana status is'); + + const serverStatus = await testSubjects.getAttribute('serverStatusTitleBadge', 'aria-label'); + expect(serverStatus).to.be('Green'); + }); }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d07d92a028d8d..846330146cf07 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -360,6 +360,21 @@ "core.fatalErrors.tryRefreshingPageDescription": "ページを更新してみてください。うまくいかない場合は、前のページに戻るか、セッションデータを消去してください。", "core.notifications.errorToast.closeModal": "閉じる", "core.notifications.unableUpdateUISettingNotificationMessageTitle": "UI 設定を更新できません", + "core.statusPage.loadStatus.serverIsDownErrorMessage": "サーバーステータスのリクエストに失敗しました。サーバーがダウンしている可能性があります。", + "core.statusPage.loadStatus.serverStatusCodeErrorMessage": "サーバーステータスのリクエストに失敗しました。ステータスコード: {responseStatus}", + "core.statusPage.metricsTiles.columns.heapTotalHeader": "ヒープ合計", + "core.statusPage.metricsTiles.columns.heapUsedHeader": "使用ヒープ", + "core.statusPage.metricsTiles.columns.loadHeader": "読み込み", + "core.statusPage.metricsTiles.columns.requestsPerSecHeader": "1 秒あたりのリクエスト", + "core.statusPage.metricsTiles.columns.resTimeAvgHeader": "平均応答時間", + "core.statusPage.metricsTiles.columns.resTimeMaxHeader": "最長応答時間", + "core.statusPage.serverStatus.statusTitle": "Kibana のステータス: {kibanaStatus}", + "core.statusPage.statusApp.loadingErrorText": "ステータスの読み込み中にエラーが発生しました", + "core.statusPage.statusApp.statusActions.buildText": "{buildNum} を作成", + "core.statusPage.statusApp.statusActions.commitText": "{buildSha} を確定", + "core.statusPage.statusApp.statusTitle": "プラグインステータス", + "core.statusPage.statusTable.columns.idHeader": "ID", + "core.statusPage.statusTable.columns.statusHeader": "ステータス", "core.toasts.errorToast.seeFullError": "完全なエラーを表示", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動", "core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle": "Elasticに確認する", @@ -2470,21 +2485,6 @@ "server.status.redTitle": "赤", "server.status.uninitializedTitle": "アンインストールしました", "server.status.yellowTitle": "黄色", - "statusPage.loadStatus.serverIsDownErrorMessage": "サーバーステータスのリクエストに失敗しました。サーバーがダウンしている可能性があります。", - "statusPage.loadStatus.serverStatusCodeErrorMessage": "サーバーステータスのリクエストに失敗しました。ステータスコード: {responseStatus}", - "statusPage.metricsTiles.columns.heapTotalHeader": "ヒープ合計", - "statusPage.metricsTiles.columns.heapUsedHeader": "使用ヒープ", - "statusPage.metricsTiles.columns.loadHeader": "読み込み", - "statusPage.metricsTiles.columns.requestsPerSecHeader": "1 秒あたりのリクエスト", - "statusPage.metricsTiles.columns.resTimeAvgHeader": "平均応答時間", - "statusPage.metricsTiles.columns.resTimeMaxHeader": "最長応答時間", - "statusPage.serverStatus.statusTitle": "Kibana のステータス: {kibanaStatus}", - "statusPage.statusApp.loadingErrorText": "ステータスの読み込み中にエラーが発生しました", - "statusPage.statusApp.statusActions.buildText": "{buildNum} を作成", - "statusPage.statusApp.statusActions.commitText": "{buildSha} を確定", - "statusPage.statusApp.statusTitle": "プラグインステータス", - "statusPage.statusTable.columns.idHeader": "ID", - "statusPage.statusTable.columns.statusHeader": "ステータス", "telemetry.callout.appliesSettingTitle": "この設定に加えた変更は {allOfKibanaText} に適用され、自動的に保存されます。", "telemetry.callout.appliesSettingTitle.allOfKibanaText": "Kibana のすべて", "telemetry.callout.clusterStatisticsDescription": "これは収集される基本的なクラスター統計の例です。インデックス、シャード、ノードの数が含まれます。監視がオンになっているかどうかなどのハイレベルの使用統計も含まれます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8f844de735dc6..477858d2e74d1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -360,6 +360,21 @@ "core.fatalErrors.tryRefreshingPageDescription": "请尝试刷新页面。如果无效,请返回上一页或清除您的会话数据。", "core.notifications.errorToast.closeModal": "关闭", "core.notifications.unableUpdateUISettingNotificationMessageTitle": "无法更新 UI 设置", + "core.statusPage.loadStatus.serverIsDownErrorMessage": "无法请求服务器状态。也许您的服务器已关闭?", + "core.statusPage.loadStatus.serverStatusCodeErrorMessage": "无法使用状态代码 {responseStatus} 请求服务器状态", + "core.statusPage.metricsTiles.columns.heapTotalHeader": "堆总计", + "core.statusPage.metricsTiles.columns.heapUsedHeader": "已使用堆", + "core.statusPage.metricsTiles.columns.loadHeader": "负载", + "core.statusPage.metricsTiles.columns.requestsPerSecHeader": "每秒请求数", + "core.statusPage.metricsTiles.columns.resTimeAvgHeader": "响应时间平均值", + "core.statusPage.metricsTiles.columns.resTimeMaxHeader": "响应时间最大值", + "core.statusPage.serverStatus.statusTitle": "Kibana 状态为“{kibanaStatus}”", + "core.statusPage.statusApp.loadingErrorText": "加载状态时出错", + "core.statusPage.statusApp.statusActions.buildText": "BUILD {buildNum}", + "core.statusPage.statusApp.statusActions.commitText": "COMMIT {buildSha}", + "core.statusPage.statusApp.statusTitle": "插件状态", + "core.statusPage.statusTable.columns.idHeader": "ID", + "core.statusPage.statusTable.columns.statusHeader": "状态", "core.toasts.errorToast.seeFullError": "请参阅完整的错误信息", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页", "core.ui.chrome.headerGlobalNav.helpMenuAskElasticTitle": "问询 Elastic", @@ -2473,21 +2488,6 @@ "server.status.redTitle": "红", "server.status.uninitializedTitle": "未初始化", "server.status.yellowTitle": "黄", - "statusPage.loadStatus.serverIsDownErrorMessage": "无法请求服务器状态。也许您的服务器已关闭?", - "statusPage.loadStatus.serverStatusCodeErrorMessage": "无法使用状态代码 {responseStatus} 请求服务器状态", - "statusPage.metricsTiles.columns.heapTotalHeader": "堆总计", - "statusPage.metricsTiles.columns.heapUsedHeader": "已使用堆", - "statusPage.metricsTiles.columns.loadHeader": "负载", - "statusPage.metricsTiles.columns.requestsPerSecHeader": "每秒请求数", - "statusPage.metricsTiles.columns.resTimeAvgHeader": "响应时间平均值", - "statusPage.metricsTiles.columns.resTimeMaxHeader": "响应时间最大值", - "statusPage.serverStatus.statusTitle": "Kibana 状态为“{kibanaStatus}”", - "statusPage.statusApp.loadingErrorText": "加载状态时出错", - "statusPage.statusApp.statusActions.buildText": "BUILD {buildNum}", - "statusPage.statusApp.statusActions.commitText": "COMMIT {buildSha}", - "statusPage.statusApp.statusTitle": "插件状态", - "statusPage.statusTable.columns.idHeader": "ID", - "statusPage.statusTable.columns.statusHeader": "状态", "telemetry.callout.appliesSettingTitle": "对此设置的更改将应用到{allOfKibanaText} 且会自动保存。", "telemetry.callout.appliesSettingTitle.allOfKibanaText": "整个 Kibana", "telemetry.callout.clusterStatisticsDescription": "这是我们将收集的基本集群统计信息的示例。其包括索引、分片和节点的数目。还包括概括性的使用情况统计信息,例如监测是否打开。", diff --git a/x-pack/test/functional/page_objects/status_page.js b/x-pack/test/functional/page_objects/status_page.js index 68fc931a9140f..eba5e7dd18496 100644 --- a/x-pack/test/functional/page_objects/status_page.js +++ b/x-pack/test/functional/page_objects/status_page.js @@ -29,7 +29,7 @@ export function StatusPagePageProvider({ getService, getPageObjects }) { async expectStatusPage() { return await retry.try(async () => { log.debug(`expectStatusPage()`); - await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); + await find.byCssSelector('[data-test-subj="statusPageRoot"]', 20000); const url = await browser.getCurrentUrl(); expect(url).to.contain(`/status`); });