Skip to content

Commit

Permalink
[Cloud Posture] [Quick wins] Fields selector improvements (elastic#17…
Browse files Browse the repository at this point in the history
…3887)

## Summary

It fixes elastic#168624

This PR is part of the [Quick
Wins](elastic/security-team#8254) improvements
for 8.13.0. It adds the following improvements:

- Added selector to view all fields or only selected fields to the
Fields Selector.
- Added option to reset fields to default on the Fields Selector
- Removed gray border on top of the DataTables component 
- Added unit tests to cover FieldsSelectorTable functionalities.
- Added FTR tests to cover Fields Selector functionalities.
- Refactor the fields_selector file into multiple files under the
fields_selector folder.

### Screenshots


![image](https://github.com/elastic/kibana/assets/19270322/9c6c4243-c4d2-46f3-848f-d4c85c5b072b)


![image](https://github.com/elastic/kibana/assets/19270322/50183b1b-bbb6-4a26-9875-d6201e63e197)


https://github.com/elastic/kibana/assets/19270322/b9bab9b1-44cc-4de7-b280-3fefcc5bbbd4

---------

Co-authored-by: Maxim Kholod <[email protected]>
  • Loading branch information
opauloh and maxcold authored Dec 29, 2023
1 parent cf2bf53 commit b7a3f78
Show file tree
Hide file tree
Showing 12 changed files with 470 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const LOCAL_STORAGE_DASHBOARD_BENCHMARK_SORT_KEY =
'cloudPosture:complianceDashboard:benchmarkSort';
export const LOCAL_STORAGE_FINDINGS_LAST_SELECTED_TAB_KEY = 'cloudPosture:findings:lastSelectedTab';

export const SESSION_STORAGE_FIELDS_MODAL_SHOW_SELECTED = 'cloudPosture:fieldsModal:showSelected';

export type CloudPostureIntegrations = Record<
CloudSecurityPolicyTemplate,
CloudPostureIntegrationProps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
import { type DataView } from '@kbn/data-views-plugin/common';
import { FieldsSelectorModal } from './fields_selector';
import { FormattedMessage } from '@kbn/i18n-react';
import { FieldsSelectorModal, useFieldsModal } from './fields_selector';
import { useStyles } from './use_styles';
import { getAbbreviatedNumber } from '../../common/utils/get_abbreviated_number';
import { CSP_FIELDS_SELECTOR_OPEN_BUTTON } from '../test_subjects';

const GroupSelectorWrapper: React.FC = ({ children }) => {
const styles = useStyles();
Expand All @@ -30,6 +31,7 @@ export const AdditionalControls = ({
onAddColumn,
onRemoveColumn,
groupSelectorComponent,
onResetColumns,
}: {
total: number;
title: string;
Expand All @@ -38,21 +40,21 @@ export const AdditionalControls = ({
onAddColumn: (column: string) => void;
onRemoveColumn: (column: string) => void;
groupSelectorComponent?: JSX.Element;
onResetColumns: () => void;
}) => {
const [isFieldSelectorModalVisible, setIsFieldSelectorModalVisible] = useState(false);

const closeModal = () => setIsFieldSelectorModalVisible(false);
const showModal = () => setIsFieldSelectorModalVisible(true);
const { isFieldSelectorModalVisible, closeFieldsSelectorModal, openFieldsSelectorModal } =
useFieldsModal();

return (
<>
{isFieldSelectorModalVisible && (
<FieldsSelectorModal
columns={columns}
dataView={dataView}
closeModal={closeModal}
closeModal={closeFieldsSelectorModal}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
onResetColumns={onResetColumns}
/>
)}
<EuiFlexItem grow={0}>
Expand All @@ -62,13 +64,12 @@ export const AdditionalControls = ({
<EuiButtonEmpty
className="cspDataTableFields"
iconType="tableOfContents"
onClick={showModal}
onClick={openFieldsSelectorModal}
size="xs"
color="text"
data-test-subj={CSP_FIELDS_SELECTOR_OPEN_BUTTON}
>
{i18n.translate('xpack.csp.dataTable.fields', {
defaultMessage: 'Fields',
})}
<FormattedMessage id="xpack.csp.dataTable.fieldsButton" defaultMessage="Fields" />
</EuiButtonEmpty>
</EuiFlexItem>
{groupSelectorComponent && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ export const CloudSecurityDataTable = ({
return customCellRenderer(rows);
}, [customCellRenderer, rows]);

const onResetColumns = () => {
setColumns(defaultColumns.map((c) => c.id));
};

if (!isLoading && !rows.length) {
return <EmptyState onResetFilters={onResetFilters} />;
}
Expand All @@ -221,6 +225,7 @@ export const CloudSecurityDataTable = ({
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
groupSelectorComponent={groupSelectorComponent}
onResetColumns={onResetColumns}
/>
);

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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, { useCallback } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { type DataView } from '@kbn/data-views-plugin/common';
import useSessionStorage from 'react-use/lib/useSessionStorage';
import { SESSION_STORAGE_FIELDS_MODAL_SHOW_SELECTED } from '../../../common/constants';
import { FieldsSelectorTable } from './fields_selector_table';
import {
CSP_FIELDS_SELECTOR_CLOSE_BUTTON,
CSP_FIELDS_SELECTOR_MODAL,
CSP_FIELDS_SELECTOR_RESET_BUTTON,
} from '../../test_subjects';

interface FieldsSelectorModalProps {
dataView: DataView;
columns: string[];
onAddColumn: (column: string) => void;
onRemoveColumn: (column: string) => void;
closeModal: () => void;
onResetColumns: () => void;
}

const title = i18n.translate('xpack.csp.dataTable.fieldsModalTitle', {
defaultMessage: 'Fields',
});

export const FieldsSelectorModal = ({
closeModal,
dataView,
columns,
onAddColumn,
onRemoveColumn,
onResetColumns,
}: FieldsSelectorModalProps) => {
const [isFilterSelectedEnabled, setIsFilterSelectedEnabled] = useSessionStorage(
SESSION_STORAGE_FIELDS_MODAL_SHOW_SELECTED,
false
);

const onFilterSelectedChange = useCallback(
(enabled: boolean) => {
setIsFilterSelectedEnabled(enabled);
},
[setIsFilterSelectedEnabled]
);

return (
<EuiModal onClose={closeModal} data-test-subj={CSP_FIELDS_SELECTOR_MODAL}>
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<FieldsSelectorTable
title={title}
dataView={dataView}
columns={columns}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
isFilterSelectedEnabled={isFilterSelectedEnabled}
onFilterSelectedChange={onFilterSelectedChange}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={onResetColumns} data-test-subj={CSP_FIELDS_SELECTOR_RESET_BUTTON}>
<FormattedMessage
id="xpack.csp.dataTable.fieldsModalReset"
defaultMessage="Reset Fields"
/>
</EuiButtonEmpty>
<EuiButton onClick={closeModal} fill data-test-subj={CSP_FIELDS_SELECTOR_CLOSE_BUTTON}>
<FormattedMessage id="xpack.csp.dataTable.fieldsModalClose" defaultMessage="Close" />
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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 { render, fireEvent } from '@testing-library/react';
import { waitForEuiPopoverOpen } from '@elastic/eui/lib/test/rtl';
import { TestProvider } from '../../../test/test_provider';
import { FieldsSelectorTable, FieldsSelectorTableProps } from './fields_selector_table';

const mockDataView = {
fields: {
getAll: () => [
{ id: 'field1', name: 'field1', customLabel: 'Label 1', visualizable: true },
{ id: 'field2', name: 'field2', customLabel: 'Label 2', visualizable: true },
],
},
} as any;

const mockOnFilterSelectedChange = jest.fn();

const renderFieldsTable = (props: Partial<FieldsSelectorTableProps> = {}) => {
const defaultProps: FieldsSelectorTableProps = {
dataView: mockDataView,
columns: [],
onAddColumn: jest.fn(),
onRemoveColumn: jest.fn(),
title: 'title',
onFilterSelectedChange: mockOnFilterSelectedChange,
isFilterSelectedEnabled: false,
};

return render(
<TestProvider>
<FieldsSelectorTable {...defaultProps} {...props} />
</TestProvider>
);
};

describe('FieldsSelectorTable', () => {
it('renders the table with data correctly', () => {
const { getByText } = renderFieldsTable();

expect(getByText('Label 1')).toBeInTheDocument();
expect(getByText('Label 2')).toBeInTheDocument();
});

it('calls onAddColumn when a checkbox is checked', () => {
const onAddColumn = jest.fn();
const { getAllByRole } = renderFieldsTable({
onAddColumn,
});

const checkbox = getAllByRole('checkbox')[0];
fireEvent.click(checkbox);

expect(onAddColumn).toHaveBeenCalledWith('field1');
});

it('calls onRemoveColumn when a checkbox is unchecked', () => {
const onRemoveColumn = jest.fn();
const { getAllByRole } = renderFieldsTable({
columns: ['field1', 'field2'],
onRemoveColumn,
});

const checkbox = getAllByRole('checkbox')[1];
fireEvent.click(checkbox);

expect(onRemoveColumn).toHaveBeenCalledWith('field2');
});

describe('View selected', () => {
beforeEach(() => {
mockOnFilterSelectedChange.mockClear();
});

it('should render "view all" option when filterSelected is not enabled', () => {
const { getByTestId } = renderFieldsTable({ isFilterSelectedEnabled: false });

expect(getByTestId('viewSelectorButton').textContent).toBe('View: all');
});

it('should render "view selected" option when filterSelected is not enabled', () => {
const { getByTestId } = renderFieldsTable({ isFilterSelectedEnabled: true });

expect(getByTestId('viewSelectorButton').textContent).toBe('View: selected');
});

it('should open the view selector with button click', async () => {
const { queryByTestId, getByTestId } = renderFieldsTable();

expect(queryByTestId('viewSelectorMenu')).not.toBeInTheDocument();
expect(queryByTestId('viewSelectorOption-all')).not.toBeInTheDocument();
expect(queryByTestId('viewSelectorOption-selected')).not.toBeInTheDocument();

getByTestId('viewSelectorButton').click();
await waitForEuiPopoverOpen();

expect(getByTestId('viewSelectorMenu')).toBeInTheDocument();
expect(getByTestId('viewSelectorOption-all')).toBeInTheDocument();
expect(getByTestId('viewSelectorOption-selected')).toBeInTheDocument();
});

it('should callback when "view all" option is clicked', async () => {
const { getByTestId } = renderFieldsTable({ isFilterSelectedEnabled: false });

getByTestId('viewSelectorButton').click();
await waitForEuiPopoverOpen();
getByTestId('viewSelectorOption-all').click();
expect(mockOnFilterSelectedChange).toHaveBeenCalledWith(false);
});

it('should callback when "view selected" option is clicked', async () => {
const { getByTestId } = renderFieldsTable({ isFilterSelectedEnabled: false });

getByTestId('viewSelectorButton').click();
await waitForEuiPopoverOpen();
getByTestId('viewSelectorOption-selected').click();
expect(mockOnFilterSelectedChange).toHaveBeenCalledWith(true);
});
});
});
Loading

0 comments on commit b7a3f78

Please sign in to comment.