diff --git a/CHANGELOG.md b/CHANGELOG.md index ecd37566dd..7c4568423c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,18 @@ All notable changes to the Wazuh app project will be documented in this file. - Support for Wazuh 4.10.1 -## Wazuh v4.10.0 - OpenSearch Dashboards 2.16.0 - Revision 02 +## Wazuh v4.10.0 - OpenSearch Dashboards 2.16.0 - Revision 03 ### Added - Support for Wazuh 4.10.0 - Added sample data for YARA [#6964](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6964) - Added a custom filter and visualization for vulnerability.under_evaluation field [#6968](https://github.com/wazuh/wazuh-dashboard-plugins/issues/6968) [#7044](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7044) [#7046](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7046) +- Add vulnerabilities card to agent details page [#7058](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7058) +- 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 @@ -25,24 +30,42 @@ All notable changes to the Wazuh app project will be documented in this file. - Changed the agents summary in overview with no results to an agent deployment help message. [#7041](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7041) - Changed malware feature description [#7036](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7036) - Changed the font size of the kpi subtitles and the features descriptions [#7033](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7033) +- Changed the warning icon in events view to a info icon [#7057](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7057) - Changed feature container margins to ensure consistent separation and uniform design. [#7034](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7034) - Changed the initial width to the default columns on each selected field [#7059](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7059) +- Changed inventory, stats and configuration page to use tabs [#7089](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7089) ### Fixed - Fixed the filter are displayed cropped on screens of 575px to 767px in vulnerability detection module [#7047](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7047) - Fixed read-only users could not access to Statistics application [#7001](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7001) - Fixed no-agent-alert spawn with selected agent in agent-welcome view [#7029](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7029) +- Fixed loading state of the agents status chart in the home overview [#7120](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7120) - Fixed security policy exception when it contained deprecated actions [#7042](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7042) +- Fixed border on cells in events that disappear when clicked [#7075](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7075) - Fixed export formatted csv data with special characters from tables [#7048](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7048) - Fixed column reordering feature [#7072](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7072) - Fixed filter management to prevent hiding when adding multiple filters [#7077](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7077) +- Fixed the Mitre ATT&CK exception in the agent view, the redirections of ID, Tactics, Dashboard Icon and Event Icon in the drop-down menu and the card not displaying information when the flyout was opened [#7116](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7116) +- Fixed the filter are displayed cropped on screens of 575px to 767px in vulnerability detection module [#7047](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7047) +- Fixed ability to filter from files inventory details flyout of File Integrity Monitoring [#7119](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7119) ### Removed - Removed agent RBAC filters from dashboard queries [#6945](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6945) - Removed GET /elastic/statistics API endpoint [#7001](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7001) - Removed VirusTotal application in favor of Malware Detection [#7038](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7038) +- Removed processes state column in macOS agents [#7122](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7122) + +## Wazuh v4.9.2 - OpenSearch Dashboards 2.13.0 - Revision 01 + +### Added + +- Support for Wazuh 4.9.2 + +### Fixed + +- Fixed vulnerabilities inventory table scroll [#7128](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7128) ## Wazuh v4.9.1 - OpenSearch Dashboards 2.13.0 - Revision 04 diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index 8f99c6897e..4e93ff790c 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -1,6 +1,6 @@ --- plugin: openapi -specFile: https://raw.githubusercontent.com/wazuh/wazuh/master/api/api/spec/spec.yaml +specFile: https://raw.githubusercontent.com/wazuh/wazuh/4.10.1/api/api/spec/spec.yaml system: stores: # this store is preloaded from file 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/common/services/wz_agent_status.ts b/plugins/main/common/services/wz_agent_status.ts index 3c37e98a92..694e536917 100644 --- a/plugins/main/common/services/wz_agent_status.ts +++ b/plugins/main/common/services/wz_agent_status.ts @@ -1,14 +1,27 @@ -import { UI_COLOR_AGENT_STATUS, UI_LABEL_NAME_AGENT_STATUS, API_NAME_AGENT_STATUS } from '../constants'; +import { + UI_COLOR_AGENT_STATUS, + UI_LABEL_NAME_AGENT_STATUS, + API_NAME_AGENT_STATUS, +} from '../constants'; -type TAgentStatus = typeof API_NAME_AGENT_STATUS[keyof typeof API_NAME_AGENT_STATUS]; +export type TAgentStatus = + (typeof API_NAME_AGENT_STATUS)[keyof typeof API_NAME_AGENT_STATUS]; -type TAgentStatusColor = typeof UI_COLOR_AGENT_STATUS[keyof typeof UI_COLOR_AGENT_STATUS]; -type TAgentStatusLabel = typeof UI_LABEL_NAME_AGENT_STATUS[keyof typeof UI_LABEL_NAME_AGENT_STATUS]; +type TAgentStatusColor = + (typeof UI_COLOR_AGENT_STATUS)[keyof typeof UI_COLOR_AGENT_STATUS]; +type TAgentStatusLabel = + (typeof UI_LABEL_NAME_AGENT_STATUS)[keyof typeof UI_LABEL_NAME_AGENT_STATUS]; -export function agentStatusColorByAgentStatus(status: TAgentStatus): TAgentStatusColor{ - return UI_COLOR_AGENT_STATUS[status] || UI_COLOR_AGENT_STATUS.default; +export function agentStatusColorByAgentStatus( + status: TAgentStatus, +): TAgentStatusColor { + return UI_COLOR_AGENT_STATUS[status] || UI_COLOR_AGENT_STATUS.default; } -export function agentStatusLabelByAgentStatus(status: TAgentStatus): TAgentStatusLabel{ - return UI_LABEL_NAME_AGENT_STATUS[status] || UI_LABEL_NAME_AGENT_STATUS.default; -} \ No newline at end of file +export function agentStatusLabelByAgentStatus( + status: TAgentStatus, +): TAgentStatusLabel { + return ( + UI_LABEL_NAME_AGENT_STATUS[status] || UI_LABEL_NAME_AGENT_STATUS.default + ); +} diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index 842c07ae03..8f45a6b3cd 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -21,6 +21,7 @@ import { Settings } from './components/settings'; import { WzSecurity } from './components/security'; import $ from 'jquery'; import NavigationService from './react-services/navigation-service'; +import { SECTIONS } from './sections'; export function Application(props) { const dispatch = useDispatch(); @@ -66,33 +67,41 @@ export function Application(props) { - + - + - + } > - - + + } > } > diff --git a/plugins/main/public/components/agents/agent-status.tsx b/plugins/main/public/components/agents/agent-status.tsx index a7967a0d92..65179ba825 100644 --- a/plugins/main/public/components/agents/agent-status.tsx +++ b/plugins/main/public/components/agents/agent-status.tsx @@ -2,15 +2,29 @@ import React from 'react'; import { agentStatusColorByAgentStatus, agentStatusLabelByAgentStatus, + TAgentStatus, } from '../../../common/services/wz_agent_status'; import { ColumnWithStatusIcon } from './column-with-status-icon'; import { EuiIconTip } from '@elastic/eui'; import { AGENT_STATUS_CODE } from '../../../common/constants'; import '../../styles/common.scss'; +import { Agent } from '../endpoints-summary/types'; -export const AgentStatus = ({ status, children = null, style = {}, agent }) => { +interface AgentStatusProps { + status: TAgentStatus; + children?: string | null; + style?: React.CSSProperties; + agent?: Agent; +} + +export const AgentStatus = ({ + status, + children, + style = {}, + agent, +}: AgentStatusProps) => { const statusCodeAgent = AGENT_STATUS_CODE.find( - (status: StatusCodeAgent) => status.STATUS_CODE === agent?.status_code, + status => status.STATUS_CODE === agent?.status_code, ); return (
{ - this.setState({ filters }); - }; - - onTotalItemsChange = (totalItems: number) => { - this.setState({ totalItemsFile: totalItems }); - }; - onSelectedTabChanged = id => { this.setState({ selectedTabId: id }); }; - buildFilter(type) { - const filters = filtersToObject(this.state.filters); - const filter = { - ...filters, - limit: type === 'file' ? '15' : '1', - ...(type === 'registry' ? { q: 'type=registry_key' } : { type }), - ...(type === 'file' && { sort: '+file' }), - }; - return filter; - } - async getItemNumber(type: 'file' | 'registry') { try { const agentID = this.props.agent.id; const response = await WzRequest.apiReq('GET', `/syscheck/${agentID}`, { - params: this.buildFilter(type), + params: { + limit: 1, // reduce the size because only need the total items. 0 gives error + ...(type === 'registry' + ? { q: 'type=registry_key' } + : { q: 'type=file' }), + }, }); if (type === 'file') { return { @@ -257,8 +217,6 @@ export class Inventory extends Component { filters={filters} items={syscheck} totalItems={totalItemsFile} - onFiltersChange={this.onFiltersChange} - onTotalItemsChange={this.onTotalItemsChange} /> )} {selectedTabId === 'registry' && ( @@ -266,7 +224,6 @@ export class Inventory extends Component { {...this.props} filters={filters} totalItems={totalItemsRegistry} - onFiltersChange={this.onFiltersChange} /> )} diff --git a/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx b/plugins/main/public/components/agents/fim/inventory/fileDetail.tsx index 0dff1a4698..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!: { @@ -198,6 +199,7 @@ export class FileDetails extends Component { name: 'Last analysis', grow: 2, icon: 'clock', + link: true, transformValue: formatUIDate, }, { @@ -205,6 +207,7 @@ export class FileDetails extends Component { name: 'Last modified', grow: 2, icon: 'clock', + link: true, transformValue: formatUIDate, }, ]; @@ -290,21 +293,19 @@ export class FileDetails extends Component { } addFilter(field, value) { - const { filters, onFiltersChange } = this.props; - const newBadge: ICustomBadges = { field: 'q', value: '' }; + const { onFiltersChange } = this.props; + let filterUQL = ''; if (field === 'date' || field === 'mtime') { const value_max = moment(value).add(1, 'day'); - newBadge.value = `${field}>${moment(value).format( + filterUQL = `${field}>${moment(value).format( 'YYYY-MM-DD', - )} AND ${field}<${value_max.format('YYYY-MM-DD')}`; + )};${field}<${value_max.format('YYYY-MM-DD')}`; } else { - newBadge.value = `${field}=${ + filterUQL = `${field}=${ field === 'size' ? this.props.currentFile[field] : value }`; } - !filters.some( - item => item.field === newBadge.field && item.value === newBadge.value, - ) && onFiltersChange([...filters, newBadge]); + onFiltersChange({ q: filterUQL }); this.props.closeFlyout(); } @@ -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/agents/fim/inventory/registry-table.tsx b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx index ee96fff10c..6dfb410c46 100644 --- a/plugins/main/public/components/agents/fim/inventory/registry-table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/registry-table.tsx @@ -21,18 +21,10 @@ import { withRouterSearch } from '../../../common/hocs'; import { Route, Switch } from '../../../router-search'; import NavigationService from '../../../../react-services/navigation-service'; -const searchBarWQLOptions = { - implicitQuery: { - query: 'type=registry_key', - conjunction: ';', - }, -}; - -const searchBarWQLFilters = { default: { q: 'type=registry_key' } }; - export const RegistryTable = withRouterSearch( class RegistryTable extends Component { state: { + filters: {}; syscheck: []; isFlyoutVisible: Boolean; currentFile: { @@ -51,7 +43,7 @@ export const RegistryTable = withRouterSearch( super(props); this.state = { - syscheck: [], + filters: {}, isFlyoutVisible: false, currentFile: { file: '', @@ -73,12 +65,13 @@ export const RegistryTable = withRouterSearch( name: 'Registry', sortable: true, searchable: true, + show: true, }, { field: 'mtime', name: ( - Last Modified{' '} + Last modified{' '} + Last analysis{' '} + + + ), + sortable: true, + width: '100px', + render: formatUIDate, + searchable: false, }, ]; } + onFiltersChange = filters => { + this.setState({ + filters, + }); + }; + renderRegistryTable() { const getRowProps = item => { const { file } = item; @@ -111,6 +129,8 @@ export const RegistryTable = withRouterSearch( const columns = this.columns(); + const APIendpoint = `/syscheck/${this.props.agent.id}?type=registry_key`; + return ( @@ -118,11 +138,11 @@ export const RegistryTable = withRouterSearch( title='Registry' tableColumns={columns} tableInitialSortingField='file' - endpoint={`/syscheck/${this.props.agent.id}`} + endpoint={APIendpoint} searchBarWQL={{ - options: searchBarWQLOptions, suggestions: { field: () => [ + { label: 'date', description: 'filter by analysis time' }, { label: 'file', description: 'filter by file' }, { label: 'mtime', @@ -133,7 +153,7 @@ export const RegistryTable = withRouterSearch( try { const response = await WzRequest.apiReq( 'GET', - `/syscheck/${this.props.agent.id}`, + APIendpoint, { params: { distinct: true, @@ -142,12 +162,9 @@ export const RegistryTable = withRouterSearch( sort: `+${field}`, ...(currentValue ? { - // Add the implicit query - q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + q: `${field}~${currentValue}`, } - : { - q: `${searchBarWQLOptions.implicitQuery.query}`, - }), + : {}), }, }, ); @@ -174,11 +191,16 @@ export const RegistryTable = withRouterSearch( }, }, }} - filters={searchBarWQLFilters} + filters={this.state.filters} showReload downloadCsv={`fim-registry-${this.props.agent.id}`} searchTable={true} rowProps={getRowProps} + saveStateStorage={{ + system: 'localStorage', + key: 'wz-fim-registry-key-table', + }} + showFieldSelector /> @@ -201,6 +223,7 @@ export const RegistryTable = withRouterSearch( view='inventory' // showViewInEvents={true} {...this.props} + onFiltersChange={this.onFiltersChange} /> )} > diff --git a/plugins/main/public/components/agents/fim/inventory/table.tsx b/plugins/main/public/components/agents/fim/inventory/table.tsx index f9021891db..80157ce30c 100644 --- a/plugins/main/public/components/agents/fim/inventory/table.tsx +++ b/plugins/main/public/components/agents/fim/inventory/table.tsx @@ -21,19 +21,10 @@ import { withRouterSearch } from '../../../common/hocs'; import { Route, Switch } from '../../../router-search'; import NavigationService from '../../../../react-services/navigation-service'; -const searchBarWQLOptions = { - implicitQuery: { - query: 'type=file', - conjunction: ';', - }, -}; - -const searchBarWQLFilters = { default: { q: 'type=file' } }; - export const InventoryTable = withRouterSearch( class InventoryTable extends Component { state: { - syscheck: []; + filters: any; isFlyoutVisible: Boolean; currentFile: { file: string; @@ -46,14 +37,13 @@ export const InventoryTable = withRouterSearch( agent: any; items: []; totalItems: number; - onTotalItemsChange: Function; }; constructor(props) { super(props); this.state = { - syscheck: props.items, + filters: {}, isFlyoutVisible: false, currentFile: { file: '', @@ -80,12 +70,13 @@ export const InventoryTable = withRouterSearch( sortable: true, width: '250px', searchable: true, + show: true, }, { field: 'mtime', name: ( - Last Modified{' '} + Last modified{' '} + Last analysis{' '} + + + ), + sortable: true, + width: '100px', + render: formatUIDate, + searchable: false, + }, + { + field: 'md5', + name: 'MD5', + searchable: true, + sortable: true, + }, + { + field: 'sha1', + name: 'SHA1', + searchable: true, + sortable: true, + }, + { + field: 'sha256', + name: 'SHA256', + searchable: true, + sortable: true, }, ]; } + onFiltersChange = filters => { + this.setState({ + filters, + }); + }; + renderFilesTable() { const getRowProps = item => { const { file } = item; @@ -155,6 +194,8 @@ export const InventoryTable = withRouterSearch( }; const columns = this.columns(); + const APIendpoint = `/syscheck/${this.props.agent.id}?type=file`; + return ( @@ -162,18 +203,24 @@ export const InventoryTable = withRouterSearch( title='Files' tableColumns={columns} tableInitialSortingField='file' - endpoint={`/syscheck/${this.props.agent.id}`} + endpoint={APIendpoint} searchBarWQL={{ - options: searchBarWQLOptions, suggestions: { field: currentValue => [ + { label: 'date', description: 'filter by analysis time' }, { label: 'file', description: 'filter by file' }, { label: 'gid', description: 'filter by group id' }, { label: 'gname', description: 'filter by group name' }, + { label: 'md5', description: 'filter by MD5 checksum' }, { label: 'mtime', description: 'filter by modification time', }, + { label: 'sha1', description: 'filter by SHA1 checksum' }, + { + label: 'sha256', + description: 'filter by SHA256 checksum', + }, { label: 'size', description: 'filter by size' }, { label: 'uname', description: 'filter by user name' }, { label: 'uid', description: 'filter by user id' }, @@ -182,7 +229,7 @@ export const InventoryTable = withRouterSearch( try { const response = await WzRequest.apiReq( 'GET', - `/syscheck/${this.props.agent.id}`, + APIendpoint, { params: { distinct: true, @@ -191,12 +238,9 @@ export const InventoryTable = withRouterSearch( sort: `+${field}`, ...(currentValue ? { - // Add the implicit query - q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}${field}~${currentValue}`, + q: `${field}~${currentValue}`, } - : { - q: `${searchBarWQLOptions.implicitQuery.query}`, - }), + : {}), }, }, ); @@ -223,11 +267,16 @@ export const InventoryTable = withRouterSearch( }, }, }} - filters={searchBarWQLFilters} + filters={this.state.filters} showReload downloadCsv={`fim-files-${this.props.agent.id}`} searchTable={true} rowProps={getRowProps} + saveStateStorage={{ + system: 'localStorage', + key: 'wz-fim-files-table', + }} + showFieldSelector /> @@ -251,6 +300,7 @@ export const InventoryTable = withRouterSearch( view='inventory' showViewInEvents={true} {...this.props} + onFiltersChange={this.onFiltersChange} /> )} > diff --git a/plugins/main/public/components/agents/stats/agent-stats.test.tsx b/plugins/main/public/components/agents/stats/agent-stats.test.tsx new file mode 100644 index 0000000000..84f8e2a648 --- /dev/null +++ b/plugins/main/public/components/agents/stats/agent-stats.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { render, act } from '@testing-library/react'; +import { AgentStats } from './agent-stats'; +import { queryDataTestAttr } from '../../../../test/public/query-attr'; +import { CSS } from '../../../../test/utils/CSS'; + +jest.mock('../../../react-services', () => ({ + WzRequest: { + apiReq: jest.fn().mockResolvedValue(undefined), + }, +})); + +describe('AgentStats', () => { + it('should not render agent info ribbon', async () => { + await act(async () => { + const { container } = render(); + + const agentInfoRibbon = container.querySelector( + queryDataTestAttr('agent-info'), + ); + expect(agentInfoRibbon).toBeFalsy(); + }); + }); + + it('should render stats info ribbon', async () => { + await act(async () => { + const { container } = render(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-status')), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr('ribbon-item-buffer_enabled'), + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-msg_buffer')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-msg_count')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-msg_sent')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-last_ack')), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr('ribbon-item-last_keepalive'), + ), + ).toBeTruthy(); + + expect( + container.querySelectorAll( + queryDataTestAttr('ribbon-item-', CSS.Attribute.Substring), + ), + ).toHaveLength(7); + }); + }); +}); diff --git a/plugins/main/public/components/agents/stats/agent-stats.tsx b/plugins/main/public/components/agents/stats/agent-stats.tsx index 2d57187359..98f966281c 100644 --- a/plugins/main/public/components/agents/stats/agent-stats.tsx +++ b/plugins/main/public/components/agents/stats/agent-stats.tsx @@ -47,6 +47,7 @@ import { import { getErrorOrchestrator } from '../../../react-services/common-services'; import { endpointSummary } from '../../../utils/applications'; import NavigationService from '../../../react-services/navigation-service'; +import WzRibbon from '../../common/ribbon/ribbon'; const tableColumns = [ { @@ -142,7 +143,8 @@ export const MainAgentStats = compose( ), )(AgentStats); -function AgentStats({ agent }) { +export function AgentStats(props) { + const { agent } = props; const [loading, setLoading] = useState(); const [dataStatLogcollector, setDataStatLogcollector] = useState({}); const [dataStatAgent, setDataStatAgent] = useState(); @@ -186,33 +188,20 @@ function AgentStats({ agent }) { return ( - - - - - {statsAgents.map(stat => ( - - - {stat.title}:{' '} - {loading ? ( - - ) : ( - - {dataStatAgent !== undefined - ? stat.render - ? stat.render(dataStatAgent[stat.field]) - : dataStatAgent?.[stat.field] - : '-'} - - )} - - - ))} - - - - - + ({ + key: stat.field, + label: stat.title, + isLoading: loading, + value: + dataStatAgent !== undefined + ? stat.render + ? stat.render(dataStatAgent[stat.field]) + : dataStatAgent?.[stat.field] + : '-', + }))} + /> +
+ +
+
+ + + Cores [object Object] +
+
+
+ +
+
+ + + Memory [object Object] +
+
+
+ +
+
+ + + Arch [object Object] +
+
+
+ +
+
+ + + Operating system [object Object] +
+
+
+ +
+
+ + + CPU [object Object] +
+ +
+ +
+
+ + + Host name [object Object] +
+ +
+ +
+
+ + + Board serial [object Object] +
+ +
+ +
+
+ + + Last scan [object Object] +
- -
+
-

- Network interfaces - - (0) - -

-
-
-
-
-
-
- + Network interfaces + + (0) + + +
+
- + + + + Refresh + + + +
+
+ +
+
-
-
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + - - - - - - + + + + +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
-
- - -
+
+ + - - - - - - - - - - - + - + + + + + + + + +
-
- - - Name - - - - - - + - - MAC - - - - - + - - State - + + + Type + + + +
+
+ + No items found + +
+
+ + + + + + +
+
+
+
+
+
+
+
+

+ Network ports + + (0) - -

+ + + +
+
+
-
+
-
+
- - No items found - +
+
+ +
+
+
-
+
+
+ + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + +
+
+ + No items found + +
+
+
+
-
-
-

- Network ports - - (0) - -

+

+ Network settings + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -686,183 +1209,238 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - + + - + - + - + - - - - - + + + + -
- - No items found - -
- - - -
-
- +
+
- - Local port - - - - + + Interface + + + + + - - Local IP address - - - - + - - State + + Netmask + - - - - - - Protocol + + Protocol + - - -
+ + + +
+ + No items found + +
+ + + + +
@@ -870,23 +1448,301 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = `
+
+`; + +exports[`Inventory data Network Network interfaces table A Linux agent should render network interfaces table with correct columns and title. 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
-

- Network settings - - (0) - -

-
-
-
-
-
-
- + Network interfaces + + (0) + + +
+
- -
-
-
-
-
-
-
-
-
-
-
+
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - - - - - - - - - - -
-
- - - - - +
-
- -
-
- - No items found - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- Packages - - (0) - -

-
-
-
-
-
-
- -
-
- +
+
-
-
-
-
-
+
-
+
-
- -
-
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
-
+
+
+
+
+ +
+
+
+
+
+
+ + + + + - - + +
+
+ + - - - - - - - - - - + + + + + + + + + +
-
- - - Name - - + + State + + + + + + + +
+
+ + No items found + +
+
+ + + + + + +
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ -
+
-
+ + + + +
+
+
+
+
- +
+
+
+
+
+ + + +
+
+
+
+
+
+
- +
+
+ +
+
+
+
+
+
+ + + + - - - - - - - + + + + + + - - No items found - - - - - -
+
- Format + + Local port + - - - - - Location + + Local IP address + - - - - - Description + + Process + - - -
-
+
+ + + PID + + + + + + +
+
+ + No items found + +
+ +
+
+
-
-
-
-
-

- Processes - - (0) - -

+

+ Network settings + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -1852,287 +2692,238 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - - + + - + - + - + - + - + + + + - - - - - - - - - -
-
- - -
+
- - Effective user + + Interface + + - - - - - - PID + + Address + - - - - - - Parent PID + + Netmask + - - - - - - VM size + + Protocol + - - - - - - Priority + + Broadcast + - - - +
-
- - No items found - -
-
+
+ + + + +
@@ -2143,139 +2934,298 @@ exports[`Inventory component A Apple agent should be well rendered. 1`] = `
`; -exports[`Inventory component A Linux agent should be well rendered. 1`] = ` +exports[`Inventory data Network Network interfaces table A Windows agent should render network interfaces table with correct columns and title. 1`] = `
+ +
+
+ + + Cores [object Object] +
+
+
+ +
+
+ + + Memory [object Object] +
+
+
+ +
+
+ + + Arch [object Object] +
+
+
+ +
+
+ + + Operating system [object Object] +
+
+
+ +
+
+ + + CPU [object Object] +
+
+
+ +
+
+ + + Host name [object Object] +
+
+
+ +
+
+ + + Board serial [object Object] +
+
+
+ +
+
+ + + Last scan [object Object] +
-
-
+
-

- Network interfaces - - (0) - -

-
-
-
-
-
-
- + Network interfaces + + (0) + + +
+
- -
-
-
-
-
-
-
-
-
-
-
+
-
- -
-
+ + + + Refresh + + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
-
+
+
- - WQL - - - + + + WQL + + + +
+
-
-
-
-
-
+
-
+
-
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + - - - - + + + + + + +
+
+ + No items found + +
+
- + + + +
+
+
+
+
-
- - - - - - - - - - + +
+
+ +
+
+ + + + + + +
+
+
+
-
- - -
-
+
+
+

+ Network ports + + (0) + +

+
+
+ +
+
+
-
- - +
-
- - + + + + +
+
+
+
+
- -
+
- - No items found - +
+
+ +
+
+
-
+
+
+ + + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + + + +
+
+ + No items found + +
+
+
+
-
-
-

- Network ports - - (0) - -

+

+ Network settings + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -2828,214 +4167,238 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - + + - + - + - + - + - + + + + - - - - - - - - - -
-
- +
+
- - Local port - - - - + + Interface + + + + + - - Local IP address - - - - + + Address + + + + - - Process - - - - + + Netmask + + + + - - PID - - - - + - - State + + Broadcast + - - - +
-
- - No items found - -
-
+
+ + + + +
@@ -3043,572 +4406,10845 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = `
+
+`; + +exports[`Inventory data Network Network ports table A Apple agent should render network ports table with correct columns and title. 1`] = ` +
-
-
-
-

- Network settings - - (0) - -

-
-
-
- +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
- + Network interfaces + + (0) + + +
+
- + + + + Refresh + + + +
+
+ +
+
-
-
-
-
-
+
-
+
-
- +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
- - + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network settings + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Network Network ports table A Linux agent should render network ports table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network interfaces + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + Process + + + + + + PID + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network settings + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Network Network ports table A Windows agent should render network ports table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network interfaces + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network settings + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Network Network settings table A Apple agent should render network settings table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network interfaces + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network settings + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Network Network settings table A Linux agent should render network settings table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network interfaces + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + Process + + + + + + PID + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network settings + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Network Network settings table A Windows agent should render network settings table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network interfaces + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network ports + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + Local port + + + + + + Local IP address + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Network settings + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Processes A Apple agent should render processes table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Processes + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Processes A Linux agent should render processes table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Processes + + (0) + +

+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + No items found + +
+
+
+
+
+
+
+
+`; + +exports[`Inventory data Processes A Windows agent should render processes table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ class="euiText euiText--small euiStat__description" + > + +
+
+
+
+
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - - - - - - - - - - -
-
- - - - - - - - - -
-
- - No items found - -
-
-
+
+
-
-
+
-

- Packages - - (0) - -

+

+ Processes + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -3616,239 +15252,288 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - + + + + + - + - + - + - + + + + - - - - - - - - - -
-
-
+
+ + + + + + - - Name + + VM size + - - - - - - - Architecture + + Priority + - - - - - - Version + + NLWP + - - - - - - Vendor + + Command + - - - +
-
- - No items found - -
-
+
+ + + + +
@@ -3856,167 +15541,450 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = `
+
+`; + +exports[`Inventory data Software Packages table A Apple agent should render software table with correct columns and title. 1`] = ` +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
-

- Processes - - (0) - -

+

+ Packages + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
+ +
+
- + +
@@ -4024,408 +15992,240 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - - - - - - - + + - + - + - + - + - + + + + - - - - - - - - - -
-
- - - - - - - - - - - - -
+
- - Argvs + + Name + + - - - - - - VM size + + Version + - - - - - - Size + + Format + - - - - - - Session + + Location + - - - - - - Priority + + Description + - - - +
-
- - No items found - -
-
+
+ + + + +
@@ -4436,283 +16236,447 @@ exports[`Inventory component A Linux agent should be well rendered. 1`] = `
`; -exports[`Inventory component A Windows agent should be well rendered. 1`] = ` +exports[`Inventory data Software Packages table A Linux agent should render software table with correct columns and title. 1`] = `
+ +
+
+ + + Cores [object Object] +
+
+
+ +
+
+ + + Memory [object Object] +
+
+
+ +
+
+ + + Arch [object Object] +
+
+
+ +
+
+ + + Operating system [object Object] +
+
+
+ +
+
+ + + CPU [object Object] +
+
+
+ +
+
+ + + Host name [object Object] +
+
+
+ +
+
+ + + Board serial [object Object] +
+
+
+ +
+
+ + + Last scan [object Object] +
-
-
+
-

- Network interfaces - - (0) - -

+

+ Packages + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -4720,401 +16684,692 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - + + - + - + - + - + - - - - - + + + -
- - No items found - -
- - - -
-
-
+
- - Name + + Name + + - - - - - - - MAC + + Architecture + - - - - - - State + + Version + - - - - - - MTU + + Vendor + - - - - - - Type + + Description + - - -
+ +
+ + No items found + +
+ + + + +
+
+
+`; + +exports[`Inventory data Software Packages table A Windows agent should render software table with correct columns and title. 1`] = ` +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
-

- Network ports - - (0) - -

+

+ Windows updates + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -5122,375 +17377,308 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - - - - + + - + + + + - - - - - - - - - -
-
- - - Local port - - - - - - Local IP address - - - - - -
+
- - State + + Update code + + - - - +
-
- - No items found - -
-
+
+ + + + +
-
-
-
-
-

- Network settings - - (0) - -

+

+ Packages + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -5498,1095 +17686,976 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - + + - + - + - + - + + + + - - - - - - - - - -
-
-
+
- - Interface + + Name + + - - - - - - - Address + + Architecture + - - - - - - Netmask + + Version + - - - - - - Protocol + + Vendor + - - - +
-
- - No items found - -
-
+
+ + + + +
+
+
+`; + +exports[`Inventory data Software Windows updates table A Windows agent should render software table with correct columns and title. 1`] = ` +
+
- -
+
+
+
+
+
+
+
+ Memory + +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
- - - - - - - - - - - -
-
- -
-
- - No items found - -
-
-
+
+
-
-
+
-

- Packages - - (0) - -

+

+ Windows updates + + (0) + +

+
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
+
+
+ -
-
-
+ + + + Export formatted + + +
-
-
-
+
-
+
+
- - - Sorting + + WQL + - - -
-
-
-
-
-
- - - - - - + +
-
- - + + + + + + + + +
+
+
+
+
+
+
- -
- + + + + + + + + + + - + + + + - - - - - - - - - -
+
- - Version + + Update code + + - - - +
-
- - No items found - -
-
+ + +
+
-
-
-
-
-

- Processes - - (0) - -

+

+ Packages + + (0) + +

+
-
-
- -
-
- +
+
+ + +
-
-
-
-
+
+
-
-
-
- -
+
+
-
+
+
- - WQL + + WQL + - - + +
@@ -6594,287 +18663,216 @@ exports[`Inventory component A Windows agent should be well rendered. 1`] = `
-
-
-
-
-
+
+
+
-
+
- + +
-
- - - - - - + + - + - + - + - + + + + - - - - - - - - - -
-
- - - - -
+
- - Parent PID + + Name + + - - - - - - VM size + + Architecture + - - - - - - Priority + + Version + - - - - - - NLWP + + Vendor + - - - +
-
- - No items found - -
-
+
+ + + + +
diff --git a/plugins/main/public/components/agents/syscollector/columns/process-columns.ts b/plugins/main/public/components/agents/syscollector/columns/process-columns.ts index a21ee0447d..8cef9f3a39 100644 --- a/plugins/main/public/components/agents/syscollector/columns/process-columns.ts +++ b/plugins/main/public/components/agents/syscollector/columns/process-columns.ts @@ -1,4 +1,10 @@ -import { KeyEquivalence } from "../../../../../common/csv-key-equivalence"; +import { KeyEquivalence } from '../../../../../common/csv-key-equivalence'; + +const mapColumns = ({ field, ...rest }: { field: string; name?: string }) => ({ + ...rest, + field, + name: rest.name || KeyEquivalence[field] || field, +}); const windowsColumns = [ { field: 'name', searchable: true, sortable: true, width: '10%' }, @@ -8,7 +14,8 @@ const windowsColumns = [ { field: 'priority', searchable: true, sortable: true }, { field: 'nlwp', searchable: true, sortable: true }, { field: 'cmd', searchable: true, sortable: true, width: '30%' }, -].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); +].map(mapColumns); + const linuxColumns = [ { field: 'name', searchable: true, sortable: true, width: '10%' }, { field: 'euser', searchable: true, sortable: true }, @@ -22,7 +29,8 @@ const linuxColumns = [ { field: 'session', searchable: true, sortable: true }, { field: 'nice', searchable: true, sortable: true }, { field: 'state', searchable: true, sortable: true, width: '15%' }, -].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); +].map(mapColumns); + const macColumns = [ { field: 'name', searchable: true, sortable: true, width: '10%' }, { field: 'euser', searchable: true, sortable: true }, @@ -30,8 +38,7 @@ const macColumns = [ { field: 'ppid', searchable: true, sortable: true }, { field: 'vm_size', searchable: true, sortable: true }, { field: 'nice', searchable: true, sortable: true }, - { field: 'state', searchable: true, sortable: true, width: '15%' }, -].map(({field, ...rest}) => ({...rest, field, name: rest.name || KeyEquivalence[field] || field})); +].map(mapColumns); export const processColumns = { windows: windowsColumns, diff --git a/plugins/main/public/components/agents/syscollector/components/network-interfaces-table.tsx b/plugins/main/public/components/agents/syscollector/components/network-interfaces-table.tsx index 862c4e2cbe..2fde2feae3 100644 --- a/plugins/main/public/components/agents/syscollector/components/network-interfaces-table.tsx +++ b/plugins/main/public/components/agents/syscollector/components/network-interfaces-table.tsx @@ -10,7 +10,7 @@ const sortFieldSuggestion = (a, b) => (a.label > b.label ? 1 : -1); export const NetworkInterfacesTable = ({ agent }) => { return ( - + (a.label > b.label ? 1 : -1); export const NetworkPortsTable = withSOPlatformGuard( ({ agent, soPlatform }) => { return ( - + (a.label > b.label ? 1 : -1); export const NetworkSettingsTable = ({ agent }) => { return ( - + (a.label > b.label ? 1 : -1); export const PackagesTable = withSOPlatformGuard(({ agent, soPlatform }) => { return ( - + (a.label > b.label ? 1 : -1); export const ProcessesTable = withSOPlatformGuard(({ agent, soPlatform }) => { return ( - + ({ + formatUIDate: jest.fn().mockReturnValue('2022-06-27T16:09:49+00:00'), +})); + +jest.mock('../../../common/hooks/useGenericRequest', () => ({ + useGenericRequest: jest.fn().mockReturnValue({ + isLoading: false, + data: { + hardware: { + cpu: { + name: 'Intel(R) Core(TM) i7-10710U CPU @ 1.10GHz', + cores: 4, + threads: 8, + }, + ram: { + total: '16.0 GB', + }, + }, + os: { + platform: 'windows', + version: '10.0.19045', + }, + }, + error: '', + }), +})); + +describe('Syscollector metrics', () => { + it('should render inventory metrics', () => { + const { container } = render(); + + const inventoryMetrics = container.querySelector( + queryDataTestAttr('syscollector-metrics'), + ); + + expect(inventoryMetrics).toBeTruthy(); + }); + + it('should render syscollector ribbon items', () => { + const { container } = render(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-cores')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-memory')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-arch')), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr('ribbon-item-operating-system'), + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-cpu')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-hostname')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-board-serial')), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr('ribbon-item-last-scan')), + ).toBeTruthy(); + + expect( + container.querySelectorAll( + queryDataTestAttr('ribbon-item-', CSS.Attribute.Substring), + ), + ).toHaveLength(8); + }); +}); diff --git a/plugins/main/public/components/agents/syscollector/components/syscollector-metrics.tsx b/plugins/main/public/components/agents/syscollector/components/syscollector-metrics.tsx index be0cf63f59..682e29585b 100644 --- a/plugins/main/public/components/agents/syscollector/components/syscollector-metrics.tsx +++ b/plugins/main/public/components/agents/syscollector/components/syscollector-metrics.tsx @@ -1,15 +1,13 @@ import React, { useState } from 'react'; -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiLoadingSpinner, - EuiIcon, -} from '@elastic/eui'; -import mapValues from 'lodash'; +import { EuiPanel, EuiIcon } from '@elastic/eui'; +import _ from 'lodash'; import { useGenericRequest } from '../../../common/hooks/useGenericRequest'; import { formatUIDate } from '../../../../react-services/time-service'; +import WzRibbon from '../../../common/ribbon/ribbon'; +import { + IRibbonItem, + RibbonItemLabel, +} from '../../../common/ribbon/ribbon-item'; export function InventoryMetrics({ agent }) { const [params, setParams] = useState({}); @@ -31,8 +29,7 @@ export function InventoryMetrics({ agent }) { if ( !syscollector.isLoading && - (mapValues.isEmpty(syscollector.data.hardware) || - mapValues.isEmpty(syscollector.data.os)) + (_.isEmpty(syscollector.data.hardware) || _.isEmpty(syscollector.data.os)) ) { return ( @@ -42,106 +39,72 @@ export function InventoryMetrics({ agent }) { ); } - return ( - - - - - Cores:{' '} - {syscollector.isLoading ? ( - - ) : syscollector.data.hardware.cpu?.cores ? ( - {syscollector.data.hardware.cpu.cores} - ) : ( - - - )} - - - - - Memory:{' '} - {syscollector.isLoading ? ( - - ) : syscollector.data.hardware.ram?.total ? ( - - {(syscollector.data.hardware.ram.total / 1024).toFixed(2)} MB - - ) : ( - - - )} - - - - - Arch:{' '} - {syscollector.isLoading ? ( - - ) : ( - {syscollector.data.os.architecture} - )} - - - - - Operating system:{' '} - {syscollector.isLoading ? ( - - ) : ( - - {syscollector.data.os.os.name} {syscollector.data.os.os.version} - - )} - - - - - CPU:{' '} - {syscollector.isLoading ? ( - - ) : syscollector.data.hardware.cpu?.name ? ( - {syscollector.data.hardware.cpu.name} - ) : ( - - - )} - - - - - Host name:{' '} - {syscollector.isLoading ? ( - - ) : syscollector.data.os.hostname ? ( - {syscollector.data.os.hostname} - ) : ( - - - )} - - - - - Board serial:{' '} - {syscollector.isLoading ? ( - - ) : syscollector.data.hardware.board_serial ? ( - {syscollector.data.hardware.board_serial} - ) : ( - - - )} - - - - - Last scan:{' '} - {syscollector.isLoading ? ( - - ) : ( - - {offsetTimestamp('', syscollector.data.os.scan.time)} - - )} - - - - - ); + const render = () => { + const items: IRibbonItem[] = [ + { + key: 'cores', + label: 'Cores', + value: syscollector?.data?.hardware?.cpu?.cores, + isLoading: syscollector.isLoading, + style: { maxWidth: 100 }, + }, + { + key: 'memory', + label: 'Memory', + value: syscollector?.data?.hardware?.ram?.total + ? `${(syscollector?.data?.hardware?.ram?.total / 1024).toFixed(2)} MB` + : '-', + isLoading: syscollector.isLoading, + style: { maxWidth: 100 }, + }, + { + key: 'arch', + label: 'Arch', + value: syscollector?.data?.os?.architecture, + isLoading: syscollector.isLoading, + style: { maxWidth: 100 }, + }, + { + key: RibbonItemLabel.OPERATING_SYSTEM, + value: syscollector?.data?.os, + label: 'Operating system', + isLoading: syscollector.isLoading, + style: { maxWidth: 200 }, + }, + { + key: 'cpu', + label: 'CPU', + value: syscollector?.data?.hardware?.cpu?.name, + isLoading: syscollector.isLoading, + style: { maxWidth: 250 }, + }, + { + key: 'hostname', + label: 'Host name', + value: syscollector?.data?.os?.hostname, + isLoading: syscollector.isLoading, + style: { maxWidth: 100 }, + }, + { + key: 'board-serial', + label: 'Board serial', + value: syscollector?.data?.hardware?.board_serial, + isLoading: syscollector.isLoading, + style: { maxWidth: 100 }, + }, + { + key: 'last-scan', + label: 'Last scan', + value: syscollector?.data?.os?.scan?.time + ? offsetTimestamp('', syscollector?.data?.os?.scan?.time) + : '-', + isLoading: syscollector.isLoading, + style: { maxWidth: 180 }, + }, + ]; + + return ; + }; + + return render(); } diff --git a/plugins/main/public/components/agents/syscollector/components/windows-updates-table.tsx b/plugins/main/public/components/agents/syscollector/components/windows-updates-table.tsx index 65e3937cca..34410bc467 100644 --- a/plugins/main/public/components/agents/syscollector/components/windows-updates-table.tsx +++ b/plugins/main/public/components/agents/syscollector/components/windows-updates-table.tsx @@ -10,7 +10,7 @@ const sortFieldSuggestion = (a, b) => (a.label > b.label ? 1 : -1); export const WindowsUpdatesTable = ({ agent }) => { return ( - + { - it('A Linux agent should be well rendered.', () => { - const agent = { - os: { - uname: - 'Linux |ip-10-0-1-106 |4.9.0-9-amd64 |1 SMP Debian 4.9.168-1+deb9u2 (2019-05-13) |x86_64', - }, - }; - const wrapper = render(); - const networkPortsCard = wrapper.find('.euiFlexItem--flexGrow2').last(); - const networkPortsTitle = networkPortsCard - .find('.euiTitle.euiTitle--small') - .text(); - const networkPortsColumns = networkPortsCard.find('th'); +const TABLE_ID = '__table_7d62db31-1cd0-11ee-8e0c-33242698a3b9'; +const SOFTWARE_PACKAGES = 'Packages'; +const SOFTWARE_WINDOWS_UPDATES = 'Windows updates'; +const NETWORK_PORTS = 'Network ports'; +const NETWORK_INTERFACES = 'Network interfaces'; +const NETWORK_SETTINGS = 'Network settings'; +const PROCESSES = 'Processes'; +const AGENT = { + DEBIAN: { + os: { + uname: + 'Linux |ip-10-0-1-106 |4.9.0-9-amd64 |1 SMP Debian 4.9.168-1+deb9u2 (2019-05-13) |x86_64', + }, + }, + WINDOWS: { + os: { + platform: 'windows', + }, + }, + DARWIN: { + os: { + platform: 'darwin', + }, + }, +} as const; + +const NETWORK_PORTS_COLUMNS = { + LOCAL_PORT: 'Local port', + LOCAL_IP: 'Local IP address', + PROCESS: 'Process', + PID: 'PID', + STATE: 'State', + PROTOCOL: 'Protocol', +} as const; + +const NETWORK_INTERFACES_COLUMNS = { + NAME: 'Name', + MAC: 'MAC', + STATE: 'State', + MTU: 'MTU', + TYPE: 'Type', +} as const; + +const NETWORK_SETTINGS_COLUMNS = { + INTERFACE: 'Interface', + ADDRESS: 'Address', + NETMASK: 'Netmask', + PROTOCOL: 'Protocol', + BROADCAST: 'Broadcast', +} as const; + +const SOFTWARE_PACKAGES_COLUMNS = { + NAME: 'Name', + ARCHITECTURE: 'Architecture', + VENDOR: 'Vendor', + VERSION: 'Version', + FORMAT: 'Format', + LOCATION: 'Location', + DESCRIPTION: 'Description', +} as const; + +const SOFTWARE_WINDOWS_UPDATES_COLUMNS = { + UPDATE_CODE: 'Update code', +} as const; + +const PROCESSES_COLUMNS = { + NAME: 'Name', + EFFECTIVE_USER: 'Effective user', + EFFECTIVE_GROUP: 'Effective group', + PID: 'PID', + PARENT_PID: 'Parent PID', + COMMAND: 'Command', + ARGVS: 'Argvs', + SIZE: 'Size', + VM_SIZE: 'VM size', + SESSION: 'Session', + PRIORITY: 'Priority', + STATE: 'State', + NLWP: 'NLWP', +} as const; + +const shouldRenderTableWithCorrectColumnsAndTitle = ( + dataTestId: string, + title: string, + agentTab: (typeof AgentTabs)[keyof typeof AgentTabs], + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + const wrapper = render( + , + ); + const visTable = wrapper.find(queryDataTestAttr(dataTestId)).first(); + const visTitle = visTable + .find(queryDataTestAttr('table-wz-api-title')) + .text(); + const visColumns = visTable.find( + queryDataTestAttr('table-with-search-bar') + ' .euiTableHeaderCell', + ); + + const visTables = wrapper.find('table'); + + for (let i = 0; i < visTables.length; i++) { // This is done because the ID of the tables changes at each execution and breaks the snapshot. + visTables[i]['attribs']['id'] = TABLE_ID; + } + + expect(wrapper).toMatchSnapshot(); + expect(visTitle.trim()).toContain(title); + expect(visColumns.length).toEqual(columns.length); + for (let i = 0; i < columns.length; i++) { + expect(visColumns.eq(i).text()).toContain(columns[i]); + } +}; +const shouldRenderNetworkPortsTableWithCorrectColumnsAndTitle = ( + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + shouldRenderTableWithCorrectColumnsAndTitle( + 'network-ports-table', + NETWORK_PORTS, + AgentTabs.NETWORK, + agent, + columns, + ); +}; + +const shouldRenderNetworkInterfacesTableWithCorrectColumnsAndTitle = ( + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + shouldRenderTableWithCorrectColumnsAndTitle( + 'network-interfaces-table', + NETWORK_INTERFACES, + AgentTabs.NETWORK, + agent, + columns, + ); +}; + +const shouldRenderNetworkSettingsTableWithCorrectColumnsAndTitle = ( + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + shouldRenderTableWithCorrectColumnsAndTitle( + 'network-settings-table', + NETWORK_SETTINGS, + AgentTabs.NETWORK, + agent, + columns, + ); +}; + +const shouldRenderSoftwarePackagesTableWithCorrectColumnsAndTitle = ( + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + shouldRenderTableWithCorrectColumnsAndTitle( + 'software-packages-table', + SOFTWARE_PACKAGES, + AgentTabs.SOFTWARE, + agent, + columns, + ); +}; + +const shouldRenderSoftwareWindowsUpdatesTableWithCorrectColumnsAndTitle = ( + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + shouldRenderTableWithCorrectColumnsAndTitle( + 'software-windows-updates-table', + SOFTWARE_WINDOWS_UPDATES, + AgentTabs.SOFTWARE, + agent, + columns, + ); +}; + +const shouldRenderProcessesTableWithCorrectColumnsAndTitle = ( + agent: (typeof AGENT)[keyof typeof AGENT], + columns: string[], +) => { + shouldRenderTableWithCorrectColumnsAndTitle( + 'processes-table', + PROCESSES, + AgentTabs.PROCESSES, + agent, + columns, + ); +}; + +function findAgentInfo(wrapper: cheerio.Cheerio): any { + return wrapper.find(queryDataTestAttr('agent-info')).html(); +} + +describe('Inventory data', () => { + describe('Agent info', () => { + it("A Linux agent shouldn't render agent info", () => { + let wrapper = render( + , + ); + + let agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + + wrapper = render( + , + ); + + agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + + wrapper = render( + , + ); + + agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + }); + + it("A Windows agent shouldn't render agent info", () => { + let wrapper = render( + , + ); + + let agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + + wrapper = render( + , + ); + + agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + + wrapper = render( + , + ); + + agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + }); - const tableId = '__table_7d62db31-1cd0-11ee-8e0c-33242698a3b9'; - - const tables = wrapper.find('table'); - - for (let i = 0; i < tables.length; i++) { - tables[i]['attribs']['id'] = tableId; - } - - const columns = [ - 'Local port', - 'Local IP address', - 'Process', - 'PID', - 'State', - 'Protocol', - ]; - - expect(wrapper).toMatchSnapshot(); - expect(networkPortsTitle.trim()).toContain('Network ports'); - expect(networkPortsColumns.length).toEqual(columns.length); - for (let i = 0; i < columns.length; i++) { - expect(networkPortsColumns.eq(i).text()).toContain(columns[i]); - } + it("A Apple agent shouldn't render agent info", () => { + let wrapper = render( + , + ); + + let agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + + wrapper = render( + , + ); + + agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + + wrapper = render( + , + ); + + agentInfo = findAgentInfo(wrapper); + + expect(agentInfo).toBeFalsy(); + }); }); - it('A Windows agent should be well rendered.', () => { - const agent = { - os: { - platform: 'windows', - }, - }; - const wrapper = render(); - const networkPortsCard = wrapper.find('.euiFlexItem--flexGrow2').last(); - const networkPortsTitle = networkPortsCard - .find('.euiTitle.euiTitle--small') - .text(); - const networkPortsColumns = networkPortsCard.find('th'); + describe('Software', () => { + it('should render inventory metrics', () => { + const wrapper = render( + , + ); - // This is done because the ID of the tables changes at each execution and breaks the snapshot. + const inventoryMetrics = wrapper.find( + queryDataTestAttr('syscollector-metrics'), + ); - const tableId = '__table_7d62db31-1cd0-11ee-8e0c-33242698a3b9'; + expect(inventoryMetrics).toBeTruthy(); + }); - const tables = wrapper.find('table'); + describe(SOFTWARE_PACKAGES + ' table', () => { + it('A Linux agent should render software table with correct columns and title.', () => { + const columns = [ + SOFTWARE_PACKAGES_COLUMNS.NAME, + SOFTWARE_PACKAGES_COLUMNS.ARCHITECTURE, + SOFTWARE_PACKAGES_COLUMNS.VERSION, + SOFTWARE_PACKAGES_COLUMNS.VENDOR, + SOFTWARE_PACKAGES_COLUMNS.DESCRIPTION, + ]; - for (let i = 0; i < tables.length; i++) { - tables[i]['attribs']['id'] = tableId; - } + shouldRenderSoftwarePackagesTableWithCorrectColumnsAndTitle( + AGENT.DEBIAN, + columns, + ); + }); + it('A Windows agent should render software table with correct columns and title.', () => { + const columns = [ + SOFTWARE_PACKAGES_COLUMNS.NAME, + SOFTWARE_PACKAGES_COLUMNS.ARCHITECTURE, + SOFTWARE_PACKAGES_COLUMNS.VERSION, + SOFTWARE_PACKAGES_COLUMNS.VENDOR, + ]; - const columns = [ - 'Local port', - 'Local IP address', - 'Process', - 'State', - 'Protocol', - ]; + shouldRenderSoftwarePackagesTableWithCorrectColumnsAndTitle( + AGENT.WINDOWS, + columns, + ); + }); + it('A Apple agent should render software table with correct columns and title.', () => { + const columns = [ + SOFTWARE_PACKAGES_COLUMNS.NAME, + SOFTWARE_PACKAGES_COLUMNS.VERSION, + SOFTWARE_PACKAGES_COLUMNS.FORMAT, + SOFTWARE_PACKAGES_COLUMNS.LOCATION, + SOFTWARE_PACKAGES_COLUMNS.DESCRIPTION, + ]; - expect(wrapper).toMatchSnapshot(); - expect(networkPortsTitle.trim()).toContain('Network ports'); - expect(networkPortsColumns.length).toEqual(columns.length); - for (let i = 0; i < columns.length; i++) { - expect(networkPortsColumns.eq(i).text()).toContain(columns[i]); - } + shouldRenderSoftwarePackagesTableWithCorrectColumnsAndTitle( + AGENT.DARWIN, + columns, + ); + }); + }); + + describe(SOFTWARE_WINDOWS_UPDATES + ' table', () => { + it('A Linux agent should render software table with correct columns and title.', () => { + const wrapper = render( + , + ); + const visTable = wrapper + .find(queryDataTestAttr('software-windows-updates-table]')) + .first() + .html(); + + expect(visTable).toBeFalsy(); + }); + + it('A Windows agent should render software table with correct columns and title.', () => { + const columns = [SOFTWARE_WINDOWS_UPDATES_COLUMNS.UPDATE_CODE]; + + shouldRenderSoftwareWindowsUpdatesTableWithCorrectColumnsAndTitle( + AGENT.WINDOWS, + columns, + ); + }); + + it('A Apple agent should render software table with correct columns and title.', () => { + const wrapper = render( + , + ); + const visTable = wrapper + .find(queryDataTestAttr('software-windows-updates-table')) + .first() + .html(); + + expect(visTable).toBeFalsy(); + }); + }); }); - it('A Apple agent should be well rendered.', () => { - const agent = { - os: { - platform: 'darwin', - }, - }; - const wrapper = render(); - const networkPortsCard = wrapper.find('.euiFlexItem--flexGrow2').last(); - const networkPortsTitle = networkPortsCard - .find('.euiTitle.euiTitle--small') - .text(); - const networkPortsColumns = networkPortsCard.find('th'); + describe('Network', () => { + it('should render inventory metrics', () => { + const wrapper = render( + , + ); - // This is done because the ID of the tables changes at each execution and breaks the snapshot. + const inventoryMetrics = wrapper.find( + queryDataTestAttr('syscollector-metrics'), + ); + + expect(inventoryMetrics).toBeTruthy(); + }); + + describe(NETWORK_SETTINGS + ' table', () => { + it('A Linux agent should render network settings table with correct columns and title.', () => { + const columns = [ + NETWORK_SETTINGS_COLUMNS.INTERFACE, + NETWORK_SETTINGS_COLUMNS.ADDRESS, + NETWORK_SETTINGS_COLUMNS.NETMASK, + NETWORK_SETTINGS_COLUMNS.PROTOCOL, + NETWORK_SETTINGS_COLUMNS.BROADCAST, + ]; + + shouldRenderNetworkSettingsTableWithCorrectColumnsAndTitle( + AGENT.DEBIAN, + columns, + ); + }); + it('A Windows agent should render network settings table with correct columns and title.', () => { + const columns = [ + NETWORK_SETTINGS_COLUMNS.INTERFACE, + NETWORK_SETTINGS_COLUMNS.ADDRESS, + NETWORK_SETTINGS_COLUMNS.NETMASK, + NETWORK_SETTINGS_COLUMNS.PROTOCOL, + NETWORK_SETTINGS_COLUMNS.BROADCAST, + ]; + + shouldRenderNetworkSettingsTableWithCorrectColumnsAndTitle( + AGENT.WINDOWS, + columns, + ); + }); + it('A Apple agent should render network settings table with correct columns and title.', () => { + const columns = [ + NETWORK_SETTINGS_COLUMNS.INTERFACE, + NETWORK_SETTINGS_COLUMNS.ADDRESS, + NETWORK_SETTINGS_COLUMNS.NETMASK, + NETWORK_SETTINGS_COLUMNS.PROTOCOL, + NETWORK_SETTINGS_COLUMNS.BROADCAST, + ]; + + shouldRenderNetworkSettingsTableWithCorrectColumnsAndTitle( + AGENT.DARWIN, + columns, + ); + }); + }); + describe(NETWORK_INTERFACES + ' table', () => { + it('A Linux agent should render network interfaces table with correct columns and title.', () => { + const columns = [ + NETWORK_INTERFACES_COLUMNS.NAME, + NETWORK_INTERFACES_COLUMNS.MAC, + NETWORK_INTERFACES_COLUMNS.STATE, + NETWORK_INTERFACES_COLUMNS.MTU, + NETWORK_INTERFACES_COLUMNS.TYPE, + ]; + + shouldRenderNetworkInterfacesTableWithCorrectColumnsAndTitle( + AGENT.DEBIAN, + columns, + ); + }); + it('A Windows agent should render network interfaces table with correct columns and title.', () => { + const columns = [ + NETWORK_INTERFACES_COLUMNS.NAME, + NETWORK_INTERFACES_COLUMNS.MAC, + NETWORK_INTERFACES_COLUMNS.STATE, + NETWORK_INTERFACES_COLUMNS.MTU, + NETWORK_INTERFACES_COLUMNS.TYPE, + ]; + + shouldRenderNetworkInterfacesTableWithCorrectColumnsAndTitle( + AGENT.WINDOWS, + columns, + ); + }); + it('A Apple agent should render network interfaces table with correct columns and title.', () => { + const columns = [ + NETWORK_INTERFACES_COLUMNS.NAME, + NETWORK_INTERFACES_COLUMNS.MAC, + NETWORK_INTERFACES_COLUMNS.STATE, + NETWORK_INTERFACES_COLUMNS.MTU, + NETWORK_INTERFACES_COLUMNS.TYPE, + ]; + + shouldRenderNetworkInterfacesTableWithCorrectColumnsAndTitle( + AGENT.DARWIN, + columns, + ); + }); + }); + describe(NETWORK_PORTS + ' table', () => { + it('A Linux agent should render network ports table with correct columns and title.', () => { + const columns = [ + NETWORK_PORTS_COLUMNS.LOCAL_PORT, + NETWORK_PORTS_COLUMNS.LOCAL_IP, + NETWORK_PORTS_COLUMNS.PROCESS, + NETWORK_PORTS_COLUMNS.PID, + NETWORK_PORTS_COLUMNS.STATE, + NETWORK_PORTS_COLUMNS.PROTOCOL, + ]; + + shouldRenderNetworkPortsTableWithCorrectColumnsAndTitle( + AGENT.DEBIAN, + columns, + ); + }); + + it('A Windows agent should render network ports table with correct columns and title.', () => { + const columns = [ + NETWORK_PORTS_COLUMNS.LOCAL_PORT, + NETWORK_PORTS_COLUMNS.LOCAL_IP, + NETWORK_PORTS_COLUMNS.PROCESS, + NETWORK_PORTS_COLUMNS.STATE, + NETWORK_PORTS_COLUMNS.PROTOCOL, + ]; + shouldRenderNetworkPortsTableWithCorrectColumnsAndTitle( + AGENT.WINDOWS, + columns, + ); + }); + + it('A Apple agent should render network ports table with correct columns and title.', () => { + const columns = [ + NETWORK_PORTS_COLUMNS.LOCAL_PORT, + NETWORK_PORTS_COLUMNS.LOCAL_IP, + NETWORK_PORTS_COLUMNS.STATE, + NETWORK_PORTS_COLUMNS.PROTOCOL, + ]; + + shouldRenderNetworkPortsTableWithCorrectColumnsAndTitle( + AGENT.DARWIN, + columns, + ); + }); + }); + }); + + describe('Processes', () => { + it('should render inventory metrics', () => { + const wrapper = render( + , + ); + + const inventoryMetrics = wrapper.find( + queryDataTestAttr('syscollector-metrics'), + ); + + expect(inventoryMetrics).toBeTruthy(); + }); + + it('A Linux agent should render processes table with correct columns and title.', () => { + const columns = [ + PROCESSES_COLUMNS.NAME, + PROCESSES_COLUMNS.EFFECTIVE_USER, + PROCESSES_COLUMNS.EFFECTIVE_GROUP, + PROCESSES_COLUMNS.PID, + PROCESSES_COLUMNS.PARENT_PID, + PROCESSES_COLUMNS.COMMAND, + PROCESSES_COLUMNS.ARGVS, + PROCESSES_COLUMNS.VM_SIZE, + PROCESSES_COLUMNS.SIZE, + PROCESSES_COLUMNS.SESSION, + PROCESSES_COLUMNS.PRIORITY, + PROCESSES_COLUMNS.STATE, + ]; - const tableId = '__table_7d62db31-1cd0-11ee-8e0c-33242698a3b9'; + shouldRenderProcessesTableWithCorrectColumnsAndTitle( + AGENT.DEBIAN, + columns, + ); + }); - const tables = wrapper.find('table'); + it('A Windows agent should render processes table with correct columns and title.', () => { + const columns = [ + PROCESSES_COLUMNS.NAME, + PROCESSES_COLUMNS.PID, + PROCESSES_COLUMNS.PARENT_PID, + PROCESSES_COLUMNS.VM_SIZE, + PROCESSES_COLUMNS.PRIORITY, + PROCESSES_COLUMNS.NLWP, + PROCESSES_COLUMNS.COMMAND, + ]; - for (let i = 0; i < tables.length; i++) { - tables[i]['attribs']['id'] = tableId; - } + shouldRenderProcessesTableWithCorrectColumnsAndTitle( + AGENT.WINDOWS, + columns, + ); + }); - const columns = ['Local port', 'Local IP address', 'State', 'Protocol']; + it('A Apple agent should render processes table with correct columns and title.', () => { + const columns = [ + PROCESSES_COLUMNS.NAME, + PROCESSES_COLUMNS.EFFECTIVE_USER, + PROCESSES_COLUMNS.PID, + PROCESSES_COLUMNS.PARENT_PID, + PROCESSES_COLUMNS.VM_SIZE, + PROCESSES_COLUMNS.PRIORITY, + ]; - expect(wrapper).toMatchSnapshot(); - expect(networkPortsTitle.trim()).toContain('Network ports'); - expect(networkPortsColumns.length).toEqual(columns.length); - for (let i = 0; i < columns.length; i++) { - expect(networkPortsColumns.eq(i).text()).toContain(columns[i]); - } + shouldRenderProcessesTableWithCorrectColumnsAndTitle( + AGENT.DARWIN, + columns, + ); + }); }); }); diff --git a/plugins/main/public/components/agents/syscollector/inventory.tsx b/plugins/main/public/components/agents/syscollector/inventory.tsx index 8f5e2413c7..417db55c4c 100644 --- a/plugins/main/public/components/agents/syscollector/inventory.tsx +++ b/plugins/main/public/components/agents/syscollector/inventory.tsx @@ -11,20 +11,22 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiCallOut } from '@elastic/eui'; -import { InventoryMetrics } from './components/syscollector-metrics'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiSpacer, + EuiPage, + EuiPageBody, +} from '@elastic/eui'; import { API_NAME_AGENT_STATUS } from '../../../../common/constants'; import { compose } from 'redux'; import { withGuard } from '../../common/hocs'; import { PromptAgentNeverConnected } from '../prompts'; -import { - NetworkInterfacesTable, - NetworkPortsTable, - NetworkSettingsTable, - WindowsUpdatesTable, - ProcessesTable, - PackagesTable, -} from './components'; +import SoftwareTab from './software'; +import NetworkTab from './network'; +import ProcessesTab from './processes'; +import { InventoryMetrics } from './components'; export const SyscollectorInventory = compose( withGuard( @@ -33,7 +35,8 @@ export const SyscollectorInventory = compose( props.agent.status === API_NAME_AGENT_STATUS.NEVER_CONNECTED, PromptAgentNeverConnected, ), -)(function SyscollectorInventory({ agent }) { +)(function SyscollectorInventory(props) { + const { agent, section } = props; let soPlatform; if (agent?.os?.uname?.includes('Linux')) { soPlatform = 'linux'; @@ -48,54 +51,33 @@ export const SyscollectorInventory = compose( } return ( -
- {agent && agent.status === API_NAME_AGENT_STATUS.DISCONNECTED && ( - - - - - - )} - - - - - - - - - - - - - - - - - - - - {agent && agent.os && agent.os.platform === 'windows' && ( - - - + + + {agent?.status === API_NAME_AGENT_STATUS.DISCONNECTED && ( + + + + + )} - - - - - - + - - - - - -
+ + + {section === 'software' && ( + + )} + {section === 'network' && ( + + )} + {section === 'processes' && ( + + )} + + ); }); diff --git a/plugins/main/public/components/agents/syscollector/network.tsx b/plugins/main/public/components/agents/syscollector/network.tsx new file mode 100644 index 0000000000..db99e84f00 --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/network.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + NetworkInterfacesTable, + NetworkPortsTable, + NetworkSettingsTable, +} from './components'; +import { Agent } from '../../endpoints-summary/types'; + +interface NetworkProps { + agent: Agent; + soPlatform?: string; +} + +const NetworkTab = ({ agent, soPlatform }: NetworkProps) => { + return ( + + + + + + + + + + + + + + ); +}; + +export default NetworkTab; diff --git a/plugins/main/public/components/agents/syscollector/processes.tsx b/plugins/main/public/components/agents/syscollector/processes.tsx new file mode 100644 index 0000000000..8403be456c --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/processes.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { ProcessesTable } from './components'; +import { Agent } from '../../endpoints-summary/types'; + +interface ProcessesProps { + agent: Agent; + soPlatform?: string; +} + +const ProcessesTab = ({ agent, soPlatform }: ProcessesProps) => { + return ( + + + + + + ); +}; + +export default ProcessesTab; diff --git a/plugins/main/public/components/agents/syscollector/software.tsx b/plugins/main/public/components/agents/syscollector/software.tsx new file mode 100644 index 0000000000..a8c8f1db27 --- /dev/null +++ b/plugins/main/public/components/agents/syscollector/software.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Agent } from '../../endpoints-summary/types'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { PackagesTable, WindowsUpdatesTable } from './components'; + +interface SoftwareProps { + agent: Agent; + soPlatform?: string; +} + +const SoftwareTab = ({ agent, soPlatform }: SoftwareProps) => { + return ( + + {agent?.os?.platform === 'windows' && ( + + + + )} + + + + + ); +}; + +export default SoftwareTab; 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/hooks/use_async_action.ts b/plugins/main/public/components/common/hooks/use_async_action.ts index 24ab181926..a98495fa9c 100644 --- a/plugins/main/public/components/common/hooks/use_async_action.ts +++ b/plugins/main/public/components/common/hooks/use_async_action.ts @@ -11,24 +11,27 @@ */ import { useCallback, useState } from 'react'; -export function useAsyncAction(action, dependencies = []){ +export function useAsyncAction( + action: (...params: any[]) => any, + dependencies: any[] = [], +) { const [running, setRunning] = useState(false); const [data, setData] = useState(null); const [error, setError] = useState(null); const run = useCallback(async (...params) => { - try{ + try { setRunning(true); setError(null); setData(null); const data = await action(...params); setData(data); - }catch(error){ + } catch (error) { setError(error); - }finally{ + } finally { setRunning(false); - }; + } }, dependencies); return { data, error, running, run }; -} \ No newline at end of file +} diff --git a/plugins/main/public/components/common/modules/main-agent.test.tsx b/plugins/main/public/components/common/modules/main-agent.test.tsx new file mode 100644 index 0000000000..02ba68cd67 --- /dev/null +++ b/plugins/main/public/components/common/modules/main-agent.test.tsx @@ -0,0 +1,359 @@ +import React from 'react'; +import { MainModuleAgent } from './main-agent'; +import { AgentTabs } from '../../endpoints-summary/agent/agent-tabs'; +import { fireEvent, render } from '@testing-library/react'; +import { queryDataTestAttr } from '../../../../test/public/query-attr'; + +const GENERATE_REPORT_BUTTON = 'generate-report-button'; +const ARIA_SELECTED = '[aria-selected="true"]'; + +const REPORT_TAB = { + STATS: 'agent-tab-stats', + CONFIGURATION: 'agent-tab-configuration', + SOFTWARE: 'agent-tab-software', + NETWORK: 'agent-tab-network', + PROCESSES: 'agent-tab-processes', +}; + +jest.mock('../../../react-services/reporting', () => ({ + ReportingService: { + startVis2Png: jest.fn(), + startConfigReport: jest.fn(), + }, +})); + +jest.mock('../data-source', () => ({ + useDataSource: jest.fn().mockImplementation(() => ({ + dataSource: {}, + fetchFilters: {}, + isLoading: false, + })), + AlertsDataSourceRepository: jest.fn(), + AlertsDataSource: jest.fn(), + __esModule: true, +})); + +describe('Main Agent', () => { + let switchTab: jest.Mock; + + beforeEach(() => { + switchTab = jest.fn(); + }); + + describe('Agent tabs', () => { + it('should render agent tab overview when section is stats', () => { + const { container } = render( + , + ); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.STATS) + ARIA_SELECTED, + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.CONFIGURATION)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.SOFTWARE)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.NETWORK)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.PROCESSES)), + ).toBeFalsy(); + }); + + it('should render agent tab overview when section is configuration', () => { + const { container } = render( + , + ); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.CONFIGURATION) + ARIA_SELECTED, + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.STATS)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.SOFTWARE)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.NETWORK)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.PROCESSES)), + ).toBeFalsy(); + }); + + it('should render agent tab overview when section is software', () => { + const { container } = render( + , + ); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.SOFTWARE) + ARIA_SELECTED, + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.NETWORK)), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.NETWORK) + ARIA_SELECTED, + ), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.PROCESSES)), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.PROCESSES) + ARIA_SELECTED, + ), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.CONFIGURATION)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.STATS)), + ).toBeFalsy(); + }); + + it('should render agent tab overview when section is network', () => { + const { container } = render( + , + ); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.SOFTWARE)), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.SOFTWARE) + ARIA_SELECTED, + ), + ).toBeFalsy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.NETWORK) + ARIA_SELECTED, + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.PROCESSES)), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.PROCESSES) + ARIA_SELECTED, + ), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.CONFIGURATION)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.STATS)), + ).toBeFalsy(); + }); + + it('should render agent tab overview when section is processes', () => { + const { container } = render( + , + ); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.SOFTWARE)), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.SOFTWARE) + ARIA_SELECTED, + ), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.NETWORK)), + ).toBeTruthy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.NETWORK) + ARIA_SELECTED, + ), + ).toBeFalsy(); + + expect( + container.querySelector( + queryDataTestAttr(REPORT_TAB.PROCESSES) + ARIA_SELECTED, + ), + ).toBeTruthy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.CONFIGURATION)), + ).toBeFalsy(); + + expect( + container.querySelector(queryDataTestAttr(REPORT_TAB.STATS)), + ).toBeFalsy(); + }); + + it('should be call switchTab when click on tab', () => { + const { container } = render( + , + ); + + fireEvent.click( + container.querySelector( + queryDataTestAttr(REPORT_TAB.SOFTWARE), + ) as Element, + ); + + expect(switchTab).toHaveBeenCalledTimes(1); + expect(switchTab).toHaveBeenCalledWith(AgentTabs.SOFTWARE); + + fireEvent.click( + container.querySelector( + queryDataTestAttr(REPORT_TAB.NETWORK), + ) as Element, + ); + + expect(switchTab).toHaveBeenCalledTimes(2); + expect(switchTab).toHaveBeenCalledWith(AgentTabs.NETWORK); + + fireEvent.click( + container.querySelector( + queryDataTestAttr(REPORT_TAB.PROCESSES), + ) as Element, + ); + + expect(switchTab).toHaveBeenCalledTimes(3); + expect(switchTab).toHaveBeenCalledWith(AgentTabs.PROCESSES); + }); + }); + + describe('Generate report button', () => { + it("shouldn't render generate report button when section is stats", () => { + const { container } = render( + , + ); + + const generateReportButton = container.querySelector( + queryDataTestAttr(GENERATE_REPORT_BUTTON), + ); + + expect(generateReportButton).toBeFalsy(); + }); + + it("shouldn't render generate report button when section is configuration", () => { + const { container } = render( + , + ); + + const generateReportButton = container.querySelector( + queryDataTestAttr(GENERATE_REPORT_BUTTON), + ); + + expect(generateReportButton).toBeFalsy(); + }); + + it('should render generate report button when section is software', () => { + const { container } = render( + , + ); + + const generateReportButton = container.querySelector( + queryDataTestAttr(GENERATE_REPORT_BUTTON), + ); + + expect(generateReportButton).toBeTruthy(); + }); + + it('should render generate report button when section is network', () => { + const { container } = render( + , + ); + + const generateReportButton = container.querySelector( + queryDataTestAttr(GENERATE_REPORT_BUTTON), + ); + + expect(generateReportButton).toBeTruthy(); + }); + + it('should render generate report button when section is processes', () => { + const { container } = render( + , + ); + + const generateReportButton = container.querySelector( + queryDataTestAttr(GENERATE_REPORT_BUTTON), + ); + + expect(generateReportButton).toBeTruthy(); + }); + }); +}); diff --git a/plugins/main/public/components/common/modules/main-agent.tsx b/plugins/main/public/components/common/modules/main-agent.tsx index b7d477038f..74ebf831e0 100644 --- a/plugins/main/public/components/common/modules/main-agent.tsx +++ b/plugins/main/public/components/common/modules/main-agent.tsx @@ -14,20 +14,13 @@ import React, { Component, Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiTitle, EuiButtonEmpty, + EuiTabs, + EuiTab, } from '@elastic/eui'; -import { euiThemeVars } from '@osd/ui-shared-deps/theme'; import '../../common/modules/module.scss'; import store from '../../../redux/store'; -import { FilterHandler } from '../../../utils/filter-handler'; -import { AppState } from '../../../react-services/app-state'; import { ReportingService } from '../../../react-services/reporting'; -import { WAZUH_MODULES } from '../../../../common/wazuh-modules'; -import { AgentInfo } from '../../common/welcome/agents-info'; -import { compose } from 'redux'; -import { withGlobalBreadcrumb } from '../hocs'; -import { endpointSummary } from '../../../utils/applications'; import { AlertsDataSource, AlertsDataSourceRepository, @@ -37,140 +30,103 @@ import { useDataSource, } from '../data-source'; import { useAsyncAction } from '../hooks'; -import NavigationService from '../../../react-services/navigation-service'; +import { toTitleCase } from '../util/change-case'; +import clsx from 'clsx'; +import { AgentTabs } from '../../endpoints-summary/agent/agent-tabs'; +import { Agent } from '../../endpoints-summary/types'; export class MainModuleAgent extends Component { props!: { - [key: string]: any; + agent: Agent; + section: string; + switchTab?: (tab: string) => void; + selectView?: boolean; + tabs?: any[]; + renderTabs?: () => JSX.Element; + agentsSelectionProps?: any; }; - state: { - selectView: Boolean; - loadingReport: Boolean; - switchModule: Boolean; - showAgentInfo: Boolean; - }; - reportingService: ReportingService; - filterHandler: FilterHandler; - constructor(props) { - super(props); - this.reportingService = new ReportingService(); - this.filterHandler = new FilterHandler(AppState.getCurrentPattern()); - this.state = { - selectView: false, - loadingReport: false, - switchModule: false, - showAgentInfo: false, - }; - } + inventoryTabs = [AgentTabs.SOFTWARE, AgentTabs.NETWORK, AgentTabs.PROCESSES]; renderTitle() { + const { agent, section, switchTab } = this.props; return ( - - - - - - -

- { - NavigationService.getInstance().navigate( - `/agents?tab=welcome&agent=${this.props.agent.id}`, - ); - }} - > - -  {this.props.agent.name}    - - -

-
-
-
- - {this.props.section === 'syscollector' && ( - - - + + + + {this.inventoryTabs.includes(section) ? ( + <> + {this.inventoryTabs.map(tab => ( + switchTab?.(tab)} + > + {toTitleCase(tab)} + + ))} + + ) : ( + + {toTitleCase(section)} + )} - + + + {[AgentTabs.SOFTWARE, AgentTabs.NETWORK, AgentTabs.PROCESSES].includes( + section, + ) && ( + + + + )}
); } render() { const { agent, section, selectView } = this.props; - const ModuleTabView = (this.props.tabs || []).find( - tab => tab.id === selectView, - ); + const ModuleTabView = this.props.tabs?.find(tab => tab.id === selectView); + + const hasTabs = this.props.tabs?.length; + return ( -
- {agent && agent.os && ( +
+ {agent?.os && (
{this.renderTitle()}
-
- {this.state.showAgentInfo && ( -
- -
- )} - {this.props.tabs && this.props.tabs.length && ( +
+ {hasTabs && (
- {this.props.renderTabs()} + {this.props.renderTabs?.()} - {ModuleTabView && - ModuleTabView.buttons && - ModuleTabView.buttons.map( - (ModuleViewButton, index) => - typeof ModuleViewButton !== 'string' ? ( - - - - ) : null, - )} + {ModuleTabView?.buttons?.map( + (ModuleViewButton, index) => + typeof ModuleViewButton !== 'string' ? ( + + + + ) : null, + )} @@ -178,9 +134,8 @@ export class MainModuleAgent extends Component { )}
- {!['syscollector', 'configuration'].includes(section) && - ModuleTabView && - ModuleTabView.component && ( + {[AgentTabs.STATS].includes(section) && + ModuleTabView?.component && ( )} @@ -190,41 +145,6 @@ export class MainModuleAgent extends Component { } } -export default compose( - withGlobalBreadcrumb(({ agent, section }) => { - if (section === 'welcome') { - return [ - { - text: endpointSummary.breadcrumbLabel, - href: NavigationService.getInstance().getUrlForApp( - endpointSummary.id, - { - path: `#/agents-preview`, - }, - ), - }, - { text: agent.id }, - ]; - } else { - return [ - { - text: endpointSummary.breadcrumbLabel, - href: NavigationService.getInstance().getUrlForApp( - endpointSummary.id, - { - path: `#/agents-preview`, - }, - ), - }, - { agent: agent }, - { - text: WAZUH_MODULES[section].title, - }, - ]; - } - }), -)(MainModuleAgent); - export class AgentInventoryDataSource extends AlertsDataSource { constructor(id: string, title: string) { super(id, title); @@ -238,7 +158,7 @@ export class AgentInventoryDataSource extends AlertsDataSource { } } -const GenerateSyscollectorReportButton = ({ agent }) => { +const GenerateReportButton = ({ agent }: { agent: Agent }) => { const { dataSource, fetchFilters, @@ -254,7 +174,7 @@ const GenerateSyscollectorReportButton = ({ agent }) => { (agent || store.getState().appStateReducers.currentAgentData || {}).id || false; await reportingService.startVis2Png('syscollector', agentID, { - indexPattern: dataSource.indexPattern, + indexPattern: dataSource?.indexPattern, query: { query: '', language: 'kuery' }, filters: fetchFilters, time: { @@ -266,6 +186,7 @@ const GenerateSyscollectorReportButton = ({ agent }) => { return ( {ModuleTabView && diff --git a/plugins/main/public/components/common/modules/module.scss b/plugins/main/public/components/common/modules/module.scss index 8182b3d601..dbbd4491f1 100644 --- a/plugins/main/public/components/common/modules/module.scss +++ b/plugins/main/public/components/common/modules/module.scss @@ -7,14 +7,9 @@ background: #fafbfd; } -.wz-module-header-agent { - height: 50px; - padding: 16px; -} - -.wz-module-header-agent .euiHealth svg { - width: 16px; - height: 24px; +.wz-module-header-agent .euiTab__content { + font-size: 16px !important; + font-weight: 400; } .wzApp .euiFlyoutBody .euiFlyoutBody__overflowContent { @@ -25,6 +20,10 @@ margin-top: -16px; } +.wz-module-header-agent > .euiFlexGroup { + margin-top: -6px; +} + .wz-module-header-nav > .euiFlexGroup .euiFlexItem { margin-top: 0px; } @@ -39,20 +38,9 @@ border-radius: 100px; } -.wz-module-header-agent-title-btn { - cursor: pointer; -} - .wz-font-weight-normal { font-weight: normal; } -.wz-module-header-agent h1 { - font-weight: 400; -} - -.wz-module-header-agent h1 b { - font-weight: 500; -} .wz-module-header-nav .euiTabs { padding-top: 6px; @@ -96,11 +84,6 @@ discover-app-w .sidebar-container { @media only screen and (max-width: 767px) { .wz-module-header-agent { height: auto; - h1 { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } } .wz-module-body { margin-top: -160px; diff --git a/plugins/main/public/components/common/modules/modules-defaults.tsx b/plugins/main/public/components/common/modules/modules-defaults.tsx index 0fc4b4b5bc..215c3dd14f 100644 --- a/plugins/main/public/components/common/modules/modules-defaults.tsx +++ b/plugins/main/public/components/common/modules/modules-defaults.tsx @@ -444,7 +444,13 @@ export const ModulesDefaults = { ], availableFor: ['manager', 'agent'], }, - syscollector: { + software: { + notModule: true, + }, + network: { + notModule: true, + }, + processes: { notModule: true, }, configuration: { diff --git a/plugins/main/public/components/common/ribbon/ribbon-item.scss b/plugins/main/public/components/common/ribbon/ribbon-item.scss new file mode 100644 index 0000000000..bf543a6a40 --- /dev/null +++ b/plugins/main/public/components/common/ribbon/ribbon-item.scss @@ -0,0 +1,4 @@ +.wz-ribbon-item .euiStat .euiText { + font-size: 12px; + font-family: sans-serif; +} diff --git a/plugins/main/public/components/common/ribbon/ribbon-item.tsx b/plugins/main/public/components/common/ribbon/ribbon-item.tsx new file mode 100644 index 0000000000..29741c8a6b --- /dev/null +++ b/plugins/main/public/components/common/ribbon/ribbon-item.tsx @@ -0,0 +1,148 @@ +import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import { WAZUH_AGENTS_OS_TYPE } from '../../../../common/constants'; +import { AgentStatus } from '../../agents/agent-status'; +import { Agent } from '../../endpoints-summary/types'; +import { WzStat } from '../../wz-stat'; +import { GroupTruncate } from '../util/agent-group-truncate'; +import WzTextWithTooltipIfTruncated from '../wz-text-with-tooltip-if-truncated'; +import './ribbon-item.scss'; +import { getAgentOSType } from '../../../react-services'; + +const FONT_SIZE = 12; + +export enum RibbonItemLabel { + GROUPS = 'groups', + OPERATING_SYSTEM = 'operating-system', + AGENT_STATUS = 'agent-status', +} + +export type IRibbonItem