From 85d6537279414fb49ba4256547e16873c884e2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= <ester.martivilaseca@elastic.co> Date: Mon, 23 Aug 2021 14:29:10 +0200 Subject: [PATCH] [Stack Monitoring] Add initial react app (#109218) * Add feature toggle * Add basic react app with router and simple loading page * Add title hook * Add loading page with page template and clusters hook * fix types * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/hooks/use_clusters.ts | 65 +++++++++++++++++++ .../public/application/hooks/use_title.ts | 24 +++++++ .../monitoring/public/application/index.tsx | 61 +++++++++++++++++ .../public/application/pages/loading_page.tsx | 41 ++++++++++++ .../application/pages/page_template.tsx | 20 ++++++ .../monitoring/public/components/index.d.ts | 8 +++ x-pack/plugins/monitoring/public/plugin.ts | 40 +++++++----- .../plugins/monitoring/server/config.test.ts | 1 + x-pack/plugins/monitoring/server/config.ts | 1 + 9 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts create mode 100644 x-pack/plugins/monitoring/public/application/hooks/use_title.ts create mode 100644 x-pack/plugins/monitoring/public/application/index.tsx create mode 100644 x-pack/plugins/monitoring/public/application/pages/loading_page.tsx create mode 100644 x-pack/plugins/monitoring/public/application/pages/page_template.tsx create mode 100644 x-pack/plugins/monitoring/public/components/index.d.ts diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts new file mode 100644 index 0000000000000..49f6464b2ce3e --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts @@ -0,0 +1,65 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; + +export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, ccs?: any) { + const clusterUuid = fetchAllClusters ? null : ''; + const { services } = useKibana<{ data: any }>(); + + const bounds = services.data?.query.timefilter.timefilter.getBounds(); + const [min] = useState(bounds.min.toISOString()); + const [max] = useState(bounds.max.toISOString()); + + const [clusters, setClusters] = useState([]); + const [loaded, setLoaded] = useState<boolean | null>(false); + + let url = '../api/monitoring/v1/clusters'; + if (clusterUuid) { + url += `/${clusterUuid}`; + } + + useEffect(() => { + const fetchClusters = async () => { + try { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min, + max, + }, + codePaths, + }), + }); + + setClusters(formatClusters(response)); + } catch (err) { + // TODO: handle errors + } finally { + setLoaded(null); + } + }; + + fetchClusters(); + }, [ccs, services.http, codePaths, url, min, max]); + + return { clusters, loaded }; +} + +function formatClusters(clusters: any) { + return clusters.map(formatCluster); +} + +function formatCluster(cluster: any) { + if (cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID) { + cluster.cluster_name = 'Standalone Cluster'; + } + return cluster; +} diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_title.ts b/x-pack/plugins/monitoring/public/application/hooks/use_title.ts new file mode 100644 index 0000000000000..25cc2c5b40dff --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_title.ts @@ -0,0 +1,24 @@ +/* + * 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 { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; + +// TODO: verify that works for all pages +export function useTitle(cluster: string, suffix: string) { + const { services } = useKibana(); + let clusterName = get(cluster, 'cluster_name'); + clusterName = clusterName ? `- ${clusterName}` : ''; + suffix = suffix ? `- ${suffix}` : ''; + + services.chrome?.docTitle.change( + i18n.translate('xpack.monitoring.stackMonitoringDocTitle', { + defaultMessage: 'Stack Monitoring {clusterName} {suffix}', + values: { clusterName, suffix }, + }) + ); +} diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx new file mode 100644 index 0000000000000..a0c9afd73f0ce --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -0,0 +1,61 @@ +/* + * 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 { CoreStart, AppMountParameters } from 'kibana/public'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Route, Switch, Redirect, HashRouter } from 'react-router-dom'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { LoadingPage } from './pages/loading_page'; +import { MonitoringStartPluginDependencies } from '../types'; + +export const renderApp = ( + core: CoreStart, + plugins: MonitoringStartPluginDependencies, + { element }: AppMountParameters +) => { + ReactDOM.render(<MonitoringApp core={core} plugins={plugins} />, element); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +}; + +const MonitoringApp: React.FC<{ + core: CoreStart; + plugins: MonitoringStartPluginDependencies; +}> = ({ core, plugins }) => { + return ( + <KibanaContextProvider services={{ ...core, ...plugins }}> + <HashRouter> + <Switch> + <Route path="/loading" component={LoadingPage} /> + <Route path="/no-data" component={NoData} /> + <Route path="/home" component={Home} /> + <Route path="/overview" component={ClusterOverview} /> + <Redirect + to={{ + pathname: '/loading', + }} + /> + </Switch> + </HashRouter> + </KibanaContextProvider> + ); +}; + +const NoData: React.FC<{}> = () => { + return <div>No data page</div>; +}; + +const Home: React.FC<{}> = () => { + return <div>Home page (Cluster listing)</div>; +}; + +const ClusterOverview: React.FC<{}> = () => { + return <div>Cluster overview page</div>; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx new file mode 100644 index 0000000000000..4bd09f73ac75a --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx @@ -0,0 +1,41 @@ +/* + * 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 { Redirect } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { PageTemplate } from './page_template'; +import { PageLoading } from '../../components'; +import { useClusters } from '../hooks/use_clusters'; +import { CODE_PATH_ELASTICSEARCH } from '../../../common/constants'; + +const CODE_PATHS = [CODE_PATH_ELASTICSEARCH]; + +export const LoadingPage = () => { + const { clusters, loaded } = useClusters(CODE_PATHS, true); + const title = i18n.translate('xpack.monitoring.loading.pageTitle', { + defaultMessage: 'Loading', + }); + + return ( + <PageTemplate title={title}> + {loaded === false ? <PageLoading /> : renderRedirections(clusters)} + </PageTemplate> + ); +}; + +const renderRedirections = (clusters: any) => { + if (!clusters || !clusters.length) { + return <Redirect to="/no-data" />; + } + if (clusters.length === 1) { + // Bypass the cluster listing if there is just 1 cluster + return <Redirect to="/overview" />; + } + + return <Redirect to="/home" />; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx new file mode 100644 index 0000000000000..fb766af6c8cbe --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -0,0 +1,20 @@ +/* + * 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 { useTitle } from '../hooks/use_title'; + +interface PageTemplateProps { + title: string; + children: React.ReactNode; +} + +export const PageTemplate = ({ title, children }: PageTemplateProps) => { + useTitle('', title); + + return <div>{children}</div>; +}; diff --git a/x-pack/plugins/monitoring/public/components/index.d.ts b/x-pack/plugins/monitoring/public/components/index.d.ts new file mode 100644 index 0000000000000..d027298c81c4c --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/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 PageLoading: FunctionComponent<Props>; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index 710b453e7f21e..df0496d438013 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -94,6 +94,7 @@ export class MonitoringPlugin mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const { AngularApp } = await import('./angular'); + const externalConfig = this.getExternalConfig(); const deps: MonitoringStartPluginDependencies = { navigation: pluginsStart.navigation, kibanaLegacy: pluginsStart.kibanaLegacy, @@ -102,27 +103,33 @@ export class MonitoringPlugin data: pluginsStart.data, isCloud: Boolean(plugins.cloud?.isCloudEnabled), pluginInitializerContext: this.initializerContext, - externalConfig: this.getExternalConfig(), + externalConfig, triggersActionsUi: pluginsStart.triggersActionsUi, usageCollection: plugins.usageCollection, appMountParameters: params, }; - const monitoringApp = new AngularApp(deps); - const removeHistoryListener = params.history.listen((location) => { - if (location.pathname === '' && location.hash === '') { - monitoringApp.applyScope(); - } - }); - - const removeHashChange = this.setInitialTimefilter(deps); - return () => { - if (removeHashChange) { - removeHashChange(); - } - removeHistoryListener(); - monitoringApp.destroy(); - }; + const config = Object.fromEntries(externalConfig); + if (config.renderReactApp) { + const { renderApp } = await import('./application'); + return renderApp(coreStart, pluginsStart, params); + } else { + const monitoringApp = new AngularApp(deps); + const removeHistoryListener = params.history.listen((location) => { + if (location.pathname === '' && location.hash === '') { + monitoringApp.applyScope(); + } + }); + + const removeHashChange = this.setInitialTimefilter(deps); + return () => { + if (removeHashChange) { + removeHashChange(); + } + removeHistoryListener(); + monitoringApp.destroy(); + }; + } }, }; @@ -163,6 +170,7 @@ export class MonitoringPlugin ['showLicenseExpiration', monitoring.ui.show_license_expiration], ['showCgroupMetricsElasticsearch', monitoring.ui.container.elasticsearch.enabled], ['showCgroupMetricsLogstash', monitoring.ui.container.logstash.enabled], + ['renderReactApp', monitoring.ui.render_react_app], ]; } diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index f39e5a0703d22..90c6e68314b71 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -106,6 +106,7 @@ describe('config schema', () => { "index": "metricbeat-*", }, "min_interval_seconds": 10, + "render_react_app": false, "show_license_expiration": true, }, } diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 98fd02b03539c..5c2bdc1424f93 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -51,6 +51,7 @@ export const configSchema = schema.object({ }), min_interval_seconds: schema.number({ defaultValue: 10 }), show_license_expiration: schema.boolean({ defaultValue: true }), + render_react_app: schema.boolean({ defaultValue: false }), }), kibana: schema.object({ collection: schema.object({