From 8a4d73dd9be43facddbc7b891cdc566c556c2808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Wed, 15 Sep 2021 11:58:01 +0200 Subject: [PATCH] [Stack Monitoring] Elasticsearch Overview view migration (#111941) * Elasticsearch overview first version * Fix timezone in react components * Extraxt on brush behaviour from elasticsearch overview page * Fix overview component type * Add elasticsearch pages template with some tabs * Conditionally add some tabs for elasticsearch pages * fix import and types * Filter angular errors in react * remove disabled property from tabs * Add comment --- .../public/application/hooks/use_charts.tsx | 81 ++++++++++++++++ .../application/hooks/use_monitoring_time.ts | 14 ++- .../monitoring/public/application/index.tsx | 10 ++ .../pages/cluster/overview_page.tsx | 4 +- .../elasticsearch/elasticsearch_template.tsx | 69 +++++++++++++ .../pages/elasticsearch/overview.tsx | 97 +++++++++++++++++++ .../application/pages/page_template.tsx | 51 ++++++++-- .../public/application/route_init.tsx | 12 ++- .../components/chart/get_chart_options.js | 13 ++- .../components/elasticsearch/index.d.ts | 8 ++ .../monitoring/public/components/logs/logs.js | 15 ++- 11 files changed, 355 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/application/hooks/use_charts.tsx create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx create mode 100644 x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_charts.tsx b/x-pack/plugins/monitoring/public/application/hooks/use_charts.tsx new file mode 100644 index 0000000000000..25d7f139e5bbe --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_charts.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import moment from 'moment'; +import { useContext, useState, useEffect, useRef } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { MonitoringTimeContainer } from '../hooks/use_monitoring_time'; + +export function useCharts() { + const { services } = useKibana<{ data: any }>(); + const history = useHistory(); + const { handleTimeChange } = useContext(MonitoringTimeContainer.Context); + + const [zoomInLevel, setZoomInLevel] = useState(0); + + // We need something to know when the onBrush event was fired because the pop state event + // is also fired when the onBrush event is fired (although only on the first onBrush event) and + // causing the zoomInLevel to change. + // In Angular, this was handled by removing the listener before updating the state and adding + // it again after some milliseconds, but the same trick didn't work in React. + const [onBrushHappened, _setOnBrushHappened] = useState(false); + + const onBrushHappenedRef = useRef(onBrushHappened); + + const setOnBrushHappened = (data: boolean) => { + onBrushHappenedRef.current = data; + _setOnBrushHappened(data); + }; + + useEffect(() => { + const popstateHandler = () => { + if (onBrushHappenedRef.current) { + setOnBrushHappened(false); + } else { + setZoomInLevel((currentZoomInLevel) => { + if (currentZoomInLevel > 0) { + return currentZoomInLevel - 1; + } + return 0; + }); + } + }; + + window.addEventListener('popstate', popstateHandler); + return () => window.removeEventListener('popstate', popstateHandler); + }, []); + + const onBrush = ({ xaxis }: any) => { + const { to, from } = xaxis; + const timezone = services.uiSettings?.get('dateFormat:tz'); + const offset = getOffsetInMS(timezone); + const fromTime = moment(from - offset); + const toTime = moment(to - offset); + handleTimeChange(fromTime.toISOString(), toTime.toISOString()); + setOnBrushHappened(true); + setZoomInLevel(zoomInLevel + 1); + }; + + const zoomInfo = { + zoomOutHandler: () => history.goBack(), + showZoomOutBtn: () => zoomInLevel > 0, + }; + + return { + onBrush, + zoomInfo, + }; +} + +const getOffsetInMS = (timezone: string) => { + if (timezone === 'Browser') { + return 0; + } + const offsetInMinutes = moment.tz(timezone).utcOffset(); + const offsetInMS = offsetInMinutes * 1 * 60 * 1000; + return offsetInMS; +}; diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts b/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts index 8a343a5c61cd6..e512f90d76e69 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts @@ -4,9 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useCallback, useState } from 'react'; +import { useCallback, useState, useContext } from 'react'; import createContainer from 'constate'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { Legacy } from '../../legacy_shims'; +import { GlobalStateContext } from '../../application/global_state_context'; interface TimeOptions { from: string; @@ -25,6 +27,8 @@ const DEFAULT_REFRESH_INTERVAL_PAUSE = false; export const useMonitoringTime = () => { const { services } = useKibana<{ data: any }>(); + const state = useContext(GlobalStateContext); + const defaultTimeRange = { ...DEFAULT_TIMERANGE, ...services.data?.query.timefilter.timefilter.getTime(), @@ -39,8 +43,14 @@ export const useMonitoringTime = () => { const handleTimeChange = useCallback( (start: string, end: string) => { setTimeRange({ ...currentTimerange, from: start, to: end }); + state.time = { + from: start, + to: end, + }; + Legacy.shims.timefilter.setTime(state.time); + state.save?.(); }, - [currentTimerange, setTimeRange] + [currentTimerange, setTimeRange, state] ); return { diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index 8d6c718d77ebb..6db9343035237 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -18,6 +18,8 @@ import { GlobalStateProvider } from './global_state_context'; import { ExternalConfigContext, ExternalConfig } from './external_config_context'; import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; +import { ElasticsearchOverviewPage } from './pages/elasticsearch/overview'; +import { CODE_PATH_ELASTICSEARCH } from '../../common/constants'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; @@ -72,6 +74,14 @@ const MonitoringApp: React.FC<{ codePaths={['all']} fetchAllClusters={false} /> + + {/* ElasticSearch Views */} + = () => { { id: 'clusterName', label: clusters[0].cluster_name, - disabled: false, - description: clusters[0].cluster_name, - onClick: () => {}, testSubj: 'clusterName', + route: '/overview', }, ]; } diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx new file mode 100644 index 0000000000000..13e21912df896 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { includes } from 'lodash'; +import { PageTemplate } from '../page_template'; +import { TabMenuItem, PageTemplateProps } from '../page_template'; +import { ML_SUPPORTED_LICENSES } from '../../../../common/constants'; + +interface ElasticsearchTemplateProps extends PageTemplateProps { + cluster: any; +} + +export const ElasticsearchTemplate: React.FC = ({ + cluster, + ...props +}) => { + const tabs: TabMenuItem[] = [ + { + id: 'overview', + label: i18n.translate('xpack.monitoring.esNavigation.overviewLinkText', { + defaultMessage: 'Overview', + }), + route: '/elasticsearch', + }, + { + id: 'nodes', + label: i18n.translate('xpack.monitoring.esNavigation.nodesLinkText', { + defaultMessage: 'Nodes', + }), + route: '/elasticsearch/nodes', + }, + { + id: 'indices', + label: i18n.translate('xpack.monitoring.esNavigation.indicesLinkText', { + defaultMessage: 'Indices', + }), + route: '/elasticsearch/indices', + }, + ]; + + if (mlIsSupported(cluster.license)) { + tabs.push({ + id: 'ml', + label: i18n.translate('xpack.monitoring.esNavigation.jobsLinkText', { + defaultMessage: 'Machine learning jobs', + }), + route: '/elasticsearch/ml_jobs', + }); + } + + if (cluster.isCcrEnabled) { + tabs.push({ + id: 'ccr', + label: i18n.translate('xpack.monitoring.esNavigation.ccrLinkText', { + defaultMessage: 'CCR', + }), + route: '/elasticsearch/ccr', + }); + } + + return ; +}; + +const mlIsSupported = (license: any) => includes(ML_SUPPORTED_LICENSES, license.type); diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx new file mode 100644 index 0000000000000..4e885229b436a --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext, useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; +import { ElasticsearchTemplate } from './elasticsearch_template'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { GlobalStateContext } from '../../global_state_context'; +import { ElasticsearchOverview } from '../../../components/elasticsearch'; +import { ComponentProps } from '../../route_init'; +import { useCharts } from '../../hooks/use_charts'; + +export const ElasticsearchOverviewPage: React.FC = ({ clusters }) => { + const globalState = useContext(GlobalStateContext); + const { zoomInfo, onBrush } = useCharts(); + const { services } = useKibana<{ data: any }>(); + const clusterUuid = globalState.cluster_uuid; + const ccs = globalState.ccs; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }); + const [data, setData] = useState(null); + const [showShardActivityHistory, setShowShardActivityHistory] = useState(false); + const toggleShardActivityHistory = () => { + setShowShardActivityHistory(!showShardActivityHistory); + }; + const filterShardActivityData = (shardActivity: any) => { + return shardActivity.filter((row: any) => { + return showShardActivityHistory || row.stage !== 'DONE'; + }); + }; + + const title = i18n.translate('xpack.monitoring.elasticsearch.overview.title', { + defaultMessage: 'Elasticsearch', + }); + + const pageTitle = i18n.translate('xpack.monitoring.elasticsearch.overview.pageTitle', { + defaultMessage: 'Elasticsearch overview', + }); + + const getPageData = useCallback(async () => { + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch`; + + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); + + const renderOverview = (overviewData: any) => { + if (overviewData === null) { + return null; + } + const { clusterStatus, metrics, shardActivity, logs } = overviewData || {}; + const shardActivityData = shardActivity && filterShardActivityData(shardActivity); // no filter on data = null + + return ( + + ); + }; + + return ( + +
{renderOverview(data)}
+
+ ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index 9ce717b37051b..7c6a6c56a1322 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -7,24 +7,26 @@ import { EuiTab, EuiTabs } from '@elastic/eui'; import React, { useContext, useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { useTitle } from '../hooks/use_title'; import { MonitoringToolbar } from '../../components/shared/toolbar'; import { MonitoringTimeContainer } from '../hooks/use_monitoring_time'; import { PageLoading } from '../../components'; +import { getSetupModeState, isSetupModeFeatureEnabled } from '../setup_mode/setup_mode'; +import { SetupModeFeature } from '../../../common/enums'; export interface TabMenuItem { id: string; label: string; - description: string; - disabled: boolean; - onClick: () => void; - testSubj: string; + testSubj?: string; + route: string; } -interface PageTemplateProps { +export interface PageTemplateProps { title: string; pageTitle?: string; tabs?: TabMenuItem[]; getPageData?: () => Promise; + product?: string; } export const PageTemplate: React.FC = ({ @@ -32,12 +34,14 @@ export const PageTemplate: React.FC = ({ pageTitle, tabs, getPageData, + product, children, }) => { useTitle('', title); const { currentTimerange } = useContext(MonitoringTimeContainer.Context); const [loaded, setLoaded] = useState(false); + const history = useHistory(); useEffect(() => { getPageData?.() @@ -55,6 +59,10 @@ export const PageTemplate: React.FC = ({ }); }; + const createHref = (route: string) => history.createHref({ pathname: route }); + + const isTabSelected = (route: string) => history.location.pathname === route; + return (
@@ -64,10 +72,11 @@ export const PageTemplate: React.FC = ({ return ( {item.label} @@ -79,3 +88,31 @@ export const PageTemplate: React.FC = ({
); }; + +function isDisabledTab(product: string | undefined) { + const setupMode = getSetupModeState(); + if (!isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { + return false; + } + + if (!setupMode.data) { + return false; + } + + if (!product) { + return false; + } + + const data = setupMode.data[product] || {}; + if (data.totalUniqueInstanceCount === 0) { + return true; + } + if ( + data.totalUniqueInternallyCollectedCount === 0 && + data.totalUniqueFullyMigratedCount === 0 && + data.totalUniquePartiallyMigratedCount === 0 + ) { + return true; + } + return false; +} diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index cf3b0c6646d0f..8a9a906dbd563 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -10,9 +10,12 @@ import { useClusters } from './hooks/use_clusters'; import { GlobalStateContext } from './global_state_context'; import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; +export interface ComponentProps { + clusters: []; +} interface RouteInitProps { path: string; - component: React.ComponentType; + component: React.ComponentType; codePaths: string[]; fetchAllClusters: boolean; unsetGlobalState?: boolean; @@ -58,7 +61,12 @@ export const RouteInit: React.FC = ({ } } - return loaded ? : null; + const Component = component; + return loaded ? ( + + + + ) : null; }; const isExpired = (license: any): boolean => { diff --git a/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js index 5cf983829b5e0..641125dd3e943 100644 --- a/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js +++ b/x-pack/plugins/monitoring/public/components/chart/get_chart_options.js @@ -10,8 +10,17 @@ import { merge } from 'lodash'; import { CHART_LINE_COLOR, CHART_TEXT_COLOR } from '../../../common/constants'; export async function getChartOptions(axisOptions) { - const $injector = Legacy.shims.getAngularInjector(); - const timezone = $injector.get('config').get('dateFormat:tz'); + let timezone; + try { + const $injector = Legacy.shims.getAngularInjector(); + timezone = $injector.get('config').get('dateFormat:tz'); + } catch (error) { + if (error.message === 'Angular has been removed.') { + timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); + } else { + throw error; + } + } const opts = { legend: { show: false, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts new file mode 100644 index 0000000000000..4460b8432134b --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ElasticsearchOverview: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/logs/logs.js b/x-pack/plugins/monitoring/public/components/logs/logs.js index 409b773a24856..3021240a157d3 100644 --- a/x-pack/plugins/monitoring/public/components/logs/logs.js +++ b/x-pack/plugins/monitoring/public/components/logs/logs.js @@ -16,9 +16,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Reason } from './reason'; const getFormattedDateTimeLocal = (timestamp) => { - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); - return formatDateTimeLocal(timestamp, timezone); + try { + const injector = Legacy.shims.getAngularInjector(); + const timezone = injector.get('config').get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, timezone); + } catch (error) { + if (error.message === 'Angular has been removed.') { + const timezone = Legacy.shims.uiSettings?.get('dateFormat:tz'); + return formatDateTimeLocal(timestamp, timezone); + } else { + throw error; + } + } }; const columnTimestampTitle = i18n.translate('xpack.monitoring.logs.listing.timestampTitle', {