mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Infrastructure UI] Asset Details: Add pins to the metadata table (#161074)
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](https://github.com/elastic/kibana/issues/155190#issuecomment-1523335704),
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
62873e7e
-b5f0-444c-94ff-5e19f2f46f58
This commit is contained in:
parent
8afb9b086c
commit
b641a22438
5 changed files with 166 additions and 14 deletions
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiSearchBarProps,
|
||||
type HorizontalAlignment,
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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} />;
|
||||
},
|
||||
},
|
||||
|
@ -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}
|
||||
|
|
|
@ -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<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];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue