Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Infrastructure UI] Hosts Flyout: Add pins to the metadata table #155904

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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']>;
setPinnedItems: Dispatch<React.SetStateAction<Array<Field['name']> | 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 (
<span>
<EuiToolTip
content={i18n.translate('xpack.infra.hostsViewPage.flyout.metadata.unpinField', {
defaultMessage: 'Unpin field',
})}
>
<EuiButtonIcon
size="s"
color="primary"
iconType="pinFilled"
data-test-subj="hostsView-flyout-metadata-remove-pin"
aria-label={i18n.translate('xpack.infra.hostsViewPage.flyout.metadata.pinAriaLabel', {
defaultMessage: 'Pinned field',
})}
onClick={() => handleRemovePin(fieldName)}
/>
</EuiToolTip>
</span>
);
}

return (
<span className="euiTableCellContent__hoverItem expandedItemActions__completelyHide">
<EuiToolTip content={PIN_FIELD}>
<EuiButtonIcon
color="primary"
size="s"
iconType="pin"
data-test-subj="hostsView-flyout-metadata-add-pin"
aria-label={PIN_FIELD}
onClick={() => handleAddPin(fieldName)}
/>
</EuiToolTip>
</span>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,25 @@ import {
EuiLink,
EuiInMemoryTable,
EuiSearchBarProps,
EuiIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo, useState } from 'react';
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;
}

Expand Down Expand Up @@ -72,6 +74,18 @@ export const Table = (props: Props) => {
const { rows, loading } = props;
const [searchError, setSearchError] = useState<SearchErrorType | null>(null);
const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen();
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 debouncedSearchOnChange = useMemo(
() =>
Expand Down Expand Up @@ -108,6 +122,23 @@ export const Table = (props: Props) => {

const columns = 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 ?? []}
setPinnedItems={setPinnedItems}
/>
);
},
},
{
field: 'name',
name: FIELD_LABEL,
Expand All @@ -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) => <ExpandableContent values={item.value} />,
render: (_name: string, item: Field) => <ExpandableContent values={item.value} />,
},
{
field: 'value',
name: 'Actions',
width: '10%',
sortable: false,
showOnHover: true,
render: (_name: string, item: Row) => {
render: (_name: string, item: Field) => {
return <AddMetadataFilterButton item={item} />;
},
},
],
[]
[pinnedItems, setPinnedItems]
);

return (
Expand All @@ -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}
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];
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
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 @@ -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 () => {
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 14 additions & 1 deletion x-pack/test/functional/page_objects/infra_hosts_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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');
},
Expand All @@ -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');
Expand Down