Skip to content

Commit

Permalink
[Cloud Security]Added Support to handle Labels on Unified Data Table (e…
Browse files Browse the repository at this point in the history
…lastic#184295)

## Summary

Added Support to handle Labels on Unified Data Table. Previously
Findings and Vulnerabilities table columns shows Labels by using the
customLabel field in Dataview and we did this by Updating the Dataview
when we first retrieve the Dataview. However, we have removed the Update
logic, as such we would need a different way to do this which is this PR

We used the **settings** prop on UnifiedDataTable to pass on the Column
Labels that will be shown in Findings/Vulnerabilities table
  • Loading branch information
animehart authored May 30, 2024
1 parent dff2754 commit 1bd6c24
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,14 @@ export const DataTableTimeColumnHeader = ({
dataView,
dataViewField,
headerRowHeight = 1,
columnLabel,
}: {
dataView: DataView;
dataViewField?: DataViewField;
headerRowHeight?: number;
columnLabel?: string;
}) => {
const timeFieldName = dataViewField?.customLabel ?? dataView.timeFieldName;
const timeFieldName = columnLabel || (dataViewField?.customLabel ?? dataView.timeFieldName);
const primaryTimeAriaLabel = i18n.translate(
'unifiedDataTable.tableHeader.timeFieldIconTooltipAriaLabel',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { dataViewWithoutTimefieldMock } from '../../__mocks__/data_view_without_
import { dataTableContextMock } from '../../__mocks__/table_context';
import { servicesMock } from '../../__mocks__/services';
import { ROWS_HEIGHT_OPTIONS } from '../constants';
import { UnifiedDataTableSettingsColumn } from '../types';

const columns = ['extension', 'message'];
const columnsWithTimeCol = getVisibleColumns(
Expand Down Expand Up @@ -460,4 +461,35 @@ describe('Data table columns', function () {
expect(deserializeHeaderRowHeight(2)).toBe(2);
});
});

describe('Column label display', () => {
it('Column Name should display provided label from display otherwise it defaults to columns name', () => {
const mockColumnHeaders: Record<string, UnifiedDataTableSettingsColumn> = {
test_column_1: { display: 'test_column_one' },
test_column_2: { display: 'test_column_two' },
test_column_3: { display: 'test_column_three' },
} as const;
const customizedGridColumns = getEuiGridColumns({
columns: ['test_column_1', 'test_column_2', 'test_column_4'],
settings: { columns: mockColumnHeaders },
dataView: dataViewWithTimefieldMock,
defaultColumns: false,
isSortEnabled: true,
valueToStringConverter: dataTableContextMock.valueToStringConverter,
rowsCount: 100,
headerRowHeightLines: 5,
services: {
uiSettings: servicesMock.uiSettings,
toastNotifications: servicesMock.toastNotifications,
},
hasEditDataViewPermission: () =>
servicesMock.dataViewFieldEditor.userPermissions.editIndexPattern(),
});
const columnDisplayNames = customizedGridColumns.map((column) => column.displayAsText);
expect(columnDisplayNames.includes('test_column_one')).toBeTruthy();
expect(columnDisplayNames.includes('test_column_two')).toBeTruthy();
expect(columnDisplayNames.includes('test_column_three')).toBeFalsy();
expect(columnDisplayNames.includes('test_column_4')).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,24 @@ import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_
import { buildEditFieldButton } from './build_edit_field_button';
import { DataTableColumnHeader, DataTableTimeColumnHeader } from './data_table_column_header';

const getColumnDisplayName = (
columnName: string,
dataViewFieldDisplayName: string | undefined,
columnDisplay: string | undefined
) => {
if (columnDisplay) {
return columnDisplay;
}

if (columnName === '_source') {
return i18n.translate('unifiedDataTable.grid.documentHeader', {
defaultMessage: 'Document',
});
}

return dataViewFieldDisplayName || columnName;
};

const DataTableColumnHeaderMemoized = React.memo(DataTableColumnHeader);
const DataTableTimeColumnHeaderMemoized = React.memo(DataTableTimeColumnHeader);

Expand Down Expand Up @@ -97,6 +115,7 @@ function buildEuiGridColumn({
showColumnTokens,
headerRowHeight,
customGridColumnsConfiguration,
columnDisplay,
}: {
numberOfColumns: number;
columnName: string;
Expand All @@ -117,6 +136,7 @@ function buildEuiGridColumn({
showColumnTokens?: boolean;
headerRowHeight?: number;
customGridColumnsConfiguration?: CustomGridColumnsConfiguration;
columnDisplay?: string;
}) {
const dataViewField = !isPlainRecord
? dataView.getFieldByName(columnName)
Expand All @@ -133,12 +153,12 @@ function buildEuiGridColumn({
editField &&
dataViewField &&
buildEditFieldButton({ hasEditDataViewPermission, dataView, field: dataViewField, editField });
const columnDisplayName =
columnName === '_source'
? i18n.translate('unifiedDataTable.grid.documentHeader', {
defaultMessage: 'Document',
})
: dataViewField?.displayName || columnName;

const columnDisplayName = getColumnDisplayName(
columnName,
dataViewField?.displayName,
columnDisplay
);

let cellActions: EuiDataGridColumnCellAction[];

Expand Down Expand Up @@ -212,6 +232,7 @@ function buildEuiGridColumn({
dataView={dataView}
dataViewField={dataViewField}
headerRowHeight={headerRowHeight}
columnLabel={columnDisplay}
/>
);
if (numberOfColumns > 1) {
Expand Down Expand Up @@ -306,6 +327,7 @@ export function getEuiGridColumns({
showColumnTokens,
headerRowHeight,
customGridColumnsConfiguration,
columnDisplay: settings?.columns?.[column]?.display,
})
);
}
Expand Down
6 changes: 6 additions & 0 deletions packages/kbn-unified-data-table/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export interface UnifiedDataTableSettings {

export interface UnifiedDataTableSettingsColumn {
width?: number;
/**
Optional props passed to Columns to display provided labels as column names instead of field names.
This object maps column field names to their corresponding display labels.
These labels will take precedence over the data view field names.
*/
display?: string;
}

export type ValueToStringConverter = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export interface CloudSecurityDataTableProps {
* Height override for the data grid.
*/
height?: number | string;
/* Optional props passed to Columns to display Provided Labels as Column name instead of field name */
columnHeaders?: Record<string, string>;
/**
* Specify if distribution bar is shown on data table, used to calculate height of data table in virtualized mode
*/
Expand All @@ -95,6 +97,7 @@ export const CloudSecurityDataTable = ({
customCellRenderer,
groupSelectorComponent,
height,
columnHeaders,
hasDistributionBar = true,
...rest
}: CloudSecurityDataTableProps) => {
Expand All @@ -117,7 +120,9 @@ export const CloudSecurityDataTable = ({
`${columnsLocalStorageKey}:settings`,
{
columns: defaultColumns.reduce((prev, curr) => {
const columnDefaultSettings = curr.width ? { width: curr.width } : {};
const columnDefaultSettings = curr.width
? { width: curr.width, display: columnHeaders?.[curr.id] }
: { display: columnHeaders?.[curr.id] };
const newColumn = { [curr.id]: columnDefaultSettings };
return { ...prev, ...newColumn };
}, {} as UnifiedDataTableSettings['columns']),
Expand Down Expand Up @@ -227,6 +232,7 @@ export const CloudSecurityDataTable = ({
const newColumns = { ...(grid.columns || {}) };
newColumns[colSettings.columnId] = {
width: Math.round(colSettings.width),
display: columnHeaders?.[colSettings.columnId],
};
const newGrid = { ...grid, columns: newColumns };
setSettings(newGrid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ const VIEW_MENU_SELECTED_TEXT = 'View: selected';
const mockDataView = {
fields: {
getAll: () => [
{ id: 'field1', name: 'field1', customLabel: 'Label 1', visualizable: true },
{ id: 'field2', name: 'field2', customLabel: 'Label 2', visualizable: true },
{ id: 'field3', name: 'field3', customLabel: 'Label 3', visualizable: true },
{ id: 'field4', name: 'field4', customLabel: 'Label 3.A', visualizable: true },
{ id: 'not-visible', name: 'not-visible', customLabel: 'Label 3.A', visualizable: false },
{ id: '_index', name: '_index', customLabel: 'should not be shown', visualizable: true },
{ id: 'field1', name: 'field1', visualizable: true },
{ id: 'field2', name: 'field2', visualizable: true },
{ id: 'field3', name: 'field3', visualizable: true },
{ id: 'field4', name: 'field4', visualizable: true },
{ id: 'not-visible', name: 'not-visible', visualizable: false },
{ id: '_index', name: '_index', visualizable: true },
],
},
} as any;
Expand Down Expand Up @@ -58,8 +58,8 @@ describe('FieldsSelectorTable', () => {
it('renders the table with data correctly', () => {
const { getByText } = renderFieldsTable();

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

it('calls onAddColumn when a checkbox is checked', () => {
Expand Down Expand Up @@ -174,17 +174,12 @@ describe('FieldsSelectorTable', () => {
).toEqual(4);

expect(
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'Label', false)
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'field', false)
.length
).toEqual(4);

expect(
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'Label 3', false)
.length
).toEqual(2);

expect(
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'Label 3.A', false)
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'field3', false)
.length
).toEqual(1);

Expand All @@ -207,17 +202,12 @@ describe('FieldsSelectorTable', () => {
).toEqual(2);

expect(
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'Label', true)
.length
).toEqual(2);

expect(
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'Label 3', true)
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'field', true)
.length
).toEqual(2);

expect(
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'Label 3.A', true)
filterFieldsBySearch(mockDataView.fields.getAll(), ['field3', 'field4'], 'field3', true)
.length
).toEqual(1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,6 @@ export const FieldsSelectorTable = ({
}),
sortable: true,
},
{
field: 'displayName',
name: i18n.translate('xpack.csp.dataTable.fieldsModalCustomLabel', {
defaultMessage: 'Custom Label',
}),
sortable: (field: Field) => field.displayName.toLowerCase(),
},
];

const error = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const findingsTableFieldLabels: Record<string, string> = {
'result.evaluation': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.resultColumnLabel',
{ defaultMessage: 'Result' }
),
'resource.id': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnLabel',
{ defaultMessage: 'Resource ID' }
),
'resource.name': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.resourceNameColumnLabel',
{ defaultMessage: 'Resource Name' }
),
'resource.sub_type': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.resourceTypeColumnLabel',
{ defaultMessage: 'Resource Type' }
),
'rule.benchmark.rule_number': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNumberColumnLabel',
{ defaultMessage: 'Rule Number' }
),
'rule.name': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleNameColumnLabel',
{ defaultMessage: 'Rule Name' }
),
'rule.section': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.ruleSectionColumnLabel',
{ defaultMessage: 'CIS Section' }
),
'@timestamp': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.lastCheckedColumnLabel',
{ defaultMessage: 'Last Checked' }
),
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { TimestampTableCell } from '../../../components/timestamp_table_cell';
import { CspEvaluationBadge } from '../../../components/csp_evaluation_badge';
import { CspFinding } from '../../../../common/schemas/csp_finding';
import { FindingsRuleFlyout } from '../findings_flyout/findings_flyout';
import { findingsTableFieldLabels } from './findings_table_field_labels';

interface LatestFindingsTableProps {
groupSelectorComponent?: JSX.Element;
Expand Down Expand Up @@ -136,6 +137,7 @@ export const LatestFindingsTable = ({
customCellRenderer={customCellRenderer}
groupSelectorComponent={groupSelectorComponent}
height={height}
columnHeaders={findingsTableFieldLabels}
/>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getDefaultQuery, defaultColumns } from './constants';
import { VulnerabilityFindingFlyout } from './vulnerabilities_finding_flyout/vulnerability_finding_flyout';
import { ErrorCallout } from '../configurations/layout/error_callout';
import { CVSScoreBadge, SeverityStatusBadge } from '../../components/vulnerability_badges';
import { vulnerabilitiesTableFieldLabels } from './vulnerabilities_table_field_labels';

interface LatestVulnerabilitiesTableProps {
groupSelectorComponent?: JSX.Element;
Expand Down Expand Up @@ -115,6 +116,7 @@ export const LatestVulnerabilitiesTable = ({
groupSelectorComponent={groupSelectorComponent}
height={height}
hasDistributionBar={false}
columnHeaders={vulnerabilitiesTableFieldLabels}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { i18n } from '@kbn/i18n';

export const vulnerabilitiesTableFieldLabels: Record<string, string> = {
'resource.id': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.resourceIdColumnLabel',
{ defaultMessage: 'Resource ID' }
),
'resource.name': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.resourceNameColumnLabel',
{ defaultMessage: 'Resource Name' }
),
'vulnerability.id': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.vulnerabilityIdColumnLabel',
{ defaultMessage: 'Vulnerability' }
),
'vulnerability.score.base': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.vulnerabilityScoreColumnLabel',
{ defaultMessage: 'CVSS' }
),
'vulnerability.severity': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.vulnerabilitySeverityColumnLabel',
{ defaultMessage: 'Severity' }
),
'package.name': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.packageNameColumnLabel',
{ defaultMessage: 'Package' }
),
'package.version': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.packageVersionColumnLabel',
{ defaultMessage: 'Version' }
),
'package.fixed_version': i18n.translate(
'xpack.csp.findings.findingsTable.findingsTableColumn.packageFixedVersionColumnLabel',
{ defaultMessage: 'Fix Version' }
),
} as const;
1 change: 0 additions & 1 deletion x-pack/plugins/translations/translations/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -12786,7 +12786,6 @@
"xpack.csp.dataTable.fieldsModal.viewLabel": "Afficher",
"xpack.csp.dataTable.fieldsModal.viewSelected": "sélectionné",
"xpack.csp.dataTable.fieldsModalClose": "Fermer",
"xpack.csp.dataTable.fieldsModalCustomLabel": "Étiquette personnalisée",
"xpack.csp.dataTable.fieldsModalError": "Aucun champ trouvé dans l'affichage de données",
"xpack.csp.dataTable.fieldsModalFieldsShowing": "Affichage",
"xpack.csp.dataTable.fieldsModalName": "Nom",
Expand Down
Loading

0 comments on commit 1bd6c24

Please sign in to comment.