diff --git a/x-pack/plugins/monitoring/common/constants.js b/x-pack/plugins/monitoring/common/constants.js index 852afe74c461e..c391f243eaff6 100644 --- a/x-pack/plugins/monitoring/common/constants.js +++ b/x-pack/plugins/monitoring/common/constants.js @@ -51,6 +51,8 @@ export const NORMALIZED_DERIVATIVE_UNIT = '1s'; * Values for column sorting in table options * @type {number} 1 or -1 */ +export const EUI_SORT_ASCENDING = 'asc'; +export const EUI_SORT_DESCENDING = 'desc'; export const SORT_ASCENDING = 1; export const SORT_DESCENDING = -1; diff --git a/x-pack/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js new file mode 100644 index 0000000000000..089ca454d584e --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/alerts/alerts.js @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { capitalize } from 'lodash'; +import { formatDateTimeLocal } from '../../../common/formatting'; +import { formatTimestampToDuration } from '../../../common'; +import { CALCULATE_DURATION_SINCE, EUI_SORT_DESCENDING } from '../../../common/constants'; +import { mapSeverity } from './map_severity'; +import { Tooltip } from 'plugins/monitoring/components/tooltip'; +import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; +import { EuiMonitoringTable } from 'plugins/monitoring/components/table'; +import { EuiHealth, EuiIcon } from '@elastic/eui'; + +const linkToCategories = { + 'elasticsearch/nodes': 'Elasticsearch Nodes', + 'elasticsearch/indices': 'Elasticsearch Indices', + 'kibana/instances': 'Kibana Instances', + 'logstash/instances': 'Logstash Nodes', +}; +const getColumns = (kbnUrl, scope) => ([ + { + name: 'Status', + field: 'metadata.severity', + sortable: true, + render: severity => { + const severityIcon = mapSeverity(severity); + + return ( + + + { capitalize(severityIcon.value) } + + + ); + } + }, + { + name: 'Resolved', + field: 'resolved_timestamp', + sortable: true, + render: (resolvedTimestamp) => { + const resolution = { + icon: null, + text: 'Not Resolved' + }; + + if (resolvedTimestamp) { + resolution.text = `${formatTimestampToDuration(resolvedTimestamp, CALCULATE_DURATION_SINCE)} ago`; + } else { + resolution.icon = ; + } + + return ( + + { resolution.icon } { resolution.text } + + ); + }, + }, + { + name: 'Message', + field: 'message', + sortable: true, + render: (message, alert) => ( + { + scope.$evalAsync(() => { + kbnUrl.changePath(target); + }); + }} + /> + ) + }, + { + name: 'Category', + field: 'metadata.link', + sortable: true, + render: link => linkToCategories[link] ? linkToCategories[link] : 'General' + }, + { + name: 'Last Checked', + field: 'update_timestamp', + sortable: true, + render: timestamp => formatDateTimeLocal(timestamp) + }, + { + name: 'Triggered', + field: 'timestamp', + sortable: true, + render: timestamp => formatTimestampToDuration(timestamp, CALCULATE_DURATION_SINCE) + ' ago' + }, +]); + +export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/alerts/index.js b/x-pack/plugins/monitoring/public/components/alerts/index.js new file mode 100644 index 0000000000000..c4eda37c2b252 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/alerts/index.js @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Alerts } from './alerts'; diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.js new file mode 100644 index 0000000000000..2cc7154a85c9a --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/table/eui_table.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiInMemoryTable +} from '@elastic/eui'; + +export class EuiMonitoringTable extends React.PureComponent { + render() { + const { + rows: items, + search = {}, + columns: _columns, + ...props + } = this.props; + + if (search.box && !search.box['data-test-subj']) { + search.box['data-test-subj'] = 'monitoringTableToolBar'; + } + + const columns = _columns.map(column => { + if (!column['data-test-subj']) { + column['data-test-subj'] = 'monitoringTableHasData'; + } + return column; + }); + + return ( +
+ +
+ ); + } +} diff --git a/x-pack/plugins/monitoring/public/components/table/index.js b/x-pack/plugins/monitoring/public/components/table/index.js index 38b972c1b0245..85900144cd57f 100644 --- a/x-pack/plugins/monitoring/public/components/table/index.js +++ b/x-pack/plugins/monitoring/public/components/table/index.js @@ -5,4 +5,5 @@ */ export { MonitoringTable } from './table'; -export { tableStorageGetter, tableStorageSetter } from './storage'; +export { EuiMonitoringTable } from './eui_table'; +export { tableStorageGetter, tableStorageSetter, euiTableStorageGetter, euiTableStorageSetter } from './storage'; diff --git a/x-pack/plugins/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.js index 576dddbee8098..9ddb611331cd0 100644 --- a/x-pack/plugins/monitoring/public/components/table/storage.js +++ b/x-pack/plugins/monitoring/public/components/table/storage.js @@ -33,3 +33,26 @@ export const tableStorageSetter = keyPrefix => { return localStorageData; }; }; + +export const euiTableStorageGetter = keyPrefix => { + return storage => { + const localStorageData = storage.get(STORAGE_KEY) || {}; + const sort = get(localStorageData, [ keyPrefix, 'sort' ]); + const page = get(localStorageData, [ keyPrefix, 'page' ]); + + return { page, sort }; + }; +}; + +export const euiTableStorageSetter = keyPrefix => { + return (storage, { sort, page }) => { + const localStorageData = storage.get(STORAGE_KEY) || {}; + + set(localStorageData, [ keyPrefix, 'sort' ], sort || undefined); // don`t store empty data + set(localStorageData, [ keyPrefix, 'page' ], page || undefined); + + storage.set(STORAGE_KEY, localStorageData); + + return localStorageData; + }; +}; diff --git a/x-pack/plugins/monitoring/public/directives/alerts/index.js b/x-pack/plugins/monitoring/public/directives/alerts/index.js deleted file mode 100644 index d125b0f1c074b..0000000000000 --- a/x-pack/plugins/monitoring/public/directives/alerts/index.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { capitalize } from 'lodash'; -import React from 'react'; -import { render } from 'react-dom'; -import { EuiIcon, EuiHealth } from '@elastic/eui'; -import { uiModules } from 'ui/modules'; -import { KuiTableRowCell, KuiTableRow } from '@kbn/ui-framework/components'; -import { MonitoringTable } from 'plugins/monitoring/components/table'; -import { CALCULATE_DURATION_SINCE, SORT_DESCENDING } from '../../../common/constants'; -import { Tooltip } from 'plugins/monitoring/components/tooltip'; -import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; -import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; -import { formatTimestampToDuration } from '../../../common/format_timestamp_to_duration'; -import { formatDateTimeLocal } from '../../../common/formatting'; -import { i18n } from '@kbn/i18n'; -import { injectI18n, I18nProvider, FormattedMessage } from '@kbn/i18n/react'; - -const linkToCategories = { - 'elasticsearch/nodes': i18n.translate('xpack.monitoring.alerts.esNodesCategoryLabel', { - defaultMessage: 'Elasticsearch Nodes', - }), - 'elasticsearch/indices': i18n.translate('xpack.monitoring.alerts.esIndicesCategoryLabel', { - defaultMessage: 'Elasticsearch Indices', - }), - 'kibana/instances': i18n.translate('xpack.monitoring.alerts.kibanaInstancesCategoryLabel', { - defaultMessage: 'Kibana Instances', - }), - 'logstash/instances': i18n.translate('xpack.monitoring.alerts.logstashNodesCategoryLabel', { - defaultMessage: 'Logstash Nodes', - }), -}; -const filterFields = [ 'message', 'severity_group', 'prefix', 'suffix', 'metadata.link', 'since', 'timestamp', 'update_timestamp' ]; -const columns = [ - { - title: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', { - defaultMessage: 'Status', - }), - sortKey: 'metadata.severity', - sortOrder: SORT_DESCENDING - }, - { - title: i18n.translate('xpack.monitoring.alerts.resolvedColumnTitle', { - defaultMessage: 'Resolved', - }), - sortKey: 'resolved_timestamp' - }, - { - title: i18n.translate('xpack.monitoring.alerts.messageColumnTitle', { - defaultMessage: 'Message', - }), - sortKey: 'message' - }, - { - title: i18n.translate('xpack.monitoring.alerts.categoryColumnTitle', { - defaultMessage: 'Category', - }), - sortKey: 'metadata.link' - }, - { - title: i18n.translate('xpack.monitoring.alerts.lastCheckedColumnTitle', { - defaultMessage: 'Last Checked', - }), - sortKey: 'update_timestamp' - }, - { - title: i18n.translate('xpack.monitoring.alerts.triggeredColumnTitle', { - defaultMessage: 'Triggered', - }), - sortKey: 'timestamp' - }, -]; -const alertRowFactory = (scope, kbnUrl) => { - return injectI18n(props => { - const changeUrl = target => { - scope.$evalAsync(() => { - kbnUrl.changePath(target); - }); - }; - const severityIcon = mapSeverity(props.metadata.severity); - const resolution = { - icon: null, - text: props.intl.formatMessage({ id: 'xpack.monitoring.alerts.notResolvedDescription', - defaultMessage: 'Not Resolved', - }) - }; - - if (props.resolved_timestamp) { - resolution.text = props.intl.formatMessage({ id: 'xpack.monitoring.alerts.resolvedAgoDescription', - defaultMessage: '{duration} ago', - }, { duration: formatTimestampToDuration(props.resolved_timestamp, CALCULATE_DURATION_SINCE) } - ); - } else { - resolution.icon = ( - - ); - } - - return ( - - - - - { capitalize(severityIcon.value) } - - - - - { resolution.icon } { resolution.text } - - - - - - { linkToCategories[props.metadata.link] ? linkToCategories[props.metadata.link] : - props.intl.formatMessage({ id: 'xpack.monitoring.alerts.generalCategoryLabel', defaultMessage: 'General', }) } - - - { formatDateTimeLocal(props.update_timestamp) } - - - - - - ); - }); -}; - -const uiModule = uiModules.get('monitoring/directives', []); -uiModule.directive('monitoringClusterAlertsListing', (kbnUrl, i18n) => { - return { - restrict: 'E', - scope: { alerts: '=' }, - link(scope, $el) { - const filterAlertsPlaceholder = i18n('xpack.monitoring.alerts.filterAlertsPlaceholder', { defaultMessage: 'Filter Alerts…' }); - - scope.$watch('alerts', (alerts = []) => { - const alertsTable = ( - - - - ); - render(alertsTable, $el[0]); - }); - - } - }; -}); diff --git a/x-pack/plugins/monitoring/public/directives/all.js b/x-pack/plugins/monitoring/public/directives/all.js index 98b3a10237ba2..ad101f43a903d 100644 --- a/x-pack/plugins/monitoring/public/directives/all.js +++ b/x-pack/plugins/monitoring/public/directives/all.js @@ -7,7 +7,6 @@ import './main'; import './chart'; import './sparkline'; -import './alerts'; import './cluster/overview'; import './cluster/listing'; import './elasticsearch/cluster_status'; diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.html b/x-pack/plugins/monitoring/public/views/alerts/index.html index 5ef9cc2b03473..ae92f9b6f8552 100644 --- a/x-pack/plugins/monitoring/public/views/alerts/index.html +++ b/x-pack/plugins/monitoring/public/views/alerts/index.html @@ -1,5 +1,6 @@ -
+
+ diff --git a/x-pack/plugins/monitoring/public/views/alerts/index.js b/x-pack/plugins/monitoring/public/views/alerts/index.js index 7b85d19bc8be0..ab8b57bff0723 100644 --- a/x-pack/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/plugins/monitoring/public/views/alerts/index.js @@ -4,13 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { render } from 'react-dom'; import { find, get } from 'lodash'; import uiRoutes from 'ui/routes'; import template from './index.html'; -import { MonitoringViewBaseController } from 'plugins/monitoring/views'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; import { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler'; import { timefilter } from 'ui/timefilter'; +import { Alerts } from '../../components/alerts'; +import { MonitoringViewBaseEuiTableController } from '../base_eui_table_controller'; +import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; +import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLink } from '@elastic/eui'; function getPageData($injector) { const globalState = $injector.get('globalState'); @@ -44,10 +49,11 @@ uiRoutes.when('/alerts', { alerts: getPageData }, controllerAs: 'alerts', - controller: class AlertsView extends MonitoringViewBaseController { + controller: class AlertsView extends MonitoringViewBaseEuiTableController { constructor($injector, $scope, i18n) { const $route = $injector.get('$route'); const globalState = $injector.get('globalState'); + const kbnUrl = $injector.get('kbnUrl'); // breadcrumbs + page title $scope.cluster = find($route.current.locals.clusters, { cluster_uuid: globalState.cluster_uuid }); @@ -60,6 +66,41 @@ uiRoutes.when('/alerts', { }); this.data = $route.current.locals.alerts; + + const renderReact = data => { + const app = data.message + ? (

{data.message}

) + : (); + + render( + + + + + {app} + + + + + + + + , + document.getElementById('monitoringAlertsApp') + ); + }; + $scope.$watch(() => this.data, data => renderReact(data)); } } }); diff --git a/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js new file mode 100644 index 0000000000000..c4f2ada58dff9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/views/base_eui_table_controller.js @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MonitoringViewBaseController } from './'; +import { euiTableStorageGetter, euiTableStorageSetter } from 'plugins/monitoring/components/table'; +import { SORT_ASCENDING } from '../../common/constants'; + +/** + * Class to manage common instantiation behaviors in a view controller + * And add persistent state to a table: + * - page index: in table pagination, which page are we looking at + * - filter text: what filter was entered in the table's filter bar + * - sortKey: which column field of table data is used for sorting + * - sortOrder: is sorting ordered ascending or descending + * + * This is expected to be extended, and behavior enabled using super(); + */ +export class MonitoringViewBaseEuiTableController extends MonitoringViewBaseController { + + /** + * Create a table view controller + * - used by parent class: + * @param {String} title - Title of the page + * @param {Function} getPageData - Function to fetch page data + * @param {Service} $injector - Angular dependency injection service + * @param {Service} $scope - Angular view data binding service + * @param {Boolean} options.enableTimeFilter - Whether to show the time filter + * @param {Boolean} options.enableAutoRefresh - Whether to show the auto refresh control + * - specific to this class: + * @param {String} storageKey - the namespace that will be used to keep the state data in the Monitoring localStorage object + * + */ + constructor(args) { + super(args); + const { storageKey, $injector } = args; + const storage = $injector.get('localStorage'); + + const getLocalStorageData = euiTableStorageGetter(storageKey); + const setLocalStorageData = euiTableStorageSetter(storageKey); + const { page, sort } = getLocalStorageData(storage); + + this.pagination = page || { + initialPageSize: 20, + pageSizeOptions: [5, 10, 20, 50] + }; + + this.sorting = sort || { + sort: { + field: 'name', + direction: SORT_ASCENDING + } + }; + + this.onTableChange = ({ page, sort }) => { + setLocalStorageData(storage, { + page, + sort: { + sort + } + }); + }; + } +} diff --git a/x-pack/plugins/monitoring/public/views/base_table_controller.js b/x-pack/plugins/monitoring/public/views/base_table_controller.js index 175a06aa92722..ad0a9678a36a6 100644 --- a/x-pack/plugins/monitoring/public/views/base_table_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_table_controller.js @@ -50,5 +50,4 @@ export class MonitoringViewBaseTableController extends MonitoringViewBaseControl setLocalStorageData(storage, newState); }; } - } diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js index 554aaaea31969..54164b06ce09b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/ccr/shard/index.js @@ -50,7 +50,9 @@ uiRoutes.when('/elasticsearch/ccr/:index/shard/:shardId', { this.renderReact = (props) => { super.renderReact( - + + + ); }; } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js index fae1a9431135d..0bd527523ad7d 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_logstash_for_clusters.js @@ -182,6 +182,7 @@ export function getLogstashForClusters(req, lsIndexPattern, clusters) { const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); return callWithRequest(req, 'search', params) .then(result => { + const aggregations = get(result, 'aggregations', {}); const logstashUuids = get(aggregations, 'logstash_uuids.buckets', []); const logstashVersions = get(aggregations, 'logstash_versions.buckets', []); diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 12f148d255ebe..1c8f98119d54f 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -35,6 +35,12 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { return table.findAllByTagName('tr'); } + async tableGetRowsFromContainer(subj) { + const table = await testSubjects.find(subj); + const tbody = await table.findByTagName('tbody'); + return tbody.findAllByTagName('tr'); + } + async tableSetFilter(subj, text) { return await testSubjects.setValue(subj, text); } diff --git a/x-pack/test/functional/services/monitoring/cluster_alerts.js b/x-pack/test/functional/services/monitoring/cluster_alerts.js index a47af62bfbdcd..6b51380600f04 100644 --- a/x-pack/test/functional/services/monitoring/cluster_alerts.js +++ b/x-pack/test/functional/services/monitoring/cluster_alerts.js @@ -23,8 +23,7 @@ export function MonitoringClusterAlertsProvider({ getService, getPageObjects }) const SUBJ_OVERVIEW_ACTIONS = `${SUBJ_OVERVIEW_CLUSTER_ALERTS} alertAction`; const SUBJ_OVERVIEW_VIEW_ALL = `${SUBJ_OVERVIEW_CLUSTER_ALERTS} viewAllAlerts`; - const SUBJ_LISTING_PAGE = 'clusterAlertsListingPage'; - const SUBJ_TABLE_BODY = 'alertsTableBody'; + const SUBJ_TABLE_BODY = 'alertsTableContainer'; const SUBJ_TABLE_ICONS = `${SUBJ_TABLE_BODY} alertIcon`; const SUBJ_TABLE_TEXTS = `${SUBJ_TABLE_BODY} alertText`; const SUBJ_TABLE_ACTIONS = `${SUBJ_TABLE_BODY} alertAction`; @@ -91,12 +90,12 @@ export function MonitoringClusterAlertsProvider({ getService, getPageObjects }) */ async isOnListingPage() { - const pageId = await retry.try(() => testSubjects.find(SUBJ_LISTING_PAGE)); + const pageId = await retry.try(() => testSubjects.find(SUBJ_TABLE_BODY)); return pageId !== null; } getTableAlerts() { - return PageObjects.monitoring.tableGetRows(SUBJ_TABLE_BODY); + return PageObjects.monitoring.tableGetRowsFromContainer(SUBJ_TABLE_BODY); } async getTableAlertsAll() {