Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new discover component and children components #6205

Merged
merged 12 commits into from
Dec 14, 2023
150 changes: 150 additions & 0 deletions plugins/main/public/components/common/doc_viewer/doc_viewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { escapeRegExp } from 'lodash';
import { i18n } from '@osd/i18n';
import { FieldIcon } from '../../../../../../src/plugins/opensearch_dashboards_react/public';
import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';

const COLLAPSE_LINE_LENGTH = 350;
const DOT_PREFIX_RE = /(.).+?\./g;

export type tDocViewerProps = {
flattened: any;
formatted: any;
mapping: any;
indexPattern: any;
}

/**
* Convert a dot.notated.string into a short
* version (d.n.string)
*/
export const shortenDottedString = (input: string) => input.replace(DOT_PREFIX_RE, '$1.');

export function getFieldTypeName(type: string) {
Machi3mfl marked this conversation as resolved.
Show resolved Hide resolved
switch (type) {
case 'boolean':
return i18n.translate('discover.fieldNameIcons.booleanAriaLabel', {
defaultMessage: 'Boolean field',
});
case 'conflict':
return i18n.translate('discover.fieldNameIcons.conflictFieldAriaLabel', {
defaultMessage: 'Conflicting field',
});
case 'date':
return i18n.translate('discover.fieldNameIcons.dateFieldAriaLabel', {
defaultMessage: 'Date field',
});
case 'geo_point':
return i18n.translate('discover.fieldNameIcons.geoPointFieldAriaLabel', {
defaultMessage: 'Geo point field',
});
case 'geo_shape':
return i18n.translate('discover.fieldNameIcons.geoShapeFieldAriaLabel', {
defaultMessage: 'Geo shape field',
});
case 'ip':
return i18n.translate('discover.fieldNameIcons.ipAddressFieldAriaLabel', {
defaultMessage: 'IP address field',
});
case 'murmur3':
return i18n.translate('discover.fieldNameIcons.murmur3FieldAriaLabel', {
defaultMessage: 'Murmur3 field',
});
case 'number':
return i18n.translate('discover.fieldNameIcons.numberFieldAriaLabel', {
defaultMessage: 'Number field',
});
case 'source':
// Note that this type is currently not provided, type for _source is undefined
return i18n.translate('discover.fieldNameIcons.sourceFieldAriaLabel', {
defaultMessage: 'Source field',
});
case 'string':
return i18n.translate('discover.fieldNameIcons.stringFieldAriaLabel', {
defaultMessage: 'String field',
});
case 'nested':
return i18n.translate('discover.fieldNameIcons.nestedFieldAriaLabel', {
defaultMessage: 'Nested field',
});
default:
return i18n.translate('discover.fieldNameIcons.unknownFieldAriaLabel', {
defaultMessage: 'Unknown field',
});
}
}

const DocViewer = (props: tDocViewerProps) => {
const [fieldRowOpen, setFieldRowOpen] = useState({} as Record<string, boolean>);
const { flattened, formatted, mapping, indexPattern } = props;

return (<>
{flattened && (
<table className="table table-condensed osdDocViewerTable">
Machi3mfl marked this conversation as resolved.
Show resolved Hide resolved
<tbody>
{Object.keys(flattened)
.sort()
.map((field, index) => {
const value = String(formatted[field]);
const fieldMapping = mapping(field);
const isCollapsible = value.length > COLLAPSE_LINE_LENGTH;
const isCollapsed = isCollapsible && !fieldRowOpen[field];
const valueClassName = classNames({
// eslint-disable-next-line @typescript-eslint/naming-convention
osdDocViewer__value: true,
'truncate-by-height': isCollapsible && isCollapsed,
});
const isNestedField =
!indexPattern.fields.getByName(field) &&
!!indexPattern.fields.getAll().find((patternField) => {
// We only want to match a full path segment
const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)');
return nestedRootRegex.test(patternField.subType?.nested?.path ?? '');
});
const fieldType = isNestedField ? 'nested' : indexPattern.fields.getByName(field)?.type;
const typeName = getFieldTypeName(String(fieldType));
const displayName = field;
const fieldIconProps = { fill: 'none', color: 'gray' }
const scripted = Boolean(fieldMapping?.scripted)

return (
<tr key={index} data-test-subj={`tableDocViewRow-${field}`}>
<td className="osdDocViewer__field">
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<FieldIcon type={fieldType} label={typeName} scripted={scripted} {...fieldIconProps} />
</EuiFlexItem>
<EuiFlexItem className="eui-textTruncate">
<EuiToolTip
position="top"
content={displayName}
delay="long"
anchorClassName="eui-textTruncate"
>
<span>{displayName}</span>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</td>
<td>
<div
className={valueClassName}
data-test-subj={`tableDocViewRow-${field}-value`}
/*
* Justification for dangerouslySetInnerHTML:
* We just use values encoded by our field formatters
*/
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: value as string }}
/>
</td>
</tr>
);
})}
</tbody>
</table>
)}</>)
};

export default DocViewer;
2 changes: 2 additions & 0 deletions plugins/main/public/components/common/doc_viewer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './use_doc_viewer';
export * from './doc_viewer';
28 changes: 28 additions & 0 deletions plugins/main/public/components/common/doc_viewer/use_doc_viewer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { tDocViewerProps } from "./doc_viewer"
import { IndexPattern } from "../../../../../../src/plugins/data/common";

type tUseDocViewerInputs = {
indexPattern: IndexPattern;
doc: any;
}

export const useDocViewer = (props: tUseDocViewerInputs): tDocViewerProps => {
const { indexPattern, doc } = props;

if (!indexPattern || !doc) {
return {
flattened: {},
formatted: {},
indexPattern: undefined,
mapping: undefined
}
}

const mapping = indexPattern?.fields.getByName;
return {
flattened: indexPattern?.flattenHit(doc),
formatted: indexPattern?.formatHit(doc, 'html'),
indexPattern,
mapping
}
}
Machi3mfl marked this conversation as resolved.
Show resolved Hide resolved
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/use_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, `vulnerabilities_inventory-${new Date().toISOString()}.csv`);
Machi3mfl marked this conversation as resolved.
Show resolved Hide resolved
}
}

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,
},
};
}) || [];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './data_grid_service';
export * from './use_data_grid';
Loading
Loading