[Infrastructure UI] Save selected tab for Hosts View into url state (#151975)

## 📓 Summary

Closes #150856 

This PR refactor the existing implementation of the tabs, extracting
this piece of logic into its own hook `useTabId` which stores the
selected tab in the URL and allows sharing this preference with others.

To keep the optimization performed in [[Infrastructure UI] Add Alerts
tab into Hosts View](https://github.com/elastic/kibana/pull/149579), has
been necessary to push down the memoization of the AlertSummary
component.

This PR also hide the count of alerts when it is equal to `0`.

## 🧪 Testing
- Navigate to the hosts' view
- Create an Inventory rule to generate alerts for all our hosts (it can
be something like cpu-usage > 0%)
- Refresh the hosts' view page and start switching between tabs.
- Refresh again the page to verify the selected tab is preserved and its
value is stored in the URL.


https://user-images.githubusercontent.com/34506779/220871592-8a4ae1b7-3b9c-4ab1-be3f-4bd673ab9f2f.mov

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
Marco Antonio Ghiani 2023-02-24 11:59:13 +01:00 committed by GitHub
parent a70de0387b
commit 4a775b83a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 38 deletions

View file

@ -0,0 +1,16 @@
/*
* 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 { useRef, MutableRefObject } from 'react';
export const useLazyRef = <Type>(initializer: () => Type) => {
const ref = useRef<Type | null>(null);
if (ref.current === null) {
ref.current = initializer();
}
return ref as MutableRefObject<Type>;
};

View file

@ -27,35 +27,26 @@ import {
DEFAULT_INTERVAL,
infraAlertFeatureIds,
} from '../config';
import { useAlertsQuery } from '../../../hooks/use_alerts_query';
import { AlertsEsQuery, useAlertsQuery } from '../../../hooks/use_alerts_query';
import AlertsStatusFilter from './alerts_status_filter';
import { HostsState } from '../../../hooks/use_unified_search_url_state';
export const AlertsTabContent = React.memo(() => {
export const AlertsTabContent = () => {
const { services } = useKibana<InfraClientCoreStart & InfraClientStartDeps>();
const { alertStatus, setAlertStatus, alertsEsQueryByStatus } = useAlertsQuery();
const { unifiedSearchDateRange } = useUnifiedSearchContext();
const summaryTimeRange = useSummaryTimeRange(unifiedSearchDateRange);
const { application, cases, triggersActionsUi } = services;
const { application, cases, charts, triggersActionsUi } = services;
const {
alertsTableConfigurationRegistry,
getAlertsStateTable: AlertsStateTable,
getAlertSummaryWidget: AlertSummaryWidget,
} = triggersActionsUi;
const { alertsTableConfigurationRegistry, getAlertsStateTable: AlertsStateTable } =
triggersActionsUi;
const CasesContext = cases.ui.getCasesContext();
const uiCapabilities = application?.capabilities;
const casesCapabilities = cases.helpers.getUICapabilities(uiCapabilities.observabilityCases);
const chartThemes = {
theme: charts.theme.useChartsTheme(),
baseTheme: charts.theme.useChartsBaseTheme(),
};
return (
<HeightRetainer>
<EuiFlexGroup direction="column" gutterSize="m">
@ -65,12 +56,9 @@ export const AlertsTabContent = React.memo(() => {
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexItem>
<AlertSummaryWidget
chartThemes={chartThemes}
featureIds={infraAlertFeatureIds}
filter={alertsEsQueryByStatus}
fullSize
timeRange={summaryTimeRange}
<MemoAlertSummaryWidget
alertsQuery={alertsEsQueryByStatus}
dateRange={unifiedSearchDateRange}
/>
</EuiFlexItem>
{alertsEsQueryByStatus && (
@ -97,7 +85,38 @@ export const AlertsTabContent = React.memo(() => {
</EuiFlexGroup>
</HeightRetainer>
);
});
};
interface MemoAlertSummaryWidgetProps {
alertsQuery: AlertsEsQuery;
dateRange: HostsState['dateRange'];
}
const MemoAlertSummaryWidget = React.memo(
({ alertsQuery, dateRange }: MemoAlertSummaryWidgetProps) => {
const { services } = useKibana<InfraClientStartDeps>();
const summaryTimeRange = useSummaryTimeRange(dateRange);
const { charts, triggersActionsUi } = services;
const { getAlertSummaryWidget: AlertSummaryWidget } = triggersActionsUi;
const chartThemes = {
theme: charts.theme.useChartsTheme(),
baseTheme: charts.theme.useChartsBaseTheme(),
};
return (
<AlertSummaryWidget
chartThemes={chartThemes}
featureIds={infraAlertFeatureIds}
filter={alertsQuery}
fullSize
timeRange={summaryTimeRange}
/>
);
}
);
const useSummaryTimeRange = (unifiedSearchDateRange: TimeRange) => {
const timeBuckets = useTimeBuckets();

View file

@ -36,9 +36,12 @@ export const AlertsTabBadge = () => {
);
}
return (
const shouldRenderBadge =
typeof alertsCount?.activeAlertCount === 'number' && alertsCount.activeAlertCount > 0;
return shouldRenderBadge ? (
<EuiNotificationBadge className="eui-alignCenter" size="m">
{alertsCount?.activeAlertCount}
</EuiNotificationBadge>
);
) : null;
};

View file

@ -5,14 +5,15 @@
* 2.0.
*/
import React, { useRef, useState } from 'react';
import React from 'react';
import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useLazyRef } from '../../../../../hooks/use_lazy_ref';
import { MetricsGrid } from './metrics/metrics_grid';
import { AlertsTabContent } from './alerts';
import { AlertsTabBadge } from './alerts_tab_badge';
import { TabIds } from '../../types';
import { TabIds, useTabId } from '../../hooks/use_tab_id';
const tabs = [
{
@ -32,14 +33,11 @@ const tabs = [
},
];
const initialRenderedTabsSet = new Set([tabs[0].id]);
export const Tabs = () => {
const [selectedTabId, setSelectedTabId] = useTabId(tabs[0].id);
// This map allow to keep track of which tabs content have been rendered the first time.
// We need it in order to load a tab content only if it gets clicked, and then keep it in the DOM for performance improvement.
const renderedTabsSet = useRef(initialRenderedTabsSet);
const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);
const renderedTabsSet = useLazyRef(() => new Set([selectedTabId]));
const tabEntries = tabs.map((tab, index) => (
<EuiTab

View file

@ -8,7 +8,7 @@ import { useCallback, useMemo, useState } from 'react';
import createContainer from 'constate';
import { getTime } from '@kbn/data-plugin/common';
import { TIMESTAMP } from '@kbn/rule-data-utils';
import { buildEsQuery, Filter, Query } from '@kbn/es-query';
import { BoolQuery, buildEsQuery, Filter, Query } from '@kbn/es-query';
import { SnapshotNode } from '../../../../../common/http_api';
import { useUnifiedSearchContext } from './use_unified_search';
import { HostsState } from './use_unified_search_url_state';
@ -16,6 +16,10 @@ import { useHostsView } from './use_hosts_view';
import { AlertStatus } from '../types';
import { ALERT_STATUS_QUERY } from '../constants';
export interface AlertsEsQuery {
bool: BoolQuery;
}
export const useAlertsQueryImpl = () => {
const { hostNodes } = useHostsView();
@ -60,7 +64,7 @@ const createAlertsEsQuery = ({
dateRange: HostsState['dateRange'];
hostNodes: SnapshotNode[];
status?: AlertStatus;
}) => {
}): AlertsEsQuery => {
const alertStatusQuery = createAlertStatusQuery(status);
const dateFilter = createDateFilter(dateRange);

View file

@ -0,0 +1,38 @@
/*
* 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 * as rt from 'io-ts';
import { pipe } from 'fp-ts/lib/pipeable';
import { fold } from 'fp-ts/lib/Either';
import { constant, identity } from 'fp-ts/lib/function';
import { useUrlState } from '../../../../utils/use_url_state';
const TAB_ID_URL_STATE_KEY = 'tabId';
export const useTabId = (initialValue: TabId = TabIds.METRICS): [TabId, TabIdUpdater] => {
return useUrlState<TabId>({
defaultState: initialValue,
decodeUrlState: makeDecodeUrlState(initialValue),
encodeUrlState,
urlStateKey: TAB_ID_URL_STATE_KEY,
});
};
const TabIdRT = rt.union([rt.literal('alerts'), rt.literal('metrics')]);
export enum TabIds {
ALERTS = 'alerts',
METRICS = 'metrics',
}
type TabId = rt.TypeOf<typeof TabIdRT>;
type TabIdUpdater = (tabId: TabId) => void;
const encodeUrlState = TabIdRT.encode;
const makeDecodeUrlState = (initialValue: TabId) => (value: unknown) => {
return pipe(TabIdRT.decode(value), fold(constant(initialValue), identity));
};

View file

@ -8,11 +8,6 @@
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import { ALERT_STATUS_ALL } from './constants';
export enum TabIds {
ALERTS = 'alerts',
METRICS = 'metrics',
}
export type AlertStatus =
| typeof ALERT_STATUS_ACTIVE
| typeof ALERT_STATUS_RECOVERED