Skip to content

Commit

Permalink
[Cloud Security] Add Fields selector to the CloudSecurityDataTable (#…
Browse files Browse the repository at this point in the history
…167844)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: @Omolola-Akinleye
  • Loading branch information
opauloh and kibanamachine authored Oct 5, 2023
1 parent 6f62f7b commit 0c71076
Show file tree
Hide file tree
Showing 9 changed files with 483 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty, EuiFlexItem } from '@elastic/eui';
import { type DataView } from '@kbn/data-views-plugin/common';
import numeral from '@elastic/numeral';
import { FieldsSelectorModal } from './fields_selector';
import { FindingsGroupBySelector } from '../../pages/configurations/layout/findings_group_by_selector';
import { useStyles } from './use_styles';

const formatNumber = (value: number) => {
return value < 1000 ? value : numeral(value).format('0.0a');
};

export const AdditionalControls = ({
total,
title,
dataView,
columns,
onAddColumn,
onRemoveColumn,
}: {
total: number;
title: string;
dataView: DataView;
columns: string[];
onAddColumn: (column: string) => void;
onRemoveColumn: (column: string) => void;
}) => {
const styles = useStyles();

const [isFieldSelectorModalVisible, setIsFieldSelectorModalVisible] = useState(false);

const closeModal = () => setIsFieldSelectorModalVisible(false);
const showModal = () => setIsFieldSelectorModalVisible(true);

return (
<>
{isFieldSelectorModalVisible && (
<FieldsSelectorModal
columns={columns}
dataView={dataView}
closeModal={closeModal}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
)}
<EuiFlexItem grow={0}>
<span className="cspDataTableTotal">{`${formatNumber(total)} ${title}`}</span>
</EuiFlexItem>
<EuiFlexItem grow={0}>
<EuiButtonEmpty
className="cspDataTableFields"
iconType="tableOfContents"
onClick={showModal}
size="xs"
color="text"
>
{i18n.translate('xpack.csp.dataTable.fields', {
defaultMessage: 'Fields',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false} className={styles.groupBySelector}>
<FindingsGroupBySelector type="default" />
</EuiFlexItem>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,32 +15,23 @@ import {
SORT_DEFAULT_ORDER_SETTING,
} from '@kbn/discover-utils';
import { DataTableRecord } from '@kbn/discover-utils/types';
import {
EuiDataGridCellValueElementProps,
EuiDataGridStyle,
EuiFlexItem,
EuiProgress,
} from '@elastic/eui';
import { EuiDataGridCellValueElementProps, EuiDataGridStyle, EuiProgress } from '@elastic/eui';
import { AddFieldFilterHandler } from '@kbn/unified-field-list';
import { generateFilters } from '@kbn/data-plugin/public';
import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import numeral from '@elastic/numeral';
import { useKibana } from '../../common/hooks/use_kibana';
import { CloudPostureTableResult } from '../../common/hooks/use_cloud_posture_table';
import { FindingsGroupBySelector } from '../../pages/configurations/layout/findings_group_by_selector';
import { EmptyState } from '../empty_state';
import { MAX_FINDINGS_TO_LOAD } from '../../common/constants';
import { useStyles } from './use_styles';
import { AdditionalControls } from './additional_controls';

export interface CloudSecurityDefaultColumn {
id: string;
width?: number;
}

const formatNumber = (value: number) => {
return value < 1000 ? value : numeral(value).format('0.0a');
};

const gridStyle: EuiDataGridStyle = {
border: 'horizontal',
cellPadding: 'l',
Expand All @@ -50,6 +41,9 @@ const gridStyle: EuiDataGridStyle = {

const useNewFieldsApi = true;

// Hide Checkbox, enable open details Flyout
const controlColumnIds = ['openDetails'];

interface CloudSecurityDataGridProps {
dataView: DataView;
isLoading: boolean;
Expand Down Expand Up @@ -113,7 +107,8 @@ export const CloudSecurityDataTable = ({
`${columnsLocalStorageKey}:settings`,
{
columns: defaultColumns.reduce((prev, curr) => {
const newColumn = { [curr.id]: {} };
const columnDefaultSettings = curr.width ? { width: curr.width } : {};
const newColumn = { [curr.id]: columnDefaultSettings };
return { ...prev, ...newColumn };
}, {} as UnifiedDataTableSettings['columns']),
}
Expand Down Expand Up @@ -153,7 +148,12 @@ export const CloudSecurityDataTable = ({
dataViewFieldEditor,
};

const { columns: currentColumns, onSetColumns } = useColumns({
const {
columns: currentColumns,
onSetColumns,
onAddColumn,
onRemoveColumn,
} = useColumns({
capabilities,
defaultOrder: uiSettings.get(SORT_DEFAULT_ORDER_SETTING),
dataView,
Expand Down Expand Up @@ -205,25 +205,39 @@ export const CloudSecurityDataTable = ({
return <EmptyState onResetFilters={onResetFilters} />;
}

const externalAdditionalControls = (
<AdditionalControls
total={total}
dataView={dataView}
title={title}
columns={currentColumns}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
);

const dataTableStyle = {
// Change the height of the grid to fit the page
// If there are filters, leave space for the filter bar
// Todo: Replace this component with EuiAutoSizer
height: `calc(100vh - ${filters.length > 0 ? 443 : 403}px)`,
};

const rowHeightState =
uiSettings.get(ROW_HEIGHT_OPTION) === -1 ? 0 : uiSettings.get(ROW_HEIGHT_OPTION);

const loadingStyle = {
opacity: isLoading ? 1 : 0,
};

return (
<CellActionsProvider getTriggerCompatibleActions={uiActions.getTriggerCompatibleActions}>
<div
data-test-subj={rest['data-test-subj']}
className={styles.gridContainer}
style={{
// Change the height of the grid to fit the page
// If there are filters, leave space for the filter bar
// Todo: Replace this component with EuiAutoSizer
height: `calc(100vh - ${filters.length > 0 ? 454 : 414}px)`,
}}
style={dataTableStyle}
>
<EuiProgress
size="xs"
color="accent"
style={{
opacity: isLoading ? 1 : 0,
}}
/>
<EuiProgress size="xs" color="accent" style={loadingStyle} />
<UnifiedDataTable
className={styles.gridStyle}
ariaLabelledBy={title}
Expand All @@ -245,31 +259,18 @@ export const CloudSecurityDataTable = ({
services={services}
useNewFieldsApi
onUpdateRowsPerPage={onChangeItemsPerPage}
configRowHeight={uiSettings.get(ROW_HEIGHT_OPTION)}
rowHeightState={rowHeightState}
showMultiFields={uiSettings.get(SHOW_MULTIFIELDS)}
showTimeCol={false}
settings={settings}
onFetchMoreRecords={loadMore}
externalCustomRenderers={externalCustomRenderers}
rowHeightState={uiSettings.get(ROW_HEIGHT_OPTION)}
externalAdditionalControls={<AdditionalControls total={total} title={title} />}
externalAdditionalControls={externalAdditionalControls}
gridStyleOverride={gridStyle}
rowLineHeightOverride="24px"
controlColumnIds={controlColumnIds}
/>
</div>
</CellActionsProvider>
);
};

const AdditionalControls = ({ total, title }: { total: number; title: string }) => {
const styles = useStyles();
return (
<>
<EuiFlexItem>
<span className="cspDataTableTotal">{`${formatNumber(total)} ${title}`}</span>
</EuiFlexItem>
<EuiFlexItem grow={false} className={styles.groupBySelector}>
<FindingsGroupBySelector type="default" />
</EuiFlexItem>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 { FieldsSelectorTable, FieldsSelectorCommonProps } from './fields_selector';
import { TestProvider } from '../../test/test_provider';

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 renderFieldsTable = (props: Partial<FieldsSelectorCommonProps> = {}) => {
const defaultProps: FieldsSelectorCommonProps = {
dataView: mockDataView,
columns: [],
onAddColumn: jest.fn(),
onRemoveColumn: jest.fn(),
};

return render(
<TestProvider>
<FieldsSelectorTable title="Fields" {...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');
});
});
Loading

0 comments on commit 0c71076

Please sign in to comment.