diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f54d57c6..f283ba3b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added an "Agents management" menu and moved the sections: "Endpoint Groups" and "Endpoint Summary" which changed its name to "Summary".[#7112](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7112) - Added ability to filter from File Integrity Monitoring registry inventory [#7119](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7119) - Added new field columns and ability to select the visible fields in the File Integrity Monitoring Files and Registry tables [#7119](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7119) +- Added filter by value to document details fields [#7081](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7081) ### Changed diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index 7b25a1d98c..4da147db02 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -530,3 +530,8 @@ export const OSD_URL_STATE_STORAGE_ID = 'state:storeInSessionStorage'; export const APP_STATE_URL_KEY = '_a'; export const GLOBAL_STATE_URL_KEY = '_g'; + +export enum FilterStateStore { + APP_STATE = 'appState', + GLOBAL_STATE = 'globalState', +} diff --git a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx index ea2fe09158..899a0c9387 100644 --- a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx +++ b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx @@ -52,6 +52,7 @@ import { RedirectAppLinks } from '../../../../../../../src/plugins/opensearch_da import TechniqueRowDetails from '../../../overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details'; import { DATA_SOURCE_FILTER_CONTROLLED_CLUSTER_MANAGER } from '../../../../../common/constants'; import NavigationService from '../../../../react-services/navigation-service'; +import { setFilters } from '../../../common/search-bar/set-filters'; export class FileDetails extends Component { props!: { @@ -586,6 +587,8 @@ export class FileDetails extends Component { this.discoverFilterManager.addFilters(newFilter); }} + filters={[]} + setFilters={setFilters(this.discoverFilterManager)} /> ); } diff --git a/plugins/main/public/components/common/data-grid/cell-filter-actions.test.tsx b/plugins/main/public/components/common/data-grid/cell-filter-actions.test.tsx index 8eb829a886..7eb175314c 100644 --- a/plugins/main/public/components/common/data-grid/cell-filter-actions.test.tsx +++ b/plugins/main/public/components/common/data-grid/cell-filter-actions.test.tsx @@ -61,7 +61,7 @@ describe('cell-filter-actions', () => { fireEvent.click(component); expect(onFilter).toHaveBeenCalledTimes(1); - expect(onFilter).toHaveBeenCalledWith(TEST_COLUMN_ID, TEST_VALUE, 'is'); + expect(onFilter).toHaveBeenCalledWith(TEST_COLUMN_ID, 'is', TEST_VALUE); }); }); @@ -102,8 +102,8 @@ describe('cell-filter-actions', () => { expect(onFilter).toHaveBeenCalledTimes(1); expect(onFilter).toHaveBeenCalledWith( TEST_COLUMN_ID, - TEST_VALUE, 'is not', + TEST_VALUE, ); }); }); diff --git a/plugins/main/public/components/common/data-grid/cell-filter-actions.tsx b/plugins/main/public/components/common/data-grid/cell-filter-actions.tsx index 6f9007dffc..cab188384b 100644 --- a/plugins/main/public/components/common/data-grid/cell-filter-actions.tsx +++ b/plugins/main/public/components/common/data-grid/cell-filter-actions.tsx @@ -15,14 +15,14 @@ export const filterIsAction = ( rows: any[], pageSize: number, onFilter: ( - columndId: string, - value: any, + field: string, operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT, + value?: any, ) => void, ) => { return ({ rowIndex, - columnId, + columnId: field, Component, }: EuiDataGridColumnCellActionProps) => { const filterForValueText = i18n.translate('discover.filterForValue', { @@ -30,7 +30,7 @@ export const filterIsAction = ( }); const filterForValueLabel = i18n.translate('discover.filterForValueLabel', { defaultMessage: 'Filter for value: {value}', - values: { value: columnId }, + values: { value: field }, }); const handleClick = () => { @@ -38,7 +38,7 @@ export const filterIsAction = ( const flattened = indexPattern.flattenHit(row); if (flattened) { - onFilter(columnId, flattened[columnId], FILTER_OPERATOR.IS); + onFilter(field, FILTER_OPERATOR.IS, flattened[field]); } }; @@ -61,18 +61,22 @@ export const filterIsNotAction = rows: any[], pageSize: number, onFilter: ( - columndId: string, - value: any, + field: string, operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT, + value?: any, ) => void, ) => - ({ rowIndex, columnId, Component }: EuiDataGridColumnCellActionProps) => { + ({ + rowIndex, + columnId: field, + Component, + }: EuiDataGridColumnCellActionProps) => { const filterOutValueText = i18n.translate('discover.filterOutValue', { defaultMessage: 'Filter out value', }); const filterOutValueLabel = i18n.translate('discover.filterOutValueLabel', { defaultMessage: 'Filter out value: {value}', - values: { value: columnId }, + values: { value: field }, }); const handleClick = () => { @@ -80,7 +84,7 @@ export const filterIsNotAction = const flattened = indexPattern.flattenHit(row); if (flattened) { - onFilter(columnId, flattened[columnId], FILTER_OPERATOR.IS_NOT); + onFilter(field, FILTER_OPERATOR.IS_NOT, flattened[field]); } }; @@ -103,9 +107,9 @@ export function cellFilterActions( rows: any[], pageSize: number, onFilter: ( - columndId: string, - value: any, + field: string, operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT, + value: any, ) => void, ) { if (!field.filterable) return; diff --git a/plugins/main/public/components/common/data-grid/data-grid-service.ts b/plugins/main/public/components/common/data-grid/data-grid-service.ts index bdd6cace7e..b209d05bdf 100644 --- a/plugins/main/public/components/common/data-grid/data-grid-service.ts +++ b/plugins/main/public/components/common/data-grid/data-grid-service.ts @@ -10,10 +10,7 @@ import { export const MAX_ENTRIES_PER_QUERY = 10000; import { tDataGridColumn } from './use-data-grid'; import { cellFilterActions } from './cell-filter-actions'; -import { - FILTER_OPERATOR, - PatternDataSourceFilterManager, -} from '../data-source/pattern/pattern-data-source-filter-manager'; +import { onFilterCellActions } from './filter-cell-actions'; type ParseData = | { @@ -192,26 +189,6 @@ export const exportSearchToCSV = async ( } }; -const onFilterCellActions = ( - indexPatternId: string, - filters: Filter[], - setFilters: (filters: Filter[]) => void, -) => { - return ( - columndId: string, - value: any, - operation: FILTER_OPERATOR.IS | FILTER_OPERATOR.IS_NOT, - ) => { - const newFilter = PatternDataSourceFilterManager.createFilter( - operation, - columndId, - value, - indexPatternId, - ); - setFilters([...filters, newFilter]); - }; -}; - const mapToDataGridColumn = ( field: IFieldType, indexPattern: IndexPattern, diff --git a/plugins/main/public/components/common/data-grid/filter-cell-actions.test.ts b/plugins/main/public/components/common/data-grid/filter-cell-actions.test.ts new file mode 100644 index 0000000000..a6dae0615f --- /dev/null +++ b/plugins/main/public/components/common/data-grid/filter-cell-actions.test.ts @@ -0,0 +1,265 @@ +import { FilterStateStore } from '../../../../common/constants'; +import { onFilterCellActions } from './filter-cell-actions'; +import { FILTER_OPERATOR } from '../data-source'; + +const KEY = 'test-key'; +const INDEX_PATTERN_ID = 'index-pattern-test'; + +const buildMatchFilter = ( + key: string, + operation: string, + value: string | string[] | any, +) => { + return { + meta: { + alias: null, + controlledBy: undefined, + disabled: false, + key: key, + params: value, + value: Array.isArray(value) ? value.join(', ') : value, + negate: operation.includes('not'), + type: Array.isArray(value) ? 'phrases' : 'phrase', + index: INDEX_PATTERN_ID, + }, + query: { match_phrase: { [key]: { query: value } } }, + $state: { store: FilterStateStore.APP_STATE }, + }; +}; + +const buildExistsFilter = (key: string, operation: string) => { + return { + exists: { field: key }, + meta: { + alias: null, + controlledBy: undefined, + disabled: false, + key: key, + value: 'exists', + negate: operation.includes('not'), + type: 'exists', + index: INDEX_PATTERN_ID, + }, + $state: { store: FilterStateStore.APP_STATE }, + }; +}; + +describe('onFilterCellActions', () => { + let setFilters: jest.Mock; + + beforeEach(() => { + setFilters = jest.fn(); + }); + + it('should add single filter with given key and number value (3)', () => { + const value = 3; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with is not operator for given key and number value (3)', () => { + const value = 3; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with given key and string value (19003)', () => { + const value = '19003'; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with is not operator for given key and string value (19003)', () => { + const value = '19003'; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with given key and boolean value (true)', () => { + const value = true; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with is not operator for given key and boolean value (true)', () => { + const value = true; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with given key and date value (2024-10-19T18:44:40.487Z)', () => { + const value = '2024-10-19T18:44:40.487Z'; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter is not operator for given key and date value (2024-10-19T18:44:40.487Z)', () => { + const value = '2024-10-19T18:44:40.487Z'; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with given key and ip value (10.0.2.2)', () => { + const value = '10.0.2.2'; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add single filter with is not operator for given key and ip value (10.0.2.2)', () => { + const value = '10.0.2.2'; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + value, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, value), + ]); + }); + + it('should add two filters with given key and values (group1, group2) respectively', () => { + const values = ['group1', 'group2']; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + values, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, values[0]), + buildMatchFilter(KEY, operation, values[1]), + ]); + }); + + it('should add two filters with is not operator for given key and values (group1, group2) respectively', () => { + const values = ['group1', 'group2']; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + values, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildMatchFilter(KEY, operation, values[0]), + buildMatchFilter(KEY, operation, values[1]), + ]); + }); + + it('should add single filter with given key and undefined value', () => { + const values = undefined; + const operation = FILTER_OPERATOR.IS; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + values, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildExistsFilter(KEY, FILTER_OPERATOR.DOES_NOT_EXISTS), + ]); + }); + + it('should add single filter with is not operator for given key and undefined value', () => { + const values = undefined; + const operation = FILTER_OPERATOR.IS_NOT; + + onFilterCellActions(INDEX_PATTERN_ID, [], setFilters)( + KEY, + operation, + values, + ); + + expect(setFilters).toHaveBeenCalledWith([ + buildExistsFilter(KEY, FILTER_OPERATOR.EXISTS), + ]); + }); +}); diff --git a/plugins/main/public/components/common/data-grid/filter-cell-actions.ts b/plugins/main/public/components/common/data-grid/filter-cell-actions.ts new file mode 100644 index 0000000000..8b108e67a7 --- /dev/null +++ b/plugins/main/public/components/common/data-grid/filter-cell-actions.ts @@ -0,0 +1,57 @@ +import { + FILTER_OPERATOR, + PatternDataSourceFilterManager, +} from '../data-source/pattern/pattern-data-source-filter-manager'; +import { Filter } from '../../../../../../src/plugins/data/common'; +import { isNullish } from '../util'; + +export const onFilterCellActions = ( + indexPatternId: string, + filters: Filter[], + setFilters: (filters: Filter[]) => void, +) => { + return ( + field: string, + operation: + | FILTER_OPERATOR.EXISTS + | FILTER_OPERATOR.IS + | FILTER_OPERATOR.IS_NOT, + values?: boolean | number | string | (boolean | number | string)[], + ) => { + // https://github.com/opensearch-project/OpenSearch-Dashboards/blob/4e34a7a5141d089f6c341a535be5a7ba2737d965/src/plugins/data/public/query/filter_manager/lib/generate_filters.ts#L89 + const negated = [FILTER_OPERATOR.IS_NOT].includes(operation); + let _operation: FILTER_OPERATOR = operation; + if (isNullish(values) && ![FILTER_OPERATOR.EXISTS].includes(operation)) { + if (negated) { + _operation = FILTER_OPERATOR.EXISTS; + } else { + _operation = FILTER_OPERATOR.DOES_NOT_EXISTS; + } + } + + const newFilters: Filter[] = []; + if (isNullish(values)) { + newFilters.push( + PatternDataSourceFilterManager.createFilter( + _operation, + field, + values, + indexPatternId, + ), + ); + } else { + values = Array.isArray(values) ? values : [values]; + values.forEach(item => { + newFilters.push( + PatternDataSourceFilterManager.createFilter( + _operation, + field, + item, + indexPatternId, + ), + ); + }); + } + setFilters([...filters, ...newFilters]); + }; +}; diff --git a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts index 8e4781c360..40bd4c2cd6 100644 --- a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts +++ b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-filter-manager.ts @@ -10,11 +10,10 @@ import { tDataSource, tFilterManager, } from '../index'; -import { - DATA_SOURCE_FILTER_CONTROLLED_EXCLUDE_SERVER, - AUTHORIZED_AGENTS, -} from '../../../../../common/constants'; +import { DATA_SOURCE_FILTER_CONTROLLED_EXCLUDE_SERVER } from '../../../../../common/constants'; import { PinnedAgentManager } from '../../../wz-agent-selector/wz-agent-selector-service'; +import { FilterStateStore } from '../../../../../common/constants'; + const MANAGER_AGENT_ID = '000'; const AGENT_ID_KEY = 'agent.id'; @@ -36,7 +35,7 @@ export function getFilterExcludeManager(indexPatternId: string) { controlledBy: DATA_SOURCE_FILTER_CONTROLLED_EXCLUDE_SERVER, }, query: { match_phrase: { [AGENT_ID_KEY]: MANAGER_AGENT_ID } }, - $state: { store: 'appState' }, + $state: { store: FilterStateStore.APP_STATE }, }; } @@ -278,7 +277,7 @@ export class PatternDataSourceFilterManager }; //@ts-ignore managerFilter.$state = { - store: 'appState', + store: FilterStateStore.APP_STATE, }; //@ts-ignore return [managerFilter] as tFilter[]; @@ -382,7 +381,7 @@ export class PatternDataSourceFilterManager controlledBy, }, exists: { field: key }, - $state: { store: 'appState' }, + $state: { store: FilterStateStore.APP_STATE }, }; case FILTER_OPERATOR.IS_ONE_OF: case FILTER_OPERATOR.IS_NOT_ONE_OF: @@ -429,7 +428,7 @@ export class PatternDataSourceFilterManager lte: value[1] || NaN, }, }, - $state: { store: 'appState' }, + $state: { store: FilterStateStore.APP_STATE }, }; default: throw new Error('Invalid filter type'); @@ -484,7 +483,7 @@ export class PatternDataSourceFilterManager controlledBy, }, ...query, - $state: { store: 'appState' }, + $state: { store: FilterStateStore.APP_STATE }, }; } diff --git a/plugins/main/public/components/common/doc-viewer/doc-viewer.scss b/plugins/main/public/components/common/doc-viewer/doc-viewer.scss new file mode 100644 index 0000000000..7f46b8f7c9 --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/doc-viewer.scss @@ -0,0 +1,94 @@ +.wzDocViewerTable { + pre, + .wzDocViewer__value { + display: inline-block; + word-break: break-all; + word-wrap: break-word; + white-space: pre-wrap; + color: $euiColorFullShade; + vertical-align: top; + padding-top: 2px; + } + + .wzDocViewer__field { + padding-top: 8px; + } + + .dscFieldName { + color: $euiColorDarkShade; + } + + td, + pre { + font-family: $euiCodeFontFamily; + } + + tr { + position: relative; + } + + tr:first-child td { + border-top-color: transparent; + } + + tr:hover { + .wzDocViewer__buttons { + display: flex; + gap: 2px; + position: absolute; + right: 0; + top: 0; + transform: translateY(calc(50% - 8px)); + + > span { + z-index: 2; + } + + .wzDocViewer__actionButton { + opacity: 1; + } + + &::before { + content: ''; + position: absolute; + display: block; + right: 0; + top: 0; + height: 100%; + width: 100%; + background-image: linear-gradient( + to right, + transparent 0, + aliceblue 4px + ); + z-index: 1; + } + } + } +} + +.wzDocViewer__buttons, +.wzDocViewer__field { + white-space: nowrap; +} + +.wzDocViewer__buttons { + display: none; +} + +.wzDocViewer__field { + width: 160px; + white-space: break-spaces; +} + +.wzDocViewer__actionButton { + opacity: 0; + + &:hover { + opacity: 1; + } +} + +.wzDocViewer__warning { + margin-right: $euiSizeS; +} diff --git a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx index f0249a1eaf..9ebc58e339 100644 --- a/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx +++ b/plugins/main/public/components/common/doc-viewer/doc-viewer.tsx @@ -4,6 +4,13 @@ import { escapeRegExp } from 'lodash'; import { i18n } from '@osd/i18n'; import { FieldIcon } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { FILTER_OPERATOR } from '../data-source'; +import { DocViewTableRowBtnFilterAdd } from './table-row-btn-filter-add'; +import { DocViewTableRowBtnFilterRemove } from './table-row-btn-filter-remove'; +import { DocViewTableRowBtnFilterExists } from './table-row-btn-filter-exists'; +import './doc-viewer.scss'; +import { onFilterCellActions } from '../data-grid/filter-cell-actions'; +import { Filter } from '../../../../../../src/plugins/data/common'; const COLLAPSE_LINE_LENGTH = 350; const DOT_PREFIX_RE = /(.).+?\./g; @@ -15,6 +22,9 @@ export type tDocViewerProps = { mapping: any; indexPattern: any; docJSON: any; + filters: Filter[]; + setFilters: (filters: Filter[]) => void; + onFilter?: () => void; }; /** @@ -82,13 +92,39 @@ const DocViewer = (props: tDocViewerProps) => { const [fieldRowOpen, setFieldRowOpen] = useState( {} as Record, ); - const { flattened, formatted, mapping, indexPattern, renderFields, docJSON } = - props; + const { + flattened, + formatted, + mapping, + indexPattern, + renderFields, + docJSON, + filters, + setFilters, + onFilter: onClose, + } = props; + + const onFilter = ( + field: string, + operation: + | FILTER_OPERATOR.IS + | FILTER_OPERATOR.IS_NOT + | FILTER_OPERATOR.EXISTS, + value?: string | string[], + ) => { + const _onFilter = onFilterCellActions( + indexPattern?.id, + filters, + setFilters, + ); + _onFilter(field, operation, value); + onClose?.(); + }; return ( <> {flattened && ( - +
{Object.keys(flattened) .sort() @@ -99,7 +135,7 @@ const DocViewer = (props: tDocViewerProps) => { const isCollapsed = isCollapsible && !fieldRowOpen[field]; const valueClassName = classNames({ // eslint-disable-next-line @typescript-eslint/naming-convention - osdDocViewer__value: true, + wzDocViewer__value: true, 'truncate-by-height': isCollapsible && isCollapsed, }); const isNestedField = @@ -123,7 +159,7 @@ const DocViewer = (props: tDocViewerProps) => { return ( -
+ { +
+ + onFilter( + field, + FILTER_OPERATOR.IS, + flattened[field], + ) + } + /> + + onFilter( + field, + FILTER_OPERATOR.IS_NOT, + flattened[field], + ) + } + /> + + onFilter(field, FILTER_OPERATOR.EXISTS) + } + scripted={fieldMapping && fieldMapping.scripted} + /> +
{renderFields && renderFields?.find( (field: string) => field?.id === displayName, diff --git a/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-add.tsx b/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-add.tsx new file mode 100644 index 0000000000..cf405e126d --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-add.tsx @@ -0,0 +1,76 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import styles from './doc-viewer.scss'; + +export interface Props { + onClick: () => void; + disabled: boolean; +} + +export function DocViewTableRowBtnFilterAdd({ + onClick, + disabled = false, +}: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-exists.tsx b/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-exists.tsx new file mode 100644 index 0000000000..033ee42005 --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-exists.tsx @@ -0,0 +1,84 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; + scripted?: boolean; +} + +export function DocViewTableRowBtnFilterExists({ + onClick, + disabled = false, + scripted = false, +}: Props) { + const tooltipContent = disabled ? ( + scripted ? ( + + ) : ( + + ) + ) : ( + + ); + + return ( + + + + ); +} diff --git a/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-remove.tsx b/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-remove.tsx new file mode 100644 index 0000000000..22318d3b1b --- /dev/null +++ b/plugins/main/public/components/common/doc-viewer/table-row-btn-filter-remove.tsx @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; +import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; + +export interface Props { + onClick: () => void; + disabled?: boolean; +} + +export function DocViewTableRowBtnFilterRemove({ + onClick, + disabled = false, +}: Props) { + const tooltipContent = disabled ? ( + + ) : ( + + ); + + return ( + + + + ); +} diff --git a/plugins/main/public/components/common/search-bar/set-filters.ts b/plugins/main/public/components/common/search-bar/set-filters.ts new file mode 100644 index 0000000000..955bfb2934 --- /dev/null +++ b/plugins/main/public/components/common/search-bar/set-filters.ts @@ -0,0 +1,25 @@ +import { + Filter, + FilterManager, +} from '../../../../../../src/plugins/data/public'; + +const isSameNegatedFilter = (filter: Filter, prevFilter: Filter) => { + return ( + filter.meta.key === prevFilter.meta.key && + filter.meta.type === prevFilter.meta.type && + filter.meta.params === prevFilter.meta.params.query && + filter.meta.negate !== prevFilter.meta.negate + ); +}; + +export const setFilters = + (filterManager: FilterManager) => (filters: Filter[]) => { + const prevFilters = filterManager + .getFilters() + .filter( + prevFilter => + !filters.find(filter => isSameNegatedFilter(filter, prevFilter)), + ); + const newFilters = [...filters, ...prevFilters]; + filterManager.setFilters(newFilters, undefined); + }; diff --git a/plugins/main/public/components/common/util/index.ts b/plugins/main/public/components/common/util/index.ts index 906515e7ec..2971d4bbad 100644 --- a/plugins/main/public/components/common/util/index.ts +++ b/plugins/main/public/components/common/util/index.ts @@ -16,3 +16,4 @@ export { TruncateHorizontalComponents } from './truncate-horizontal-components/t export { GroupingComponents } from './grouping-components'; export * from './markdown/markdown'; export * from './wz-overlay-mask-interface'; +export * from './is-nullish'; diff --git a/plugins/main/public/components/common/util/is-nullish.ts b/plugins/main/public/components/common/util/is-nullish.ts new file mode 100644 index 0000000000..94666d7bef --- /dev/null +++ b/plugins/main/public/components/common/util/is-nullish.ts @@ -0,0 +1,3 @@ +export const isNullish = ( + value: T | null | undefined, +): value is null | undefined => value === null || value === undefined; diff --git a/plugins/main/public/components/common/wazuh-data-grid/wz-data-grid.tsx b/plugins/main/public/components/common/wazuh-data-grid/wz-data-grid.tsx index 0da3e2e1c5..ff1413aa35 100644 --- a/plugins/main/public/components/common/wazuh-data-grid/wz-data-grid.tsx +++ b/plugins/main/public/components/common/wazuh-data-grid/wz-data-grid.tsx @@ -20,10 +20,10 @@ import { } from '../data-grid'; import { getWazuhCorePlugin } from '../../../kibana-services'; import { + Filter, IndexPattern, SearchResponse, } from '../../../../../../src/plugins/data/public'; -import { useDocViewer } from '../doc-viewer'; import { ErrorHandler, ErrorFactory, @@ -53,6 +53,8 @@ export type tWazuhDataGridProps = { pageSize: number; }) => void; onChangeSorting: (sorting: { columns: any[]; onSort: any }) => void; + filters: Filter[]; + setFilters: (filters: Filter[]) => void; }; const WazuhDataGrid = (props: tWazuhDataGridProps) => { @@ -67,6 +69,8 @@ const WazuhDataGrid = (props: tWazuhDataGridProps) => { onChangeSorting, query, dateRange, + filters, + setFilters, } = props; const [inspectedHit, setInspectedHit] = useState(undefined); const [isExporting, setIsExporting] = useState(false); @@ -115,11 +119,6 @@ const WazuhDataGrid = (props: tWazuhDataGridProps) => { onChangeSorting && onChangeSorting(sorting || []); }, [JSON.stringify(sorting)]); - const docViewerProps = useDocViewer({ - doc: inspectedHit, - indexPattern: indexPattern as IndexPattern, - }); - const timeField = indexPattern?.timeFieldName ? indexPattern.timeFieldName : undefined; @@ -150,6 +149,8 @@ const WazuhDataGrid = (props: tWazuhDataGridProps) => { } }; + const closeFlyoutHandler = () => setInspectedHit(undefined); + return ( <> {isLoading ? : null} @@ -178,7 +179,7 @@ const WazuhDataGrid = (props: tWazuhDataGridProps) => { ) : null} {inspectedHit && ( - setInspectedHit(undefined)} size='m'> + { defaultTableColumns, wzDiscoverRenderColumns, )} + filters={filters} + setFilters={setFilters} + onFilter={closeFlyoutHandler} /> diff --git a/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx b/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx index 2494628f72..1826e15a2a 100644 --- a/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx +++ b/plugins/main/public/components/common/wazuh-discover/components/data-grid-additional-controls.tsx @@ -74,7 +74,7 @@ const DiscoverDataGridAdditionalControls = ( className='euiDataGrid__controlBtn' onClick={onHandleExportResults} > - Export Formated + Export Formatted ); diff --git a/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx b/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx index da923c4e81..c52e6c9e44 100644 --- a/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx +++ b/plugins/main/public/components/common/wazuh-discover/components/doc-details.tsx @@ -1,17 +1,34 @@ -import React, { useState } from 'react'; +import React from 'react'; import { useDocViewer } from '../../doc-viewer'; import DocViewer from '../../doc-viewer/doc-viewer'; -import { IndexPattern } from '../../../../../../../src/plugins/data/common'; +import { + Filter, + IndexPattern, +} from '../../../../../../../src/plugins/data/common'; import { EuiCodeBlock, EuiFlexGroup, EuiTabbedContent } from '@elastic/eui'; -const DocDetails = ({ doc, item, indexPattern }) => { +interface DocDetailsProps { + doc: any; + item: any; + indexPattern: IndexPattern; + filters: Filter[]; + setFilters: (filters: Filter[]) => void; +} + +const DocDetails = ({ + doc, + item, + indexPattern, + filters, + setFilters, +}: DocDetailsProps) => { const docViewerProps = useDocViewer({ doc, indexPattern: indexPattern as IndexPattern, }); return ( - + { name: 'Table', content: ( <> - + ), }, @@ -29,9 +50,9 @@ const DocDetails = ({ doc, item, indexPattern }) => { content: ( {JSON.stringify(item, null, 2)} diff --git a/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx b/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx index 2592e340b9..04598fa777 100644 --- a/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx +++ b/plugins/main/public/components/common/wazuh-discover/components/document-view-table-and-json.tsx @@ -1,14 +1,29 @@ import React from 'react'; import { EuiFlexItem, EuiCodeBlock, EuiTabbedContent } from '@elastic/eui'; -import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { + IndexPattern, + Filter, +} from '../../../../../../../src/plugins/data/common'; import DocViewer from '../../doc-viewer/doc-viewer'; import { useDocViewer } from '../../doc-viewer'; +interface DocumentViewTableAndJsonProps { + document: any; + indexPattern: IndexPattern; + renderFields?: any; + filters: Filter[]; + setFilters: (filters: Filter[]) => void; + onFilter?: () => void; +} + export const DocumentViewTableAndJson = ({ document, indexPattern, renderFields, -}) => { + filters, + setFilters, + onFilter, +}: DocumentViewTableAndJsonProps) => { const docViewerProps = useDocViewer({ doc: document, indexPattern: indexPattern as IndexPattern, @@ -22,7 +37,13 @@ export const DocumentViewTableAndJson = ({ id: 'table', name: 'Table', content: ( - + ), }, { diff --git a/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx b/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx index cfd96a987b..8cffce3fed 100644 --- a/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx +++ b/plugins/main/public/components/common/wazuh-discover/wz-discover.tsx @@ -209,6 +209,8 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { } }; + const closeFlyoutHandler = () => setInspectedHit(undefined); + return ( {isDataSourceLoading ? ( @@ -290,7 +292,7 @@ const WazuhDiscoverComponent = (props: WazuhDiscoverProps) => { {inspectedHit && ( - setInspectedHit(undefined)} size='m'> + { defaultTableColumns, wzDiscoverRenderColumns, )} + filters={filters} + setFilters={setFilters} + onFilter={closeFlyoutHandler} /> diff --git a/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx b/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx index 3a9852b723..9b05e4798b 100644 --- a/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx +++ b/plugins/main/public/components/common/wazuh-discover/wz-flyout-discover.tsx @@ -270,7 +270,13 @@ const WazuhFlyoutDiscoverComponent = (props: WazuhDiscoverProps) => { indexPattern, }) ) : ( - + ); }; diff --git a/plugins/main/public/components/overview/compliance-table/compliance-table.tsx b/plugins/main/public/components/overview/compliance-table/compliance-table.tsx index 045ad5a32c..4cace57b3b 100644 --- a/plugins/main/public/components/overview/compliance-table/compliance-table.tsx +++ b/plugins/main/public/components/overview/compliance-table/compliance-table.tsx @@ -355,6 +355,8 @@ export const ComplianceTable = withAgentSupportModule(props => { getRegulatoryComplianceRequirementFilter } {...complianceData} + filters={filters} + setFilters={setFilters} /> diff --git a/plugins/main/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx b/plugins/main/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx index 607adfd9fe..0f579e8d29 100644 --- a/plugins/main/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx +++ b/plugins/main/public/components/overview/compliance-table/components/requirement-flyout/requirement-flyout.tsx @@ -35,9 +35,13 @@ import { WazuhFlyoutDiscover } from '../../../../common/wazuh-discover/wz-flyout import { PatternDataSource } from '../../../../common/data-source'; import { formatUIDate } from '../../../../../react-services'; import TechniqueRowDetails from '../../../mitre/framework/components/techniques/components/flyout-technique/technique-row-details'; -import { buildPhraseFilter } from '../../../../../../../../src/plugins/data/common'; +import { + buildPhraseFilter, + Filter, +} from '../../../../../../../../src/plugins/data/common'; import { connect } from 'react-redux'; import { wzDiscoverRenderColumns } from '../../../../common/wazuh-discover/render-columns'; +import { setFilters } from '../../../../common/search-bar/set-filters'; const mapStateToProps = state => ({ currentAgentData: state.appStateReducers.currentAgentData, @@ -173,6 +177,8 @@ export const RequirementFlyout = connect(mapStateToProps)( this.filterManager.addFilters(newFilter); }} + filters={[]} + setFilters={setFilters(this.filterManager)} /> ); } diff --git a/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx b/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx index 79bb050d2c..5f515b75cc 100644 --- a/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx +++ b/plugins/main/public/components/overview/compliance-table/components/subrequirements/subrequirements.tsx @@ -361,6 +361,8 @@ export class ComplianceSubrequirements extends Component { ]} openDashboard={(e, itemId) => this.openDashboard(e, itemId)} openDiscover={(e, itemId) => this.openDiscover(e, itemId)} + filters={this.props.filters} + setFilters={this.props.setFilters} /> )} diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx index b0eace1ea7..958311c76f 100644 --- a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/flyout-technique.tsx @@ -47,6 +47,7 @@ import { buildPhraseFilter } from '../../../../../../../../../../../src/plugins/ import store from '../../../../../../../../redux/store'; import NavigationService from '../../../../../../../../react-services/navigation-service'; import { wzDiscoverRenderColumns } from '../../../../../../../common/wazuh-discover/render-columns'; +import { setFilters } from '../../../../../../../common/search-bar/set-filters'; type tFlyoutTechniqueProps = { currentTechnique: string; @@ -227,7 +228,14 @@ export const FlyoutTechnique = (props: tFlyoutTechniqueProps) => { }; const expandedRow = (props: { doc: any; item: any; indexPattern: any }) => { - return ; + return ( + + ); }; const addRenderColumn = columns => { diff --git a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx index b808662c8e..c07de35380 100644 --- a/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx +++ b/plugins/main/public/components/overview/mitre/framework/components/techniques/components/flyout-technique/technique-row-details.tsx @@ -3,17 +3,29 @@ import { EuiCodeBlock, EuiFlexGroup, EuiTabbedContent } from '@elastic/eui'; import { useDocViewer } from '../../../../../../../common/doc-viewer/use-doc-viewer'; import DocViewer from '../../../../../../../common/doc-viewer/doc-viewer'; import RuleDetails from '../rule-details'; -import { IndexPattern } from '../../../../../../../../../../../src/plugins/data/common'; +import { + IndexPattern, + Filter, +} from '../../../../../../../../../../../src/plugins/data/common'; import { WzRequest } from '../../../../../../../../react-services/wz-request'; -type Props = { +type TechniqueRowDetailsProps = { doc: any; item: any; indexPattern: IndexPattern; onRuleItemClick?: (value: any, indexPattern: IndexPattern) => void; + filters: Filter[]; + setFilters: (filters: Filter[]) => void; }; -const TechniqueRowDetails = ({ doc, item, indexPattern, onRuleItemClick }) => { +const TechniqueRowDetails = ({ + doc, + item, + indexPattern, + onRuleItemClick, + filters, + setFilters, +}: TechniqueRowDetailsProps) => { const docViewerProps = useDocViewer({ doc, indexPattern: indexPattern as IndexPattern, @@ -23,13 +35,16 @@ const TechniqueRowDetails = ({ doc, item, indexPattern, onRuleItemClick }) => { const getRuleData = async () => { const params = { q: `id=${item.rule.id}` }; - const rulesDataResponse = await WzRequest.apiReq('GET', `/rules`, { params }); - const ruleData = ((rulesDataResponse.data || {}).data || {}).affected_items[0] || {}; + const rulesDataResponse = await WzRequest.apiReq('GET', `/rules`, { + params, + }); + const ruleData = + ((rulesDataResponse.data || {}).data || {}).affected_items[0] || {}; setRuleData(ruleData); }; const onAddFilter = (filter: { [key: string]: string }) => { - onRuleItemClick(filter, indexPattern); + onRuleItemClick?.(filter, indexPattern); }; useEffect(() => { @@ -46,7 +61,11 @@ const TechniqueRowDetails = ({ doc, item, indexPattern, onRuleItemClick }) => { name: 'Table', content: ( <> - + ), }, @@ -56,9 +75,9 @@ const TechniqueRowDetails = ({ doc, item, indexPattern, onRuleItemClick }) => { content: ( {JSON.stringify(item, null, 2)} diff --git a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx index d77b4f2a8d..2497b0af16 100644 --- a/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx +++ b/plugins/main/public/components/overview/vulnerabilities/dashboards/inventory/inventory.tsx @@ -203,6 +203,8 @@ const InventoryVulsComponent = () => { getUnderEvaluation(filters || []), ); + const closeFlyoutHandler = () => setInspectedHit(undefined); + return ( <> @@ -286,7 +288,7 @@ const InventoryVulsComponent = () => { className='euiDataGrid__controlBtn' onClick={onClickExportResults} > - Export Formated + Export Formatted ), @@ -296,7 +298,7 @@ const InventoryVulsComponent = () => { ) : null} {inspectedHit && ( - setInspectedHit(undefined)} size='m'> +

Vulnerability details

@@ -311,6 +313,9 @@ const InventoryVulsComponent = () => { inventoryTableDefaultColumns, wzDiscoverRenderColumns, )} + filters={filters} + setFilters={setFilters} + onFilter={closeFlyoutHandler} />