mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[POC] Track time to first meaningful paint (#180309)
part of https://github.com/elastic/observability-dev/issues/3269
#### Time to First Meaningful Paint (TTFMP)
Measures the time from the start of navigation to the point at which the
most meaningful element appears on the screen. TTFMP is instrumented
differently on each page as the most meaningful element varies from page
to page.
example:
```
event_type : "performance_metric" and eventName: "kibana:plugin_render_time"
```
#### Usage
To instrument TTFMP you need to `PerfomanceContextProvider` at the root
at the app and run `onPageReady` function once meaningful data are
fetched . The meaningful data can be one or more
```
import { usePerformanceContext } from '@kbn/ebt-tools';
const { onPageReady } = usePerformanceContext();
```
#### Current instrumentation
Based on telemetry, I instrumented the TTFMP for most viewed pages in
apm and infra:
1. /services
2. /traces
3. /service-map
4. /hosts
5. infra /inventory
All pages except hosts have one component so the page ready once the
data is loaded for the component. For hosts I set it when the hosts list
table and the hosts number is loaded.
#### Telemetry
Example dashboard of the metrics I sent from my local setup
-
[Dashboard](f240fff6
-fac9-491b-81d1-ac39006c5c94?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-7d,to:now)))
## Updates for reviewers
Previously the PR was checking both the component AND the time to first
meaningful paint. We decided to focus on the time to first meaningful
paint
### Notes
TTFMP is subject to change for each page based on what we're going to
define as meaningful
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
599f01598e
commit
a19143c198
21 changed files with 334 additions and 69 deletions
|
@ -102,6 +102,7 @@
|
|||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@elastic/apm-rum": "^5.16.0",
|
||||
"@elastic/apm-rum-core": "^5.21.0",
|
||||
"@elastic/apm-rum-react": "^2.0.2",
|
||||
"@elastic/charts": "64.1.0",
|
||||
"@elastic/datemath": "5.0.3",
|
||||
|
|
|
@ -23,10 +23,22 @@ function findRegisteredEventTypeByName(eventTypeName: string) {
|
|||
([{ eventType }]) => eventType === eventTypeName
|
||||
)!;
|
||||
}
|
||||
interface MockEntryList {
|
||||
getEntries: () => [object];
|
||||
}
|
||||
type ObsCallback = (_entries: MockEntryList, _obs: object) => undefined;
|
||||
|
||||
describe('AnalyticsService', () => {
|
||||
let analyticsService: AnalyticsService;
|
||||
beforeEach(() => {
|
||||
const mockObs = { observe: jest.fn, disconnect: jest.fn };
|
||||
const mockPerformanceObserver = function (callback: ObsCallback) {
|
||||
callback({ getEntries: () => [{}] }, mockObs);
|
||||
return mockObs;
|
||||
};
|
||||
|
||||
(global.PerformanceObserver as unknown) = mockPerformanceObserver;
|
||||
|
||||
jest.clearAllMocks();
|
||||
analyticsService = new AnalyticsService(coreContextMock.create());
|
||||
});
|
||||
|
|
|
@ -13,7 +13,9 @@ import { registerPerformanceMetricEventType } from '@kbn/ebt-tools';
|
|||
import type { CoreContext } from '@kbn/core-base-browser-internal';
|
||||
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
|
||||
import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import { trackPerformanceMeasureEntries } from './track_performance_measure_entries';
|
||||
import { trackClicks } from './track_clicks';
|
||||
|
||||
import { getSessionId } from './get_session_id';
|
||||
import { createLogger } from './logger';
|
||||
import { trackViewportSize } from './track_viewport_size';
|
||||
|
@ -44,6 +46,9 @@ export class AnalyticsService {
|
|||
this.registerSessionIdContext();
|
||||
this.registerBrowserInfoAnalyticsContext();
|
||||
this.subscriptionsHandler.add(trackClicks(this.analyticsClient, core.env.mode.dev));
|
||||
this.subscriptionsHandler.add(
|
||||
trackPerformanceMeasureEntries(this.analyticsClient, core.env.mode.dev)
|
||||
);
|
||||
this.subscriptionsHandler.add(trackViewportSize(this.analyticsClient));
|
||||
|
||||
// Register a flush method in the browser so CI can explicitly call it before closing the browser.
|
||||
|
|
|
@ -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
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { AnalyticsClient } from '@kbn/analytics-client';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
|
||||
export function trackPerformanceMeasureEntries(analytics: AnalyticsClient, isDevMode: boolean) {
|
||||
function perfObserver(
|
||||
list: PerformanceObserverEntryList,
|
||||
observer: PerformanceObserver,
|
||||
droppedEntriesCount: number
|
||||
) {
|
||||
list.getEntries().forEach((entry: any) => {
|
||||
if (entry.entryType === 'measure' && entry.detail?.type === 'kibana:performance') {
|
||||
const target = entry?.name;
|
||||
const duration = entry.duration;
|
||||
|
||||
if (isDevMode) {
|
||||
if (!target) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to report the performance entry. Measure name is undefined`);
|
||||
}
|
||||
|
||||
if (!duration) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Failed to report the performance entry. Duration for the measure: ${target} is undefined`
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`The measure ${target} completed in ${duration / 1000}s`);
|
||||
}
|
||||
|
||||
if (droppedEntriesCount > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`${droppedEntriesCount} performance entries got dropped due to the buffer being full.`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
reportPerformanceMetricEvent(analytics, {
|
||||
eventName: entry.detail.eventName,
|
||||
duration,
|
||||
meta: {
|
||||
target,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Failed to report the performance event`, { event, error });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const observer = new PerformanceObserver(perfObserver as PerformanceObserverCallback);
|
||||
observer.observe({ type: 'measure', buffered: true });
|
||||
}
|
|
@ -6,4 +6,6 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './src/performance_metrics';
|
||||
|
||||
export * from './src/performance_metric_events';
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { afterFrame } from '@elastic/apm-rum-core';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { perfomanceMarkers } from '../performance_markers';
|
||||
import { PerformanceApi, PerformanceContext } from './use_performance_context';
|
||||
|
||||
function measureInteraction() {
|
||||
performance.mark(perfomanceMarkers.startPageChange);
|
||||
const trackedRoutes: string[] = [];
|
||||
return {
|
||||
/**
|
||||
* Marks the end of the page ready state and measures the performance between the start of the page change and the end of the page ready state.
|
||||
* @param pathname - The pathname of the page.
|
||||
*/
|
||||
pageReady(pathname: string) {
|
||||
performance.mark(perfomanceMarkers.endPageReady);
|
||||
|
||||
if (!trackedRoutes.includes(pathname)) {
|
||||
performance.measure(pathname, {
|
||||
detail: { eventName: 'kibana:plugin_render_time', type: 'kibana:performance' },
|
||||
start: perfomanceMarkers.startPageChange,
|
||||
end: perfomanceMarkers.endPageReady,
|
||||
});
|
||||
trackedRoutes.push(pathname);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function PerformanceContextProvider({ children }: { children: React.ReactElement }) {
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const location = useLocation();
|
||||
const interaction = measureInteraction();
|
||||
|
||||
React.useEffect(() => {
|
||||
afterFrame(() => {
|
||||
setIsRendered(true);
|
||||
});
|
||||
return () => {
|
||||
setIsRendered(false);
|
||||
performance.clearMeasures(location.pathname);
|
||||
};
|
||||
}, [location.pathname]);
|
||||
|
||||
const api = useMemo<PerformanceApi>(
|
||||
() => ({
|
||||
onPageReady() {
|
||||
if (isRendered) {
|
||||
interaction.pageReady(location.pathname);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[isRendered, location.pathname, interaction]
|
||||
);
|
||||
|
||||
return <PerformanceContext.Provider value={api}>{children}</PerformanceContext.Provider>;
|
||||
}
|
||||
// dynamic import
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default PerformanceContextProvider;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface PerformanceApi {
|
||||
onPageReady(): void;
|
||||
}
|
||||
|
||||
export const PerformanceContext = createContext<PerformanceApi | undefined>(undefined);
|
||||
|
||||
export function usePerformanceContext() {
|
||||
const api = useContext(PerformanceContext);
|
||||
|
||||
if (!api) {
|
||||
throw new Error('Missing Performance API in context');
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
25
packages/kbn-ebt-tools/src/performance_metrics/index.tsx
Normal file
25
packages/kbn-ebt-tools/src/performance_metrics/index.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
type Loader<TElement extends React.ComponentType<any>> = () => Promise<{
|
||||
default: TElement;
|
||||
}>;
|
||||
|
||||
function dynamic<TElement extends React.ComponentType<any>, TRef = {}>(loader: Loader<TElement>) {
|
||||
const Component = React.lazy(loader);
|
||||
|
||||
return React.forwardRef<TRef, React.ComponentPropsWithRef<TElement>>((props, ref) => (
|
||||
<Suspense fallback={null}>{React.createElement(Component, { ...props, ref })}</Suspense>
|
||||
));
|
||||
}
|
||||
|
||||
export { usePerformanceContext } from './context/use_performance_context';
|
||||
export { perfomanceMarkers } from './performance_markers';
|
||||
export const PerformanceContextProvider = dynamic(() => import('./context/performance_context'));
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
// Time To First Meaningful Paint (ttfmp)
|
||||
export const perfomanceMarkers = {
|
||||
startPageChange: 'start::pageChange',
|
||||
endPageReady: 'end::pageReady',
|
||||
};
|
11
packages/kbn-ebt-tools/src/types/apm_rum_core.d.ts
vendored
Normal file
11
packages/kbn-ebt-tools/src/types/apm_rum_core.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
declare module '@elastic/apm-rum-core' {
|
||||
export function afterFrame(callback: () => void): void;
|
||||
}
|
|
@ -2,19 +2,9 @@
|
|||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/analytics-client",
|
||||
"@kbn/logging-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"kbn_references": ["@kbn/analytics-client", "@kbn/logging-mocks"],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
@ -177,6 +178,7 @@ function useServicesDetailedStatisticsFetcher({
|
|||
|
||||
export function ServiceInventory() {
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = useStateDebounced('');
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const [renderedItems, setRenderedItems] = useState<ServiceListItem[]>([]);
|
||||
|
||||
|
@ -281,6 +283,15 @@ export function ServiceInventory() {
|
|||
});
|
||||
}, [mainStatisticsStatus, mainStatisticsData.items, setScreenContext]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
comparisonFetch.status === FETCH_STATUS.SUCCESS
|
||||
) {
|
||||
onPageReady();
|
||||
}
|
||||
}, [mainStatisticsStatus, comparisonFetch.status, onPageReady]);
|
||||
|
||||
const { fields, isSaving, saveSingleSetting } = useEditableSettings([
|
||||
apmEnableServiceInventoryTableSearchBar,
|
||||
]);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
|
@ -95,6 +96,7 @@ export function ServiceMap({
|
|||
const license = useLicenseContext();
|
||||
const serviceName = useServiceName();
|
||||
const { config } = useApmPluginContext();
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const {
|
||||
data = { elements: [] },
|
||||
|
@ -172,6 +174,10 @@ export function ServiceMap({
|
|||
);
|
||||
}
|
||||
|
||||
if (status === FETCH_STATUS.SUCCESS) {
|
||||
onPageReady();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar showTimeComparison />
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { EuiIcon, EuiToolTip, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { TypeOf } from '@kbn/typed-react-router-config';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { ApmRoutes } from '../../routing/apm_route_config';
|
||||
import { asMillisecondDuration, asTransactionRate } from '../../../../common/utils/formatters';
|
||||
|
@ -132,10 +133,18 @@ const noItemsMessage = (
|
|||
|
||||
export function TraceList({ response }: Props) {
|
||||
const { data: { items } = { items: [] }, status } = response;
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const { query } = useApmParams('/traces');
|
||||
|
||||
const traceListColumns = useMemo(() => getTraceListColumns({ query }), [query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === FETCH_STATUS.SUCCESS) {
|
||||
onPageReady();
|
||||
}
|
||||
}, [status, onPageReady]);
|
||||
|
||||
return (
|
||||
<ManagedTable
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
|
||||
import { KibanaContextProvider, useDarkMode } from '@kbn/kibana-react-plugin/public';
|
||||
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
|
||||
|
@ -71,36 +72,37 @@ export function ApmAppRoot({
|
|||
<i18nCore.Context>
|
||||
<TimeRangeIdContextProvider>
|
||||
<RouterProvider history={history} router={apmRouter as any}>
|
||||
<ApmErrorBoundary>
|
||||
<RedirectDependenciesToDependenciesInventory>
|
||||
<RedirectWithDefaultEnvironment>
|
||||
<RedirectWithDefaultDateRange>
|
||||
<RedirectWithOffset>
|
||||
<TrackPageview>
|
||||
<UpdateExecutionContextOnRouteChange>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<InspectorContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</InspectorContextProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</UpdateExecutionContextOnRouteChange>
|
||||
</TrackPageview>
|
||||
</RedirectWithOffset>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</RedirectWithDefaultEnvironment>
|
||||
</RedirectDependenciesToDependenciesInventory>
|
||||
</ApmErrorBoundary>
|
||||
<PerformanceContextProvider>
|
||||
<ApmErrorBoundary>
|
||||
<RedirectDependenciesToDependenciesInventory>
|
||||
<RedirectWithDefaultEnvironment>
|
||||
<RedirectWithDefaultDateRange>
|
||||
<RedirectWithOffset>
|
||||
<TrackPageview>
|
||||
<UpdateExecutionContextOnRouteChange>
|
||||
<BreadcrumbsContextProvider>
|
||||
<UrlParamsProvider>
|
||||
<LicenseProvider>
|
||||
<AnomalyDetectionJobsContextProvider>
|
||||
<InspectorContextProvider>
|
||||
<ApmThemeProvider>
|
||||
<MountApmHeaderActionMenu />
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<RouteRenderer />
|
||||
</ApmThemeProvider>
|
||||
</InspectorContextProvider>
|
||||
</AnomalyDetectionJobsContextProvider>
|
||||
</LicenseProvider>
|
||||
</UrlParamsProvider>
|
||||
</BreadcrumbsContextProvider>
|
||||
</UpdateExecutionContextOnRouteChange>
|
||||
</TrackPageview>
|
||||
</RedirectWithOffset>
|
||||
</RedirectWithDefaultDateRange>
|
||||
</RedirectWithDefaultEnvironment>
|
||||
</RedirectDependenciesToDependenciesInventory>
|
||||
</ApmErrorBoundary>
|
||||
</PerformanceContextProvider>
|
||||
</RouterProvider>
|
||||
</TimeRangeIdContextProvider>
|
||||
</i18nCore.Context>
|
||||
|
|
|
@ -76,7 +76,7 @@ export function SparkPlot({
|
|||
);
|
||||
}
|
||||
|
||||
function SparkPlotItem({
|
||||
export function SparkPlotItem({
|
||||
type,
|
||||
color,
|
||||
isLoading,
|
||||
|
|
|
@ -116,7 +116,8 @@
|
|||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/core-http-request-handler-context-server",
|
||||
"@kbn/search-types",
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/presentation-publishing"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import { History } from 'history';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import React from 'react';
|
||||
|
@ -99,12 +99,14 @@ const MetricsApp: React.FC<{
|
|||
<SourceProvider sourceId="default">
|
||||
<PluginConfigProvider value={pluginConfig}>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route path="/link-to" component={LinkToMetricsPage} />
|
||||
{uiCapabilities?.infrastructure?.show && (
|
||||
<Route path="/" component={InfrastructurePage} />
|
||||
)}
|
||||
</Routes>
|
||||
<PerformanceContextProvider>
|
||||
<Routes>
|
||||
<Route path="/link-to" component={LinkToMetricsPage} />
|
||||
{uiCapabilities?.infrastructure?.show && (
|
||||
<Route path="/" component={InfrastructurePage} />
|
||||
)}
|
||||
</Routes>
|
||||
</PerformanceContextProvider>
|
||||
</Router>
|
||||
</PluginConfigProvider>
|
||||
</SourceProvider>
|
||||
|
|
|
@ -27,21 +27,21 @@ export const HostsContent = () => {
|
|||
) : (
|
||||
<HostsViewProvider>
|
||||
<HostsTableProvider>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostCountProvider>
|
||||
<HostCountProvider>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem grow={false}>
|
||||
<KPIGrid />
|
||||
</HostCountProvider>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostsTable />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsQueryProvider>
|
||||
<Tabs />
|
||||
</AlertsQueryProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HostsTable />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsQueryProvider>
|
||||
<Tabs />
|
||||
</AlertsQueryProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</HostCountProvider>
|
||||
</HostsTableProvider>
|
||||
</HostsViewProvider>
|
||||
)}
|
||||
|
|
|
@ -5,18 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { EuiBasicTable } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { HostNodeRow, useHostsTableContext } from '../hooks/use_hosts_table';
|
||||
import { useHostsViewContext } from '../hooks/use_hosts_view';
|
||||
import { useHostCountContext } from '../hooks/use_host_count';
|
||||
import { FlyoutWrapper } from './host_details_flyout/flyout_wrapper';
|
||||
import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../constants';
|
||||
import { FilterAction } from './table/filter_action';
|
||||
|
||||
export const HostsTable = () => {
|
||||
const { loading } = useHostsViewContext();
|
||||
const { isRequestRunning: hostCountLoading } = useHostCountContext();
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const {
|
||||
columns,
|
||||
|
@ -33,6 +37,12 @@ export const HostsTable = () => {
|
|||
filterSelectedHosts,
|
||||
} = useHostsTableContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !hostCountLoading) {
|
||||
onPageReady();
|
||||
}
|
||||
}, [loading, hostCountLoading, onPageReady]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterAction
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCurrentEuiBreakpoint } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
|
@ -63,6 +64,7 @@ export const NodesOverview = ({
|
|||
}: Props) => {
|
||||
const currentBreakpoint = useCurrentEuiBreakpoint();
|
||||
const [{ detailsItemId }, setFlyoutUrlState] = useAssetDetailsFlyoutState();
|
||||
const { onPageReady } = usePerformanceContext();
|
||||
|
||||
const closeFlyout = useCallback(
|
||||
() => setFlyoutUrlState({ detailsItemId: null }),
|
||||
|
@ -115,6 +117,10 @@ export const NodesOverview = ({
|
|||
const bounds = autoBounds ? dataBounds : boundsOverride;
|
||||
const isStatic = ['xs', 's'].includes(currentBreakpoint!);
|
||||
|
||||
if (!loading) {
|
||||
onPageReady();
|
||||
}
|
||||
|
||||
if (view === 'table') {
|
||||
return (
|
||||
<TableContainer>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue