diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/add_pin_to_row.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/add_pin_to_row.tsx new file mode 100644 index 0000000000000..031f9cd5c6c78 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/add_pin_to_row.tsx @@ -0,0 +1,75 @@ +/* + * 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, { Dispatch } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import type { Field } from './utils'; + +interface AddMetadataPinToRowProps { + fieldName: Field['name']; + pinnedItems: Array; + setPinnedItems: Dispatch | undefined>>; +} + +const PIN_FIELD = i18n.translate('xpack.infra.hostsViewPage.flyout.metadata.pinField', { + defaultMessage: 'Pin Field', +}); + +export const AddMetadataPinToRow = ({ + fieldName, + pinnedItems, + setPinnedItems, +}: AddMetadataPinToRowProps) => { + const handleAddPin = (pin: Field['name']) => { + setPinnedItems([...pinnedItems, pin]); + }; + + const handleRemovePin = (pin: Field['name']) => { + if (pinnedItems && pinnedItems.includes(pin)) { + setPinnedItems((pinnedItems ?? []).filter((pinName: string) => pin !== pinName)); + } + }; + + if (pinnedItems?.includes(fieldName)) { + return ( + + + handleRemovePin(fieldName)} + /> + + + ); + } + + return ( + + + handleAddPin(fieldName)} + /> + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.tsx index 4cc037335c773..e39815a415887 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/table.tsx @@ -12,6 +12,7 @@ import { EuiLink, EuiInMemoryTable, EuiSearchBarProps, + EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useMemo, useState } from 'react'; @@ -19,16 +20,17 @@ import { FormattedMessage } from '@kbn/i18n-react'; import useToggle from 'react-use/lib/useToggle'; import { debounce } from 'lodash'; import { Query } from '@elastic/eui'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import type { HorizontalAlignment } from '@elastic/eui'; import { useHostFlyoutOpen } from '../../../hooks/use_host_flyout_open_url_state'; import { AddMetadataFilterButton } from './add_metadata_filter_button'; - -interface Row { - name: string; - value: string | string[] | undefined; -} +import { AddMetadataPinToRow } from './add_pin_to_row'; +import { LOCAL_STORAGE_PINNED_METADATA_ROWS } from '../../../constants'; +import { getRowsWithPins } from './utils'; +import type { Field } from './utils'; interface Props { - rows: Row[]; + rows: Field[]; loading: boolean; } @@ -72,6 +74,18 @@ export const Table = (props: Props) => { const { rows, loading } = props; const [searchError, setSearchError] = useState(null); const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen(); + const [fieldsWithPins, setFieldsWithPins] = useState(rows); + + const [pinnedItems, setPinnedItems] = useLocalStorage>( + LOCAL_STORAGE_PINNED_METADATA_ROWS, + [] + ); + + useMemo(() => { + if (pinnedItems) { + setFieldsWithPins(getRowsWithPins(rows, pinnedItems) ?? rows); + } + }, [rows, pinnedItems]); const debouncedSearchOnChange = useMemo( () => @@ -108,6 +122,23 @@ export const Table = (props: Props) => { const columns = useMemo( () => [ + { + field: 'value', + name: , + align: 'center' as HorizontalAlignment, + width: '5%', + sortable: false, + showOnHover: true, + render: (_name: string, item: Field) => { + return ( + + ); + }, + }, { field: 'name', name: FIELD_LABEL, @@ -118,21 +149,22 @@ export const Table = (props: Props) => { { field: 'value', name: VALUE_LABEL, - width: '55%', + width: '50%', sortable: false, - render: (_name: string, item: Row) => , + render: (_name: string, item: Field) => , }, { field: 'value', name: 'Actions', + width: '10%', sortable: false, showOnHover: true, - render: (_name: string, item: Row) => { + render: (_name: string, item: Field) => { return ; }, }, ], - [] + [pinnedItems, setPinnedItems] ); return ( @@ -141,7 +173,7 @@ export const Table = (props: Props) => { tableLayout={'fixed'} responsive={false} columns={columns} - items={rows} + items={fieldsWithPins} rowProps={{ className: 'euiTableRow-hasActions' }} search={search} loading={loading} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts index 06ca4e85cc5e6..15654600901de 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/utils.ts @@ -7,6 +7,11 @@ import type { InfraMetadata } from '../../../../../../../common/http_api'; +export interface Field { + name: string; + value: string | string[] | undefined; +} + export const getAllFields = (metadata: InfraMetadata | null) => { if (!metadata?.info) return []; return prune([ @@ -105,5 +110,17 @@ export const getAllFields = (metadata: InfraMetadata | null) => { ]); }; -const prune = (fields: Array<{ name: string; value: string | string[] | undefined }>) => - fields.filter((f) => !!f.value); +const prune = (fields: Field[]) => fields.filter((f) => !!f.value); + +export const getRowsWithPins = (rows: Field[], pinnedItems: Array) => { + if (pinnedItems.length > 0) { + const { pinned, other } = rows.reduce( + (acc, row) => { + (pinnedItems.includes(row.name) ? acc.pinned : acc.other).push(row); + return acc; + }, + { pinned: [] as Field[], other: [] as Field[] } + ); + return [...pinned, ...other]; + } +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index 69cfc446d0095..d9cb16d922ddb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -17,6 +17,7 @@ export const DEFAULT_HOST_LIMIT: HostLimitOptions = 100; export const DEFAULT_PAGE_SIZE = 10; export const LOCAL_STORAGE_HOST_LIMIT_KEY = 'hostsView:hostLimitSelection'; export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; +export const LOCAL_STORAGE_PINNED_METADATA_ROWS = 'hostsView:pinnedMetadataRows'; export const ALL_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ALL, diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index e9000a9cf3e6d..c44fc9b782ebb 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -151,7 +151,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Tests // Failing: See https://github.com/elastic/kibana/issues/155429 - describe.skip('Hosts View', function () { + describe('Hosts View', function () { this.tags('includeFirefox'); before(async () => { @@ -260,6 +260,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { expect(removeFilterShouldNotExist).to.be(false); }); + it('should render metadata tab, pin and unpin table row', async () => { + const metadataTab = await pageObjects.infraHostsView.getMetadataTabName(); + expect(metadataTab).to.contain('Metadata'); + + // Add Pin + await pageObjects.infraHostsView.clickAddMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); + + // Persist pin after refresh + await browser.refresh(); + await pageObjects.infraHome.waitForLoading(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(true); + + // Remove Pin + await pageObjects.infraHostsView.clickRemoveMetadataPin(); + expect(await pageObjects.infraHostsView.getRemovePinExist()).to.be(false); + }); + it('should navigate to Uptime after click', async () => { await pageObjects.infraHostsView.clickFlyoutUptimeLink(); await pageObjects.infraHome.waitForLoading(); diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index 6478d208226ad..88a5aad0765e3 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -52,6 +52,14 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.click('hostsView-flyout-metadata-remove-filter'); }, + async clickAddMetadataPin() { + return testSubjects.click('hostsView-flyout-metadata-add-pin'); + }, + + async clickRemoveMetadataPin() { + return testSubjects.click('hostsView-flyout-metadata-remove-pin'); + }, + async getHostsLandingPageDisabled() { const container = await testSubjects.find('hostView-no-enable-access'); const containerText = await container.getVisibleText(); @@ -139,7 +147,7 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.existOrFail('embeddablePanelAction-openInLens'); }, - // Flyout Tabs + // Flyout Tabs: Metadata getMetadataTab() { return testSubjects.find('hostsView-flyout-tabs-metadata'); }, @@ -161,6 +169,11 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { return testSubjects.exists('hostsView-flyout-metadata-remove-filter'); }, + async getRemovePinExist() { + return testSubjects.exists('hostsView-flyout-metadata-remove-pin'); + }, + + // Flyout Tabs: Processes async getProcessesTabContentTitle(index: number) { const processesListElements = await testSubjects.findAll('infraProcessesSummaryTableItem'); return processesListElements[index].findByCssSelector('dt');