mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
c12e17c6dc
commit
3d05fc6ada
35 changed files with 545 additions and 476 deletions
|
@ -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({});
|
||||
|
|
|
@ -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: () => {} },
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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 {
|
|
@ -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: {
|
|
@ -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}
|
|
@ -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>
|
|
@ -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,
|
|
@ -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 = {
|
|
@ -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={
|
|
@ -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 [];
|
|
@ -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',
|
|
@ -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,
|
|
@ -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>
|
|
@ -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'];
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
};
|
|
@ -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));
|
||||
};
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue