diff --git a/CHANGELOG.md b/CHANGELOG.md index 81597512ef..0b2c3e89ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Added the option to sort by the agents count in the group table. [#4323](https://github.com/wazuh/wazuh-kibana-app/pull/4323) - Added agent synchronization status in the agent module. [#3874](https://github.com/wazuh/wazuh-kibana-app/pull/3874) +- Redesign the SCA table from agent's dashboard [#4512](https://github.com/wazuh/wazuh-kibana-app/pull/4512) ### Changed diff --git a/public/components/agents/sca/index.ts b/public/components/agents/sca/index.ts index 0184fc57f7..90f98e1d76 100644 --- a/public/components/agents/sca/index.ts +++ b/public/components/agents/sca/index.ts @@ -1,2 +1,3 @@ export { MainSca } from './main'; -export { Inventory } from './inventory'; \ No newline at end of file +export { Inventory } from './inventory'; +export * from './inventory/index' \ No newline at end of file diff --git a/public/components/agents/sca/inventory.tsx b/public/components/agents/sca/inventory.tsx index 2c95cbcfb0..0fcebede02 100644 --- a/public/components/agents/sca/inventory.tsx +++ b/public/components/agents/sca/inventory.tsx @@ -16,7 +16,6 @@ import { EuiFlexGroup, EuiPanel, EuiPage, - EuiBasicTable, EuiSpacer, EuiText, EuiProgress, @@ -44,38 +43,49 @@ import { import { API_NAME_AGENT_STATUS, UI_LOGGER_LEVELS } from '../../../../common/constants'; import { getErrorOrchestrator } from '../../../react-services/common-services'; import { VisualizationBasic } from '../../common/charts/visualizations/basic'; +import { AppNavigate } from '../../../react-services/app-navigate'; +import SCAPoliciesTable from './inventory/agent-policies-table'; import { InventoryPolicyChecksTable } from './inventory/checks-table'; +import { RuleText } from './components'; type InventoryProps = { agent: { [key: string]: any }; }; type InventoryState = { - agent: object; itemIdToExpandedRowMap: object; showMoreInfo: boolean; loading: boolean; + checksIsLoading: boolean; + redirect: boolean; filters: object[]; pageTableChecks: { pageIndex: number; pageSize?: number }; policies: object[]; - lookingPolicy: { [key: string]: any } | false; + checks: object[]; + lookingPolicy: { [key: string]: any } | boolean; loadingPolicy: boolean; + secondTable: boolean; + secondTableBack: boolean; }; export class Inventory extends Component { _isMount = false; + agent: { [key: string]: any } = {}; columnsPolicies: object[]; lookingPolicy: { [key: string]: any } | false = false; constructor(props) { super(props); - const { agent } = this.props; this.state = { - agent, itemIdToExpandedRowMap: {}, showMoreInfo: false, loading: false, filters: [], pageTableChecks: { pageIndex: 0 }, policies: [], + checks: [], + redirect: false, + secondTable: false, + secondTableBack: false, + checksIsLoading: false, lookingPolicy: false, loadingPolicy: false, }; @@ -174,6 +184,13 @@ export class Inventory extends Component { pageTableChecks: { pageIndex: 0, pageSize: this.state.pageTableChecks.pageSize }, }); } + + const regex = new RegExp('redirectPolicyTable=' + '[^&]*'); + const match = window.location.href.match(regex); + if (match && match[0] && !this.state.secondTable && !this.state.secondTableBack) { + this.loadScaPolicy(match[0].split('=')[1], true) + this.setState({secondTableBack: true, checksIsLoading: true}) + } } componentWillUnmount() { @@ -207,28 +224,31 @@ export class Inventory extends Component { } } - /** - * - * @param policy - */ - async loadScaPolicy(policy) { + handleBack (ev) { + AppNavigate.navigateToModule(ev, 'agents', { tab: 'welcome', agent: this.props.agent.id }); + ev.stopPropagation(); + }; + + async loadScaPolicy(policy, secondTable?) { this._isMount && this.setState({ loadingPolicy: true, itemIdToExpandedRowMap: {}, pageTableChecks: { pageIndex: 0 }, + secondTable: secondTable ? secondTable : false }); if (policy) { try { const policyResponse = await WzRequest.apiReq('GET', `/sca/${this.props.agent.id}`, { params: { - q: 'policy_id=' + policy.policy_id, + q: 'policy_id=' + policy, }, }); const [policyData] = policyResponse.data.data.affected_items; - this._isMount && this.setState({ lookingPolicy: policyData, loadingPolicy: false }); + this._isMount && + this.setState({ lookingPolicy: policyData, loadingPolicy: false, checksIsLoading: false }); } catch (error) { - this.setState({ lookingPolicy: policy, loadingPolicy: false }); + this.setState({ lookingPolicy: policy, loadingPolicy: false, checksIsLoading: false }); const options: UIErrorLog = { context: `${Inventory.name}.loadScaPolicy`, level: UI_LOGGER_LEVELS.ERROR as UILogLevel, @@ -242,16 +262,61 @@ export class Inventory extends Component { getErrorOrchestrator().handleError(options); } } else { - this._isMount && this.setState({ lookingPolicy: policy, loadingPolicy: false, items: [] }); + this._isMount && this.setState({ lookingPolicy: policy, loadingPolicy: false, items: [], checksIsLoading: false }); } } - /** - * - * @param color - * @param title - * @param time - */ + toggleDetails = (item) => { + const itemIdToExpandedRowMap = { ...this.state.itemIdToExpandedRowMap }; + + if (itemIdToExpandedRowMap[item.id]) { + delete itemIdToExpandedRowMap[item.id]; + } else { + let checks = ''; + checks += (item.rules || []).length > 1 ? 'Checks' : 'Check'; + checks += item.condition ? ` (Condition: ${item.condition})` : ''; + const complianceText = + item.compliance && item.compliance.length + ? item.compliance.map((el) => `${el.key}: ${el.value}`).join('\n') + : ''; + const listItems = [ + { + title: 'Check not applicable due to:', + description: item.reason, + }, + { + title: 'Rationale', + description: item.rationale || '-', + }, + { + title: 'Remediation', + description: item.remediation || '-', + }, + { + title: 'Description', + description: item.description || '-', + }, + { + title: (item.directory || '').includes(',') ? 'Paths' : 'Path', + description: item.directory, + }, + { + title: checks, + description: , + }, + { + title: 'Compliance', + description: , + }, + ]; + const itemsToShow = listItems.filter((x) => { + return x.description; + }); + itemIdToExpandedRowMap[item.id] = ; + } + this.setState({ itemIdToExpandedRowMap }); + }; + showToast = (color, title, time) => { getToasts().add({ color: color, @@ -295,13 +360,13 @@ export class Inventory extends Component { } render() { - const getPoliciesRowProps = (item, idx) => { - return { - 'data-test-subj': `sca-row-${idx}`, - className: 'customRowClass', - onClick: () => this.loadScaPolicy(item), - }; - }; + const { onClickRow } = this.props + + const handlePoliciesTableClickRow = async (policy) => { + onClickRow ? onClickRow(policy) : await this.loadScaPolicy(policy.policy_id) + this.setState({ loading: false, redirect: true }) + } + const buttonPopover = ( { onClick={() => this.setState({ showMoreInfo: !this.state.showMoreInfo })} > ); + const { agent } = this.props; + return (
- {this.state.loading && ( + {this.state.loading || this.state.checksIsLoading && (
@@ -320,8 +387,8 @@ export class Inventory extends Component { )}
- {this.props.agent && - (this.props.agent || {}).status !== API_NAME_AGENT_STATUS.NEVER_CONNECTED && + {agent && + (agent || {}).status !== API_NAME_AGENT_STATUS.NEVER_CONNECTED && !this.state.policies.length && !this.state.loading && ( @@ -331,8 +398,8 @@ export class Inventory extends Component { )} - {this.props.agent && - (this.props.agent || {}).status === API_NAME_AGENT_STATUS.NEVER_CONNECTED && + {agent && + (agent || {}).status === API_NAME_AGENT_STATUS.NEVER_CONNECTED && !this.state.loading && ( { )} - {this.props.agent && - (this.props.agent || {}).os && + {agent && + (agent || {}).os && !this.state.lookingPolicy && this.state.policies.length > 0 && - !this.state.loading && ( + !this.state.loading && !this.state.checksIsLoading && (
{this.state.policies.length && ( @@ -382,24 +449,24 @@ export class Inventory extends Component { ))} )} - - - - - + + + + - - - + + +
)} - {this.props.agent && - (this.props.agent || {}).os && + {agent && + (agent || {}).os && this.state.lookingPolicy && - !this.state.loading && ( + ((!this.state.loading) || (!this.state.checksIsLoading )) && (
@@ -407,7 +474,7 @@ export class Inventory extends Component { this.loadScaPolicy(false)} + onClick={this.state.secondTableBack ? (ev) => this.handleBack(ev) : () => this.loadScaPolicy(false)} iconType="arrowLeft" aria-label="Back to policies" {...{ iconSize: 'l' }} @@ -436,6 +503,22 @@ export class Inventory extends Component { + + await this.downloadCsv()} + > + Export formatted + + + + this.loadScaPolicy(this.state.lookingPolicy.policy_id)} + > + Refresh + + @@ -495,7 +578,7 @@ export class Inventory extends Component { @@ -507,3 +590,7 @@ export class Inventory extends Component { ); } } + +Inventory.defaultProps = { + onClickRow: undefined +} \ No newline at end of file diff --git a/public/components/agents/sca/inventory/agent-policies-table.tsx b/public/components/agents/sca/inventory/agent-policies-table.tsx new file mode 100644 index 0000000000..2ab8309e7e --- /dev/null +++ b/public/components/agents/sca/inventory/agent-policies-table.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { TableWzAPI } from '../../../common/tables/table-wz-api'; + +type Props = { + columns: any[]; + rowProps?: any; + agent: { [key in string]: any }; + tableProps?: any; +}; + +export default function SCAPoliciesTable(props: Props) { + const { columns, rowProps, agent, tableProps } = props; + + const getPoliciesRowProps = (item: any, idx: string) => { + return { + 'data-test-subj': `sca-row-${idx}`, + className: 'customRowClass', + onClick: rowProps ? () => rowProps(item) : null + } + } + + return ( + <> + + + ); +} diff --git a/public/components/agents/sca/inventory/checks-table.tsx b/public/components/agents/sca/inventory/checks-table.tsx index 74e01623ac..d31d614e99 100644 --- a/public/components/agents/sca/inventory/checks-table.tsx +++ b/public/components/agents/sca/inventory/checks-table.tsx @@ -231,7 +231,6 @@ export class InventoryPolicyChecksTable extends Component { item.compliance && item.compliance.length ? item.compliance.map((el) => `${el.key}: ${el.value}`).join('\n') : ''; - const rulesText = item.rules.length ? item.rules.map((el) => el.rule).join('\n') : ''; const listItems = [ { title: 'Check not applicable due to:', diff --git a/public/components/agents/sca/inventory/index.ts b/public/components/agents/sca/inventory/index.ts index be24c7ae7f..df8e32d03f 100644 --- a/public/components/agents/sca/inventory/index.ts +++ b/public/components/agents/sca/inventory/index.ts @@ -1,2 +1,3 @@ +export * from './agent-policies-table' export * from './lib'; export * from './checks-table'; diff --git a/public/components/common/modules/module.scss b/public/components/common/modules/module.scss index 8147faf135..debb1fa6c1 100644 --- a/public/components/common/modules/module.scss +++ b/public/components/common/modules/module.scss @@ -156,3 +156,8 @@ discover-app-w .sidebar-container { } } } + +.wz-section-sca-euiFlexGroup { + display: flex; + justify-content: space-between; +} \ No newline at end of file diff --git a/public/components/common/tables/__snapshots__/table-default.test.tsx.snap b/public/components/common/tables/__snapshots__/table-default.test.tsx.snap index e023d4b871..3133597b81 100644 --- a/public/components/common/tables/__snapshots__/table-default.test.tsx.snap +++ b/public/components/common/tables/__snapshots__/table-default.test.tsx.snap @@ -75,6 +75,7 @@ exports[`Table Default component renders correctly to match the snapshot 1`] = ` onChange={[Function]} pagination={ Object { + "hidePerPageOptions": false, "pageIndex": 0, "pageSize": 15, "pageSizeOptions": Array [ diff --git a/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap b/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap index 77a7e65b17..086cba0b6a 100644 --- a/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap +++ b/public/components/common/tables/__snapshots__/table-wz-api.test.tsx.snap @@ -153,6 +153,7 @@ exports[`Table WZ API component renders correctly to match the snapshot 1`] = ` onChange={[Function]} pagination={ Object { + "hidePerPageOptions": false, "pageIndex": 0, "pageSize": 15, "pageSizeOptions": Array [ diff --git a/public/components/common/tables/table-default.tsx b/public/components/common/tables/table-default.tsx index c8cf794592..97e3d50974 100644 --- a/public/components/common/tables/table-default.tsx +++ b/public/components/common/tables/table-default.tsx @@ -20,6 +20,7 @@ export function TableDefault({ onSearch, tableColumns, tablePageSizeOptions = [15, 25, 50, 100], + hidePerPageOptions = false, tableInitialSortingDirection = 'asc', tableInitialSortingField = '', tableProps = {}, @@ -107,8 +108,10 @@ export function TableDefault({ ...pagination, totalItemCount: totalItems, pageSizeOptions: tablePageSizeOptions, + hidePerPageOptions }; return ( + <> + ); } diff --git a/public/components/common/welcome/agents-welcome.js b/public/components/common/welcome/agents-welcome.js index 104c58b686..36b3f4fe25 100644 --- a/public/components/common/welcome/agents-welcome.js +++ b/public/components/common/welcome/agents-welcome.js @@ -493,7 +493,7 @@ class AgentsWelcome extends Component { -
+
{ _isMount = false; - props!: { - [key: string]: any - } + router; state: { lastScan: { [key: string]: any }, isLoading: Boolean, + firstTable: Boolean, + policies: any[], } constructor(props) { @@ -66,9 +70,12 @@ export const ScaScan = compose( this.state = { lastScan: {}, isLoading: true, + firstTable: true, + policies: [], }; } + async componentDidMount() { this._isMount = true; const $injector = getAngularModule().$injector; @@ -112,21 +119,72 @@ export const ScaScan = compose( } } + onClickRow = (policy) => { + window.location.href = `#/overview?tab=sca&redirectPolicyTable=${policy.policy_id}`; + store.dispatch(updateCurrentAgentData(this.props.agent)); + this.router.reload(); + } + renderScanDetails() { const { isLoading, lastScan } = this.state; if (isLoading || lastScan === undefined) return; + + const columnsPolicies = [ + { + field: 'name', + name: 'Policy', + width: '40%', + }, + { + field: 'end_scan', + name: 'End scan', + dataType: 'date', + render: formatUIDate, + width: '20%', + }, + { + field: 'pass', + name: 'Pass', + width: '10%', + }, + { + field: 'fail', + name: 'Fail', + width: '10%', + }, + { + field: 'invalid', + name: 'Not applicable', + width: '10%', + }, + { + field: 'score', + name: 'Score', + width: '10%', + render: (score) => { + return `${score}%`; + } + }, + ]; + + const tableProps = { + tablePageSizeOptions: [4], + hidePerPageOptions: true + } + return ( { - window.location.href = `#/overview?tab=sca&redirectPolicy=${lastScan.policy_id}`; + window.location.href = `#/overview?tab=sca&redirectPolicyTable=${lastScan.policy_id}`; store.dispatch(updateCurrentAgentData(this.props.agent)); this.router.reload(); } }>

{lastScan.name}

+
@@ -134,63 +192,14 @@ export const ScaScan = compose( {lastScan.policy_id}
- - - -

{lastScan.description}

-
-
-
- - - - - - - - - - - - - - - - - - - - Start time: {formatUIDate(lastScan.start_scan)} - - - - - Duration: {this.durationScan()} - - - + + +
) } @@ -225,20 +234,30 @@ export const ScaScan = compose( - - -

SCA: Last scan

-
+ + + + { + window.location.href = `#/overview?tab=sca&redirectPolicy=${lastScan.policy_id}`; + store.dispatch(updateCurrentAgentData(this.props.agent)); + this.router.reload(); + } + }> +

SCA: Lastest scans

+
+
+
{ - window.location.href = `#/overview?tab=sca`; - store.dispatch(updateCurrentAgentData(this.props.agent)); - this.router.reload(); - } + window.location.href = `#/overview?tab=sca`; + store.dispatch(updateCurrentAgentData(this.props.agent)); + this.router.reload(); + } } aria-label="Open SCA Scans" />