Skip to content

Commit

Permalink
[Stack Monitoring] Elasticsearch Overview view migration (#111941) (#…
Browse files Browse the repository at this point in the history
…112233)

* 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

Co-authored-by: Ester Martí Vilaseca <ester.martivilaseca@elastic.co>
  • Loading branch information
kibanamachine and estermv authored Sep 15, 2021
1 parent a0e992a commit 9a525a1
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 19 deletions.
81 changes: 81 additions & 0 deletions x-pack/plugins/monitoring/public/application/hooks/use_charts.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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(),
Expand All @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/monitoring/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -72,6 +74,14 @@ const MonitoringApp: React.FC<{
codePaths={['all']}
fetchAllClusters={false}
/>

{/* ElasticSearch Views */}
<RouteInit
path="/elasticsearch"
component={ElasticsearchOverviewPage}
codePaths={[CODE_PATH_ELASTICSEARCH]}
fetchAllClusters={false}
/>
<Redirect
to={{
pathname: '/loading',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,8 @@ export const ClusterOverview: React.FC<{}> = () => {
{
id: 'clusterName',
label: clusters[0].cluster_name,
disabled: false,
description: clusters[0].cluster_name,
onClick: () => {},
testSubj: 'clusterName',
route: '/overview',
},
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ElasticsearchTemplateProps> = ({
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 <PageTemplate {...props} tabs={tabs} product="elasticsearch" />;
};

const mlIsSupported = (license: any) => includes(ML_SUPPORTED_LICENSES, license.type);
Original file line number Diff line number Diff line change
@@ -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<ComponentProps> = ({ 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 (
<ElasticsearchOverview
clusterStatus={clusterStatus}
metrics={metrics}
logs={logs}
cluster={cluster}
shardActivity={shardActivityData}
onBrush={onBrush}
showShardActivityHistory={showShardActivityHistory}
toggleShardActivityHistory={toggleShardActivityHistory}
zoomInfo={zoomInfo}
data-test-subj="elasticsearchOverviewPage"
/>
);
};

return (
<ElasticsearchTemplate
title={title}
pageTitle={pageTitle}
getPageData={getPageData}
data-test-subj="elasticsearchOverviewPage"
cluster={cluster}
>
<div data-test-subj="elasticsearchOverviewPage">{renderOverview(data)}</div>
</ElasticsearchTemplate>
);
};
Loading

0 comments on commit 9a525a1

Please sign in to comment.