Skip to content

Commit

Permalink
Add new discover component and children components (#6205)
Browse files Browse the repository at this point in the history
* Add search_bar hook

* Add doc_viewer component and hook

* Add data_grid hook

* Add discover component

* Fixed use_search_bar unit tests

* Add index file in doc_viewer folder

* Resolve review requested changes

* Solve requested changes

* Solve requested change

* Relocated data_grid and search_bar folders

* Replace naming convention to kebab-case

* Update CHANGELOG
  • Loading branch information
Machi3mfl authored Dec 14, 2023
1 parent 10f26fb commit 4b967e2
Show file tree
Hide file tree
Showing 13 changed files with 1,274 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to the Wazuh app project will be documented in this file.

- Support for Wazuh 4.9.0
- Added AngularJS dependencies [#6145](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6145)
- Remove embedded discover [#6120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6120)

## Wazuh v4.8.1 - OpenSearch Dashboards 2.10.0 - Revision 00

Expand Down
163 changes: 163 additions & 0 deletions plugins/main/public/components/common/data-grid/data-grid-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { SearchResponse } from "../../../../../../src/core/server";
import * as FileSaver from '../../../services/file-saver';
import { beautifyDate } from "../../agents/vuls/inventory/lib";
import { SearchParams, search } from "../search-bar/search-bar-service";
import { IFieldType, IndexPattern } from "../../../../../../src/plugins/data/common";
export const MAX_ENTRIES_PER_QUERY = 10000;
import { EuiDataGridColumn } from '@elastic/eui';

export const parseData = (resultsHits: SearchResponse['hits']['hits']): any[] => {
const data = resultsHits.map((hit) => {
if (!hit) {
return {}
}
const source = hit._source as object;
const data = {
...source,
_id: hit._id,
_index: hit._index,
_type: hit._type,
_score: hit._score,
};
return data;
});
return data;
}


export const getFieldFormatted = (rowIndex, columnId, indexPattern, rowsParsed) => {
const field = indexPattern.fields.find((field) => field.name === columnId);
let fieldValue = null;
if (columnId.includes('.')) {
// when the column is a nested field. The column could have 2 to n levels
// get dinamically the value of the nested field
const nestedFields = columnId.split('.');
fieldValue = rowsParsed[rowIndex];
nestedFields.forEach((field) => {
if (fieldValue) {
fieldValue = fieldValue[field];
}
});
} else {
const rowValue = rowsParsed[rowIndex];
// when not exist the column in the row value then the value is null
if(!rowValue.hasOwnProperty(columnId)){
fieldValue = null;
}else{
fieldValue = rowValue[columnId]?.formatted || rowValue[columnId];
}
}
// when fieldValue is null or undefined then return a empty string
if (fieldValue === null || fieldValue === undefined) {
return '';
}
// if is date field
if (field?.type === 'date') {
// @ts-ignore
fieldValue = beautifyDate(fieldValue);
}
return fieldValue;
}

// receive search params
export const exportSearchToCSV = async (params: SearchParams): Promise<void> => {
const DEFAULT_MAX_SIZE_PER_CALL = 1000;
const { indexPattern, filters = [], query, sorting, fields, pagination } = params;
// when the pageSize is greater than the default max size per call (10000)
// then we need to paginate the search
const mustPaginateSearch = pagination?.pageSize && pagination?.pageSize > DEFAULT_MAX_SIZE_PER_CALL;
const pageSize = mustPaginateSearch ? DEFAULT_MAX_SIZE_PER_CALL : pagination?.pageSize;
const totalHits = pagination?.pageSize || DEFAULT_MAX_SIZE_PER_CALL;
let pageIndex = params.pagination?.pageIndex || 0;
let hitsCount = 0;
let allHits = [];
let searchResults;
if (mustPaginateSearch) {
// paginate the search
while (hitsCount < totalHits &&  hitsCount < MAX_ENTRIES_PER_QUERY) {
const searchParams = {
indexPattern,
filters,
query,
pagination: {
pageIndex,
pageSize,
},
sorting,
fields,
};
searchResults = await search(searchParams);
allHits = allHits.concat(searchResults.hits.hits);
hitsCount = allHits.length;
pageIndex++;
}
} else {
searchResults = await search(params);
allHits = searchResults.hits.hits;
}

const resultsFields = fields;
const data = allHits.map((hit) => {
// check if the field type is a date
const dateFields = indexPattern.fields.getByType('date');
const dateFieldsNames = dateFields.map((field) => field.name);
const flattenHit = indexPattern.flattenHit(hit);
// replace the date fields with the formatted date
dateFieldsNames.forEach((field) => {
if (flattenHit[field]) {
flattenHit[field] = beautifyDate(flattenHit[field]);
}
});
return flattenHit;
});

if (!resultsFields || resultsFields.length === 0){
return;
}

if (!data || data.length === 0)
return;

const parsedData = data.map((row) => {
const parsedRow = resultsFields?.map((field) => {
const value = row[field];
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return `"${value}"`;
});
return parsedRow?.join(',');
}).join('\n');

// create a csv file using blob
const blobData = new Blob(
[
`${resultsFields?.join(',')}\n${parsedData}`
],
{ type: 'text/csv' }
);

if (blobData) {
// @ts-ignore
FileSaver?.saveAs(blobData, `events-${new Date().toISOString()}.csv`);
}
}

export const parseColumns = (fields: IFieldType[]): EuiDataGridColumn[] => {
// remove _source field becuase is a object field and is not supported
fields = fields.filter((field) => field.name !== '_source');
return fields.map((field) => {
return {
...field,
id: field.name,
display: field.name,
schema: field.type,
actions: {
showHide: true,
},
};
}) || [];
}
2 changes: 2 additions & 0 deletions plugins/main/public/components/common/data-grid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './data-grid-service';
export * from './use-data-grid';
103 changes: 103 additions & 0 deletions plugins/main/public/components/common/data-grid/use-data-grid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { EuiDataGridCellValueElementProps, EuiDataGridColumn, EuiDataGridProps, EuiDataGridSorting } from "@elastic/eui"
import React, { useEffect, useMemo, useState, Fragment } from "react";
import { SearchResponse } from "@opensearch-project/opensearch/api/types";
// ToDo: check how create this methods
import { parseData, getFieldFormatted, parseColumns } from './data-grid-service';
import { IndexPattern } from '../../../../../../src/plugins/data/common';

const MAX_ENTRIES_PER_QUERY = 10000;

export type tDataGridColumn = {
render?: (value: any) => string | React.ReactNode;
} & EuiDataGridColumn;

type tDataGridProps = {
indexPattern: IndexPattern;
results: SearchResponse;
defaultColumns: tDataGridColumn[];
DocViewInspectButton: ({ rowIndex }: EuiDataGridCellValueElementProps) => React.JSX.Element
ariaLabelledBy: string;
};


export const useDataGrid = (props: tDataGridProps): EuiDataGridProps => {
const { indexPattern, DocViewInspectButton, results, defaultColumns } = props;
/** Columns **/
const [columns, setColumns] = useState<tDataGridColumn[]>(defaultColumns);
const [columnVisibility, setVisibility] = useState(() =>
columns.map(({ id }) => id)
);
/** Rows */
const [rows, setRows] = useState<any[]>([]);
const rowCount = results ? results?.hits?.total as number : 0;
/** Sorting **/
// get default sorting from default columns
const getDefaultSorting = () => {
const defaultSort = columns.find((column) => column.isSortable || column.defaultSortDirection);
return defaultSort ? [{ id: defaultSort.id, direction: defaultSort.defaultSortDirection || 'desc' }] : [];
}
const defaultSorting: EuiDataGridSorting['columns'] = getDefaultSorting();
const [sortingColumns, setSortingColumns] = useState(defaultSorting);
const onSort = (sortingColumns) => { setSortingColumns(sortingColumns) };
/** Pagination **/
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 20 });
const onChangeItemsPerPage = useMemo(() => (pageSize) =>
setPagination((pagination) => ({
...pagination,
pageSize,
pageIndex: 0,
})), [rows, rowCount]);
const onChangePage = (pageIndex) => setPagination((pagination) => ({ ...pagination, pageIndex }))

useEffect(() => {
setRows(results?.hits?.hits || [])
}, [results, results?.hits, results?.hits?.total])

useEffect(() => {
setPagination((pagination) => ({ ...pagination, pageIndex: 0 }));
}, [rowCount])

const renderCellValue = ({ rowIndex, columnId, setCellProps }) => {
const rowsParsed = parseData(rows);
// On the context data always is stored the current page data (pagination)
// then the rowIndex is relative to the current page
const relativeRowIndex = rowIndex % pagination.pageSize;
if(rowsParsed.hasOwnProperty(relativeRowIndex)){
const fieldFormatted = getFieldFormatted(relativeRowIndex, columnId, indexPattern, rowsParsed);
// check if column have render method initialized
const column = columns.find((column) => column.id === columnId);
if (column && column.render) {
return column.render(fieldFormatted);
}
return fieldFormatted;
}
return null
};

const leadingControlColumns = useMemo(() => {
return [
{
id: 'inspectCollapseColumn',
headerCellRender: () => null,
rowCellRender: (props) => DocViewInspectButton({ ...props, rowIndex: props.rowIndex % pagination.pageSize }),
width: 40,
},
];
}, [results]);

return {
"aria-labelledby": props.ariaLabelledBy,
columns: parseColumns(indexPattern?.fields || []),
columnVisibility: { visibleColumns: columnVisibility, setVisibleColumns: setVisibility },
renderCellValue: renderCellValue,
leadingControlColumns: leadingControlColumns,
rowCount: rowCount < MAX_ENTRIES_PER_QUERY ? rowCount : MAX_ENTRIES_PER_QUERY,
sorting: { columns: sortingColumns, onSort },
pagination: {
...pagination,
pageSizeOptions: [20, 50, 100],
onChangeItemsPerPage: onChangeItemsPerPage,
onChangePage: onChangePage,
}
}
}
Loading

0 comments on commit 4b967e2

Please sign in to comment.