-
Notifications
You must be signed in to change notification settings - Fork 192
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new discover component and children components (#6205)
* 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
Showing
13 changed files
with
1,274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
163 changes: 163 additions & 0 deletions
163
plugins/main/public/components/common/data-grid/data-grid-service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}; | ||
}) || []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
103
plugins/main/public/components/common/data-grid/use-data-grid.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
Oops, something went wrong.