mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Cloud Security] Add Fields selector to the CloudSecurityDataTable (#167844)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: @Omolola-Akinleye
This commit is contained in:
parent
6f62f7b5a6
commit
0c71076f92
9 changed files with 483 additions and 101 deletions
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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']),
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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, useMemo } from 'react';
|
||||
import {
|
||||
EuiBasicTableColumn,
|
||||
EuiButton,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiSearchBarProps,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { type DataView } from '@kbn/data-views-plugin/common';
|
||||
|
||||
interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface FieldsSelectorCommonProps {
|
||||
dataView: DataView;
|
||||
columns: string[];
|
||||
onAddColumn: (column: string) => void;
|
||||
onRemoveColumn: (column: string) => void;
|
||||
}
|
||||
|
||||
const ACTION_COLUMN_WIDTH = '24px';
|
||||
const defaultSorting = {
|
||||
sort: {
|
||||
field: 'name',
|
||||
direction: 'asc',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const FieldsSelectorTable = ({
|
||||
title,
|
||||
dataView,
|
||||
columns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
}: FieldsSelectorCommonProps & {
|
||||
title: string;
|
||||
}) => {
|
||||
const dataViewFields = useMemo<Field[]>(() => {
|
||||
return dataView.fields
|
||||
.getAll()
|
||||
.filter((field) => {
|
||||
return field.name !== '@timestamp' && field.name !== '_index' && field.visualizable;
|
||||
})
|
||||
.map((field) => ({
|
||||
id: field.name,
|
||||
name: field.name,
|
||||
displayName: field.customLabel || '',
|
||||
}));
|
||||
}, [dataView.fields]);
|
||||
|
||||
const [fields, setFields] = useState(dataViewFields);
|
||||
|
||||
let debounceTimeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
const onQueryChange: EuiSearchBarProps['onChange'] = ({ query }) => {
|
||||
clearTimeout(debounceTimeoutId);
|
||||
|
||||
debounceTimeoutId = setTimeout(() => {
|
||||
const filteredItems = dataViewFields.filter((field) => {
|
||||
const normalizedName = `${field.name} ${field.displayName}`.toLowerCase();
|
||||
const normalizedQuery = query?.text.toLowerCase() || '';
|
||||
return normalizedName.indexOf(normalizedQuery) !== -1;
|
||||
});
|
||||
|
||||
setFields(filteredItems);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const [fieldsSelected, setFieldsSelected] = useState<string[]>(columns);
|
||||
|
||||
const tableColumns: Array<EuiBasicTableColumn<Field>> = [
|
||||
{
|
||||
field: 'action',
|
||||
name: '',
|
||||
width: ACTION_COLUMN_WIDTH,
|
||||
sortable: false,
|
||||
render: (_, { id }: Field) => {
|
||||
return (
|
||||
<EuiCheckbox
|
||||
checked={fieldsSelected.includes(id)}
|
||||
id={id}
|
||||
onChange={(e) => {
|
||||
const isChecked = e.target.checked;
|
||||
setFieldsSelected(
|
||||
isChecked ? [...fieldsSelected, id] : fieldsSelected.filter((f) => f !== id)
|
||||
);
|
||||
return isChecked ? onAddColumn(id) : onRemoveColumn(id);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('xpack.csp.dataTable.fieldsModalName', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'displayName',
|
||||
name: i18n.translate('xpack.csp.dataTable.fieldsModalCustomLabel', {
|
||||
defaultMessage: 'Custom Label',
|
||||
}),
|
||||
sortable: (field: Field) => field.displayName.toLowerCase(),
|
||||
},
|
||||
];
|
||||
|
||||
const error = useMemo(() => {
|
||||
if (!dataView || dataView.fields.length === 0) {
|
||||
return i18n.translate('xpack.csp.dataTable.fieldsModalError', {
|
||||
defaultMessage: 'No fields found in the data view',
|
||||
});
|
||||
}
|
||||
return '';
|
||||
}, [dataView]);
|
||||
|
||||
const search: EuiSearchBarProps = {
|
||||
onChange: onQueryChange,
|
||||
box: {
|
||||
incremental: true,
|
||||
placeholder: i18n.translate('xpack.csp.dataTable.fieldsModalSearch', {
|
||||
defaultMessage: 'Search field name',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const tableHeader = useMemo(() => {
|
||||
const totalFields = fields.length;
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText data-test-subj="csp:dataTable:fieldsModal:fieldsShowing" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.csp.dataTable.fieldsModalFieldsShowing"
|
||||
defaultMessage="Showing"
|
||||
/>{' '}
|
||||
<strong data-test-subj="csp:dataTable:fieldsModal:fieldsCount">{totalFields}</strong>{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.csp.dataTable.fieldsModalFieldsCount"
|
||||
defaultMessage="{totalFields, plural, one {field} other {fields}}"
|
||||
values={{
|
||||
totalFields,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}, [fields.length]);
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
tableCaption={title}
|
||||
items={fields}
|
||||
columns={tableColumns}
|
||||
search={search}
|
||||
pagination
|
||||
sorting={defaultSorting}
|
||||
error={error}
|
||||
childrenBetween={tableHeader}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const FieldsSelectorModal = ({
|
||||
closeModal,
|
||||
dataView,
|
||||
columns,
|
||||
onAddColumn,
|
||||
onRemoveColumn,
|
||||
}: FieldsSelectorCommonProps & {
|
||||
closeModal: () => void;
|
||||
}) => {
|
||||
const title = i18n.translate('xpack.csp.dataTable.fieldsModalTitle', {
|
||||
defaultMessage: 'Fields',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiModal onClose={closeModal}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<FieldsSelectorTable
|
||||
title={title}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
onAddColumn={onAddColumn}
|
||||
onRemoveColumn={onRemoveColumn}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={closeModal} fill>
|
||||
Close
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -57,6 +57,9 @@ export const useStyles = () => {
|
|||
& .cspDataTableTotal {
|
||||
font-size: ${euiTheme.size.m};
|
||||
font-weight: ${euiTheme.font.weight.bold};
|
||||
border-right: ${euiTheme.border.thin};
|
||||
margin-right: ${euiTheme.size.s};
|
||||
padding-right: ${euiTheme.size.m};
|
||||
}
|
||||
& .euiDataGrid__rightControls {
|
||||
display: none;
|
||||
|
|
|
@ -43,7 +43,7 @@ const getDefaultQuery = ({
|
|||
});
|
||||
|
||||
const defaultColumns: CloudSecurityDefaultColumn[] = [
|
||||
{ id: 'result.evaluation' },
|
||||
{ id: 'result.evaluation', width: 80 },
|
||||
{ id: 'resource.id' },
|
||||
{ id: 'resource.name' },
|
||||
{ id: 'resource.sub_type' },
|
||||
|
@ -88,7 +88,7 @@ const flyoutComponent = (row: DataTableRecord, onCloseFlyout: () => void): JSX.E
|
|||
);
|
||||
};
|
||||
|
||||
const columnsLocalStorageKey = 'cloudSecurityPostureLatestFindingsColumns';
|
||||
const columnsLocalStorageKey = 'cloudPosture:latestFindings:columns';
|
||||
|
||||
const title = i18n.translate('xpack.csp.findings.latestFindings.tableRowTypeLabel', {
|
||||
defaultMessage: 'Findings',
|
||||
|
@ -162,7 +162,7 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => {
|
|||
failed={failed}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<EuiSpacer size="xs" />
|
||||
<CloudSecurityDataTable
|
||||
data-test-subj={TEST_SUBJECTS.LATEST_FINDINGS_TABLE}
|
||||
dataView={dataView}
|
||||
|
|
|
@ -303,7 +303,7 @@ const FilterableCell: React.FC<{
|
|||
export const LimitedResultsBar = () => (
|
||||
<>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiBottomBar data-test-subj="test-bottom-bar">
|
||||
<EuiBottomBar data-test-subj="test-bottom-bar" paddingSize="s">
|
||||
<EuiText textAlign="center">
|
||||
<FormattedMessage
|
||||
id="xpack.csp.findings..bottomBarLabel"
|
||||
|
|
|
@ -19,6 +19,9 @@ export const useStyles = () => {
|
|||
`;
|
||||
|
||||
const gridStyle = css`
|
||||
& .euiDataGrid__content {
|
||||
background: transparent;
|
||||
}
|
||||
& .euiDataGridHeaderCell__icon {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -290,6 +290,14 @@ const VulnerabilitiesDataGrid = ({
|
|||
return <EmptyState onResetFilters={onResetFilters} />;
|
||||
}
|
||||
|
||||
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 - ${urlQuery.filters.length > 0 ? 403 : 363}px)`,
|
||||
minHeight: 400,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiProgress
|
||||
|
@ -299,61 +307,66 @@ const VulnerabilitiesDataGrid = ({
|
|||
opacity: isFetching ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
<EuiDataGrid
|
||||
className={cx({ [styles.gridStyle]: true }, { [styles.highlightStyle]: showHighlight })}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
schemaDetectors={[severitySchemaConfig]}
|
||||
rowCount={limitedTotalItemCount}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
append: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
<div style={dataTableStyle}>
|
||||
<EuiDataGrid
|
||||
className={cx({ [styles.gridStyle]: true }, { [styles.highlightStyle]: showHighlight })}
|
||||
aria-label={VULNERABILITIES}
|
||||
columns={columns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
schemaDetectors={[severitySchemaConfig]}
|
||||
rowCount={limitedTotalItemCount}
|
||||
toolbarVisibility={{
|
||||
showColumnSelector: false,
|
||||
showDisplaySelector: false,
|
||||
showKeyboardShortcuts: false,
|
||||
showFullScreenSelector: false,
|
||||
additionalControls: {
|
||||
left: {
|
||||
append: (
|
||||
<>
|
||||
<EuiButtonEmpty size="xs" color="text">
|
||||
{i18n.translate('xpack.csp.vulnerabilities.totalVulnerabilities', {
|
||||
defaultMessage:
|
||||
'{total, plural, one {# Vulnerability} other {# Vulnerabilities}}',
|
||||
values: { total: data?.total },
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
),
|
||||
},
|
||||
right: (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
<FindingsGroupBySelector
|
||||
type="default"
|
||||
pathnameHandler={vulnerabilitiesPathnameHandler}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
),
|
||||
},
|
||||
right: (
|
||||
<EuiFlexItem grow={false} className={styles.groupBySelector}>
|
||||
<FindingsGroupBySelector
|
||||
type="default"
|
||||
pathnameHandler={vulnerabilitiesPathnameHandler}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
),
|
||||
},
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort: onSortHandler }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
}}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
cellPadding: 'l',
|
||||
stripes: false,
|
||||
rowHover: 'none',
|
||||
header: 'underline',
|
||||
}}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'enhancements' }}
|
||||
sorting={{ columns: sort, onSort: onSortHandler }}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageSizeOptions: [10, 25, 100],
|
||||
onChangeItemsPerPage,
|
||||
onChangePage,
|
||||
}}
|
||||
virtualizationOptions={{
|
||||
overscanRowCount: 20,
|
||||
}}
|
||||
/>
|
||||
{isLastLimitedPage && <LimitedResultsBar />}
|
||||
</div>
|
||||
{showVulnerabilityFlyout && selectedVulnerability && (
|
||||
<VulnerabilityFindingFlyout
|
||||
flyoutIndex={selectedVulnerabilityIndex}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue