Skip to content

Commit

Permalink
[Infrastructure UI] Asset Details: Add pins to the metadata table (#1…
Browse files Browse the repository at this point in the history
…61074)

Closes #155190

## Summary

This PR adds the possibility to pin different rows inside the metadata
table in asset details embeddable. The pins are persisted in the local
storage and should be available after refreshing/reopening the host
flyout. The order and sorting are explained in [this
comment](#155190 (comment)),
so basically we keep the original sorting order of the table (`host`,
`cloud`, `agent`) also for the pins.

## Testing 
- Go to hosts view and open a single host flyout (metadata tab)
- Try to add / remove pins
- Check if the pins are persisted after a page refresh



https://github.com/elastic/kibana/assets/14139027/62873e7e-b5f0-444c-94ff-5e19f2f46f58
  • Loading branch information
jennypavlova authored Jul 6, 2023
1 parent 8afb9b0 commit b641a22
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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<Field['name']>;
onPinned: Dispatch<React.SetStateAction<Array<Field['name']> | undefined>>;
}

const PIN_FIELD = i18n.translate('xpack.infra.metadataEmbeddable.pinField', {
defaultMessage: 'Pin Field',
});

export const AddMetadataPinToRow = ({
fieldName,
pinnedItems,
onPinned,
}: AddMetadataPinToRowProps) => {
const handleAddPin = () => {
onPinned([...pinnedItems, fieldName]);
};

const handleRemovePin = () => {
if (pinnedItems && pinnedItems.includes(fieldName)) {
onPinned((pinnedItems ?? []).filter((pinName: string) => fieldName !== pinName));
}
};

if (pinnedItems?.includes(fieldName)) {
return (
<span>
<EuiToolTip
content={i18n.translate('xpack.infra.metadataEmbeddable.unpinField', {
defaultMessage: 'Unpin field',
})}
>
<EuiButtonIcon
size="s"
color="primary"
iconType="pinFilled"
data-test-subj="infraMetadataEmbeddableRemovePin"
aria-label={i18n.translate('xpack.infra.metadataEmbeddable.pinAriaLabel', {
defaultMessage: 'Pinned field',
})}
onClick={handleRemovePin}
/>
</EuiToolTip>
</span>
);
}

return (
<span className="euiTableCellContent__hoverItem expandedItemActions__completelyHide">
<EuiToolTip content={PIN_FIELD}>
<EuiButtonIcon
color="primary"
size="s"
iconType="pin"
data-test-subj="infraMetadataEmbeddableAddPin"
aria-label={PIN_FIELD}
onClick={handleAddPin}
/>
</EuiToolTip>
</span>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiIcon,
EuiInMemoryTable,
EuiSearchBarProps,
type HorizontalAlignment,
Expand All @@ -20,15 +21,13 @@ 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 { AddMetadataFilterButton } from './add_metadata_filter_button';

interface Row {
name: string;
value: string | string[] | undefined;
}
import { type Field, getRowsWithPins } from './utils';
import { AddMetadataPinToRow } from './add_pin_to_row';

export interface Props {
rows: Row[];
rows: Field[];
loading: boolean;
showActionsColumn?: boolean;
search?: string;
Expand Down Expand Up @@ -65,12 +64,43 @@ const LOADING = i18n.translate('xpack.infra.metadataEmbeddable.loading', {
defaultMessage: 'Loading...',
});

const LOCAL_STORAGE_PINNED_METADATA_ROWS = 'hostsView:pinnedMetadataRows';

export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn }: Props) => {
const [searchError, setSearchError] = useState<SearchErrorType | null>(null);
const [metadataSearch, setMetadataSearch] = useState(search);
const [fieldsWithPins, setFieldsWithPins] = useState(rows);

const [pinnedItems, setPinnedItems] = useLocalStorage<Array<Field['name']>>(
LOCAL_STORAGE_PINNED_METADATA_ROWS,
[]
);

useMemo(() => {
if (pinnedItems) {
setFieldsWithPins(getRowsWithPins(rows, pinnedItems) ?? rows);
}
}, [rows, pinnedItems]);

const defaultColumns = useMemo(
() => [
{
field: 'value',
name: <EuiIcon type="pin" />,
align: 'center' as HorizontalAlignment,
width: '5%',
sortable: false,
showOnHover: true,
render: (_name: string, item: Field) => {
return (
<AddMetadataPinToRow
fieldName={item.name}
pinnedItems={pinnedItems ?? []}
onPinned={setPinnedItems}
/>
);
},
},
{
field: 'name',
name: FIELD_LABEL,
Expand All @@ -81,12 +111,12 @@ export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn
{
field: 'value',
name: VALUE_LABEL,
width: '55%',
width: '50%',
sortable: false,
render: (_name: string, item: Row) => <ExpandableContent values={item.value} />,
render: (_name: string, item: Field) => <ExpandableContent values={item.value} />,
},
],
[]
[pinnedItems, setPinnedItems]
);

const debouncedSearchOnChange = useMemo(
Expand Down Expand Up @@ -134,7 +164,7 @@ export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn
sortable: false,
showOnHover: true,
align: 'center' as HorizontalAlignment,
render: (_name: string, item: Row) => {
render: (_name: string, item: Field) => {
return <AddMetadataFilterButton item={item} />;
},
},
Expand All @@ -149,7 +179,7 @@ export const Table = ({ loading, rows, onSearchChange, search, showActionsColumn
tableLayout={'fixed'}
responsive={false}
columns={columns}
items={rows}
items={fieldsWithPins}
rowProps={{ className: 'euiTableRow-hasActions' }}
search={searchBar}
loading={loading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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<Field['name']>) => {
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];
}
};
20 changes: 19 additions & 1 deletion x-pack/test/functional/apps/infra/hosts_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,23 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});

describe('Metadata Tab', () => {
it('should render metadata tab, add and remove filter', async () => {
it('should render metadata tab, pin/unpin row, add and remove filter', 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);

await pageObjects.infraHostsView.clickAddMetadataFilter();
await pageObjects.header.waitUntilLoadingHasFinished();

Expand All @@ -289,6 +302,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
});

it('should render metadata tab, pin and unpin table row', async () => {
const metadataTab = await pageObjects.infraHostsView.getMetadataTabName();
expect(metadataTab).to.contain('Metadata');
});

describe('Processes Tab', () => {
it('should render processes tab and with Total Value summary', async () => {
await pageObjects.infraHostsView.clickProcessesFlyoutTab();
Expand Down
12 changes: 12 additions & 0 deletions x-pack/test/functional/page_objects/infra_hosts_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
return testSubjects.click('hostsView-flyout-apm-services-link');
},

async clickAddMetadataPin() {
return testSubjects.click('infraMetadataEmbeddableAddPin');
},

async clickRemoveMetadataPin() {
return testSubjects.click('infraMetadataEmbeddableRemovePin');
},

async clickAddMetadataFilter() {
return testSubjects.click('hostsView-flyout-metadata-add-filter');
},
Expand Down Expand Up @@ -190,6 +198,10 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
return tabTitle.getVisibleText();
},

async getRemovePinExist() {
return testSubjects.exists('infraMetadataEmbeddableRemovePin');
},

async getAppliedFilter() {
const filter = await testSubjects.find(
"filter-badge-'host.architecture: arm64' filter filter-enabled filter-key-host.architecture filter-value-arm64 filter-unpinned filter-id-0"
Expand Down

0 comments on commit b641a22

Please sign in to comment.