[Infrastructure UI] Asset Details clean up and improvements (#158511)

Part of: [#156696](https://github.com/elastic/kibana/issues/156696)

## Summary

This PR refactors some of the newly introduced Asset Details embeddable
code to make it easier to consume by other pages, decoupling its props
from what the Host View used to send to the old Host View Flyout.

Besides adjustments to the component responsiveness, this PR also
improves the logic around adding tabs and managing their state.

### How to test this PR

- Run `yarn storybook infra`
  - Check all the storybooks related to the Asset Detail Embeddable
- Start Kibana
  - Go to the Hosts View page, open the flyout, and play with it. 

### For reviewers
The Asset Details Embeddable still depends on a few things from the
`pages/*` folder, and a couple of components are still tightly coupled
with the Host View. We'll address these things to keep this PR small in
the next ones.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2023-05-30 11:57:52 -03:00 committed by GitHub
parent c12e17c6dc
commit 3d05fc6ada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 545 additions and 476 deletions

View file

@ -5,15 +5,15 @@
* 2.0.
*/
import React from 'react';
import { EuiButton, EuiCard } from '@elastic/eui';
import { I18nProvider } from '@kbn/i18n-react';
import type { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { DecorateWithKibanaContext } from './asset_details.story_decorators';
import { AssetDetails, FlyoutTabIds, type AssetDetailsProps } from './asset_details';
import { AssetDetails } from './asset_details';
import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme';
import { FlyoutTabIds, type AssetDetailsProps } from './types';
export default {
title: 'infra/Asset Details View/Asset Details Embeddable',
@ -42,20 +42,12 @@ export default {
memoryTotal: 34359738368,
},
nodeType: 'host',
closeFlyout: () => {},
onTabClick: () => {},
renderedTabsSet: { current: new Set(['metadata']) },
currentTimeRange: {
interval: '1s',
from: 1683630468,
to: 1683630469,
},
hostFlyoutOpen: {
clickedItemId: 'host1-macos',
selectedTabId: 'metadata',
searchFilter: '',
metadataSearch: '',
},
selectedTabId: 'metadata',
tabs: [
{
id: FlyoutTabIds.METADATA,
@ -73,7 +65,7 @@ export default {
},
],
links: ['apmServices', 'uptime'],
},
} as AssetDetailsProps,
} as Meta;
const Template: Story<AssetDetailsProps> = (args) => {
@ -91,26 +83,32 @@ const FlyoutTemplate: Story<AssetDetailsProps> = (args) => {
>
Open flyout
</EuiButton>
<div hidden={!isOpen}>{isOpen && <AssetDetails {...args} closeFlyout={closeFlyout} />}</div>
<div hidden={!isOpen}>
{isOpen && <AssetDetails {...args} renderMode={{ showInFlyout: true, closeFlyout }} />}
</div>
</div>
);
};
export const DefaultAssetDetailsWithMetadataTabSelected = Template.bind({});
DefaultAssetDetailsWithMetadataTabSelected.args = {
showActionsColumn: true,
overrides: {
metadata: {
showActionsColumn: true,
},
},
};
export const AssetDetailsWithMetadataTabSelectedWithPersistedSearch = Template.bind({});
AssetDetailsWithMetadataTabSelectedWithPersistedSearch.args = {
showActionsColumn: true,
hostFlyoutOpen: {
clickedItemId: 'host1-macos',
selectedTabId: 'metadata',
searchFilter: '',
metadataSearch: 'ip',
overrides: {
metadata: {
showActionsColumn: true,
query: 'ip',
},
},
setHostFlyoutState: () => {},
activeTabId: 'metadata',
onTabsStateChange: () => {},
};
export const AssetDetailsWithMetadataWithoutActions = Template.bind({});
@ -120,22 +118,21 @@ export const AssetDetailsWithMetadataWithoutLinks = Template.bind({});
AssetDetailsWithMetadataWithoutLinks.args = { links: [] };
export const AssetDetailsAsFlyout = FlyoutTemplate.bind({});
AssetDetailsAsFlyout.args = { showInFlyout: true };
AssetDetailsAsFlyout.args = {
renderMode: {
showInFlyout: true,
closeFlyout: () => {},
},
};
export const AssetDetailsWithProcessesTabSelected = Template.bind({});
AssetDetailsWithProcessesTabSelected.args = {
renderedTabsSet: { current: new Set(['processes']) },
activeTabId: 'processes',
currentTimeRange: {
interval: '1s',
from: 1683630468,
to: 1683630469,
},
hostFlyoutOpen: {
clickedItemId: 'host1-macos',
selectedTabId: 'processes',
searchFilter: '',
metadataSearch: '',
},
};
export const AssetDetailsWithMetadataTabOnly = Template.bind({});

View file

@ -9,6 +9,7 @@ import type { StoryContext } from '@storybook/react';
import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { of } from 'rxjs';
import { SourceProvider } from '../../containers/metrics_source';
export const DecorateWithKibanaContext = <StoryFnReactReturnType extends React.ReactNode>(
@ -172,7 +173,7 @@ export const DecorateWithKibanaContext = <StoryFnReactReturnType extends React.R
const mockServices = {
application: {
currentAppId$: { title: 'infra', subscribe: () => {} },
currentAppId$: of('infra'),
navigateToUrl: () => {},
},
dataViews: { create: () => {} },

View file

@ -5,188 +5,77 @@
* 2.0.
*/
import React, { useState } from 'react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutBody,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTabs,
EuiTab,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { LinkToUptime } from './links/link_to_uptime';
import { LinkToApmServices } from './links/link_to_apm_services';
import type { HostNodeRow } from './types';
import React from 'react';
import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui';
import type { AssetDetailsProps, RenderMode } from './types';
import type { InventoryItemType } from '../../../common/inventory_models/types';
import type { SetNewHostFlyoutOpen } from '../../pages/metrics/hosts/hooks/use_host_flyout_open_url_state';
import { AssetDetailsTabContent } from './tabs_content/tabs_content';
export enum FlyoutTabIds {
METADATA = 'metadata',
PROCESSES = 'processes',
}
export type TabIds = `${FlyoutTabIds}`;
export interface Tab {
id: FlyoutTabIds;
name: string;
'data-test-subj': string;
}
export interface AssetDetailsProps {
node: HostNodeRow;
nodeType: InventoryItemType;
closeFlyout: () => void;
renderedTabsSet: React.MutableRefObject<Set<TabIds>>;
currentTimeRange: {
interval: string;
from: number;
to: number;
};
tabs: Tab[];
hostFlyoutOpen?: {
clickedItemId: string;
selectedTabId: TabIds;
searchFilter: string;
metadataSearch: string;
};
setHostFlyoutState?: SetNewHostFlyoutOpen;
onTabClick?: (tab: Tab) => void;
links?: Array<'uptime' | 'apmServices'>;
showInFlyout?: boolean;
showActionsColumn?: boolean;
}
import { TabContent } from './tab_content/tab_content';
import { Header } from './header/header';
import { TabSwitcherProvider } from './hooks/use_tab_switcher';
// Setting host as default as it will be the only supported type for now
const NODE_TYPE = 'host' as InventoryItemType;
interface ContentTemplateProps {
header: React.ReactElement;
body: React.ReactElement;
renderMode: RenderMode;
}
const ContentTemplate = ({ header, body, renderMode }: ContentTemplateProps) => {
return renderMode.showInFlyout ? (
<EuiFlyout onClose={renderMode.closeFlyout} ownFocus={false}>
<EuiFlyoutHeader hasBorder>{header}</EuiFlyoutHeader>
<EuiFlyoutBody>{body}</EuiFlyoutBody>
</EuiFlyout>
) : (
<>
{header}
{body}
</>
);
};
export const AssetDetails = ({
node,
closeFlyout,
onTabClick,
renderedTabsSet,
currentTimeRange,
hostFlyoutOpen,
setHostFlyoutState,
activeTabId,
overrides,
onTabsStateChange,
tabs,
showInFlyout,
links,
showActionsColumn,
nodeType = NODE_TYPE,
renderMode = {
showInFlyout: false,
},
}: AssetDetailsProps) => {
const { euiTheme } = useEuiTheme();
const [selectedTabId, setSelectedTabId] = useState<TabIds>('metadata');
const onTabSelectClick = (tab: Tab) => {
renderedTabsSet.current.add(tab.id); // On a tab click, mark the tab content as allowed to be rendered
setSelectedTabId(tab.id);
};
const tabEntries = tabs.map((tab) => (
<EuiTab
{...tab}
key={tab.id}
onClick={() => (onTabClick ? onTabClick(tab) : onTabSelectClick(tab))}
isSelected={tab.id === hostFlyoutOpen?.selectedTabId ?? selectedTabId}
>
{tab.name}
</EuiTab>
));
const linksMapping = {
apmServices: (
<EuiFlexItem grow={false}>
<LinkToApmServices hostName={node.name} apmField={'host.hostname'} />
</EuiFlexItem>
),
uptime: (
<EuiFlexItem
grow={false}
css={css`
margin-right: ${euiTheme.size.l};
`}
>
<LinkToUptime nodeType={nodeType} node={node} />
</EuiFlexItem>
),
};
const headerLinks = links?.map((link) => linksMapping[link]);
if (!showInFlyout) {
return (
<>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="l">
<h1>{node.name}</h1>
</EuiTitle>
</EuiFlexItem>
{links && headerLinks}
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiTabs
css={css`
margin-bottom: ${euiTheme.size.l};
`}
size="l"
>
{tabEntries}
</EuiTabs>
<AssetDetailsTabContent
node={node}
nodeType={nodeType}
currentTimeRange={currentTimeRange}
hostFlyoutOpen={hostFlyoutOpen}
showActionsColumn={showActionsColumn}
renderedTabsSet={renderedTabsSet}
selectedTabId={hostFlyoutOpen?.selectedTabId ?? selectedTabId}
setHostFlyoutState={setHostFlyoutState}
/>
</>
);
}
return (
<EuiFlyout onClose={closeFlyout} ownFocus={false}>
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem>
<EuiTitle size="xs">
<h2>{node.name}</h2>
</EuiTitle>
</EuiFlexItem>
{links && headerLinks}
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiTabs
css={css`
margin-bottom: -25px;
`}
size="s"
>
{tabEntries}
</EuiTabs>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<AssetDetailsTabContent
node={node}
nodeType={nodeType}
currentTimeRange={currentTimeRange}
hostFlyoutOpen={hostFlyoutOpen}
showActionsColumn={showActionsColumn}
renderedTabsSet={renderedTabsSet}
selectedTabId={hostFlyoutOpen?.selectedTabId ?? selectedTabId}
setHostFlyoutState={setHostFlyoutState}
/>
</EuiFlyoutBody>
</EuiFlyout>
<TabSwitcherProvider
initialActiveTabId={tabs.length > 0 ? activeTabId ?? tabs[0].id : undefined}
>
<ContentTemplate
header={
<Header
node={node}
nodeType={nodeType}
compact={renderMode.showInFlyout}
tabs={tabs}
links={links}
onTabsStateChange={onTabsStateChange}
/>
}
body={
<TabContent
node={node}
nodeType={nodeType}
currentTimeRange={currentTimeRange}
overrides={overrides}
onTabsStateChange={onTabsStateChange}
/>
}
renderMode={renderMode}
/>
</TabSwitcherProvider>
);
};

View file

@ -14,7 +14,7 @@ import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { CoreProviders } from '../../apps/common_providers';
import { InfraClientStartDeps, InfraClientStartExports } from '../../types';
import { LazyAssetDetailsWrapper } from './lazy_asset_details_wrapper';
import type { AssetDetailsProps } from './asset_details';
import type { AssetDetailsProps } from './types';
export const ASSET_DETAILS_EMBEDDABLE = 'ASSET_DETAILS_EMBEDDABLE';
@ -71,18 +71,15 @@ export class AssetDetailsEmbeddable extends Embeddable<AssetDetailsEmbeddableInp
<EuiThemeProvider>
<div style={{ width: '100%' }}>
<LazyAssetDetailsWrapper
activeTabId={this.input.activeTabId}
currentTimeRange={this.input.currentTimeRange}
node={this.input.node}
nodeType={this.input.nodeType}
showActionsColumn={this.input.showActionsColumn}
closeFlyout={this.input.closeFlyout}
renderedTabsSet={this.input.renderedTabsSet}
overrides={this.input.overrides}
renderMode={this.input.renderMode}
tabs={this.input.tabs}
hostFlyoutOpen={this.input.hostFlyoutOpen}
setHostFlyoutState={this.input.setHostFlyoutState}
onTabClick={this.input.onTabClick}
onTabsStateChange={this.input.onTabsStateChange}
links={this.input.links}
showInFlyout={this.input.showInFlyout}
/>
</div>
</EuiThemeProvider>

View file

@ -0,0 +1,122 @@
/*
* 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 {
EuiTitle,
EuiSpacer,
EuiFlexItem,
EuiFlexGroup,
EuiTabs,
EuiTab,
useEuiTheme,
useEuiMaxBreakpoint,
useEuiMinBreakpoint,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { EuiShowFor } from '@elastic/eui';
import type { AssetDetailsProps } from '../types';
import { LinkToApmServices } from '../links/link_to_apm_services';
import { LinkToUptime } from '../links/link_to_uptime';
import { useTabSwitcherContext } from '../hooks/use_tab_switcher';
import type { TabIds } from '../types';
type Props = Pick<
AssetDetailsProps,
'node' | 'nodeType' | 'links' | 'tabs' | 'onTabsStateChange'
> & {
compact: boolean;
};
export const Header = ({ nodeType, node, tabs, links, compact, onTabsStateChange }: Props) => {
const { euiTheme } = useEuiTheme();
const { showTab, activeTabId } = useTabSwitcherContext();
const onTabClick = (tabId: TabIds) => {
if (onTabsStateChange) {
onTabsStateChange({ activeTabId: tabId });
}
showTab(tabId);
};
const tabEntries = tabs.map((tab) => (
<EuiTab
{...tab}
key={tab.id}
onClick={() => onTabClick(tab.id)}
isSelected={tab.id === activeTabId}
>
{tab.name}
</EuiTab>
));
const linkComponent = {
apmServices: <LinkToApmServices hostName={node.name} apmField={'host.hostname'} />,
uptime: <LinkToUptime nodeType={nodeType} node={node} />,
};
const headerLinks = links?.map((link, index) => (
<EuiFlexItem key={index} grow={false}>
{linkComponent[link]}
</EuiFlexItem>
));
return (
<>
<EuiFlexGroup gutterSize="m" justifyContent="spaceBetween">
{!compact && (
<EuiShowFor sizes={['l', 'xl']}>
<EuiFlexItem grow={1} />
</EuiShowFor>
)}
<EuiFlexItem
grow
css={css`
${useEuiMaxBreakpoint('l')} {
align-items: flex-start;
}
`}
>
<EuiTitle size={compact ? 'xs' : 'l'}>
<h1>{node.name}</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={compact ? 0 : 1}
css={css`
align-items: flex-start;
${useEuiMinBreakpoint('m')} {
align-items: flex-end;
}
`}
>
<EuiFlexGroup
gutterSize="m"
responsive={false}
justifyContent="flexEnd"
alignItems="center"
css={css`
margin-right: ${compact ? euiTheme.size.l : 0};
`}
>
{headerLinks}
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size={compact ? 's' : 'l'} />
<EuiTabs
bottomBorder={!compact}
css={css`
margin-bottom: calc(${compact ? '-1 *' : ''} (${euiTheme.size.l} + 1px));
`}
size={compact ? 's' : 'l'}
>
{tabEntries}
</EuiTabs>
</>
);
};

View file

@ -0,0 +1,33 @@
/*
* 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 } from 'react';
import createContainer from 'constate';
import { useLazyRef } from '../../../hooks/use_lazy_ref';
import type { TabIds } from '../types';
export function useTabSwitcher({ initialActiveTabId }: { initialActiveTabId?: TabIds }) {
const [activeTabId, setActiveTabId] = useState<TabIds | undefined>(initialActiveTabId);
// This set keeps 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 = useLazyRef(() => new Set([initialActiveTabId]));
const showTab = (tabId: TabIds) => {
renderedTabsSet.current.add(tabId); // On a tab click, mark the tab content as allowed to be rendered
setActiveTabId(tabId);
};
return {
activeTabId,
renderedTabsSet,
showTab,
};
}
export const TabSwitcher = createContainer(useTabSwitcher);
export const [TabSwitcherProvider, useTabSwitcherContext] = TabSwitcher;

View file

@ -7,7 +7,7 @@
import React from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { AssetDetailsProps } from './asset_details';
import type { AssetDetailsProps } from './types';
const AssetDetails = React.lazy(() => import('./asset_details'));

View file

@ -10,12 +10,13 @@ import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { of } from 'rxjs';
import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme';
import { LinkToApmServices, type LinkToApmServicesProps } from './link_to_apm_services';
const mockServices = {
application: {
currentAppId$: { title: 'infra', subscribe: () => {} },
currentAppId$: of('infra'),
navigateToUrl: () => {},
},
http: {

View file

@ -0,0 +1,71 @@
/*
* 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 { useTabSwitcherContext } from '../hooks/use_tab_switcher';
import Metadata from '../tabs/metadata/metadata';
import { Processes } from '../tabs/processes/processes';
import { FlyoutTabIds, type TabState, type AssetDetailsProps } from '../types';
type Props = Pick<
AssetDetailsProps,
'currentTimeRange' | 'node' | 'nodeType' | 'overrides' | 'onTabsStateChange'
>;
export const TabContent = ({
overrides,
currentTimeRange,
node,
nodeType,
onTabsStateChange,
}: Props) => {
const onChange = (state: TabState) => {
if (!onTabsStateChange) {
return;
}
onTabsStateChange(state);
};
return (
<>
<TabPanel activeWhen={FlyoutTabIds.METADATA}>
<Metadata
currentTimeRange={currentTimeRange}
node={node}
nodeType={nodeType}
showActionsColumn={overrides?.metadata?.showActionsColumn}
search={overrides?.metadata?.query}
onSearchChange={(query) => onChange({ metadata: { query } })}
/>
</TabPanel>
<TabPanel activeWhen={FlyoutTabIds.PROCESSES}>
<Processes
node={node}
nodeType={nodeType}
currentTime={currentTimeRange.to}
searchFilter={overrides?.processes?.query}
onSearchFilterChange={(query) => onChange({ processes: { query } })}
/>
</TabPanel>
</>
);
};
const TabPanel = ({
activeWhen,
children,
}: {
activeWhen: FlyoutTabIds;
children: React.ReactNode;
}) => {
const { renderedTabsSet, activeTabId } = useTabSwitcherContext();
return renderedTabsSet.current.has(activeWhen) ? (
<div hidden={activeTabId !== activeWhen}>{children}</div>
) : null;
};

View file

@ -8,9 +8,9 @@
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
import { useMetricsDataViewContext } from '../../../pages/metrics/hosts/hooks/use_data_view';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useUnifiedSearchContext } from '../../../pages/metrics/hosts/hooks/use_unified_search';
import { useMetricsDataViewContext } from '../../../../pages/metrics/hosts/hooks/use_data_view';
import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana';
import { useUnifiedSearchContext } from '../../../../pages/metrics/hosts/hooks/use_unified_search';
import { buildMetadataFilter } from './build_metadata_filter';
interface AddMetadataFilterButtonProps {

View file

@ -8,14 +8,14 @@
import React from 'react';
import { Metadata, type MetadataProps } from './metadata';
import { useMetadata } from '../hooks/use_metadata';
import { useSourceContext } from '../../../containers/metrics_source';
import { useMetadata } from '../../hooks/use_metadata';
import { useSourceContext } from '../../../../containers/metrics_source';
import { render } from '@testing-library/react';
import { I18nProvider } from '@kbn/i18n-react';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
jest.mock('../../../containers/metrics_source');
jest.mock('../hooks/use_metadata');
jest.mock('../../../../containers/metrics_source');
jest.mock('../../hooks/use_metadata');
const metadataProps: MetadataProps = {
currentTimeRange: {

View file

@ -9,14 +9,14 @@ import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { InventoryItemType } from '../../../../common/inventory_models/types';
import { findInventoryModel } from '../../../../common/inventory_models';
import type { MetricsTimeInput } from '../../../pages/metrics/metric_detail/hooks/use_metrics_time';
import { useMetadata } from '../hooks/use_metadata';
import { useSourceContext } from '../../../containers/metrics_source';
import type { InventoryItemType } from '../../../../../common/inventory_models/types';
import { findInventoryModel } from '../../../../../common/inventory_models';
import type { MetricsTimeInput } from '../../../../pages/metrics/metric_detail/hooks/use_metrics_time';
import { useMetadata } from '../../hooks/use_metadata';
import { useSourceContext } from '../../../../containers/metrics_source';
import { Table } from './table';
import { getAllFields } from './utils';
import type { HostNodeRow } from '../types';
import type { HostNodeRow } from '../../types';
export interface MetadataSearchUrlState {
metadataSearchUrlState: string;
@ -28,15 +28,17 @@ export interface MetadataProps {
node: HostNodeRow;
nodeType: InventoryItemType;
showActionsColumn?: boolean;
persistMetadataSearchToUrlState?: MetadataSearchUrlState;
search?: string;
onSearchChange?: (query: string) => void;
}
export const Metadata = ({
node,
currentTimeRange,
nodeType,
showActionsColumn,
persistMetadataSearchToUrlState,
search,
showActionsColumn = false,
onSearchChange,
}: MetadataProps) => {
const nodeId = node.name;
const inventoryModel = findInventoryModel(nodeType);
@ -81,7 +83,8 @@ export const Metadata = ({
return (
<Table
persistMetadataSearchToUrlState={persistMetadataSearchToUrlState}
search={search}
onSearchChange={onSearchChange}
showActionsColumn={showActionsColumn}
rows={fields}
loading={metadataLoading}

View file

@ -11,8 +11,8 @@ import ReactDOM from 'react-dom';
import { Subscription } from 'rxjs';
import { Embeddable, EmbeddableInput, IContainer } from '@kbn/embeddable-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { CoreProviders } from '../../../apps/common_providers';
import { InfraClientStartDeps, InfraClientStartExports } from '../../../types';
import { CoreProviders } from '../../../../apps/common_providers';
import { InfraClientStartDeps, InfraClientStartExports } from '../../../../types';
import { LazyMetadataWrapper } from './lazy_metadata_wrapper';
import type { MetadataProps } from './metadata';
@ -75,7 +75,8 @@ export class MetadataEmbeddable extends Embeddable<MetadataEmbeddableInput> {
node={this.input.node}
nodeType={this.input.nodeType}
showActionsColumn={this.input.showActionsColumn}
persistMetadataSearchToUrlState={this.input.persistMetadataSearchToUrlState}
onSearchChange={this.input.onSearchChange}
search={this.input.search}
/>
</div>
</EuiThemeProvider>

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import type { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import type { InfraClientStartServicesAccessor } from '../../../types';
import type { InfraClientStartServicesAccessor } from '../../../../types';
import {
MetadataEmbeddable,
MetadataEmbeddableInput,

View file

@ -10,7 +10,7 @@ import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { Meta, Story } from '@storybook/react/types-6-0';
import React from 'react';
import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme';
import { decorateWithGlobalStorybookThemeProviders } from '../../../../test_utils/use_global_storybook_theme';
import { Table, Props } from './table';
const mockServices = {

View file

@ -21,7 +21,6 @@ import useToggle from 'react-use/lib/useToggle';
import { debounce } from 'lodash';
import { Query } from '@elastic/eui';
import { AddMetadataFilterButton } from './add_metadata_filter_button';
import { MetadataSearchUrlState } from './metadata';
interface Row {
name: string;
@ -32,7 +31,8 @@ export interface Props {
rows: Row[];
loading: boolean;
showActionsColumn?: boolean;
persistMetadataSearchToUrlState?: MetadataSearchUrlState;
search?: string;
onSearchChange?: (query: string) => void;
}
interface SearchErrorType {
@ -65,10 +65,9 @@ const LOADING = i18n.translate('xpack.infra.metadataEmbeddable.loading', {
defaultMessage: 'Loading...',
});
export const Table = (props: Props) => {
const { rows, loading, showActionsColumn } = props;
export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn }: Props) => {
const [searchError, setSearchError] = useState<SearchErrorType | null>(null);
const [metadataSearch, setMetadataSearch] = useState('');
const [metadataSearch, setMetadataSearch] = useState(search);
const defaultColumns = useMemo(
() => [
@ -93,13 +92,12 @@ export const Table = (props: Props) => {
const debouncedSearchOnChange = useMemo(
() =>
debounce<(queryText: string) => void>((queryText) => {
return props.persistMetadataSearchToUrlState
? props.persistMetadataSearchToUrlState.setMetadataSearchUrlState({
metadataSearch: String(queryText) ?? '',
})
: setMetadataSearch(String(queryText) ?? '');
if (onSearchChange) {
onSearchChange(queryText);
}
setMetadataSearch(queryText);
}, 500),
[props.persistMetadataSearchToUrlState]
[onSearchChange]
);
const searchBarOnChange = useCallback(
@ -114,7 +112,7 @@ export const Table = (props: Props) => {
[debouncedSearchOnChange]
);
const search: EuiSearchBarProps = {
const searchBar: EuiSearchBarProps = {
onChange: searchBarOnChange,
box: {
'data-test-subj': 'infraHostMetadataSearchBarInput',
@ -122,13 +120,7 @@ export const Table = (props: Props) => {
schema: true,
placeholder: SEARCH_PLACEHOLDER,
},
query: props.persistMetadataSearchToUrlState
? props.persistMetadataSearchToUrlState.metadataSearchUrlState
? Query.parse(props.persistMetadataSearchToUrlState.metadataSearchUrlState)
: Query.MATCH_ALL
: metadataSearch
? Query.parse(metadataSearch)
: Query.MATCH_ALL,
query: metadataSearch ? Query.parse(metadataSearch) : Query.MATCH_ALL,
};
const columns = useMemo(
@ -159,7 +151,7 @@ export const Table = (props: Props) => {
columns={columns}
items={rows}
rowProps={{ className: 'euiTableRow-hasActions' }}
search={search}
search={searchBar}
loading={loading}
error={searchError ? `${searchError.message}` : ''}
message={

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { InfraMetadata } from '../../../../common/http_api';
import type { InfraMetadata } from '../../../../../common/http_api';
export const getAllFields = (metadata: InfraMetadata | null) => {
if (!metadata?.info) return [];

View file

@ -12,7 +12,7 @@ import React from 'react';
import { DecorateWithKibanaContext } from './processes.story_decorators';
import { Processes, type ProcessesProps } from './processes';
import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme';
import { decorateWithGlobalStorybookThemeProviders } from '../../../../test_utils/use_global_storybook_theme';
export default {
title: 'infra/Asset Details View/Components/Processes',

View file

@ -12,7 +12,7 @@ import React from 'react';
import { useParameter } from '@storybook/addons';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { SourceProvider } from '../../../containers/metrics_source';
import { SourceProvider } from '../../../../containers/metrics_source';
export const DecorateWithKibanaContext = <StoryFnReactReturnType extends React.ReactNode>(
wrappedStory: () => StoryFnReactReturnType,

View file

@ -21,22 +21,22 @@ import { parseSearchString } from './parse_search_string';
import { ProcessesTable } from './processes_table';
import { STATE_NAMES } from './states';
import { SummaryTable } from './summary_table';
import { TabContent } from '../../../pages/metrics/inventory_view/components/node_details/tabs/shared';
import { TabContent } from '../../../../pages/metrics/inventory_view/components/node_details/tabs/shared';
import {
SortBy,
useProcessList,
ProcessListContextProvider,
} from '../../../pages/metrics/inventory_view/hooks/use_process_list';
import { getFieldByType } from '../../../../common/inventory_models';
import type { HostNodeRow } from '../types';
import type { InventoryItemType } from '../../../../common/inventory_models/types';
} from '../../../../pages/metrics/inventory_view/hooks/use_process_list';
import { getFieldByType } from '../../../../../common/inventory_models';
import type { HostNodeRow } from '../../types';
import type { InventoryItemType } from '../../../../../common/inventory_models/types';
export interface ProcessesProps {
node: HostNodeRow;
nodeType: InventoryItemType;
currentTime: number;
searchFilter?: string;
setSearchFilter?: (searchFilter: { searchFilter: string }) => void;
onSearchFilterChange?: (searchFilter: string) => void;
}
const options = Object.entries(STATE_NAMES).map(([value, view]: [string, string]) => ({
@ -49,7 +49,7 @@ export const Processes = ({
node,
nodeType,
searchFilter,
setSearchFilter,
onSearchFilterChange,
}: ProcessesProps) => {
const [searchText, setSearchText] = useState(searchFilter ?? '');
const [searchBarState, setSearchBarState] = useState<Query>(() =>
@ -75,12 +75,12 @@ export const Processes = ({
const debouncedSearchOnChange = useMemo(() => {
return debounce<(queryText: string) => void>((queryText) => {
if (setSearchFilter) {
setSearchFilter({ searchFilter: queryText });
if (onSearchFilterChange) {
onSearchFilterChange(queryText);
}
setSearchText(queryText);
}, 500);
}, [setSearchFilter]);
}, [onSearchFilterChange]);
const searchBarOnChange = useCallback(
({ query, queryText }) => {
@ -92,11 +92,11 @@ export const Processes = ({
const clearSearchBar = useCallback(() => {
setSearchBarState(Query.MATCH_ALL);
if (setSearchFilter) {
setSearchFilter({ searchFilter: '' });
if (onSearchFilterChange) {
onSearchFilterChange('');
}
setSearchText('');
}, [setSearchFilter]);
}, [onSearchFilterChange]);
return (
<TabContent>

View file

@ -25,13 +25,13 @@ import {
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import { FORMATTERS } from '../../../../common/formatters';
import type { SortBy } from '../../../pages/metrics/inventory_view/hooks/use_process_list';
import { FORMATTERS } from '../../../../../common/formatters';
import type { SortBy } from '../../../../pages/metrics/inventory_view/hooks/use_process_list';
import type { Process } from './types';
import { ProcessRow } from '../../../pages/metrics/inventory_view/components/node_details/tabs/processes/process_row';
import { ProcessRow } from '../../../../pages/metrics/inventory_view/components/node_details/tabs/processes/process_row';
import { StateBadge } from './state_badge';
import { STATE_ORDER } from './states';
import type { ProcessListAPIResponse } from '../../../../common/http_api';
import type { ProcessListAPIResponse } from '../../../../../common/http_api';
interface TableProps {
processList: ProcessListAPIResponse['processList'];

View file

@ -18,7 +18,7 @@ import {
EuiHorizontalRule,
} from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import type { ProcessListAPIResponse } from '../../../../common/http_api';
import type { ProcessListAPIResponse } from '../../../../../common/http_api';
import { STATE_NAMES } from './states';
interface Props {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import type { MetricsExplorerSeries } from '../../../../common/http_api';
import type { MetricsExplorerSeries } from '../../../../../common/http_api';
import { STATE_NAMES } from './states';
export interface Process {

View file

@ -1,70 +0,0 @@
/*
* 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 { type AssetDetailsProps, type TabIds, FlyoutTabIds } from '../asset_details';
import Metadata from '../metadata/metadata';
import { Processes } from '../processes/processes';
export const AssetDetailsTabContent = ({
renderedTabsSet,
hostFlyoutOpen,
currentTimeRange,
node,
nodeType,
showActionsColumn,
setHostFlyoutState,
selectedTabId,
}: Pick<
AssetDetailsProps,
| 'renderedTabsSet'
| 'hostFlyoutOpen'
| 'currentTimeRange'
| 'node'
| 'nodeType'
| 'showActionsColumn'
| 'setHostFlyoutState'
> & { selectedTabId: TabIds }) => {
const persistMetadataSearchToUrlState =
setHostFlyoutState && hostFlyoutOpen
? {
metadataSearchUrlState: hostFlyoutOpen.metadataSearch,
setMetadataSearchUrlState: setHostFlyoutState,
}
: undefined;
const isTabSelected = (flyoutTabId: FlyoutTabIds) => {
return selectedTabId === flyoutTabId;
};
return (
<>
{renderedTabsSet.current.has(FlyoutTabIds.METADATA) && (
<div hidden={!isTabSelected(FlyoutTabIds.METADATA)}>
<Metadata
currentTimeRange={currentTimeRange}
node={node}
nodeType={nodeType}
showActionsColumn={showActionsColumn}
persistMetadataSearchToUrlState={persistMetadataSearchToUrlState}
/>
</div>
)}
{renderedTabsSet.current.has(FlyoutTabIds.PROCESSES) && (
<div hidden={!isTabSelected(FlyoutTabIds.PROCESSES)}>
<Processes
node={node}
nodeType={nodeType}
currentTime={currentTimeRange.to}
searchFilter={hostFlyoutOpen?.searchFilter}
setSearchFilter={setHostFlyoutState}
/>
</div>
)}
</>
);
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { InventoryItemType } from '../../../common/inventory_models/types';
import { InfraAssetMetricType } from '../../../common/http_api';
export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider';
@ -21,3 +22,55 @@ export type HostNodeRow = HostMetadata &
HostMetrics & {
name: string;
};
export enum FlyoutTabIds {
METADATA = 'metadata',
PROCESSES = 'processes',
}
export type TabIds = `${FlyoutTabIds}`;
export interface TabState {
metadata?: {
query?: string;
showActionsColumn?: boolean;
};
processes?: {
query?: string;
};
}
export interface FlyoutProps {
closeFlyout: () => void;
showInFlyout: true;
}
export interface FullPageProps {
showInFlyout: false;
}
export type RenderMode = FlyoutProps | FullPageProps;
export interface Tab {
id: FlyoutTabIds;
name: string;
'data-test-subj': string;
}
export interface AssetDetailsProps {
node: HostNodeRow;
nodeType: InventoryItemType;
currentTimeRange: {
interval: string;
from: number;
to: number;
};
tabs: Tab[];
activeTabId?: TabIds;
overrides?: TabState;
renderMode?: RenderMode;
onTabsStateChange?: TabsStateChangeFn;
links?: Array<'uptime' | 'apmServices'>;
}
export type TabsStateChangeFn = (state: TabState & { activeTabId?: TabIds }) => void;

View file

@ -8,10 +8,8 @@
import React, { useMemo } from 'react';
import type { InventoryItemType } from '../../../../../../common/inventory_models/types';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
import { useLazyRef } from '../../../../../hooks/use_lazy_ref';
import type { HostNodeRow } from '../../hooks/use_hosts_table';
import type { Tab } from '../../../../../components/asset_details/asset_details';
import { useHostFlyoutOpen } from '../../hooks/use_host_flyout_open_url_state';
import { HostFlyout, useHostFlyoutUrlState } from '../../hooks/use_host_flyout_url_state';
import { AssetDetails } from '../../../../../components/asset_details/asset_details';
import { metadataTab, processesTab } from './tabs';
@ -32,31 +30,36 @@ export const FlyoutWrapper = ({ node, closeFlyout }: Props) => {
[getDateRangeAsTimestamp]
);
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen();
// 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 = useLazyRef(() => new Set([hostFlyoutOpen?.selectedTabId]));
const onTabClick = (tab: Tab) => {
renderedTabsSet.current.add(tab.id); // On a tab click, mark the tab content as allowed to be rendered
setHostFlyoutOpen({ selectedTabId: tab.id });
};
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutUrlState();
return (
<AssetDetails
node={node}
closeFlyout={closeFlyout}
onTabClick={onTabClick}
nodeType={NODE_TYPE}
currentTimeRange={currentTimeRange}
hostFlyoutOpen={hostFlyoutOpen}
setHostFlyoutState={setHostFlyoutOpen}
showActionsColumn
showInFlyout
renderedTabsSet={renderedTabsSet}
activeTabId={hostFlyoutOpen?.selectedTabId}
overrides={{
metadata: {
query: hostFlyoutOpen?.metadataSearch,
showActionsColumn: true,
},
processes: {
query: hostFlyoutOpen?.processSearch,
},
}}
onTabsStateChange={(state) =>
setHostFlyoutOpen({
metadataSearch: state.metadata?.query,
processSearch: state.processes?.query,
selectedTabId: state.activeTabId as HostFlyout['selectedTabId'],
})
}
tabs={[metadataTab, processesTab]}
links={['apmServices', 'uptime']}
nodeType={NODE_TYPE}
renderMode={{
showInFlyout: true,
closeFlyout,
}}
/>
);
};

View file

@ -6,8 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { Tab } from '../../../../../components/asset_details/asset_details';
import { FlyoutTabIds } from '../../hooks/use_host_flyout_open_url_state';
import { FlyoutTabIds, type Tab } from '../../../../../components/asset_details/types';
export const processesTab: Tab = {
id: FlyoutTabIds.PROCESSES,

View file

@ -1,95 +0,0 @@
/*
* 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';
export enum FlyoutTabIds {
METADATA = 'metadata',
PROCESSES = 'processes',
}
export const GET_DEFAULT_TABLE_PROPERTIES = {
clickedItemId: '',
selectedTabId: FlyoutTabIds.METADATA,
searchFilter: '',
metadataSearch: '',
};
const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'hostFlyoutOpen';
type Action = rt.TypeOf<typeof ActionRT>;
export type SetNewHostFlyoutOpen = (newProp: Action) => void;
type SetNewHostFlyoutClose = () => void;
export const useHostFlyoutOpen = (): [
HostFlyoutOpen,
SetNewHostFlyoutOpen,
SetNewHostFlyoutClose
] => {
const [urlState, setUrlState] = useUrlState<HostFlyoutUrl>({
defaultState: '',
decodeUrlState,
encodeUrlState,
urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY,
});
const setHostFlyoutOpen = (newProps: Action) =>
typeof urlState !== 'string'
? setUrlState({ ...urlState, ...newProps })
: setUrlState({ ...GET_DEFAULT_TABLE_PROPERTIES, ...newProps });
const setFlyoutClosed = () => setUrlState('');
return [urlState as HostFlyoutOpen, setHostFlyoutOpen, setFlyoutClosed];
};
const FlyoutTabIdRT = rt.union([rt.literal('metadata'), rt.literal('processes')]);
const ClickedItemIdRT = rt.string;
const SearchFilterRT = rt.string;
const SetFlyoutTabId = rt.partial({
selectedTabId: FlyoutTabIdRT,
});
const SetClickedItemIdRT = rt.partial({
clickedItemId: ClickedItemIdRT,
});
const SetSearchFilterRT = rt.partial({
searchFilter: SearchFilterRT,
});
const SetMetadataSearchRT = rt.partial({
metadataSearch: SearchFilterRT,
});
const ActionRT = rt.intersection([
SetClickedItemIdRT,
SetFlyoutTabId,
SetSearchFilterRT,
SetMetadataSearchRT,
]);
const HostFlyoutOpenRT = rt.type({
clickedItemId: ClickedItemIdRT,
selectedTabId: FlyoutTabIdRT,
searchFilter: SearchFilterRT,
metadataSearch: SearchFilterRT,
});
const HostFlyoutUrlRT = rt.union([HostFlyoutOpenRT, rt.string]);
type HostFlyoutUrl = rt.TypeOf<typeof HostFlyoutUrlRT>;
type HostFlyoutOpen = rt.TypeOf<typeof HostFlyoutOpenRT>;
const encodeUrlState = HostFlyoutUrlRT.encode;
const decodeUrlState = (value: unknown) => {
return pipe(HostFlyoutUrlRT.decode(value), fold(constant('undefined'), identity));
};

View file

@ -0,0 +1,73 @@
/*
* 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 isEmpty from 'lodash/isEmpty';
import omitBy from 'lodash/omitBy';
import { FlyoutTabIds } from '../../../../components/asset_details/types';
import { useUrlState } from '../../../../utils/use_url_state';
export const DEFAULT_STATE: HostFlyout = {
clickedItemId: '',
selectedTabId: FlyoutTabIds.METADATA,
processSearch: undefined,
metadataSearch: undefined,
};
const HOST_FLYOUT_URL_STATE_KEY = 'hostFlyoutOpen';
type SetHostFlyoutState = (newProp: Payload | null) => void;
export const useHostFlyoutUrlState = (): [HostFlyoutUrl, SetHostFlyoutState] => {
const [urlState, setUrlState] = useUrlState<HostFlyoutUrl>({
defaultState: null,
decodeUrlState,
encodeUrlState,
urlStateKey: HOST_FLYOUT_URL_STATE_KEY,
});
const setHostFlyoutState = (newProps: Payload | null) => {
if (!newProps) {
setUrlState(DEFAULT_STATE);
} else {
const payload = omitBy(newProps, isEmpty);
setUrlState({ ...(urlState ?? DEFAULT_STATE), ...payload });
}
};
return [urlState as HostFlyoutUrl, setHostFlyoutState];
};
const FlyoutTabIdRT = rt.union([
rt.literal(FlyoutTabIds.METADATA),
rt.literal(FlyoutTabIds.PROCESSES),
]);
const HostFlyoutStateRT = rt.intersection([
rt.type({
clickedItemId: rt.string,
selectedTabId: FlyoutTabIdRT,
}),
rt.partial({
processSearch: rt.string,
metadataSearch: rt.string,
}),
]);
const HostFlyoutUrlRT = rt.union([HostFlyoutStateRT, rt.null]);
type HostFlyoutState = rt.TypeOf<typeof HostFlyoutStateRT>;
type HostFlyoutUrl = rt.TypeOf<typeof HostFlyoutUrlRT>;
type Payload = Partial<HostFlyoutState>;
export type HostFlyout = rt.TypeOf<typeof HostFlyoutStateRT>;
const encodeUrlState = HostFlyoutUrlRT.encode;
const decodeUrlState = (value: unknown) => {
return pipe(HostFlyoutUrlRT.decode(value), fold(constant(undefined), identity));
};

View file

@ -20,7 +20,7 @@ import {
InfraAssetMetricsItem,
InfraAssetMetricType,
} from '../../../../../common/http_api';
import { useHostFlyoutOpen } from './use_host_flyout_open_url_state';
import { useHostFlyoutUrlState } from './use_host_flyout_url_state';
import { Sorting, useHostsTableUrlState } from './use_hosts_table_url_state';
import { useHostsViewContext } from './use_hosts_view';
import { useUnifiedSearchContext } from './use_unified_search';
@ -171,9 +171,9 @@ export const useHostsTable = () => {
services: { telemetry },
} = useKibanaContextForPlugin();
const [hostFlyoutOpen, setHostFlyoutOpen, setFlyoutClosed] = useHostFlyoutOpen();
const [hostFlyoutState, setHostFlyoutState] = useHostFlyoutUrlState();
const closeFlyout = () => setFlyoutClosed();
const closeFlyout = useCallback(() => setHostFlyoutState(null), [setHostFlyoutState]);
const reportHostEntryClick = useCallback(
({ name, cloudProvider }: HostNodeRow['title']) => {
@ -204,8 +204,8 @@ export const useHostsTable = () => {
const items = useMemo(() => buildItemsList(hostNodes), [hostNodes]);
const clickedItem = useMemo(
() => items.find(({ id }) => id === hostFlyoutOpen.clickedItemId),
[hostFlyoutOpen.clickedItemId, items]
() => items.find(({ id }) => id === hostFlyoutState?.clickedItemId),
[hostFlyoutState?.clickedItemId, items]
);
const currentPage = useMemo(() => {
@ -228,19 +228,19 @@ export const useHostsTable = () => {
name: toggleDialogActionLabel,
description: toggleDialogActionLabel,
icon: ({ id }) =>
hostFlyoutOpen.clickedItemId && id === hostFlyoutOpen.clickedItemId
hostFlyoutState?.clickedItemId && id === hostFlyoutState?.clickedItemId
? 'minimize'
: 'expand',
type: 'icon',
'data-test-subj': 'hostsView-flyout-button',
onClick: ({ id }) => {
setHostFlyoutOpen({
setHostFlyoutState({
clickedItemId: id,
});
if (id === hostFlyoutOpen.clickedItemId) {
setFlyoutClosed();
if (id === hostFlyoutState?.clickedItemId) {
setHostFlyoutState(null);
} else {
setHostFlyoutOpen({ clickedItemId: id });
setHostFlyoutState({ clickedItemId: id });
}
},
},
@ -317,11 +317,10 @@ export const useHostsTable = () => {
},
],
[
hostFlyoutOpen.clickedItemId,
hostFlyoutState?.clickedItemId,
reportHostEntryClick,
searchCriteria.dateRange,
setFlyoutClosed,
setHostFlyoutOpen,
setHostFlyoutState,
]
);
@ -331,7 +330,7 @@ export const useHostsTable = () => {
currentPage,
closeFlyout,
items,
isFlyoutOpen: !!hostFlyoutOpen.clickedItemId,
isFlyoutOpen: !!hostFlyoutState?.clickedItemId,
onTableChange,
pagination,
sorting,