diff --git a/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts b/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts index 86b9692cbfc43b..c8e98703cdbf0f 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/api/use_latest_findings_data_view.ts @@ -62,15 +62,19 @@ export const useLatestFindingsDataView = (dataView: string) => { } if (dataView === LATEST_FINDINGS_INDEX_PATTERN) { + let shouldUpdate = false; Object.entries(cloudSecurityFieldLabels).forEach(([field, label]) => { if ( !dataViewObj.getFieldAttrs()[field]?.customLabel || dataViewObj.getFieldAttrs()[field]?.customLabel === field ) { dataViewObj.setFieldCustomLabel(field, label); + shouldUpdate = true; } }); - await dataViews.updateSavedObject(dataViewObj); + if (shouldUpdate) { + await dataViews.updateSavedObject(dataViewObj); + } } return dataViewObj; diff --git a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts index 0becb56e6ec227..d06e29a95e46d7 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/hooks/use_cloud_posture_table/use_cloud_posture_table.ts @@ -40,9 +40,9 @@ export interface CloudPostureTableResult { getRowsFromPages: (data: Array<{ page: DataTableRecord[] }> | undefined) => DataTableRecord[]; } -/* - Hook for managing common table state and methods for Cloud Posture -*/ +/** + * @deprecated will be replaced by useCloudPostureDataTable + */ export const useCloudPostureTable = ({ defaultQuery = getDefaultQuery, dataView, diff --git a/x-pack/plugins/cloud_security_posture/public/common/types.ts b/x-pack/plugins/cloud_security_posture/public/common/types.ts index a4c26643293fdb..ac483445407e48 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/types.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/types.ts @@ -22,6 +22,8 @@ export interface FindingsBaseURLQuery { export interface FindingsBaseProps { dataView: DataView; + dataViewRefetch?: () => void; + dataViewIsRefetching?: boolean; } export interface FindingsBaseESQueryConfig { diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx new file mode 100644 index 00000000000000..7ddbe28a7da077 --- /dev/null +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { render } from '@testing-library/react'; +import React from 'react'; +import { TestProvider } from '../../test/test_provider'; +import { CloudSecurityDataTable, CloudSecurityDataTableProps } from './cloud_security_data_table'; + +const mockDataView = { + fields: { + getAll: () => [ + { id: 'field1', name: 'field1', customLabel: 'Label 1', visualizable: true }, + { id: 'field2', name: 'field2', customLabel: 'Label 2', visualizable: true }, + ], + getByName: (name: string) => ({ id: name }), + }, + getFieldByName: (name: string) => ({ id: name }), + getFormatterForField: (name: string) => ({ + convert: (value: string) => value, + }), +} as any; + +const mockDefaultColumns = [{ id: 'field1' }, { id: 'field2' }]; + +const mockCloudPostureDataTable = { + setUrlQuery: jest.fn(), + columnsLocalStorageKey: 'test', + filters: [], + onSort: jest.fn(), + sort: [], + query: {}, + queryError: undefined, + pageIndex: 0, + urlQuery: {}, + setTableOptions: jest.fn(), + handleUpdateQuery: jest.fn(), + pageSize: 10, + setPageSize: jest.fn(), + onChangeItemsPerPage: jest.fn(), + onChangePage: jest.fn(), + onResetFilters: jest.fn(), + getRowsFromPages: jest.fn(), +} as any; + +const renderDataTable = (props: Partial = {}) => { + const defaultProps: CloudSecurityDataTableProps = { + dataView: mockDataView, + isLoading: false, + defaultColumns: mockDefaultColumns, + rows: [], + total: 0, + flyoutComponent: () => <>, + cloudPostureDataTable: mockCloudPostureDataTable, + loadMore: jest.fn(), + title: 'Test Table', + }; + + return render( + + + + ); +}; + +describe('CloudSecurityDataTable', () => { + it('renders loading state', () => { + const { getByTestId } = renderDataTable({ isLoading: true }); + expect(getByTestId('unifiedDataTableLoading')).toBeInTheDocument(); + }); + + it('renders empty state when no rows are present', () => { + const { getByTestId } = renderDataTable(); + expect(getByTestId('csp:empty-state')).toBeInTheDocument(); + }); + + it('renders data table with rows', async () => { + const mockRows = [ + { + id: '1', + raw: { + field1: 'Label 1', + field2: 'Label 2', + }, + flattened: { + field1: 'Label 1', + field2: 'Label 2', + }, + }, + ] as any; + const { getByTestId, getByText } = renderDataTable({ + rows: mockRows, + total: mockRows.length, + }); + + expect(getByTestId('discoverDocTable')).toBeInTheDocument(); + expect(getByText('Label 1')).toBeInTheDocument(); + expect(getByText('Label 2')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx index 84cea3e64115eb..3f0c3da73a9862 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/cloud_security_data_table/cloud_security_data_table.tsx @@ -17,7 +17,7 @@ import { generateFilters } from '@kbn/data-plugin/public'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { useKibana } from '../../common/hooks/use_kibana'; -import { CloudPostureTableResult } from '../../common/hooks/use_cloud_posture_table'; +import { CloudPostureDataTableResult } from '../../common/hooks/use_cloud_posture_data_table'; import { EmptyState } from '../empty_state'; import { MAX_FINDINGS_TO_LOAD } from '../../common/constants'; import { useStyles } from './use_styles'; @@ -40,7 +40,7 @@ const useNewFieldsApi = true; // Hide Checkbox, enable open details Flyout const controlColumnIds = ['openDetails']; -interface CloudSecurityDataGridProps { +export interface CloudSecurityDataTableProps { dataView: DataView; isLoading: boolean; defaultColumns: CloudSecurityDefaultColumn[]; @@ -52,10 +52,10 @@ interface CloudSecurityDataGridProps { */ flyoutComponent: (hit: DataTableRecord, onCloseFlyout: () => void) => JSX.Element; /** - * This is the object that contains all the data and functions from the useCloudPostureTable hook. + * This is the object that contains all the data and functions from the useCloudPostureDataTable hook. * This is also used to manage the table state from the parent component. */ - cloudPostureTable: CloudPostureTableResult; + cloudPostureDataTable: CloudPostureDataTableResult; title: string; /** * This is a function that returns a map of column ids to custom cell renderers. @@ -78,6 +78,16 @@ interface CloudSecurityDataGridProps { * Height override for the data grid. */ height?: number; + /** + * Callback Function when the DataView field is edited. + * Required to enable editing of the field in the data grid. + */ + dataViewRefetch?: () => void; + /** + * Flag to indicate if the data view is refetching. + * Required for smoothing re-rendering the DataTable columns. + */ + dataViewIsRefetching?: boolean; } export const CloudSecurityDataTable = ({ @@ -87,14 +97,16 @@ export const CloudSecurityDataTable = ({ rows, total, flyoutComponent, - cloudPostureTable, + cloudPostureDataTable, loadMore, title, customCellRenderer, groupSelectorComponent, height, + dataViewRefetch, + dataViewIsRefetching, ...rest -}: CloudSecurityDataGridProps) => { +}: CloudSecurityDataTableProps) => { const { columnsLocalStorageKey, pageSize, @@ -104,7 +116,7 @@ export const CloudSecurityDataTable = ({ onResetFilters, filters, sort, - } = cloudPostureTable; + } = cloudPostureDataTable; const [columns, setColumns] = useLocalStorage( columnsLocalStorageKey, @@ -242,6 +254,9 @@ export const CloudSecurityDataTable = ({ opacity: isLoading ? 1 : 0, }; + const loadingState = + isLoading || dataViewIsRefetching ? DataLoadingState.loading : DataLoadingState.loaded; + return (
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx index d1b35ab617a96b..7b7bd42561204a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.test.tsx @@ -26,7 +26,6 @@ import { render } from '@testing-library/react'; import { expectIdsInDoc } from '../../test/utils'; import { PACKAGE_NOT_INSTALLED_TEST_SUBJECT } from '../../components/cloud_posture_page'; import { useLicenseManagementLocatorApi } from '../../common/api/use_license_management_locator_api'; -import { useCloudPostureTable } from '../../common/hooks/use_cloud_posture_table'; jest.mock('../../common/api/use_latest_findings_data_view'); jest.mock('../../common/api/use_setup_status_api'); @@ -34,7 +33,6 @@ jest.mock('../../common/api/use_license_management_locator_api'); jest.mock('../../common/hooks/use_subscription_status'); jest.mock('../../common/navigation/use_navigate_to_cis_integration_policies'); jest.mock('../../common/navigation/use_csp_integration_link'); -jest.mock('../../common/hooks/use_cloud_posture_table'); const chance = new Chance(); @@ -54,13 +52,6 @@ beforeEach(() => { data: true, }) ); - - (useCloudPostureTable as jest.Mock).mockImplementation(() => ({ - getRowsFromPages: jest.fn(), - columnsLocalStorageKey: 'test', - filters: [], - sort: [], - })); }); const renderFindingsPage = () => { diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx index 9e87d1d8eda6d1..7e8bbfeedb832a 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/configurations.tsx @@ -50,7 +50,11 @@ export const Configurations = () => { path={findingsNavigation.findings_default.path} render={() => ( - + )} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx index 9d536f0f0b180f..e070847b6df554 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_container.tsx @@ -18,7 +18,11 @@ import { groupPanelRenderer, groupStatsRenderer } from './latest_findings_group_ import { FindingsDistributionBar } from '../layout/findings_distribution_bar'; import { ErrorCallout } from '../layout/error_callout'; -export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { +export const LatestFindingsContainer = ({ + dataView, + dataViewRefetch, + dataViewIsRefetching, +}: FindingsBaseProps) => { const renderChildComponent = useCallback( (groupFilters: Filter[]) => { return ( @@ -27,10 +31,12 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { nonPersistedFilters={groupFilters} height={DEFAULT_TABLE_HEIGHT} showDistributionBar={false} + dataViewRefetch={dataViewRefetch} + dataViewIsRefetching={dataViewIsRefetching} /> ); }, - [dataView] + [dataView, dataViewIsRefetching, dataViewRefetch] ); const { @@ -94,7 +100,12 @@ export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => { return ( <> - + ); }; diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx index be6d34a1df933a..3adb10259871d0 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/latest_findings_table.tsx @@ -27,6 +27,8 @@ type LatestFindingsTableProps = FindingsBaseProps & { height?: number; showDistributionBar?: boolean; nonPersistedFilters?: Filter[]; + dataViewRefetch?: () => void; + dataViewIsRefetching?: boolean; }; /** @@ -87,9 +89,11 @@ export const LatestFindingsTable = ({ height, showDistributionBar = true, nonPersistedFilters, + dataViewRefetch, + dataViewIsRefetching, }: LatestFindingsTableProps) => { const { - cloudPostureTable, + cloudPostureDataTable, rows, error, isFetching, @@ -134,12 +138,14 @@ export const LatestFindingsTable = ({ rows={rows} total={total} flyoutComponent={flyoutComponent} - cloudPostureTable={cloudPostureTable} + cloudPostureDataTable={cloudPostureDataTable} loadMore={fetchNextPage} title={title} customCellRenderer={customCellRenderer} groupSelectorComponent={groupSelectorComponent} height={height} + dataViewRefetch={dataViewRefetch} + dataViewIsRefetching={dataViewIsRefetching} /> )} diff --git a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx index 50ebb01f363f09..b60eefac2ac81c 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/configurations/latest_findings/use_latest_findings_table.tsx @@ -28,7 +28,7 @@ export const useLatestFindingsTable = ({ nonPersistedFilters?: Filter[]; showDistributionBar?: boolean; }) => { - const cloudPostureTable = useCloudPostureDataTable({ + const cloudPostureDataTable = useCloudPostureDataTable({ dataView, paginationLocalStorageKey: LOCAL_STORAGE_DATA_TABLE_PAGE_SIZE_KEY, columnsLocalStorageKey, @@ -36,7 +36,7 @@ export const useLatestFindingsTable = ({ nonPersistedFilters, }); - const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureTable; + const { query, sort, queryError, setUrlQuery, filters, getRowsFromPages } = cloudPostureDataTable; const { data, @@ -72,7 +72,7 @@ export const useLatestFindingsTable = ({ const canShowDistributionBar = showDistributionBar && total > 0; return { - cloudPostureTable, + cloudPostureDataTable, rows, error, isFetching, diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts index c711c2300e1bea..eb7ea675601548 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/findings_page.ts @@ -220,6 +220,13 @@ export function FindingsPageProvider({ getService, getPageObjects }: FtrProvider const flyoutButton = await table.findAllByTestSubject('docTableExpandToggleColumn'); await flyoutButton[rowIndex].click(); }, + + async toggleEditDataViewFieldsOption(columnId: string) { + const element = await this.getElement(); + const column = await element.findByCssSelector(`[data-gridcell-column-id="${columnId}"]`); + const button = await column.findByCssSelector('.euiDataGridHeaderCell__button'); + return await button.click(); + }, }); const createTableObject = (tableTestSubject: string) => ({ diff --git a/x-pack/test/cloud_security_posture_functional/pages/findings.ts b/x-pack/test/cloud_security_posture_functional/pages/findings.ts index 69d100c1fbf621..5f4520277b2f36 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/findings.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/findings.ts @@ -95,7 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const ruleName1 = data[0].rule.name; const ruleName2 = data[1].rule.name; - describe('Findings Page', function () { + describe('Findings Page - DataTable', function () { this.tags(['cloud_security_posture_findings']); let findings: typeof pageObjects.findings; let latestFindingsTable: typeof findings.latestFindingsTable; @@ -211,6 +211,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); + describe('DataTable features', () => { + it('Edit data view field option is Enabled', async () => { + await latestFindingsTable.toggleEditDataViewFieldsOption('result.evaluation'); + expect(await testSubjects.find('gridEditFieldButton')).to.be.ok(); + await latestFindingsTable.toggleEditDataViewFieldsOption('result.evaluation'); + }); + }); + describe('Findings - Fields selector', () => { const CSP_FIELDS_SELECTOR_MODAL = 'cloudSecurityFieldsSelectorModal'; const CSP_FIELDS_SELECTOR_OPEN_BUTTON = 'cloudSecurityFieldsSelectorOpenButton';