diff --git a/CHANGELOG.md b/CHANGELOG.md index e1496ed0d5..c3fa3da403 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to the Wazuh app project will be documented in this file. - Rework SCA modules visualizations, global detail for all agents without pinning, replaced `/sca` endpoint with `wazuh-states-sca-*` index pattern, added sample data section [#7578](https://github.com/wazuh/wazuh-dashboard-plugins/issues/7578) - Split the FIM registry inventory into 2 index patterns and change some fields in the FIM files and registries sample data [#7604](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7604) +- Changed health check [#7694](https://github.com/wazuh/wazuh-dashboard-plugins/pull/7694) ### Removed diff --git a/docker/osd-dev/dev.sh b/docker/osd-dev/dev.sh index 59a100f6ca..46292dea83 100755 --- a/docker/osd-dev/dev.sh +++ b/docker/osd-dev/dev.sh @@ -122,6 +122,8 @@ export OSD_MAJOR_NUMBER=$(echo $OSD_VERSION | cut -d. -f1) export COMPOSE_PROJECT_NAME=os-dev-${OSD_VERSION//./} export WAZUH_STACK="" export WAZUH_VERSION_DEVELOPMENT=$(jq -r '.version' $PACKAGE_PATH) +export SRC_DASHBOARD=$(realpath ../../../wazuh-dashboard) +export SRC_SECURITY_PLUGIN=$(realpath ../../../wazuh-security-dashboards-plugin) if [[ "$OSD_MAJOR_NUMBER" -ge 2 ]]; then export OSD_MAJOR="2.x" diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts index daa59c1788..5eea178c79 100644 --- a/plugins/main/common/constants.ts +++ b/plugins/main/common/constants.ts @@ -636,9 +636,6 @@ export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = 'user-manual/wazuh-dashboard/troubleshooting.html'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = 'user-manual/wazuh-dashboard/settings.html'; -export const PLUGIN_PLATFORM_URL_GUIDE = - 'https://opensearch.org/docs/2.10/about'; -export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'OpenSearch guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { 'osd-xsrf': 'kibana', diff --git a/plugins/main/docs/prompts.md b/plugins/main/docs/prompts.md new file mode 100644 index 0000000000..7621abaed7 --- /dev/null +++ b/plugins/main/docs/prompts.md @@ -0,0 +1,101 @@ +# Prompts + +The plugin uses prompts to display some problems or missing dependencies for the views. + +## Data source has an error. The filter related to the server API context can not be created due to the required data was not found. This is usually caused because there is no selected a server API. Ensure the server API is available and select it in the server API selector. + +This means the filter related to the selected server API (`cluster.name`/`manager.name` in the alerts case or `wazuh.cluster.name` in the inventories data) can not be created due to the required information is not available in some dashboard or inventory view. The required data to create the filter is stored in the `clusterInfo` cookie in the client browser. + +The cookie is set when getting the cluster information after the server API is selected through the selector or automatically when enters to some apps of Wazuh dashboard if possible. + +### Troubleshooting + +1. Ensure there is an server API selected in the selector (Wazuh dashboard header top right corner) +2. Server API is available and the Wazuh dashboard can communicate with it. + > Use the `ping` command or try to do a request (`cURL`) from the Wazuh dashboard host to the server API to ensure teh communication using the host configuration in the `wazuh.yml`. +3. Review the server API host configuration (URL, port, user credentials of the Wazuh server) are valid. The internal user should have permissions to retrieve the cluster information data so this can be set in the cookie. +4. Ensure the `clusterInfo` cookie is set in the browser. + +## Data source has an error. Index pattern with ID or title [INDEX_PATTERN] not found. Review if you have at least one index pattern with this configuration. You can create the index patterns from Dashboard Management application if there are matching indices. If there are no matching indices, this could indicate the data collection is disabled or there is a problem in the collection or ingestion. + +This means the index pattern, that is used in some view or panel where the error is visible, could not be found. + +This could be caused because the expected index pattern does not exist. The Wazuh dashboard could try to create the index patterns if this find matching indices, so this could mean there is no matching indices due to the data collection could be disabled or there is a problem in the data collection (server) or ingestion (server and indexer). + +In some cases, it searches by index pattern ID, and in others, this could be the ID or title. This requirement is specified in the error depending on the view or panel. + +### Troubleshooting + +1. Ensure you have an index pattern with the requirements of ID or title that are speficied in the error. + The available index patterns can be listed in the **Dashboard Management** > **Index patterns**. + +In the case the index pattern is not created, you can create in the **Dashboard Management** > **Index patterns** if there are matching indices. + +If there is not matching indices, this could indicate: + +- the data collection is disabled: in this case the prompt is expected because the related indices do not exist +- the data collection or ingestion has a problem: review the logs of the components (server and indexer) that run these actions for more information + +## Data source has an error. There is no selected index pattern for alerts. Ensure there is a compatible index pattern and select it using the index pattern selector. The index pattern selector is only available when there are multiple compatibles index patterns. If there is only a compatible index pattern, the selector is not visible, and it could indicate the index pattern was not selected due to some error or you could need to select it. + +This error indicates the index pattern related to alerts is not selected. + +The selection of the alerts index pattern is stored in the `currentPattern` cookie that stores the ID in the browser. If this cookie is not present or this has a falsy value, the error is thrown. + +Ensure the alerts index pattern is selected if this is available. + +### Troubleshooting + +1. Ensure the index pattern related to alerts is selected in the selector (Wazuh dashboard header top right corner). +2. Ensure the environment has compatible index patterns with the alerts data. You can list the index patterns in **Dashboard Management** > **Index patterns**. The compatible index patterns are these have the following fields: + +- `timestamp` +- `rule.groups` +- `manager.name` +- `agent.id` + +If there is not a compatible index pattern, it can be created if there are matching indices in **Dashboard Management** > **Index patterns**. + +If there is no matching indices and the alerts generation is enabled, this could indicate a problem in the +data collection (server) or ingestion (server, Filebeat, indexer). Review the components responsibles of these actions. + +3. Ensure the `currentPattern` cookie is set in the browser with the ID of the index. + +## Server API is not available. The server API is currently not available. Review the connection with the selected server API, the service is running and the API host configuration is valid. + +This means the dashboard can not connect with the server API host. + +This could be caused by: + +- Server API host is not reachable from the Wazuh dashboard host. + - Network problem (firewall). +- Server API is down/stopped. +- Wrong server API host configuration (URL, port and credentials) + +### Troubleshooting + +1. Ensure the server API is running + +``` +systemctl status wazuh-manager +``` + +2. Ensure the server API host is reachable from the Wazuh dashboard host. + +Use the `ping` command or `cURL` to try the communication using the configuration for the server API host in the Wazuh dashboard. + +3. Review the server API host configuration in the Wazuh dashboard side (URL, port and credentials) + +## Server API is not selected. The server API is not selected. Select it using the server API selector. + +This means the server API host is not selected. + +The selection of the server API host is set in the `currentApi` cookie in the browser. If this is not set or has a falsy value, the error is displayed. + +This can be caused because the server API host is not selected or this could be set when entering to some apps in Wazuh dashboard. + +### Troubleshooting + +1. Ensure the server API host is selected in the selector (Wazuh dashboard header top right corner). +2. Ensure the server API is available (online). +3. Ensure the server API is reachable by the Wazuh dashboard host and its configuration (URL, port and creandentials) is ok. diff --git a/plugins/main/public/app-router.tsx b/plugins/main/public/app-router.tsx index a0fbba49df..13d35135c8 100644 --- a/plugins/main/public/app-router.tsx +++ b/plugins/main/public/app-router.tsx @@ -10,7 +10,6 @@ import { WzMenuWrapper } from './components/wz-menu/wz-menu-wrapper'; import { WzAgentSelectorWrapper } from './components/wz-agent-selector/wz-agent-selector-wrapper'; import { ToastNotificationsModal } from './components/notifications/modal'; import { WzUpdatesNotification } from './components/wz-updates-notification'; -import { WzBlankScreen } from './components/wz-blank-screen/wz-blank-screen'; import { RegisterAgent } from './components/endpoints-summary/register-agent'; import { MainEndpointsSummary } from './components/endpoints-summary'; import { AgentView } from './components/endpoints-summary/agent'; @@ -23,6 +22,7 @@ import NavigationService from './react-services/navigation-service'; import { SECTIONS } from './sections'; import { withGuardAsync } from './components/common/hocs'; import { WzRequest } from './react-services/wz-request'; +import { AlertsDataSourceSetup } from './components/common/data-source'; export const Application = withGuardAsync( async (_props: any) => { @@ -34,6 +34,11 @@ export const Application = withGuardAsync( // Load the app state loadAppConfig(), ]); + + await Promise.allSettled([ + // Setup the alerts index pattern + AlertsDataSourceSetup(), + ]); } catch {} return { @@ -112,11 +117,6 @@ export const Application = withGuardAsync( exact render={props => } > - } - > diff --git a/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js b/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js index e989492937..21adacce5e 100644 --- a/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js +++ b/plugins/main/public/components/add-modules-data/WzSampleDataWrapper.js @@ -65,5 +65,5 @@ export class WzSampleDataProvider extends Component { export const WzSampleDataWrapper = compose( withErrorBoundary, - withUserAuthorizationPrompt(null, { isAdmininistrator: true }), + withUserAuthorizationPrompt(null, { isAdministrator: true }), )(WzSampleDataProvider); diff --git a/plugins/main/public/components/add-modules-data/sample-data.tsx b/plugins/main/public/components/add-modules-data/sample-data.tsx index 501c74c2bd..2591e7e5c2 100644 --- a/plugins/main/public/components/add-modules-data/sample-data.tsx +++ b/plugins/main/public/components/add-modules-data/sample-data.tsx @@ -49,6 +49,7 @@ import { sampleSecurityInformationApplication, sampleThreatDetectionApplication, } from './helper'; +import { GenericRequest } from '../../react-services'; export default class WzSampleData extends Component { categories: { @@ -148,7 +149,7 @@ export default class WzSampleData extends Component { // Check if sample data for each category was added const results = await DeepPromiseResolver( this.categories.reduce((accum, cur) => { - accum[cur.categorySampleDataIndex] = WzRequest.genericReq( + accum[cur.categorySampleDataIndex] = GenericRequest.request( 'GET', `/indexer/sampledata/${cur.categorySampleDataIndex}`, ); @@ -173,10 +174,22 @@ export default class WzSampleData extends Component { const clusterName = AppState.getClusterInfo().cluster; const managerName = AppState.getClusterInfo().manager; + if (!clusterName && !managerName) { + throw new Error( + 'The data related to the server API context could not be obtained. This is required when adding sample data to match the server API context.', + ); + } + // eslint-disable-next-line camelcase this.generateAlertsParams.api_id = JSON.parse( AppState.getCurrentAPI() || '{}', )?.id; + + if (!this.generateAlertsParams.api_id) { + throw new Error( + 'The server API is not selected. Select it using the server API selector.', + ); + } this.generateAlertsParams.manager = { name: managerName, }; diff --git a/plugins/main/public/components/agents/stats/table.test.tsx b/plugins/main/public/components/agents/stats/table.test.tsx index c1968e57be..725ec69925 100644 --- a/plugins/main/public/components/agents/stats/table.test.tsx +++ b/plugins/main/public/components/agents/stats/table.test.tsx @@ -22,6 +22,11 @@ jest.mock('../../../kibana-services', () => ({ prepend: str => str, }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); jest.mock( '../../../../../../node_modules/@elastic/eui/lib/services/accessibility/html_id_generator', 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 f7b73037eb..265ed742ae 100644 --- a/plugins/main/public/components/agents/syscollector/components/syscollector-metrics.tsx +++ b/plugins/main/public/components/agents/syscollector/components/syscollector-metrics.tsx @@ -159,7 +159,7 @@ export const InventoryMetrics = withSystemInventoryDataSource( }; fetchInventoryHardwareSystemData(); } - }, [itHygieneDataSource.isLoading, agent.id]); + }, [itHygieneDataSource.isLoading, itHygieneDataSource.fetchFilters]); const items: IRibbonItem[] = [ { diff --git a/plugins/main/public/components/common/dashboards/panel.tsx b/plugins/main/public/components/common/dashboards/panel.tsx index f1a0c0fcdb..47149a10d3 100644 --- a/plugins/main/public/components/common/dashboards/panel.tsx +++ b/plugins/main/public/components/common/dashboards/panel.tsx @@ -68,10 +68,7 @@ export const createPanel = ({ isLoadingNameProp: 'dataSource.isLoading', LoadingComponent: LoadingSearchbarProgress, }), - withDataSourceInitiated({ - isLoadingNameProp: 'dataSource.isLoading', - dataSourceNameProp: 'dataSource.dataSource', - }), + withDataSourceInitiated({}), )( ({ dataSource: { diff --git a/plugins/main/public/components/common/data-source/components/wz-data-source-selector/wz-data-source-selector.tsx b/plugins/main/public/components/common/data-source/components/wz-data-source-selector/wz-data-source-selector.tsx index 14270d3b5d..1cfa1c47d3 100644 --- a/plugins/main/public/components/common/data-source/components/wz-data-source-selector/wz-data-source-selector.tsx +++ b/plugins/main/public/components/common/data-source/components/wz-data-source-selector/wz-data-source-selector.tsx @@ -1,12 +1,11 @@ import React from 'react'; -import { useEffect, useState } from 'react'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { useState } from 'react'; +import { EuiFormRow, EuiSelect, EuiButtonIcon } from '@elastic/eui'; import { tDataSource, tDataSourceSelector, PatternDataSourceFactory, PatternDataSource, - AlertsDataSourceRepository, tParsedIndexPattern, } from '../../index'; import { @@ -15,52 +14,96 @@ import { HttpError, } from '../../../../../react-services/error-management'; import { PatternDataSourceSelector } from '../../pattern/pattern-data-source-selector'; +import { useAsyncActionRunOnStart } from '../../../hooks'; +import NavigationService from '../../../../../react-services/navigation-service'; +import { UI_LOGGER_LEVELS } from '../../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; +import { getErrorOrchestrator } from '../../../../../react-services/common-services'; +import { + Selector, + SelectorContainer, + SelectorLabel, +} from '../../../../wz-menu/selectors'; -type tWzDataSourceSelector< - T extends tParsedIndexPattern, - K extends tDataSource, -> = { +type tWzDataSourceSelector = { name: 'string'; onChange?: (dataSource: T) => void; - dataSourceSelector?: tDataSourceSelector; + DataSourceRepositoryCreator: any; + DataSource: any; + refetchDependencies?: any[]; + showSelectorsInPopover: boolean; }; +async function fetchDataSourceList(...params) { + const [ + DataSource, + DataSourceRepositoryCreator, + setDataSourceSelector, + setSelectedPattern, + ] = params; + const factory = new PatternDataSourceFactory(); + const repository = new DataSourceRepositoryCreator(); + const dataSources = await factory.createAll( + DataSource, + await repository.getAll(), + ); + const selector = new PatternDataSourceSelector(dataSources, repository); + setDataSourceSelector(selector); + try { + const defaultIndexPattern = await selector.getSelectedDataSource(); + setSelectedPattern(defaultIndexPattern); + } catch {} + return await selector.getAllDataSources(); +} + const WzDataSourceSelector = ( props: tWzDataSourceSelector, ) => { const { - onChange, name = 'data source', - dataSourceSelector: defaultDataSourceSelector, + DataSource, + DataSourceRepositoryCreator, + refetchDependencies, + showSelectorsInPopover, } = props; - const [dataSourceList, setDataSourceList] = useState([]); const [selectedPattern, setSelectedPattern] = useState(); const [dataSourceSelector, setDataSourceSelector] = useState< tDataSourceSelector | undefined - >(defaultDataSourceSelector); + >(undefined); - useEffect(() => { - init(); - }, []); + const action = useAsyncActionRunOnStart(fetchDataSourceList, [ + DataSource, + DataSourceRepositoryCreator, + setDataSourceSelector, + setSelectedPattern, + ...(refetchDependencies || []), + ]); - async function init() { - let selector; - let dataSources; - if (!dataSourceSelector) { - const factory = new PatternDataSourceFactory(); - const repository = new AlertsDataSourceRepository(); - dataSources = await factory.createAll( - PatternDataSource, - await repository.getAll(), - ); - selector = new PatternDataSourceSelector(dataSources, repository); - setDataSourceSelector(selector); + const onChange = async () => { + try { + /* TODO: this reloads the page to force the components are remounted with the new + selection of. To avoid this refresh, we would have to do the components are able to react + to these changes redoing the requests, etc... This will need a considerable time to + apply the changes. The reload of the pages is the same behavior used for the routing based + on AngularJS. + */ + NavigationService.getInstance().reload(); + } catch (error) { + const options = { + context: `${WzDataSourceSelector.name}.onChangePattern`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: false, + display: true, + error: { + error: error, + message: error.message || error, + title: `Error changing the Index Pattern`, + }, + }; + getErrorOrchestrator().handleError(options); } - const defaultIndexPattern = await selector.getSelectedDataSource(); - dataSources = await selector.getAllDataSources(); - setSelectedPattern(defaultIndexPattern); - setDataSourceList(dataSources); - } + }; async function selectDataSource(e) { const dataSourceId = e.target.value; @@ -81,17 +124,53 @@ const WzDataSourceSelector = ( } } + let style = { maxWidth: 250, maxHeight: 50 }; + if (showSelectorsInPopover) { + style = { width: '100%', maxHeight: 50, minWidth: 220 }; + } + + const notSelected = !Boolean(selectedPattern?.id); + const actionError = + action.error?.message || + (!action.running && notSelected && 'Index pattern is not selected'); + + const isInvalid = Boolean(actionError); + return ( - - { - return { value: item.id, text: item.title }; - })} - value={selectedPattern?.id} - onChange={selectDataSource} - aria-label={name} - /> - + + + Index pattern + + +
+ + { + return { value: item.id, text: item.title }; + })} + value={selectedPattern?.id} + onChange={selectDataSource} + aria-label={name} + hasNoInitialSelection={notSelected} + isInvalid={isInvalid} + append={ + { + action.run(); + }} + > + } + /> + +
+
+
); }; diff --git a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts index 719ff3ae24..c349e83a4f 100644 --- a/plugins/main/public/components/common/data-source/hooks/use-data-source.ts +++ b/plugins/main/public/components/common/data-source/hooks/use-data-source.ts @@ -168,14 +168,18 @@ export function useDataSource< // what the filters update subscription = dataSourceFilterManager.getUpdates$().subscribe({ next: () => { - if (!isComponentMounted()) return; - // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar - dataSourceFilterManager.setFilters( - dataSourceFilterManager.getFilters(), - ); - setAllFilters(dataSourceFilterManager.getFilters()); - setFetchFilters(dataSourceFilterManager.getFetchFilters()); - setFixedFilters(dataSourceFilterManager.getFixedFilters()); + try { + if (!isComponentMounted()) return; + // this is necessary to remove the hidden filters from the filter manager and not show them in the search bar + dataSourceFilterManager.setFilters( + dataSourceFilterManager.getFilters(), + ); + setAllFilters(dataSourceFilterManager.getFilters()); + setFetchFilters(dataSourceFilterManager.getFetchFilters()); + setFixedFilters(dataSourceFilterManager.getFixedFilters()); + } catch (error) { + setError(error?.message); + } }, }); setAllFilters(dataSourceFilterManager.getFilters()); @@ -198,8 +202,12 @@ export function useDataSource< useEffect(() => { if (dataSourceFilterManager && dataSource) { - setFixedFilters(dataSourceFilterManager.getFixedFilters()); - setFetchFilters(dataSourceFilterManager.getFetchFilters()); + try { + setFixedFilters(dataSourceFilterManager.getFixedFilters()); + setFetchFilters(dataSourceFilterManager.getFetchFilters()); + } catch (error) { + setError(error?.message); + } } }, [JSON.stringify(pinnedAgent)]); diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.test.ts b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.test.ts new file mode 100644 index 0000000000..29ce702fda --- /dev/null +++ b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.test.ts @@ -0,0 +1,98 @@ +import { tSavedObjectResponse } from '../pattern-data-source-repository'; +import { AlertsDataSourceRepository } from '../../index'; +jest.mock('../../../../../react-services/generic-request'); +import { AppState } from '../../../../../react-services'; +jest.mock('../../../../../react-services/app-state'); +jest.mock('../../../../../kibana-services', () => ({ + ...(jest.requireActual('../../../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: url => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name, value, options) => { + return true; + }, + get: () => { + return '{}'; + }, + remove: () => { + return; + }, + }), + getUiSettings: jest.fn().mockReturnValue({ + get: name => { + return true; + }, + }), +})); + +const mockedSavedObject = { + data: { + attributes: { + title: 'test-pattern-title', + fields: + '[{"name":"timestamp","type":"date","count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + }, + id: 'test-pattern-id', + migrationVersion: { + 'index-pattern': '7.10.0', + }, + namespace: ['default'], + references: [], + score: 0, + type: 'index-pattern', + updated_at: '2021-08-23T14:05:54.000Z', + version: 'WzIwMjEsM', + }, +} as tSavedObjectResponse; + +describe('AlertsDataSourceRepository', () => { + let repository: AlertsDataSourceRepository; + + beforeEach(() => { + repository = new AlertsDataSourceRepository(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should create a new instance', () => { + expect(repository).toBeInstanceOf(AlertsDataSourceRepository); + }); + + it('should set default pattern in storage', async () => { + const mockedIndexPattern = { + ...mockedSavedObject.data, + id: 'test-id', + attributes: { + ...mockedSavedObject.data.attributes, + title: 'test-title', + }, + }; + const parsedIndexPatternData = + repository.parseIndexPattern(mockedIndexPattern); + await repository.setDefault(parsedIndexPatternData); + expect(AppState.setCurrentPattern).toHaveBeenCalledWith( + mockedIndexPattern.id, + ); + }); + + it('should return an ERROR when default index pattern is not saved in storage', async () => { + AppState.getCurrentPattern = jest.fn().mockReturnValue(null); + try { + await repository.getDefault(); + } catch (error) { + expect(error.message).toBe( + 'There is no selected index pattern for alerts. Ensure there is a compatible index pattern and select it using the index pattern selector. The index pattern selector is only available when there are multiple compatibles index patterns. If there is only a compatible index pattern, the selector is not visible, and it could indicate the index pattern was not selected due to some error or you could need to select it.', + ); + } + }); +}); diff --git a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.ts b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.ts index ea0b6b4b37..d851d95447 100644 --- a/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.ts +++ b/plugins/main/public/components/common/data-source/pattern/alerts/alerts-data-source-repository.ts @@ -1,5 +1,10 @@ import { AppState } from '../../../../../react-services'; -import { PatternDataSourceRepository } from '../pattern-data-source-repository'; +import { PatternDataSourceFactory } from '../pattern-data-source-factory'; +import { + PatternDataSourceRepository, + tParsedIndexPattern, +} from '../pattern-data-source-repository'; +import { AlertsDataSource } from './alerts-data-source'; const ALERTS_REQUIRED_FIELDS = [ 'timestamp', @@ -15,6 +20,7 @@ export class AlertsDataSourceRepository extends PatternDataSourceRepository { async getAll() { const indexPatterns = await super.getAll(); + // FIXME: this should take into account the ip.ignore setting to filter the index patterns return indexPatterns.filter(this.checkIfAlertsIndexPattern); } @@ -65,7 +71,54 @@ export class AlertsDataSourceRepository extends PatternDataSourceRepository { return dataSource; } + setDefault(dataSource: tParsedIndexPattern): void { + if (!dataSource) { + throw new Error('Index pattern is required'); + } + AppState.setCurrentPattern(dataSource.id); + } + getStoreIndexPatternId(): string { return AppState.getCurrentPattern(); } + + async setupDefault(dataSources): Promise { + if (!this.getStoreIndexPatternId()) { + const [dataSource] = dataSources; + + if (!dataSource) { + throw new Error('No compatible index patterns found.'); + } + + AppState.setCurrentPattern(dataSource.id); + } + } +} + +/* WORKAROUND: This is a workaround to ensure the default alerts index pattern is set when the app starts. + Multiple UI views depend on the alerts index pattern is created. This method try to set the cookie + where the alerts index pattern ID is stored. + + This logic could be moved to another service. +*/ +export async function AlertsDataSourceSetup() { + const selectedIndexPatternId = AppState.getCurrentPattern(); + + const factory = new PatternDataSourceFactory(); + const repository = new AlertsDataSourceRepository(); + + const dataSources = await factory.createAll( + AlertsDataSource, + await repository.getAll(), + ); + + // Check if the selected index pattern Id in cookie exists and skip + if ( + selectedIndexPatternId && + dataSources.find(({ id }) => id === selectedIndexPatternId) + ) { + return; + } + + await repository.setupDefault(dataSources); } 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 54b7fa2028..fff08f925f 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 @@ -264,12 +264,20 @@ export class PatternDataSourceFilterManager key?: string, ): tFilter[] { const filterHandler = new FilterHandler(); - const isCluster = AppState.getClusterInfo().status == 'enabled'; + const { status, cluster, manager } = AppState.getClusterInfo(); + + const isClusterEnabled = status === 'enabled'; + const filterValue = isClusterEnabled ? cluster : manager; + + if (status === undefined || filterValue === undefined) { + throw new Error( + 'The filter related to the server API context can not be created due to the required data was not found. This is usually caused because there is no selected a server API. Ensure the server API is available and select it in the server API selector.', + ); + } + const managerFilter = filterHandler.managerQuery( - isCluster - ? AppState.getClusterInfo().cluster - : AppState.getClusterInfo().manager, - isCluster, + filterValue, + isClusterEnabled, key, ); managerFilter.meta = { diff --git a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.test.ts b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.test.ts index f21c512e0a..4ea2d99811 100644 --- a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.test.ts +++ b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.test.ts @@ -1,50 +1,77 @@ -import { PatternDataSourceRepository, tSavedObjectResponse } from './pattern-data-source-repository'; +import { + PatternDataSourceRepository, + tSavedObjectResponse, +} from './pattern-data-source-repository'; import { PatternDataSource, tDataSource } from '../index'; import { GenericRequest } from '../../../../react-services/generic-request'; jest.mock('../../../../react-services/generic-request'); -import { AppState } from '../../../../react-services/app-state'; jest.mock('../../../../react-services/app-state'); jest.mock('../../../../kibana-services', () => ({ - ...(jest.requireActual('../../../../kibana-services') as object), - getHttp: jest.fn().mockReturnValue({ - basePath: { - get: () => { - return 'http://localhost:5601'; - }, - prepend: url => { - return `http://localhost:5601${url}`; - }, - }, - }), - getCookies: jest.fn().mockReturnValue({ - set: (name, value, options) => { - return true; - }, - get: () => { - return '{}'; - }, - remove: () => { - return; - }, - }), - getUiSettings: jest.fn().mockReturnValue({ - get: name => { - return true; - }, - }), + ...(jest.requireActual('../../../../kibana-services') as object), + getHttp: jest.fn().mockReturnValue({ + basePath: { + get: () => { + return 'http://localhost:5601'; + }, + prepend: url => { + return `http://localhost:5601${url}`; + }, + }, + }), + getCookies: jest.fn().mockReturnValue({ + set: (name, value, options) => { + return true; + }, + get: () => { + return '{}'; + }, + remove: () => { + return; + }, + }), + getUiSettings: jest.fn().mockReturnValue({ + get: name => { + return true; + }, + }), })); const mockedSavedObject = { - data: { + data: { + attributes: { + title: 'test-pattern-title', + fields: + '[{"name":"timestamp","type":"date","count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + }, + id: 'test-pattern-id', + migrationVersion: { + 'index-pattern': '7.10.0', + }, + namespace: ['default'], + references: [], + score: 0, + type: 'index-pattern', + updated_at: '2021-08-23T14:05:54.000Z', + version: 'WzIwMjEsM', + }, +} as tSavedObjectResponse; + +function createMockedSavedObjectListResponse( + list: { id: string; title: string }[], +): tSavedObjectResponse[] { + return list.map(item => { + return { + data: { attributes: { - title: 'test-pattern-title', - fields: '[{"name":"timestamp","type":"date","count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', + title: item.title, + fields: + '[{"name":"timestamp","type":"date","count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', }, - id: 'test-pattern-id', + id: item.id, migrationVersion: { - 'index-pattern': '7.10.0', + 'index-pattern': '7.10.0', }, namespace: ['default'], references: [], @@ -52,205 +79,141 @@ const mockedSavedObject = { type: 'index-pattern', updated_at: '2021-08-23T14:05:54.000Z', version: 'WzIwMjEsM', - } -} as tSavedObjectResponse; - -function createMockedSavedObjectListResponse(list: { id: string, title: string }[]): tSavedObjectResponse[] { - return list.map(item => { - return { - data: { - attributes: { - title: item.title, - fields: '[{"name":"timestamp","type":"date","count":0,"scripted":false,"searchable":true,"aggregatable":true,"readFromDocValues":true}]', - }, - id: item.id, - migrationVersion: { - 'index-pattern': '7.10.0', - }, - namespace: ['default'], - references: [], - score: 0, - type: 'index-pattern', - updated_at: '2021-08-23T14:05:54.000Z', - version: 'WzIwMjEsM', - } - } as tSavedObjectResponse; - }); - + }, + } as tSavedObjectResponse; + }); } describe('PatternDataSourceRepository', () => { - let repository: PatternDataSourceRepository; - - beforeEach(() => { - repository = new PatternDataSourceRepository(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should create a new instance', () => { - expect(repository).toBeInstanceOf(PatternDataSourceRepository); - }); - - it('should return a pattern by id', async () => { - const mockId = 'test-pattern-id'; - const mockTitle = 'test-pattern-title'; - const expectedResponse = { - data: { - ...mockedSavedObject.data, - id: mockId, - attributes: { - ...mockedSavedObject.data.attributes, - title: mockTitle, - } - } - } as tSavedObjectResponse; - const mockRequest = jest.fn().mockResolvedValue(expectedResponse); - GenericRequest.request = mockRequest; - - const result = await repository.get(mockId); - - expect(result.id).toEqual(expectedResponse.data.id); - expect(result.title).toEqual(expectedResponse.data.attributes.title); - expect(result._fields).toEqual(JSON.parse(expectedResponse.data.attributes.fields)); - }); - - it('should return an ERROR when the request throw an error', async () => { - const id = 'not-exists'; - const expectedError = new Error(`Error 404 or any other error in response`); - const mockRequest = jest.fn().mockRejectedValue(expectedError); - GenericRequest.request = mockRequest; - - await expect(repository.get(id)).rejects.toThrow(`Error getting index pattern: ${expectedError.message}`); - }); - - it('should return an ERROR when the response not return a saved object data', async () => { - const id = 'test-id'; - const expectedResponse = { - }; - const mockRequest = jest.fn().mockResolvedValue(expectedResponse); - GenericRequest.request = mockRequest; - try { - await repository.get(id); - } catch (error) { - expect(error.message).toBe(`Error getting index pattern: Index pattern "${id}" not found`); - } - }); - - - it('should return an ERROR when the request not return and id or a title', async () => { - const id = 'test-id'; - const expectedResponse = { - data: { - ...mockedSavedObject.data, - id: null, - attributes: { - ...mockedSavedObject.data.attributes, - title: null, - } - } - }; - const mockRequest = jest.fn().mockResolvedValue(expectedResponse); - GenericRequest.request = mockRequest; - try { - await repository.get(id); - } catch (error) { - expect(error.message).toBe('Error getting index pattern: Invalid index pattern data'); - } - }); - - it('should return all the index patterns', async () => { - const listOfMockedPatterns = createMockedSavedObjectListResponse([ - { id: 'id-1', title: 'title-1' }, - { id: 'id-2', title: 'title-2' }, - { id: 'id-3', title: 'title-3' }, - ]); - - const mockRequest = jest.fn().mockResolvedValue(listOfMockedPatterns); - GenericRequest.request = mockRequest; - const result = await repository.getAll(); - // check if the response have the same id and title and exists the _fields property - result.forEach((item, index) => { - expect(item.id).toBe(listOfMockedPatterns[index].data.id); - expect(item.title).toBe(listOfMockedPatterns[index].data.attributes.title); - expect(item._fields).toBeDefined(); - }); - - }); - - it('should return ERROR when the getAll request throw an error ', async () => { - const expectedError = new Error('Error 404 or any other error in response'); - const mockRequest = jest.fn().mockRejectedValue(expectedError); - GenericRequest.request = mockRequest; - - await expect(repository.getAll()).rejects.toThrow(`Error getting index patterns: ${expectedError.message}`); - }); - - it('should parse index pattern data', () => { - const mockedIndexPatternData = { - ...mockedSavedObject.data, - id: 'test-pattern-id', - attributes: { - ...mockedSavedObject.data.attributes, - title: 'test-pattern-title', - } - } - const result = repository.parseIndexPattern(mockedIndexPatternData); - expect(result.id).toEqual(mockedIndexPatternData.id); - expect(result.title).toEqual(mockedIndexPatternData.attributes.title); - expect(result._fields).toBeDefined(); - }); - - it('should set default pattern in storage', async () => { - const mockedIndexPattern = { - ...mockedSavedObject.data, - id: 'test-id', - attributes: { - ...mockedSavedObject.data.attributes, - title: 'test-title', - } - } - const parsedIndexPatternData = repository.parseIndexPattern(mockedIndexPattern); - await repository.setDefault(parsedIndexPatternData); - expect(AppState.setCurrentPattern).toHaveBeenCalledWith(mockedIndexPattern.id); - }); - - it('should return ERROR when not receive an index pattern to setting like default', async () => { - try { - repository.setDefault(null as any) - }catch(error){ - expect(error.message).toBe('Index pattern is required'); - } - }); - - it('should return default index pattern stored', async () => { - AppState.getCurrentPattern = jest.fn().mockReturnValue('test-pattern-id'); - const mockedDefaultIndexPattern = { - data: { - ...mockedSavedObject.data, - id: 'test-pattern-id', - attributes: { - ...mockedSavedObject.data.attributes, - title: 'test-pattern-title', - } - } - } - const mockRequest = jest.fn().mockResolvedValue(mockedDefaultIndexPattern); - GenericRequest.request = mockRequest; - const result = await repository.getDefault(); - // mock the get method to return the current pattern - expect(result).toBeDefined(); - expect(result?.id).toEqual('test-pattern-id'); - }); - - it('should return an ERROR when default index pattern is not saved in storage', async () => { - AppState.getCurrentPattern = jest.fn().mockReturnValue(null); - try { - await repository.getDefault(); - }catch(error){ - expect(error.message).toBe('No default pattern set'); - } + let repository: PatternDataSourceRepository; + + beforeEach(() => { + repository = new PatternDataSourceRepository(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should create a new instance', () => { + expect(repository).toBeInstanceOf(PatternDataSourceRepository); + }); + + it('should return a pattern by id', async () => { + const mockId = 'test-pattern-id'; + const mockTitle = 'test-pattern-title'; + const expectedResponse = { + data: { + ...mockedSavedObject.data, + id: mockId, + attributes: { + ...mockedSavedObject.data.attributes, + title: mockTitle, + }, + }, + } as tSavedObjectResponse; + const mockRequest = jest.fn().mockResolvedValue(expectedResponse); + GenericRequest.request = mockRequest; + + const result = await repository.get(mockId); + + expect(result.id).toEqual(expectedResponse.data.id); + expect(result.title).toEqual(expectedResponse.data.attributes.title); + expect(result._fields).toEqual( + JSON.parse(expectedResponse.data.attributes.fields), + ); + }); + + it('should return an ERROR when the request throw an error', async () => { + const id = 'not-exists'; + const expectedError = new Error(`Error 404 or any other error in response`); + const mockRequest = jest.fn().mockRejectedValue(expectedError); + GenericRequest.request = mockRequest; + + await expect(repository.get(id)).rejects.toThrow( + `Error getting index pattern: ${expectedError.message}`, + ); + }); + + it('should return an ERROR when the response not return a saved object data', async () => { + const id = 'test-id'; + const expectedResponse = {}; + const mockRequest = jest.fn().mockResolvedValue(expectedResponse); + GenericRequest.request = mockRequest; + try { + await repository.get(id); + } catch (error) { + expect(error.message).toBe( + `Error getting index pattern: Index pattern "${id}" not found`, + ); + } + }); + + it('should return an ERROR when the request not return and id or a title', async () => { + const id = 'test-id'; + const expectedResponse = { + data: { + ...mockedSavedObject.data, + id: null, + attributes: { + ...mockedSavedObject.data.attributes, + title: null, + }, + }, + }; + const mockRequest = jest.fn().mockResolvedValue(expectedResponse); + GenericRequest.request = mockRequest; + try { + await repository.get(id); + } catch (error) { + expect(error.message).toBe( + 'Error getting index pattern: Invalid index pattern data', + ); + } + }); + + it('should return all the index patterns', async () => { + const listOfMockedPatterns = createMockedSavedObjectListResponse([ + { id: 'id-1', title: 'title-1' }, + { id: 'id-2', title: 'title-2' }, + { id: 'id-3', title: 'title-3' }, + ]); + + const mockRequest = jest.fn().mockResolvedValue(listOfMockedPatterns); + GenericRequest.request = mockRequest; + const result = await repository.getAll(); + // check if the response have the same id and title and exists the _fields property + result.forEach((item, index) => { + expect(item.id).toBe(listOfMockedPatterns[index].data.id); + expect(item.title).toBe( + listOfMockedPatterns[index].data.attributes.title, + ); + expect(item._fields).toBeDefined(); }); + }); + + it('should return ERROR when the getAll request throw an error ', async () => { + const expectedError = new Error('Error 404 or any other error in response'); + const mockRequest = jest.fn().mockRejectedValue(expectedError); + GenericRequest.request = mockRequest; + + await expect(repository.getAll()).rejects.toThrow( + `Error getting index patterns: ${expectedError.message}`, + ); + }); + + it('should parse index pattern data', () => { + const mockedIndexPatternData = { + ...mockedSavedObject.data, + id: 'test-pattern-id', + attributes: { + ...mockedSavedObject.data.attributes, + title: 'test-pattern-title', + }, + }; + const result = repository.parseIndexPattern(mockedIndexPatternData); + expect(result.id).toEqual(mockedIndexPatternData.id); + expect(result.title).toEqual(mockedIndexPatternData.attributes.title); + expect(result._fields).toBeDefined(); + }); }); diff --git a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.ts b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.ts index 9be96366a7..9ea25af7fe 100644 --- a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.ts +++ b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-repository.ts @@ -40,7 +40,7 @@ export type tParsedIndexPattern = { _fields: any[]; } & object; -export class PatternDataSourceRepository +export abstract class PatternDataSourceRepository implements tDataSourceRepository { async get(id: string): Promise { @@ -90,14 +90,6 @@ export class PatternDataSourceRepository }; } - setDefault(dataSource: tParsedIndexPattern): void { - if (!dataSource) { - throw new Error('Index pattern is required'); - } - AppState.setCurrentPattern(dataSource.id); - return; - } - async getDefault(dataSources): Promise { - // This method should be redefined by the subclasses - } + abstract setDefault(dataSource: tParsedIndexPattern): void; + abstract getDefault(dataSources): Promise; } diff --git a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-selector.test.ts b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-selector.test.ts index 8721b8e8eb..8772aae2bd 100644 --- a/plugins/main/public/components/common/data-source/pattern/pattern-data-source-selector.test.ts +++ b/plugins/main/public/components/common/data-source/pattern/pattern-data-source-selector.test.ts @@ -158,22 +158,6 @@ describe('PatternDataSourceSelector', () => { expect(result.id).toEqual(mockedList[0].id); expect(repository.getDefault).toHaveBeenCalledTimes(1); }); - - it('should return the first data source when the repository does not have a selected data source', async () => { - jest.spyOn(repository, 'getDefault').mockResolvedValue(null); - let selector = new PatternDataSourceSelector(mockedList, repository); - // mock spyon existsDataSource method to return 2 times differents values - jest - .spyOn(selector, 'existsDataSource') - .mockResolvedValueOnce(false) - .mockResolvedValueOnce(true); - jest.spyOn(selector, 'selectDataSource').mockResolvedValue(); - const result = await selector.getSelectedDataSource(); - expect(result.id).toEqual(mockedList[1].id); - expect(repository.getDefault).toHaveBeenCalledTimes(1); - expect(selector.existsDataSource).toHaveBeenCalledTimes(2); - expect(selector.selectDataSource).toHaveBeenCalledTimes(1); - }); }); describe('selectDataSource', () => { diff --git a/plugins/main/public/components/common/data-source/pattern/statistics/statistics-data-source.ts b/plugins/main/public/components/common/data-source/pattern/statistics/statistics-data-source.ts index 2e6c95800e..50ee598c9c 100644 --- a/plugins/main/public/components/common/data-source/pattern/statistics/statistics-data-source.ts +++ b/plugins/main/public/components/common/data-source/pattern/statistics/statistics-data-source.ts @@ -24,7 +24,12 @@ export class StatisticsDataSource extends PatternDataSource { getAPIFilter(): tFilter[] { const currentApi = AppState.getCurrentAPI(); - const parsedCurrentApi = currentApi ? JSON.parse(currentApi) : undefined; + if (!currentApi) { + throw new Error( + 'The filter related to the selected server API context can not be created due to the required data was not found. This is usually caused because there is no selected a server API. Ensure the server API is available and select it in the server API selector.', + ); + } + const parsedCurrentApi = JSON.parse(currentApi); const apiNameFilter = { meta: { removable: false, diff --git a/plugins/main/public/components/common/hocs/index.ts b/plugins/main/public/components/common/hocs/index.ts index efd1298455..912b1ff0dc 100644 --- a/plugins/main/public/components/common/hocs/index.ts +++ b/plugins/main/public/components/common/hocs/index.ts @@ -28,3 +28,4 @@ export * from './with-wrap-component'; export * from './with-inject-props'; export * from './with-health-check'; export * from './with-agent'; +export * from './with-server-api-available'; diff --git a/plugins/main/public/components/common/hocs/with-data-source.tsx b/plugins/main/public/components/common/hocs/with-data-source.tsx index 4148e3070c..4caa5f0c6d 100644 --- a/plugins/main/public/components/common/hocs/with-data-source.tsx +++ b/plugins/main/public/components/common/hocs/with-data-source.tsx @@ -18,7 +18,7 @@ export const PromptErrorInitializatingDataSource = (props: { return ( Data source was not initialized} + title={

Data source has an error

} body={<>{typeof props.error === 'string' &&

{props.error}

}} /> ); @@ -50,7 +50,11 @@ export const withDataSourceInitiated = ({ }) => withGuard( props => { - return !get(props, isLoadingNameProp) && !get(props, dataSourceNameProp); + return ( + /* data source is not defined or there is an error related to the data source initialization */ + (!get(props, isLoadingNameProp) && !get(props, dataSourceNameProp)) || + get(props, dataSourceErrorNameProp) + ); }, props => ( ; + return ( + + ); }; /** @@ -154,17 +160,20 @@ export const withDataSourceFetchOnStart = }: WithDataSourceFetchOnStartProps) => WrappedComponent => props => { - const dataSource = props[nameProp]; - const fetch = useCallback(async (...dependencies: any[]) => { - const response = await dataSource.fetchData( - mapRequestParams - ? mapRequestParams({ ...props, dataSource, dependencies }) - : {}, - ); - return mapResponse - ? mapResponse(response, { ...props, dataSource, dependencies }) - : response; - }, []); + const dataSource = get(props, nameProp); + const fetch = useCallback( + async (...dependencies: any[]) => { + const response = await dataSource.fetchData( + mapRequestParams + ? mapRequestParams({ ...props, dataSource, dependencies }) + : {}, + ); + return mapResponse + ? mapResponse(response, { ...props, dataSource, dependencies }) + : response; + }, + [dataSource.fetchFilters], + ); const actionActionRunDependencies = typeof mapFetchActionDependencies === 'function' diff --git a/plugins/main/public/components/common/hocs/with-server-api-available.tsx b/plugins/main/public/components/common/hocs/with-server-api-available.tsx new file mode 100644 index 0000000000..cbc2cee1e4 --- /dev/null +++ b/plugins/main/public/components/common/hocs/with-server-api-available.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { useSelectedServerApi } from '../hooks/use-selected-server-api'; +import { useServerApiAvailable } from '../hooks/use-server-api-available'; + +const PromptServerAPIUnavailable = () => ( + Server API is not available} + body={ +
+

+ The server API is currently not available. Review the connection with + the selected server API, the service is running and the API host + configuration is valid. +

+
+ } + /> +); + +export const withServerAPIAvailable = + (WrappedComponent: React.FC) => (props: any) => { + const { isAvailable } = useServerApiAvailable(); + + if (!isAvailable) { + return ; + } + + return ; + }; + +const PromptServerAPINotSelected = () => ( + Server API is not selected} + body={ +
+

+ The server API is not selected. Select it using the server API + selector. +

+
+ } + /> +); + +export const withSelectedServerAPI = + (WrappedComponent: React.FC) => (props: any) => { + const { selectedAPI } = useSelectedServerApi(); + + if (!selectedAPI) { + return ; + } + + return ; + }; diff --git a/plugins/main/public/components/common/hocs/withGuard.tsx b/plugins/main/public/components/common/hocs/withGuard.tsx index 6aa1a63aaa..309b040a12 100644 --- a/plugins/main/public/components/common/hocs/withGuard.tsx +++ b/plugins/main/public/components/common/hocs/withGuard.tsx @@ -12,9 +12,9 @@ import React, { useEffect, useState } from 'react'; export const withGuard = - (condition: (props: any) => boolean, ComponentFulfillsCondition) => - WrappedComponent => - props => { + (condition: (props: any) => boolean, ComponentFulfillsCondition: React.FC) => + (WrappedComponent: React.FC) => + (props: any) => { return condition(props) ? ( ) : ( @@ -25,11 +25,11 @@ export const withGuard = export const withGuardAsync = ( condition: (props: any) => Promise<{ ok: boolean; data: any }>, - ComponentFulfillsCondition: React.JSX.Element, - ComponentLoadingResolution: null | React.JSX.Element = null, + ComponentFulfillsCondition: React.FC, + ComponentLoadingResolution: null | React.FC = null, ) => - WrappedComponent => - props => { + (WrappedComponent: React.FC) => + (props: any) => { const [loading, setLoading] = useState(true); const [fulfillsCondition, setFulfillsCondition] = useState({ ok: false, diff --git a/plugins/main/public/components/common/hocs/withUserAuthorization.tsx b/plugins/main/public/components/common/hocs/withUserAuthorization.tsx index aea01ef14a..215a721238 100644 --- a/plugins/main/public/components/common/hocs/withUserAuthorization.tsx +++ b/plugins/main/public/components/common/hocs/withUserAuthorization.tsx @@ -16,20 +16,48 @@ import { useUserPermissionsIsAdminRequirements } from '../hooks/use-user-is-admi import { WzEmptyPromptNoPermissions } from '../permissions/prompt'; import { compose } from 'redux'; import { withUserLogged } from './withUserLogged'; -// +import { + withSelectedServerAPI, + withServerAPIAvailable, +} from './with-server-api-available'; + +interface ActionResourcePermission { + action: string; + resource: string; +} + +type ActionResourcePermissionRequirements = + | ActionResourcePermission[][] + | ActionResourcePermission[]; + +interface PermissionResolver { + (params: any): ActionResourcePermissionRequirements; +} + +type AccessPermission = + | ActionResourcePermissionRequirements + | PermissionResolver + | null; + +interface ExtraUserPermissions { + isAdministrator?: boolean | null; +} + const withUserAuthorizationPromptChanged = - (permissions = null, othersPermissions = { isAdmininistrator: null }) => - WrappedComponent => - props => { - const [userPermissionRequirements, userPermissions] = - useUserPermissionsRequirements( - typeof permissions === 'function' ? permissions(props) : permissions, - ); + ( + permissions: AccessPermission = null, + extraUserPermissions: ExtraUserPermissions = { isAdministrator: null }, + ) => + (WrappedComponent: React.FC) => + (props: any) => { + const [userPermissionRequirements] = useUserPermissionsRequirements( + typeof permissions === 'function' ? permissions(props) : permissions, + ); const [_userPermissionIsAdminRequirements] = useUserPermissionsIsAdminRequirements(); const userPermissionIsAdminRequirements = - othersPermissions?.isAdmininistrator + extraUserPermissions?.isAdministrator ? _userPermissionIsAdminRequirements : null; @@ -44,9 +72,14 @@ const withUserAuthorizationPromptChanged = }; export const withUserAuthorizationPrompt = - (permissions = null, othersPermissions = { isAdmininistrator: null }) => - WrappedComponent => + ( + permissions: AccessPermission = null, + extraPermissions: ExtraUserPermissions = { isAdministrator: null }, + ) => + (WrappedComponent: React.FC) => compose( withUserLogged, - withUserAuthorizationPromptChanged(permissions, othersPermissions), + withSelectedServerAPI, + withServerAPIAvailable, + withUserAuthorizationPromptChanged(permissions, extraPermissions), )(WrappedComponent); diff --git a/plugins/main/public/components/common/hooks/index.ts b/plugins/main/public/components/common/hooks/index.ts index 327b2d6f0f..4636d96b6d 100644 --- a/plugins/main/public/components/common/hooks/index.ts +++ b/plugins/main/public/components/common/hooks/index.ts @@ -27,3 +27,5 @@ export { useValueSuggestion, IValueSuggestion } from './use-value-suggestion'; export * from './use-state-storage'; export * from './use-router-search'; export * from './use-effect-ensure-mounted'; +export * from './use-selected-server-api'; +export * from './use-server-api-available'; diff --git a/plugins/main/public/components/common/hooks/use-selected-server-api.ts b/plugins/main/public/components/common/hooks/use-selected-server-api.ts new file mode 100644 index 0000000000..0872ef58a6 --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-selected-server-api.ts @@ -0,0 +1,11 @@ +import { AppState } from '../../../react-services'; +import useObservable from 'react-use/lib/useObservable'; + +export const useSelectedServerApi = () => { + const selectedAPI = useObservable( + AppState.selectedServerAPIChanged$, + AppState.selectedServerAPI$.getValue(), + ); + + return { selectedAPI }; +}; diff --git a/plugins/main/public/components/common/hooks/use-server-api-available.ts b/plugins/main/public/components/common/hooks/use-server-api-available.ts new file mode 100644 index 0000000000..d355c218ce --- /dev/null +++ b/plugins/main/public/components/common/hooks/use-server-api-available.ts @@ -0,0 +1,11 @@ +import { WzRequest } from '../../../react-services'; +import useObservable from 'react-use/lib/useObservable'; + +export const useServerApiAvailable = () => { + const isAvailable = useObservable( + WzRequest.serverAPIAvailableChanged$, + WzRequest.serverAPIAvailable$.getValue(), + ); + + return { isAvailable }; +}; diff --git a/plugins/main/public/components/common/hooks/use_async_action_run_on_start.ts b/plugins/main/public/components/common/hooks/use_async_action_run_on_start.ts index 44901c59e9..4df41eb6de 100644 --- a/plugins/main/public/components/common/hooks/use_async_action_run_on_start.ts +++ b/plugins/main/public/components/common/hooks/use_async_action_run_on_start.ts @@ -1,12 +1,12 @@ import { useCallback, useState, useEffect } from 'react'; -type useAsyncActionRunOnStartAction = (...any) => T; +type useAsyncActionRunOnStartAction = (...params: any[]) => Promise; type useAsyncActionRunOnStartDependencies = any[]; type useAsyncActionRunOnStartDependenciesReturns = { data: T | null; - error: any; + error: Error | null; running: boolean; - run: Promise; + run: () => Promise; }; /** @@ -23,10 +23,10 @@ export function useAsyncActionRunOnStart( { refreshDataOnPreRun = true } = { refreshDataOnPreRun: true }, ): useAsyncActionRunOnStartDependenciesReturns { const [running, setRunning] = useState(true); - const [data, setData] = useState(null); - const [error, setError] = useState(null); + const [data, setData] = useState(null); + const [error, setError] = useState(null); - const run: Promise = useCallback(async () => { + const run = useCallback(async () => { try { setRunning(true); setError(null); @@ -36,7 +36,7 @@ export function useAsyncActionRunOnStart( const data = await action(...dependencies); setData(data); } catch (error) { - setError(error); + setError(error as Error); } finally { setRunning(false); } diff --git a/plugins/main/public/components/common/search-bar/use-search-bar.test.ts b/plugins/main/public/components/common/search-bar/use-search-bar.test.ts index 4678fb01dd..f498ee4699 100644 --- a/plugins/main/public/components/common/search-bar/use-search-bar.test.ts +++ b/plugins/main/public/components/common/search-bar/use-search-bar.test.ts @@ -28,6 +28,11 @@ jest.mock('../../../kibana-services', () => { getUiSettings: jest.fn().mockImplementation(() => ({ get: () => true, })), + getCookies: jest.fn(() => { + return { + get: () => 'test', + }; + }), }; }); diff --git a/plugins/main/public/components/common/tables/components/export-table-csv.test.tsx b/plugins/main/public/components/common/tables/components/export-table-csv.test.tsx index 2d18457bc8..06c3dc52be 100644 --- a/plugins/main/public/components/common/tables/components/export-table-csv.test.tsx +++ b/plugins/main/public/components/common/tables/components/export-table-csv.test.tsx @@ -22,6 +22,11 @@ jest.mock('../../../../kibana-services', () => ({ prepend: str => str, }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); jest.mock('../../../../react-services/common-services', () => ({ diff --git a/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx b/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx index 81045996e1..0070631d55 100644 --- a/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx +++ b/plugins/main/public/components/common/tables/table-with-search-bar.test.tsx @@ -29,6 +29,11 @@ jest.mock('../../../kibana-services', () => ({ prepend: str => str, }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); jest.mock('../../../react-services/common-services', () => ({ diff --git a/plugins/main/public/components/common/tables/table-wz-api.test.tsx b/plugins/main/public/components/common/tables/table-wz-api.test.tsx index 00715188d1..f3f3b6e82c 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.test.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.test.tsx @@ -20,6 +20,7 @@ import { useAppConfig, useStateStorage } from '../hooks'; jest.mock('../hooks', () => ({ useAppConfig: jest.fn(), useStateStorage: jest.fn(), + useEffectEnsureComponentMounted: jest.fn(), })); jest.mock('../../../kibana-services', () => ({ @@ -28,6 +29,11 @@ jest.mock('../../../kibana-services', () => ({ prepend: str => str, }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); jest.mock('../../../react-services/common-services', () => ({ diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index 93b8641251..19f13e8b44 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -26,7 +26,11 @@ import { TableWithSearchBar } from './table-with-search-bar'; import { TableDefault } from './table-default'; import { WzRequest } from '../../../react-services/wz-request'; import { ExportTableCsv } from './components/export-table-csv'; -import { useStateStorage, useAppConfig } from '../hooks'; +import { + useStateStorage, + useAppConfig, + useEffectEnsureComponentMounted, +} from '../hooks'; import { formatUINumber } from '../../../react-services/format-number'; /** @@ -192,7 +196,7 @@ export function TableWzAPI({ } }; - useEffect(() => { + useEffectEnsureComponentMounted(() => { if (rest.reload) { triggerReload(); } diff --git a/plugins/main/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx b/plugins/main/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx index beda650950..51c13c9b7c 100644 --- a/plugins/main/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx +++ b/plugins/main/public/components/common/welcome/components/fim_events_table/fim_events_table.tsx @@ -88,7 +88,7 @@ export function useTimeFilter() { const FimTableDataSource = withDataSourceFetch({ DataSource: FIMDataSource, DataSourceRepositoryCreator: AlertsDataSourceRepository, - mapRequestParams: ({ agent, dataSource, dependencies }) => { + mapRequestParams: ({ dataSource, dependencies }) => { const [_, timeFilter, sort] = dependencies; const sortSearch = [ @@ -107,7 +107,12 @@ const FimTableDataSource = withDataSourceFetch({ }, }; }, - mapFetchActionDependencies: ({ timeFilter, sort }) => [timeFilter, sort], + mapFetchActionDependencies: ({ timeFilter, sort }) => [ + timeFilter, + sort, + /* Changing the agent causes the fetchFilters change, and the HOC manage this case so it is not + requried adding the agent to the dependencies */ + ], mapResponse: response => { return { total: response?.hits?.total, items: response?.hits?.hits }; }, diff --git a/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx b/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx index cab7c1822a..d68b0dd100 100644 --- a/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx +++ b/plugins/main/public/components/common/welcome/components/mitre_top/mitre-top.tsx @@ -63,7 +63,12 @@ const MitreTopTacticsTactics = compose( }; }, mapFetchActionDependencies(props) { - return [props.agentId, props.timeFilter]; + return [ + props.timeFilter, + /* Changing the agent causes the fetchFilters change, and the HOC manage this case so it is not + requried adding the agent to the dependencies */ + , + ]; }, mapResponse(response) { return response?.aggregations?.tactics?.buckets; @@ -160,7 +165,12 @@ const MitreTopTacticsTechniquesBody = compose( }; }, mapFetchActionDependencies(props) { - return [props.agentId, props.timeFilter, props.selectedTactic]; + return [ + props.timeFilter, + props.selectedTactic, + /* Changing the agent causes the fetchFilters change, and the HOC manage this case so it is not + requried adding the agent to the dependencies */ + ]; }, mapResponse(response, props) { return []; diff --git a/plugins/main/public/components/common/welcome/components/sca_scan/sca_scan.tsx b/plugins/main/public/components/common/welcome/components/sca_scan/sca_scan.tsx index e5923c8436..189623d135 100644 --- a/plugins/main/public/components/common/welcome/components/sca_scan/sca_scan.tsx +++ b/plugins/main/public/components/common/welcome/components/sca_scan/sca_scan.tsx @@ -253,9 +253,6 @@ const ScaScanBody = compose( ), }; }, - mapFetchActionDependencies(props) { - return [props.agent, props.pageIndex, props.pageSize]; - }, FetchingDataComponent: ScaScanFetchingData, }), withGuard(({ dataSourceAction }) => { diff --git a/plugins/main/public/components/common/welcome/components/top_packages_table/top_packages_table.tsx b/plugins/main/public/components/common/welcome/components/top_packages_table/top_packages_table.tsx index b432cd1187..0ba6ced9b9 100644 --- a/plugins/main/public/components/common/welcome/components/top_packages_table/top_packages_table.tsx +++ b/plugins/main/public/components/common/welcome/components/top_packages_table/top_packages_table.tsx @@ -12,17 +12,16 @@ * Find more information about this on the LICENSE file. */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { - EuiBasicTable, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiFlexGroup, + EuiInMemoryTable, } from '@elastic/eui'; // @ts-ignore -import { getDataPlugin } from '../../../../../kibana-services'; import { vulnerabilityDetection } from '../../../../../utils/applications'; import { PatternDataSourceFilterManager, @@ -31,11 +30,6 @@ import { import { WzLink } from '../../../../../components/wz-link/wz-link'; export function VulsTopPackageTable({ agentId, items, indexPatternId }) { - const [sort, setSort] = useState({ - field: 'doc_count', - direction: 'desc', - }); - const columns = [ { field: 'key', @@ -78,15 +72,12 @@ export function VulsTopPackageTable({ agentId, items, indexPatternId }) { - setSort(e.sort)} - itemId='top-packages-table' - noItemsMessage='No recent events' - /> + sorting={true} + noItemsMessage='No packages found' + > ); } diff --git a/plugins/main/public/components/common/welcome/components/vuls_panel/vuls_welcome_panel.tsx b/plugins/main/public/components/common/welcome/components/vuls_panel/vuls_welcome_panel.tsx index e4f74ef1cd..6b9803f678 100644 --- a/plugins/main/public/components/common/welcome/components/vuls_panel/vuls_welcome_panel.tsx +++ b/plugins/main/public/components/common/welcome/components/vuls_panel/vuls_welcome_panel.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { EuiPanel, EuiFlexGroup, @@ -8,14 +9,10 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import React, { Fragment, useEffect, useState } from 'react'; import { VulsTopPackageTable } from '../top_packages_table'; import VulsSeverityStat from '../vuls_severity_stat/vuls_severity_stat'; import { PatternDataSourceFilterManager, - PatternDataSource, - tParsedIndexPattern, - useDataSource, FILTER_OPERATOR, VulnerabilitiesDataSourceRepository, VulnerabilitiesDataSource, @@ -27,26 +24,53 @@ import { vulnerabilityDetection } from '../../../../../utils/applications'; import NavigationService from '../../../../../react-services/navigation-service'; import { WzLink } from '../../../../../components/wz-link/wz-link'; import { - withDataSourceInitiated, - withDataSourceLoading, + withDataSourceFetch, withErrorBoundary, } from '../../../../common/hocs'; import { compose } from 'redux'; import { withVulnerabilitiesStateDataSource } from '../../../../../components/overview/vulnerabilities/common/hocs/validate-vulnerabilities-states-index-pattern'; import { formatUINumber } from '../../../../../react-services/format-number'; -import { LoadingSearchbarProgress } from '../../../loading-searchbar-progress/loading-searchbar-progress'; -const VulsPanelContent = compose( - withDataSourceLoading({ - isLoadingNameProp: 'isDataSourceLoading', - LoadingComponent: LoadingSearchbarProgress, - }), - withDataSourceInitiated({ - dataSourceNameProp: 'dataSource', - isLoadingNameProp: 'isDataSourceLoading', - dataSourceErrorNameProp: 'error', +const VulsPanelContentInitiation = compose( + withDataSourceFetch({ + DataSource: VulnerabilitiesDataSource, + DataSourceRepositoryCreator: VulnerabilitiesDataSourceRepository, + nameProp: 'dataSource', + mapRequestParams() { + return { + aggs: { + severity: { + terms: { + field: 'vulnerability.severity', + size: 5, + order: { + _count: 'desc', + }, + }, + }, + package: { + terms: { + field: 'package.name', + size: 5, + order: { + _count: 'desc', + }, + }, + }, + }, + }; + }, + mapResponse(response) { + return { + severity: response?.aggregations?.severity?.buckets || [], + package: response?.aggregations?.package?.buckets || [], + }; + }, }), -)(({ agent, topPackagesData, dataSource, severities, severityStats }) => { +)(({ agent, dataSourceAction, dataSource: dataSourceInitiation }) => { + const { dataSource } = dataSourceInitiation; + const { severity: severityStats = [], package: topPackagesData = [] } = + dataSourceAction?.data || {}; const getSeverityValue = severity => { const value = severityStats?.find(v => v.key.toUpperCase() === severity.toUpperCase()) @@ -78,7 +102,7 @@ const VulsPanelContent = compose( @@ -107,83 +131,6 @@ const VulsPanelContent = compose( ); }); -const VulsPanelContentInitiation = ({ agent }) => { - const { - dataSource, - isLoading: isDataSourceLoading, - fetchData, - error, - } = useDataSource({ - DataSource: VulnerabilitiesDataSource, - repository: new VulnerabilitiesDataSourceRepository(), - }); - - const [isLoading, setIsLoading] = useState(true); - const [severityStats, setSeverityStats] = useState(null); - const [topPackagesData, setTopPackagesData] = useState([]); - - const fetchSeverityStatsData = async () => { - const data = await fetchData({ - aggs: { - severity: { - terms: { - field: 'vulnerability.severity', - size: 5, - order: { - _count: 'desc', - }, - }, - }, - }, - }); - setSeverityStats(data.aggregations.severity.buckets); - }; - - const fetchTopPackagesData = async () => { - fetchData({ - aggs: { - package: { - terms: { - field: 'package.name', - size: 5, - order: { - _count: 'desc', - }, - }, - }, - }, - }).then(results => { - setTopPackagesData(results.aggregations.package.buckets); - }); - }; - - useEffect(() => { - if (isDataSourceLoading) { - return; - } - setIsLoading(true); - Promise.all([fetchSeverityStatsData(), fetchTopPackagesData()]) - .catch(error => { - console.error(error); - }) - .finally(() => { - setIsLoading(false); - }); - }, [isDataSourceLoading, agent.id]); - - return ( - - ); -}; - const PanelWithVulnerabilitiesState = compose( withErrorBoundary, withVulnerabilitiesStateDataSource, @@ -191,42 +138,37 @@ const PanelWithVulnerabilitiesState = compose( const VulsPanel = ({ agent }) => { return ( - - - - - - -

Vulnerability Detection

-
-
- - - - - - - -
-
- - -
-
+ + + + + +

Vulnerability Detection

+
+
+ + + + + + + +
+
+ + +
); }; diff --git a/plugins/main/public/components/common/welcome/dashboard/events-count.tsx b/plugins/main/public/components/common/welcome/dashboard/events-count.tsx index 51967951d5..f48192e9f7 100644 --- a/plugins/main/public/components/common/welcome/dashboard/events-count.tsx +++ b/plugins/main/public/components/common/welcome/dashboard/events-count.tsx @@ -4,8 +4,6 @@ import { AlertsDataSourceRepository } from '../../data-source/pattern/alerts/ale import { getPlugins } from '../../../../kibana-services'; import { getDashboardPanels } from './dashboard_panels'; import { ViewMode } from '../../../../../../../src/plugins/embeddable/public'; -import { useDataSource } from '../../data-source/hooks'; -import { PatternDataSource, tParsedIndexPattern } from '../../data-source'; import { EuiPanel, EuiFlexItem, @@ -15,23 +13,33 @@ import { } from '@elastic/eui'; import { useTimeFilter } from '../../hooks'; import { LoadingSearchbarProgress } from '../../loading-searchbar-progress/loading-searchbar-progress'; -import { withDataSourceInitiated, withDataSourceLoading } from '../../hocs'; +import { + withDataSource, + withDataSourceInitiated, + withDataSourceLoading, +} from '../../hocs'; import { compose } from 'redux'; const plugins = getPlugins(); const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; const EventsDashboard = compose( + withDataSource({ + // FIXME: This data source has no the filter related to the server API context + DataSource: AlertsDataSource, + DataSourceRepositoryCreator: AlertsDataSourceRepository, + }), withDataSourceLoading({ - isLoadingNameProp: 'isDataSourceLoading', + isLoadingNameProp: 'dataSource.isLoading', LoadingComponent: LoadingSearchbarProgress, }), withDataSourceInitiated({ - dataSourceNameProp: 'dataSource', - isLoadingNameProp: 'isDataSourceLoading', - dataSourceErrorNameProp: 'error', + dataSourceNameProp: 'dataSource.dataSource', + isLoadingNameProp: 'dataSource.isLoading', + dataSourceErrorNameProp: 'dataSource.error', }), -)(({ dataSource, fetchFilters, timeFilter }) => { +)(({ dataSource: dataSourceInitiation, timeFilter }) => { + const { dataSource, fetchFilters } = dataSourceInitiation; return ( { - const { - dataSource, - fetchFilters, - isLoading: isDataSourceLoading, - error, - } = useDataSource({ - DataSource: AlertsDataSource, - repository: new AlertsDataSourceRepository(), - }); - const { timeFilter } = useTimeFilter(); return ( @@ -83,13 +81,7 @@ export const EventsCount = () => { - + ); diff --git a/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts b/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts deleted file mode 100644 index 1cc3725ac9..0000000000 --- a/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { useGetTotalAgents } from './agents'; -import { getAgentsService } from '../services'; - -jest.mock('../services', () => ({ - getAgentsService: jest.fn(), -})); - -describe('useGetTotalAgents hook', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should fetch initial data without any error', async () => { - (getAgentsService as jest.Mock).mockReturnValue({ - total_affected_items: 3, - }); - - const { result, waitForNextUpdate } = renderHook(() => useGetTotalAgents()); - - expect(result.current.isLoading).toBeTruthy(); - await waitForNextUpdate(); - expect(result.current.totalAgents).toEqual(3); - expect(result.current.isLoading).toBeFalsy(); - }); - - it('should handle error while fetching data', async () => { - const mockErrorMessage = 'Some error occurred'; - (getAgentsService as jest.Mock).mockRejectedValue(mockErrorMessage); - - const { result, waitForNextUpdate } = renderHook(() => useGetTotalAgents()); - - expect(result.current.isLoading).toBeTruthy(); - await waitForNextUpdate(); - expect(result.current.error).toBe(mockErrorMessage); - expect(result.current.isLoading).toBeFalsy(); - }); -}); diff --git a/plugins/main/public/components/endpoints-summary/hooks/agents.ts b/plugins/main/public/components/endpoints-summary/hooks/agents.ts deleted file mode 100644 index 3c383516fa..0000000000 --- a/plugins/main/public/components/endpoints-summary/hooks/agents.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useState, useEffect } from 'react'; -import { getAgentsService } from '../services'; - -export const useGetTotalAgents = (filters?: any) => { - const [totalAgents, setTotalAgents] = useState(); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(); - - const getTotalAgents = async () => { - try { - setIsLoading(true); - const { total_affected_items } = await getAgentsService({ - filters, - limit: 1, - }); - setTotalAgents(total_affected_items); - setError(undefined); - } catch (error: any) { - setError(error); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - getTotalAgents(); - }, []); - - return { - isLoading, - totalAgents, - error, - }; -}; diff --git a/plugins/main/public/components/endpoints-summary/hooks/index.ts b/plugins/main/public/components/endpoints-summary/hooks/index.ts index c0269a5890..53470e1bec 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/index.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/index.ts @@ -1,3 +1,2 @@ -export { useGetTotalAgents } from './agents'; export { useGetGroups } from './groups'; export { useGetUpgradeTasks } from './upgrade-tasks'; diff --git a/plugins/main/public/components/endpoints-summary/index.tsx b/plugins/main/public/components/endpoints-summary/index.tsx index 15c7735a6a..73b62f70fc 100644 --- a/plugins/main/public/components/endpoints-summary/index.tsx +++ b/plugins/main/public/components/endpoints-summary/index.tsx @@ -10,51 +10,64 @@ import { endpointSummary } from '../../utils/applications'; import { withErrorBoundary, withGlobalBreadcrumb, + withGuard, + withGuardAsync, withRouteResolvers, + withUserAuthorizationPrompt, } from '../common/hocs'; import { compose } from 'redux'; import { WzButtonPermissions } from '../common/permissions/button'; -import { UI_LOGGER_LEVELS } from '../../../common/constants'; -import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../react-services/common-services'; -import { useGetTotalAgents } from './hooks'; import { nestedResolve } from '../../services/resolves'; import NavigationService from '../../react-services/navigation-service'; +import { getAgentsService } from './services'; + +async function fetchTotalAgents() { + const { total_affected_items } = await getAgentsService({ + filters: 'id!=000', + limit: 1, + }); + + return { totalAgents: total_affected_items }; +} export const MainEndpointsSummary = compose( withErrorBoundary, withRouteResolvers({ nestedResolve }), withGlobalBreadcrumb([{ text: endpointSummary.breadcrumbLabel }]), -)(() => { - const { isLoading, totalAgents, error } = useGetTotalAgents('id!=000'); - - if (error) { - const options = { - context: `MainEndpointsSummary.getTotalAgents`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, - store: true, - error: { - error, - message: error.message || error, - title: `Could not get agents summary`, - }, - }; - getErrorOrchestrator().handleError(options); - } + withUserAuthorizationPrompt([ + [ + { action: 'agent:read', resource: 'agent:id:*' }, + { action: 'agent:read', resource: 'agent:group:*' }, + ], + ]), + withGuardAsync( + async () => { + try { + const response = fetchTotalAgents(); - if (isLoading) { - return ( + return { + ok: false, + data: response, + }; + } catch (error) { + return { + ok: false, + data: {}, + }; + } + }, + () => null, + () => ( - ); - } - - if (totalAgents === 0) { - return ( + ), + ), + withGuard( + ({ totalAgents }) => totalAgents === 0, + () => ( No agents were added to the manager} @@ -76,9 +89,9 @@ export const MainEndpointsSummary = compose( } /> - ); - } - + ), + ), +)(() => { return ( diff --git a/plugins/main/public/components/endpoints-summary/table/upgrade-task-details-modal.tsx b/plugins/main/public/components/endpoints-summary/table/upgrade-task-details-modal.tsx index 63f16fd205..73961b1487 100644 --- a/plugins/main/public/components/endpoints-summary/table/upgrade-task-details-modal.tsx +++ b/plugins/main/public/components/endpoints-summary/table/upgrade-task-details-modal.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { TableWzAPI } from '../../common/tables'; import { formatUIDate } from '../../../react-services/time-service'; import { @@ -28,13 +28,14 @@ interface AgentUpgradesTaskDetailsModalProps { export const AgentUpgradesTaskDetailsModal = ({ onClose, }: AgentUpgradesTaskDetailsModalProps) => { - const datetime = new Date(); - datetime.setMinutes(datetime.getMinutes() - 60); - const formattedDate = datetime.toISOString(); - - const defaultFilters = { - q: `last_update_time>${formattedDate}`, - }; + const defaultFilters = useMemo(() => { + const datetime = new Date(); + datetime.setMinutes(datetime.getMinutes() - 60); + const formattedDate = datetime.toISOString(); + return { + q: `last_update_time>${formattedDate}`, + }; + }, []); const handleOnCloseModal = () => onClose(); diff --git a/plugins/main/public/components/overview/azure/dashboards/dashboard.tsx b/plugins/main/public/components/overview/azure/dashboards/dashboard.tsx index 388e5b84b1..4b5b1b8fc1 100644 --- a/plugins/main/public/components/overview/azure/dashboards/dashboard.tsx +++ b/plugins/main/public/components/overview/azure/dashboards/dashboard.tsx @@ -1,153 +1,25 @@ -import React, { useEffect, useState } from 'react'; -import useSearchBar from '../../../common/search-bar/use-search-bar'; -import { compose } from 'redux'; -import { getPlugins } from '../../../../kibana-services'; -import { ViewMode } from '../../../../../../../src/plugins/embeddable/public'; +import React from 'react'; import { getDashboardPanels } from './dashboard_panels'; -import { I18nProvider } from '@osd/i18n/react'; -import { SearchResponse } from '../../../../../../../src/core/server'; -import './styles.scss'; -import { withErrorBoundary } from '../../../common/hocs'; -import { IndexPattern } from '../../../../../../../src/plugins/data/common'; -import { SampleDataWarning } from '../../../visualize/components'; -import { - ErrorFactory, - ErrorHandler, - HttpError, -} from '../../../../react-services/error-management'; import { AlertsDataSourceRepository, - PatternDataSource, - tParsedIndexPattern, - useDataSource, AzureDataSource, } from '../../../common/data-source'; -import { DiscoverNoResults } from '../../../common/no-results/no-results'; -import { LoadingSearchbarProgress } from '../../../common/loading-searchbar-progress/loading-searchbar-progress'; -import { useReportingCommunicateSearchContext } from '../../../common/hooks/use-reporting-communicate-search-context'; -import { WzSearchBar } from '../../../common/search-bar'; -import { WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY } from '../../../../../common/constants'; - -const plugins = getPlugins(); - -const DashboardByRenderer = plugins.dashboard.DashboardContainerByValueRenderer; - -const DashboardAzureComponents: React.FC = ({}) => { - const AlertsRepository = new AlertsDataSourceRepository(); - const { - filters, - dataSource, - fetchFilters, - fixedFilters, - isLoading: isDataSourceLoading, - fetchData, - setFilters, - } = useDataSource({ - DataSource: AzureDataSource, - repository: AlertsRepository, - }); - - const [results, setResults] = useState({} as SearchResponse); - const { searchBarProps, fingerprint, autoRefreshFingerprint } = useSearchBar({ - indexPattern: dataSource?.indexPattern as IndexPattern, - filters, - setFilters, - }); - - const { query, dateRangeFrom, dateRangeTo } = searchBarProps; - - useReportingCommunicateSearchContext({ - isSearching: isDataSourceLoading, - totalResults: results?.hits?.total ?? 0, - indexPattern: dataSource?.indexPattern, - filters: fetchFilters, - query: query, - time: { from: dateRangeFrom, to: dateRangeTo }, - }); - - useEffect(() => { - if (isDataSourceLoading) { - return; - } - fetchData({ - query, - dateRange: { from: dateRangeFrom, to: dateRangeTo }, - }) - .then(results => setResults(results)) - .catch(error => { - const searchError = ErrorFactory.create(HttpError, { - error, - message: 'Error fetching data', - }); - ErrorHandler.handleError(searchError); - }); - }, [ - JSON.stringify(fetchFilters), - JSON.stringify(query), - dateRangeFrom, - dateRangeTo, - fingerprint, - autoRefreshFingerprint, - ]); - return ( - <> - - {isDataSourceLoading && !dataSource ? ( - - ) : ( - <> - - {dataSource && results?.hits?.total === 0 ? ( - - ) : null} -
0 - ? '' - : 'wz-no-display' - } - > - -
- -
-
- - )} -
- - ); -}; - -export const DashboardAzure = compose(withErrorBoundary)( - DashboardAzureComponents, -); +import { WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY } from '../../../../../common/constants'; +import { createDashboard } from '../../../common/dashboards'; + +export const DashboardAzure = createDashboard({ + DataSource: AzureDataSource, + DataSourceRepositoryCreator: AlertsDataSourceRepository, + getDashboardPanels: [ + { + getDashboardPanels: getDashboardPanels, + id: 'azure-dashboard-tab', + title: 'Azure dashboard', + description: 'Dashboard of the Azure', + hidePanelTitles: false, + className: 'wz-dashboard-hide-tables-pagination-export-csv-controls', + }, + ], + sampleDataWarningCategories: [WAZUH_SAMPLE_ALERTS_CATEGORY_SECURITY], +}); diff --git a/plugins/main/public/components/overview/azure/dashboards/styles.scss b/plugins/main/public/components/overview/azure/dashboards/styles.scss deleted file mode 100644 index 140d6f90c7..0000000000 --- a/plugins/main/public/components/overview/azure/dashboards/styles.scss +++ /dev/null @@ -1,10 +0,0 @@ -.azure-dashboard-responsive { - @media (max-width: 767px) { - .react-grid-layout { - height: auto !important; - } - .dshLayout-isMaximizedPanel { - height: calc(100vh - 44px) !important; - } - } -} diff --git a/plugins/main/public/components/overview/docker/dashboards/dashboard.tsx b/plugins/main/public/components/overview/docker/dashboards/dashboard.tsx index 08da1d7d9b..cece63ee9a 100644 --- a/plugins/main/public/components/overview/docker/dashboards/dashboard.tsx +++ b/plugins/main/public/components/overview/docker/dashboards/dashboard.tsx @@ -7,7 +7,7 @@ import { import { WAZUH_SAMPLE_ALERTS_CATEGORY_THREAT_DETECTION } from '../../../../../common/constants'; import { createDashboard } from '../../../common/dashboards'; -export const DashboardAWS = createDashboard({ +export const DashboardDocker = createDashboard({ DataSource: DockerDataSource, DataSourceRepositoryCreator: AlertsDataSourceRepository, getDashboardPanels: [ diff --git a/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx b/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx index 48fbe62b41..fb355d232b 100644 --- a/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx +++ b/plugins/main/public/components/overview/mitre/intelligence/intelligence.test.tsx @@ -17,6 +17,7 @@ import { render } from 'enzyme'; import { ModuleMitreAttackIntelligence } from './intelligence'; import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +import { getCookies } from '../../../../kibana-services'; // the jest.mock of @osd/monaco is added due to a problem transcribing the files to run the tests. // https://github.com/wazuh/wazuh-dashboard-plugins/pull/6921#issuecomment-2298289550 @@ -55,6 +56,28 @@ jest.mock('../../../../react-services', () => ({ }), })); +jest.mock('../../../common/hooks/use-selected-server-api', () => ({ + useSelectedServerApi: () => ({ selectedAPI: true }), +})); + +jest.mock('../../../common/hooks/use-server-api-available', () => ({ + useServerApiAvailable: () => ({ isAvailable: true }), +})); + +jest.mock('../../../../kibana-services'); + +jest.mock('../../../../react-services/app-state', () => ({ + AppState: { + getCurrentPattern: () => 'test', + }, +})); + +getCookies.mockImplementation(() => { + return { + get: () => 'test', + }; +}); + // added to remove useLayoutEffect warning jest.mock('react', () => ({ ...(jest.requireActual('react') as object), diff --git a/plugins/main/public/components/overview/overview.tsx b/plugins/main/public/components/overview/overview.tsx index 455182fbae..c6430231e5 100644 --- a/plugins/main/public/components/overview/overview.tsx +++ b/plugins/main/public/components/overview/overview.tsx @@ -23,7 +23,7 @@ import { import { PinnedAgentManager } from '../wz-agent-selector/wz-agent-selector-service'; import { withRouteResolvers } from '../common/hocs'; import { nestedResolve } from '../../services/resolves'; -import { useRouterSearch } from '../common/hooks'; +import { useAsyncAction, useRouterSearch } from '../common/hooks'; import NavigationService from '../../react-services/navigation-service'; import { cloneDeep } from 'lodash'; import { migrateLegacyQuery } from '../../utils/migrate_legacy_query'; @@ -31,8 +31,14 @@ import { migrateLegacyQuery } from '../../utils/migrate_legacy_query'; export const Overview: React.FC = withRouteResolvers({ nestedResolve, })(() => { - const [agentsCounts, setAgentsCounts] = useState({}); - const [isAgentsLoading, setIsAgentsLoading] = useState(true); + const agentCountAction = useAsyncAction(async () => { + const { + data: { + data: { connection: data }, + }, + } = await WzRequest.apiReq('GET', '/agents/summary/status', {}); + return data; + }, []); const { tab = 'welcome', tabView = 'dashboard', agentId } = useRouterSearch(); const navigationService = NavigationService.getInstance(); const pinnedAgentManager = new PinnedAgentManager(); @@ -40,7 +46,7 @@ export const Overview: React.FC = withRouteResolvers({ useEffect(() => { pinnedAgentManager.syncPinnedAgentSources(); if (tab === 'welcome' || tab === undefined) { - getSummary(); + agentCountAction.run(); } // TODO: Analyze if this behavior should be added to Navigationsservice @@ -113,23 +119,6 @@ export const Overview: React.FC = withRouteResolvers({ }; }, [agentId]); - /** - * This fetch de agents summary - */ - const getSummary = async () => { - try { - const { - data: { - data: { connection: data }, - }, - } = await WzRequest.apiReq('GET', '/agents/summary/status', {}); - setAgentsCounts(data); - setIsAgentsLoading(false); - } catch (error) { - return Promise.reject(error); - } - }; - function switchTab(newTab: string) { navigationService.switchTab(newTab); } @@ -138,6 +127,8 @@ export const Overview: React.FC = withRouteResolvers({ navigationService.switchSubTab(subTab); }; + const agentsCount = agentCountAction.data || {}; + return ( <> {tab && tab !== 'welcome' && ( @@ -161,10 +152,14 @@ export const Overview: React.FC = withRouteResolvers({ - + - + diff --git a/plugins/main/public/components/settings/about/__snapshots__/appInfo.test.tsx.snap b/plugins/main/public/components/settings/about/__snapshots__/appInfo.test.tsx.snap index bed7b6d410..6be03acc9d 100644 --- a/plugins/main/public/components/settings/about/__snapshots__/appInfo.test.tsx.snap +++ b/plugins/main/public/components/settings/about/__snapshots__/appInfo.test.tsx.snap @@ -20,10 +20,9 @@ exports[`SettingsAboutAppInfo component should render version, revision, install
- App version: - + App version: - 4.8.0 + v4.8.0
diff --git a/plugins/main/public/components/settings/about/__snapshots__/index.test.tsx.snap b/plugins/main/public/components/settings/about/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 291c8deb48..0000000000 --- a/plugins/main/public/components/settings/about/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`SettingsAbout component should render about page 1`] = ` -
-
-
-
- App info -
-
-
- General info -
-
-
-
-`; diff --git a/plugins/main/public/components/settings/about/appInfo.test.tsx b/plugins/main/public/components/settings/about/appInfo.test.tsx index 71f590152b..bdecbbcf97 100644 --- a/plugins/main/public/components/settings/about/appInfo.test.tsx +++ b/plugins/main/public/components/settings/about/appInfo.test.tsx @@ -10,16 +10,12 @@ jest.mock('../../../react-services/time-service', () => ({ describe('SettingsAboutAppInfo component', () => { test('should render version, revision, install date and ApisUpdateStatus component', () => { const { container, getByText } = render( - , + , ); expect(container).toMatchSnapshot(); expect(getByText('App version:')).toBeInTheDocument(); - expect(getByText('4.8.0')).toBeInTheDocument(); + expect(getByText('v4.8.0')).toBeInTheDocument(); }); }); diff --git a/plugins/main/public/components/settings/about/appInfo.tsx b/plugins/main/public/components/settings/about/appInfo.tsx index c296fcabd1..9127db484f 100644 --- a/plugins/main/public/components/settings/about/appInfo.tsx +++ b/plugins/main/public/components/settings/about/appInfo.tsx @@ -1,14 +1,7 @@ -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import React from 'react'; -import { AppInfo } from '../types'; - -interface SettingsAboutAppInfoProps { - appInfo?: AppInfo; -} +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -export const SettingsAboutAppInfo = ({ - appInfo, -}: SettingsAboutAppInfoProps) => { +export const SettingsAboutAppInfo = ({ appInfo }: { appInfo: string }) => { return ( - App version:{' '} - {appInfo?.['app-version'] ? appInfo['app-version'] : ''} + App version: {appInfo} diff --git a/plugins/main/public/components/settings/about/index.test.tsx b/plugins/main/public/components/settings/about/index.test.tsx deleted file mode 100644 index f618e8899f..0000000000 --- a/plugins/main/public/components/settings/about/index.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { SettingsAbout } from '.'; - -jest.mock('./appInfo', () => ({ - SettingsAboutAppInfo: jest.fn().mockReturnValue(
App info
), -})); - -jest.mock('./generalInfo', () => ({ - SettingsAboutGeneralInfo: jest.fn().mockReturnValue(
General info
), -})); - -describe('SettingsAbout component', () => { - test('should render about page', () => { - const { container, getByText } = render( - , - ); - - expect(container).toMatchSnapshot(); - - const appInfo = getByText('App info'); - expect(appInfo).toBeInTheDocument(); - - const generalInfo = getByText('General info'); - expect(generalInfo).toBeInTheDocument(); - }); -}); diff --git a/plugins/main/public/components/settings/about/index.tsx b/plugins/main/public/components/settings/about/index.tsx index 8d19ed1a73..2f8998e305 100644 --- a/plugins/main/public/components/settings/about/index.tsx +++ b/plugins/main/public/components/settings/about/index.tsx @@ -1,22 +1,63 @@ import React from 'react'; -import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiSpacer, EuiProgress } from '@elastic/eui'; import { SettingsAboutAppInfo } from './appInfo'; import { SettingsAboutGeneralInfo } from './generalInfo'; -import { PLUGIN_APP_NAME } from '../../../../common/constants'; -import { AppInfo } from '../types'; +import { + PLUGIN_APP_NAME, + UI_LOGGER_LEVELS, +} from '../../../../common/constants'; +import { useAsyncActionRunOnStart } from '../../common/hooks'; +import { GenericRequest } from '../../../react-services'; +import { getErrorOrchestrator } from '../../../react-services/common-services'; +import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; -interface SettingsAboutProps { - appInfo?: AppInfo; +async function fetchAboutData() { + try { + const data = await GenericRequest.request('GET', '/api/setup'); + const response = data.data.data; + return response['app-version'] as string; + } catch (error) { + const options = { + context: `${SettingsAbout.name}.getAppInfo`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + getErrorOrchestrator().handleError(options); + throw error; + } } -export const SettingsAbout = (props: SettingsAboutProps) => { - const { appInfo } = props; +export const SettingsAbout: React.FC = () => { + const { data, running, error } = useAsyncActionRunOnStart(fetchAboutData); + + let headerAbout; + if (error) { + headerAbout = null; + } else if (running) { + headerAbout = ( + <> + + + + ); + } else { + headerAbout = ( + <> + {data && } + + + ); + } return ( - - + {headerAbout} diff --git a/plugins/main/public/components/settings/configuration/configuration.tsx b/plugins/main/public/components/settings/configuration/configuration.tsx index 2e75ef73b3..3f3db39797 100644 --- a/plugins/main/public/components/settings/configuration/configuration.tsx +++ b/plugins/main/public/components/settings/configuration/configuration.tsx @@ -423,5 +423,5 @@ const WzConfigurationSettingsProvider = props => { }; export const WzConfigurationSettings = compose( withErrorBoundary, - withUserAuthorizationPrompt(null, { isAdmininistrator: true }), + withUserAuthorizationPrompt(null, { isAdministrator: true }), )(WzConfigurationSettingsProvider); diff --git a/plugins/main/public/components/settings/settings.tsx b/plugins/main/public/components/settings/settings.tsx index 6f24fc083e..112b6e9b20 100644 --- a/plugins/main/public/components/settings/settings.tsx +++ b/plugins/main/public/components/settings/settings.tsx @@ -10,28 +10,16 @@ * Find more information about this on the LICENSE file. */ import React from 'react'; -import { EuiProgress, EuiTabs, EuiTab } from '@elastic/eui'; -import { AppState } from '../../react-services/app-state'; -import { GenericRequest } from '../../react-services/generic-request'; -import { WzMisc } from '../../factories/misc'; -import { ApiCheck } from '../../react-services/wz-api-check'; -import { SavedObject } from '../../react-services/saved-objects'; -import { ErrorHandler } from '../../react-services/error-handler'; import { updateGlobalBreadcrumb } from '../../redux/actions/globalBreadcrumbActions'; import { UI_LOGGER_LEVELS } from '../../../common/constants'; import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; import { getErrorOrchestrator } from '../../react-services/common-services'; -import { getAssetURL } from '../../utils/assets'; -import { getHttp, getWzCurrentAppID } from '../../kibana-services'; +import { getWzCurrentAppID } from '../../kibana-services'; import { ApiTable } from '../settings/api/api-table'; import { WzConfigurationSettings } from '../settings/configuration'; import { WzSampleDataWrapper } from '../add-modules-data/WzSampleDataWrapper'; import { SettingsAbout } from '../settings/about/index'; -import { - Applications, - serverApis, - appSettings, -} from '../../utils/applications'; +import { Applications, serverApis } from '../../utils/applications'; import { compose } from 'redux'; import { withErrorBoundary, withRouteResolvers } from '../common/hocs'; import { connect } from 'react-redux'; @@ -63,44 +51,17 @@ export const Settings = compose( return ; }); -class SettingsComponent extends React.Component { - state: { - tabs: { id: string; name: string }[] | null; - load: boolean; - currentApiEntryIndex; - indexPatterns; - apiEntries; - }; - wzMisc: WzMisc; - tabsConfiguration: { id: string; name: string }[]; - apiIsDown; - googleGroupsSVG; - currentDefault; +interface SettingsComponentProps { + tab: string; + configurationUIEditable: boolean; + updateGlobalBreadcrumb: (breadcrumb: { text: string }[]) => void; +} + +class SettingsComponent extends React.Component { appInfo: AppInfo | undefined; - constructor(props) { + constructor(props: SettingsComponentProps) { super(props); - - this.wzMisc = new WzMisc(); - - if (this.wzMisc.getWizard()) { - this.wzMisc.setWizard(false); - } - this.apiIsDown = this.wzMisc.getApiIsDown(); - this.state = { - currentApiEntryIndex: false, - tabs: null, - load: true, - indexPatterns: [], - apiEntries: [], - }; - - this.googleGroupsSVG = getHttp().basePath.prepend( - getAssetURL('images/icons/google_groups.svg'), - ); - this.tabsConfiguration = [ - { id: configurationTabID, name: 'Configuration' }, - ]; } async componentDidMount(): Promise { @@ -120,11 +81,6 @@ class SettingsComponent extends React.Component { // Set component props this.setComponentProps(urlTab); - - // Loading data - await this.getSettings(); - - await this.getAppInfo(); } catch (error) { const options = { context: `${Settings.name}.onInit`, @@ -151,195 +107,12 @@ class SettingsComponent extends React.Component { const isConfigurationUIEditable = this.isConfigurationUIEditable(); if (currentTab === configurationTabID && !isConfigurationUIEditable) { // Change the inaccessible configuration to another accessible - NavigationService.getInstance().replace( - `/settings?tab=${ - this.tabsConfiguration.find(({ id }) => id !== configurationTabID)!.id - }`, - ); - } - this.setState({ - tabs: - getWzCurrentAppID() === appSettings.id - ? // WORKAROUND: This avoids the configuration tab is displayed - /* TODO: The other view was removed, so this will leave a blank page. Migrating the app settings - to the opensearch_dashboards.yml that cause the settings are not managed by the Wazuh plugin, - should cause the App settings app should be removed. - */ - this.tabsConfiguration.filter(({ id }) => - !isConfigurationUIEditable ? id !== configurationTabID : true, - ) - : null, - }); - } - - // Get current API index - getCurrentAPIIndex() { - if (this.state.apiEntries.length) { - const idx = this.state.apiEntries - .map(entry => entry.id) - .indexOf(this.currentDefault); - this.setState({ currentApiEntryIndex: idx }); - } - } - - /** - * Returns the index of the API in the entries array - * @param {Object} api - */ - getApiIndex(api) { - return this.state.apiEntries.map(entry => entry.id).indexOf(api.id); - } - - // Get settings function - async getSettings() { - try { - try { - this.setState({ - indexPatterns: await SavedObject.getListOfWazuhValidIndexPatterns(), - }); - } catch (error) { - this.wzMisc.setBlankScr('Sorry but no valid index patterns were found'); - NavigationService.getInstance().navigate('/blank-screen'); - return; - } - - await this.getHosts(); - - const currentApi = AppState.getCurrentAPI(); - - if (currentApi) { - const { id } = JSON.parse(currentApi); - this.currentDefault = id; - } - this.getCurrentAPIIndex(); - - // TODO: what is the purpose of this? - if ( - !this.state.currentApiEntryIndex && - this.state.currentApiEntryIndex !== 0 - ) { - return; - } - } catch (error) { - const options = { - context: `${Settings.name}.getSettings`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `${error.name}: Error getting API entries`, - }, - }; - getErrorOrchestrator().handleError(options); - } - return; - } - - // Check manager connectivity - async checkManager(item, isIndex?, silent = false) { - try { - // Get the index of the API in the entries - const index = isIndex ? item : this.getApiIndex(item); - - // Get the Api information - const api = this.state.apiEntries[index]; - const { username, url, port, id } = api; - const tmpData = { - username: username, - url: url, - port: port, - cluster_info: {}, - insecure: 'true', - id: id, - }; - - // Test the connection - const data = await ApiCheck.checkApi(tmpData, true); - tmpData.cluster_info = data?.data; - const { cluster_info } = tmpData; - // Updates the cluster-information in the registry - this.state.apiEntries[index].cluster_info = cluster_info; - this.state.apiEntries[index].status = 'online'; - this.state.apiEntries[index].allow_run_as = data.data.allow_run_as; - this.wzMisc.setApiIsDown(false); - !silent && ErrorHandler.info('Connection success', 'Settings'); - } catch (error) { - this.setState({ load: false }); - if (!silent) { - const options = { - context: `${Settings.name}.checkManager`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - return Promise.reject(error); - } - } - - /** - * Returns Wazuh app info - */ - async getAppInfo() { - try { - const data = await GenericRequest.request('GET', '/api/setup'); - const response = data.data.data; - this.appInfo = { - 'app-version': response['app-version'], - }; - - this.setState({ load: false }); - // TODO: this seems not to be used to display or not the index pattern selector - AppState.setPatternSelector(this.props.configurationIPSelector); - const pattern = AppState.getCurrentPattern(); - - this.getCurrentAPIIndex(); - if ( - (this.state.currentApiEntryIndex || - this.state.currentApiEntryIndex === 0) && - this.state.currentApiEntryIndex >= 0 - ) { - await this.checkManager(this.state.currentApiEntryIndex, true, true); - } - } catch (error) { - AppState.removeNavigation(); - const options = { - context: `${Settings.name}.getAppInfo`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - /** - * Get the API hosts - */ - async getHosts() { - try { - const result = await GenericRequest.request('GET', '/hosts/apis', {}); - const hosts = result.data || []; - this.setState({ - apiEntries: hosts, - }); - return hosts; - } catch (error) { - return Promise.reject(error); + NavigationService.getInstance().replace('/settings?tab=about'); + this.props.updateGlobalBreadcrumb([{ text: 'About' }]); } } - renderView() { + render() { // WORKAROUND: This avoids the configuration view is displayed if ( this.props.tab === configurationTabID && @@ -363,7 +136,7 @@ class SettingsComponent extends React.Component {
- +
@@ -374,41 +147,4 @@ class SettingsComponent extends React.Component { ); } - - render() { - return ( -
- {this.state.load ? ( -
- -
- ) : null} - {/* It must get renderized only in configuration app to show Miscellaneous tab in configuration App */} - {!this.state.load && ( - <> - {!this.apiIsDown && this.state.tabs && ( -
- - {this.state.tabs.map(tab => ( - - NavigationService.getInstance().navigate( - `/settings?tab=${tab.id}`, - ) - } - > - {tab.name} - - ))} - -
- )} - {this.renderView()} - - )} -
- ); - } } diff --git a/plugins/main/public/components/settings/types.ts b/plugins/main/public/components/settings/types.ts deleted file mode 100644 index 1adc6ed447..0000000000 --- a/plugins/main/public/components/settings/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface AppInfo { - 'app-version': string; -} diff --git a/plugins/main/public/components/visualize/components/sample-data-warning.test.js b/plugins/main/public/components/visualize/components/sample-data-warning.test.js index e1b33e7c99..4cb19f1480 100644 --- a/plugins/main/public/components/visualize/components/sample-data-warning.test.js +++ b/plugins/main/public/components/visualize/components/sample-data-warning.test.js @@ -17,7 +17,7 @@ import { getErrorOrchestrator } from '../../../react-services/common-services'; import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../../../common/constants'; import React from 'react'; -import { getCore } from '../../../kibana-services'; +import { getCore, getCookies } from '../../../kibana-services'; const awaitForMyComponent = async wrapper => { await act(async () => { @@ -25,7 +25,11 @@ const awaitForMyComponent = async wrapper => { wrapper.update(); }); }; -jest.mock('../../../react-services'); +jest.mock('../../../react-services', () => ({ + WzRequest: { + genericReq: jest.fn(), + }, +})); jest.mock('../../../react-services/common-services'); jest.mock('../../../kibana-services'); @@ -38,6 +42,14 @@ getCore.mockImplementation(() => ({ }, })); +getCookies.mockImplementation(() => { + console.log('HELLO'); + return { + get: () => 'test', + hello: 'dadsads', + }; +}); + jest.mock('../../../react-services/navigation-service', () => ({ getInstance() { return { diff --git a/plugins/main/public/components/wz-blank-screen/wz-blank-screen.js b/plugins/main/public/components/wz-blank-screen/wz-blank-screen.js deleted file mode 100644 index 5fb1be5c37..0000000000 --- a/plugins/main/public/components/wz-blank-screen/wz-blank-screen.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Wazuh app - React component for build q queries. - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ -import React, { Component } from 'react'; -import { EuiButton, EuiSpacer, EuiLink } from '@elastic/eui'; -import { ErrorComponentPrompt } from '../common/error-boundary-prompt/error-boundary-prompt'; -import { - PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING, - PLUGIN_PLATFORM_URL_GUIDE, - PLUGIN_PLATFORM_URL_GUIDE_TITLE, - UI_LOGGER_LEVELS, -} from '../../../common/constants'; -import { webDocumentationLink } from '../../../common/services/web_documentation'; -import { WzMisc } from '../../factories/misc'; -import { getCore } from '../../kibana-services'; -import { ErrorHandler } from '../../react-services/error-handler'; -import { UI_ERROR_SEVERITIES } from '../../react-services/error-orchestrator/types'; -import { getErrorOrchestrator } from '../../react-services/common-services'; -import { overview } from '../../utils/applications'; -import { RedirectAppLinks } from '../../../../../src/plugins/opensearch_dashboards_react/public'; -import NavigationService from '../../react-services/navigation-service'; - -export class WzBlankScreen extends Component { - constructor(props) { - super(props); - this.state = { - errorToShow: null, - }; - - // Create instance of WzMisc that stores the error - this.wzMisc = new WzMisc(); - } - - componentDidMount() { - const catchedError = this.wzMisc.getBlankScr(); - if (catchedError) { - let parsed = null; - try { - parsed = ErrorHandler.handle(catchedError, '', { silent: true }); - } catch (error) { - const options = { - context: `${WzBlankScreen.name}.componentDidMount`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.UI, - error: { - error: error, - message: error.message || error, - title: error.name, - }, - }; - getErrorOrchestrator().handleError(options); - } - - this.setState({ errorToShow: parsed || catchedError }); - this.wzMisc.setBlankScr(false); - } else { - this.goOverview(); - } - } - - /** - * This navigate to overview - */ - goOverview() { - NavigationService.getInstance().navigateToApp(overview.id); - } - - render() { - if (!this.state.errorToShow) { - return null; - } - return ( - -

- - {PLUGIN_PLATFORM_URL_GUIDE_TITLE} - -
-
- - Installation guide - -

- - - - - Go to {overview.title} - - - - } - /> - ); - } -} diff --git a/plugins/main/public/components/wz-menu/selectors.tsx b/plugins/main/public/components/wz-menu/selectors.tsx new file mode 100644 index 0000000000..58b434e8a6 --- /dev/null +++ b/plugins/main/public/components/wz-menu/selectors.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; + +/** + * Container component for selectors in the menu. + * @param param0 + * @returns + */ +export const SelectorContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; + +/** + * Label component for selectors in the menu. + * @param param0 + * @returns + */ +export const SelectorLabel = ({ + actionError, + showSelectorsInPopover, + children, +}: { + actionError?: string; + showSelectorsInPopover: boolean; + children: React.ReactNode; +}) => { + return ( + <> + +

{children}

+
+ {actionError && ( + + + + )} + + ); +}; + +/** + * Selector component for selectors in the menu. This wraps the form input component. + * @param param0 + * @returns + */ +export const Selector = ({ + children, + showSelectorsInPopover, +}: { + showSelectorsInPopover: boolean; + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/plugins/main/public/components/wz-menu/wz-menu.js b/plugins/main/public/components/wz-menu/wz-menu.js index 1fd08ddb91..5bb52eaf68 100644 --- a/plugins/main/public/components/wz-menu/wz-menu.js +++ b/plugins/main/public/components/wz-menu/wz-menu.js @@ -12,6 +12,7 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, @@ -22,15 +23,9 @@ import { EuiSelect, } from '@elastic/eui'; import { AppState } from '../../react-services/app-state'; -import { PatternHandler } from '../../react-services/pattern-handler'; -import { WazuhConfig } from '../../react-services/wazuh-config'; + import { connect } from 'react-redux'; -import store from '../../redux/store'; -import { - getToasts, - getDataPlugin, - getHeaderActionMenuMounter, -} from '../../kibana-services'; +import { getHeaderActionMenuMounter } from '../../kibana-services'; import { GenericRequest } from '../../react-services/generic-request'; import { ApiCheck } from '../../react-services/wz-api-check'; import { withWindowSize } from '../../components/common/hocs/withWindowSize'; @@ -42,115 +37,148 @@ import { setBreadcrumbs } from '../common/globalBreadcrumb/platformBreadcrumb'; import WzDataSourceSelector from '../common/data-source/components/wz-data-source-selector/wz-data-source-selector'; import { PinnedAgentManager } from '../wz-agent-selector/wz-agent-selector-service'; import NavigationService from '../../react-services/navigation-service'; +import { useAsyncActionRunOnStart } from '../common/hooks'; +import { useSelectedServerApi } from '../common/hooks/use-selected-server-api'; +import { + AlertsDataSource, + AlertsDataSourceRepository, +} from '../common/data-source'; +import { Selector, SelectorContainer, SelectorLabel } from './selectors'; + +async function getServerAPIList() { + const response = await GenericRequest.request('GET', '/hosts/apis', {}); + return response?.data; +} + +const ServerAPISelector = ({ showSelectorsInPopover }) => { + const action = useAsyncActionRunOnStart(getServerAPIList, []); + + const { selectedAPI: currentAPI } = useSelectedServerApi(); + + let style = { minWidth: 100, textOverflow: 'ellipsis' }; + if (showSelectorsInPopover) { + style = { width: '100%', minWidth: 220 }; + } + + const notSelected = !Boolean(currentAPI); + const actionError = + action.error?.message || + (!action.running && notSelected && 'Server API is not selected'); + + const isInvalid = Boolean(actionError); + + const changeAPI = async event => { + try { + const apiId = event.target[event.target.selectedIndex]; + const apiEntry = action.data?.filter(item => { + return item.id === apiId.value; + }); + const response = await ApiCheck.checkApi(apiEntry[0]); + const clusterInfo = response.data || {}; + const apiData = action.data?.filter(item => { + return item.id === apiId.value; + }); + + apiData[0].cluster_info = clusterInfo; + + AppState.setClusterInfo(apiData[0].cluster_info); + AppState.setCurrentAPI( + JSON.stringify({ name: apiData[0].manager, id: apiId.value }), + ); + const pinnedAgentManager = new PinnedAgentManager(); + const isPinnedAgent = pinnedAgentManager.isPinnedAgent(); + if (isPinnedAgent) { + pinnedAgentManager.unPinAgent(); + } + + /* TODO: this reloads the page to force the components are remounted with the new + selection of. To avoid this refresh, we would have to do the components are able to react + to these changes redoing the requests, etc... This will need a considerable time to + apply the changes. The reload of the pages is the same behavior used for the routing based + on AngularJS. + */ + NavigationService.getInstance().reload(); + } catch (error) { + const options = { + context: `${WzMenu.name}.${changeAPI.name}`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + error: { + error: error, + message: error.message || error, + title: `Error changing the selected API`, + }, + }; + getErrorOrchestrator().handleError(options); + } + }; + + return ( + + + API + + +
+ { + return { value: item.id, text: item.id }; + }) || [] + } + value={currentAPI?.id} + onChange={changeAPI} + aria-label='API selector' + hasNoInitialSelection={notSelected} + isInvalid={isInvalid} + append={ + { + action.run(); + }} + > + } + /> +
+
+
+ ); +}; + +const AlertsIndexPatternSelector = ({ showSelectorsInPopover, appConfig }) => { + return ( + + ); +}; export const WzMenu = withWindowSize( class WzMenu extends Component { constructor(props) { super(props); this.state = { - menuOpened: false, - currentAPI: this.props.state.currentAPI || '', - APIlist: [], - showSelector: false, - theresPattern: false, - currentPattern: '', - patternList: [], - currentSelectedPattern: '', + isSelectorsPopoverOpen: false, }; - this.store = store; - this.genericReq = GenericRequest; - this.wazuhConfig = new WazuhConfig(); - this.indexPatterns = getDataPlugin().indexPatterns; - this.isLoading = false; - this.pinnedAgentManager = new PinnedAgentManager(); } async componentDidMount() { setBreadcrumbs(this.props.globalBreadcrumbReducers.breadcrumb); - try { - const APIlist = await this.loadApiList(); - this.setState({ APIlist: APIlist }); - if (APIlist.length) { - const { id: apiId } = JSON.parse(AppState.getCurrentAPI()); - const filteredApi = APIlist.filter(api => api.id === apiId); - const selectedApi = filteredApi[0]; - if (selectedApi) { - const apiData = await ApiCheck.checkStored(selectedApi.id); - //update cluster info - const cluster_info = apiData?.data?.data?.cluster_info; - if (cluster_info) { - AppState.setClusterInfo(cluster_info); - } - } - } - } catch (error) { - const options = { - context: `${WzMenu.name}.componentDidMount`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - - try { - const additionalState = await this.loadIndexPatternsList(); - this.setState(state => ({ ...state, ...additionalState })); - } catch (e) {} } - showToast = (color, title, text, time) => { - getToasts().add({ - color: color, - title: title, - text: text, - toastLifeTimeMs: time, - }); - }; - - loadApiList = async () => { - const result = await this.genericReq.request('GET', '/hosts/apis', {}); - const APIlist = (result || {}).data || []; - return APIlist; - }; - async componentDidUpdate(prevProps) { - let newState = {}; - const { id: apiId } = JSON.parse(AppState.getCurrentAPI()); - const { currentAPI } = this.state; - - if (this.props.windowSize) { - this.showSelectorsInPopover = this.props.windowSize.width < 1100; - } - - if ((!currentAPI && apiId) || apiId !== currentAPI) { - newState = { ...newState, currentAPI: apiId }; - } else { - if ( - currentAPI && - this.props.state.currentAPI && - currentAPI !== this.props.state.currentAPI - ) { - newState = { ...newState, currentAPI: this.props.state.currentAPI }; - } - } - if ( - !this.isLoading && - !_.isEqual( - prevProps?.appConfig?.data?.['ip.ignore'], - this.props?.appConfig?.data?.['ip.ignore'], - ) - ) { - this.isLoading = true; - newState = { ...newState, ...(await this.loadIndexPatternsList()) }; - } - if ( !_.isEqual( this.props.globalBreadcrumbReducers.breadcrumb, @@ -159,131 +187,8 @@ export const WzMenu = withWindowSize( ) { setBreadcrumbs(this.props.globalBreadcrumbReducers.breadcrumb); } - newState = { ...prevProps.state, ...newState }; - if (!_.isEqual(newState, prevProps.state)) { - // FIXME: this will not update the state if the prevProps.state is equal to the newState, this means the component state could not be updated despite this is different to the newState - // and the state is different from the previous one - this.setState(newState); - } } - async loadIndexPatternsList() { - try { - let newState = {}; - - let list = await PatternHandler.getPatternList('api'); - if (!list || (list && !list.length)) return; - this.props?.appConfig?.data?.['ip.ignore']?.length && - (list = list.filter( - indexPattern => - !this.props?.appConfig?.data?.['ip.ignore'].includes( - indexPattern.title, - ), - )); - - let filtered = false; - // If there is no current pattern, fetch it - if (!AppState.getCurrentPattern()) { - AppState.setCurrentPattern(list[0].id); - } else { - // Check if the current pattern cookie is valid - filtered = list.find(item => - item.id.includes(AppState.getCurrentPattern()), - ); - if (!filtered) AppState.setCurrentPattern(list[0].id); - } - - const data = filtered - ? filtered - : await this.indexPatterns.get(AppState.getCurrentPattern()); - newState = { - ...newState, - theresPattern: true, - currentPattern: data.title, - }; - - // Getting the list of index patterns - if (list) { - newState = { - ...newState, - patternList: list, - currentSelectedPattern: AppState.getCurrentPattern(), - }; - } - this.isLoading = false; - return newState; - } catch (error) { - this.isLoading = false; - const options = { - context: `${WzMenu.name}.load`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: true, - display: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } - } - - updatePatternAndApi = async () => { - this.setState({ - menuOpened: false, // TODO: this seems that is unused - ...{ APIlist: await this.loadApiList() }, - ...(await this.loadIndexPatternsList()), - }); - }; - - changeAPI = async event => { - try { - const apiId = event.target[event.target.selectedIndex]; - const apiEntry = this.state.APIlist.filter(item => { - return item.id === apiId.value; - }); - const response = await ApiCheck.checkApi(apiEntry[0]); - const clusterInfo = response.data || {}; - const apiData = this.state.APIlist.filter(item => { - return item.id === apiId.value; - }); - - apiData[0].cluster_info = clusterInfo; - - AppState.setClusterInfo(apiData[0].cluster_info); - AppState.setCurrentAPI( - JSON.stringify({ name: apiData[0].manager, id: apiId.value }), - ); - const isPinnedAgent = this.pinnedAgentManager.isPinnedAgent(); - if (isPinnedAgent) { - this.pinnedAgentManager.unPinAgent(); - } - if (this.state.currentMenuTab !== 'wazuh-dev') { - /* TODO: this reloads the page to force the components are remounted with the new - selection of. To avoid this refresh, we would have to do the components are able to react - to these changes redoing the requests, etc... This will need a considerable time to - apply the changes. The reload of the pages is the same behavior used for the routing based - on AngularJS. - */ - NavigationService.getInstance().reload(); - } - } catch (error) { - const options = { - context: `${WzMenu.name}.changePattern`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - error: { - error: error, - message: error.message || error, - title: `Error changing the selected API`, - }, - }; - getErrorOrchestrator().handleError(options); - } - }; - buildWazuhNotReadyYet() { const container = document.getElementsByClassName('wazuhNotReadyYet'); return ReactDOM.createPortal( @@ -323,88 +228,6 @@ export const WzMenu = withWindowSize( ); } - getApiSelectorComponent() { - let style = { minWidth: 100, textOverflow: 'ellipsis' }; - if (this.showSelectorsInPopover) { - style = { width: '100%', minWidth: 200 }; - } - - return ( - <> - -

API

-
- -
- { - return { value: item.id, text: item.id }; - })} - value={this.state.currentAPI} - onChange={this.changeAPI} - aria-label='API selector' - /> -
-
- - ); - } - - onChangePattern = async pattern => { - try { - this.setState({ currentSelectedPattern: pattern.id }); - if (this.state.currentMenuTab !== 'wazuh-dev') { - /* TODO: this reloads the page to force the components are remounted with the new - selection of. To avoid this refresh, we would have to do the components are able to react - to these changes redoing the requests, etc... This will need a considerable time to - apply the changes. The reload of the pages is the same behavior used for the routing based - on AngularJS. - */ - NavigationService.getInstance().reload(); - } - await this.updatePatternAndApi(); - } catch (error) { - const options = { - context: `${WzMenu.name}.onChangePattern`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.BUSINESS, - store: false, - display: true, - error: { - error: error, - message: error.message || error, - title: `Error changing the Index Pattern`, - }, - }; - getErrorOrchestrator().handleError(options); - } - }; - - getIndexPatternSelectorComponent() { - let style = { maxWidth: 200, maxHeight: 50 }; - if (this.showSelectorsInPopover) { - style = { width: '100%', maxHeight: 50, minWidth: 200 }; - } - - return ( - <> - -

Index pattern

-
- -
- -
-
- - ); - } - switchSelectorsPopOver() { this.setState({ isSelectorsPopoverOpen: !this.state.isSelectorsPopoverOpen, @@ -425,6 +248,9 @@ export const WzMenu = withWindowSize( ); + const showSelectorsInPopover = + this.props.windowSize && this.props.windowSize.width < 1100; + return ( <> @@ -433,47 +259,50 @@ export const WzMenu = withWindowSize( responsive={false} className='wz-margin-left-10 wz-margin-right-10 font-size-14' > - {!this.showSelectorsInPopover && - this.state.patternList.length > 1 && - this.getIndexPatternSelectorComponent()} - - {!this.showSelectorsInPopover && - this.state.APIlist.length > 1 && - this.getApiSelectorComponent()} - - {this.showSelectorsInPopover && - (this.state.patternList.length > 1 || - this.state.APIlist.length > 1) && ( - <> - - this.switchSelectorsPopOver()} + {(showSelectorsInPopover && ( + <> + + this.switchSelectorsPopOver()} + > + - {this.state.patternList.length > 1 && ( - - {this.getIndexPatternSelectorComponent()} - - )} - {this.state.APIlist.length > 1 && ( - - {this.getApiSelectorComponent()} - - )} - - - - )} + + + + + + + + + )) || ( + <> + + + + + + + + )} {this.props.state.wazuhNotReadyYet && this.buildWazuhNotReadyYet()} diff --git a/plugins/main/public/components/wz-menu/wz-menu.test.tsx b/plugins/main/public/components/wz-menu/wz-menu.test.tsx index 969c36f8cc..c6444255ca 100644 --- a/plugins/main/public/components/wz-menu/wz-menu.test.tsx +++ b/plugins/main/public/components/wz-menu/wz-menu.test.tsx @@ -16,6 +16,12 @@ import React from 'react'; import { shallow } from 'enzyme'; import { WzMenu } from './wz-menu'; +// the jest.mock of @osd/monaco is added due to a problem transcribing the files to run the tests. +// https://github.com/wazuh/wazuh-dashboard-plugins/pull/6921#issuecomment-2298289550 + +jest.mock('@osd/monaco', () => ({ + monaco: {}, +})); describe('WzMenu tests', () => { test('should render a WzMenu', () => { @@ -23,4 +29,4 @@ describe('WzMenu tests', () => { expect(component).toMatchSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/plugins/main/public/components/wz-search-bar/wz-search-bar.tsx b/plugins/main/public/components/wz-search-bar/wz-search-bar.tsx index d922f9486d..a991891421 100644 --- a/plugins/main/public/components/wz-search-bar/wz-search-bar.tsx +++ b/plugins/main/public/components/wz-search-bar/wz-search-bar.tsx @@ -51,7 +51,7 @@ export function WzSearchBar(props: IWzSearchBarProps) { inputValue, setInputValue, inputRef, - setIsOpen + setIsOpen, ); return ( @@ -60,11 +60,14 @@ export function WzSearchBar(props: IWzSearchBarProps) { status={status} inputRef={setInputRef} prepend={ - + } value={inputValue} - onKeyPress={(event) => handler && handler.onKeyPress(inputValue, event)} - onItemClick={(item) => handler && handler.onItemClick(item, inputValue)} + onKeyPress={event => handler && handler.onKeyPress(inputValue, event)} + onItemClick={item => handler && handler.onItemClick(item, inputValue)} isPopoverOpen={isOpen} onClosePopover={() => setIsOpen(false)} onPopoverFocus={() => setIsOpen(true)} @@ -79,7 +82,7 @@ export function WzSearchBar(props: IWzSearchBarProps) { props.onFiltersChange(filters)} + onChange={filters => props.onFiltersChange(filters)} /> )} @@ -89,10 +92,10 @@ export function WzSearchBar(props: IWzSearchBarProps) { function useSuggestHandler( props: IWzSearchBarProps, - inputValue, - setInputValue, - inputRef, - setIsOpen + inputValue: string, + setInputValue: (value: string) => void, + inputRef: HTMLInputElement, + setIsOpen: (value: boolean) => void, ): [suggestItem[], SuggestHandler | undefined, string, boolean] { const [handler, setHandler] = useState(); const [suggestsItems, setSuggestItems] = useState([]); @@ -101,7 +104,11 @@ function useSuggestHandler( useEffect(() => { setHandler( - new SuggestHandler({ ...props, status, setStatus, setInvalid, setIsOpen }, setInputValue, inputRef) + new SuggestHandler( + { ...props, status, setStatus, setInvalid, setIsOpen }, + setInputValue, + inputRef, + ), ); !props.noDeleteFiltersOnUpdateSuggests && props.onFiltersChange([]); }, [props.suggestions]); @@ -120,7 +127,7 @@ function useSuggestHandler( const options = { context: `${useSuggestHandler.name}.useEffect`, level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, + severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, error: { error: error, diff --git a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx index ceb9fe77e5..ecfcb6ac11 100644 --- a/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx +++ b/plugins/main/public/controllers/management/components/management/cdblists/views/list-editor.test.tsx @@ -36,6 +36,11 @@ jest.mock('../../../../../../kibana-services', () => ({ getToasts: () => ({ add: mockAdd, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); const mockStore = configureMockStore(); diff --git a/plugins/main/public/controllers/management/components/management/cluster/cluster-overview.tsx b/plugins/main/public/controllers/management/components/management/cluster/cluster-overview.tsx index f74efc79ac..dd77561f65 100644 --- a/plugins/main/public/controllers/management/components/management/cluster/cluster-overview.tsx +++ b/plugins/main/public/controllers/management/components/management/cluster/cluster-overview.tsx @@ -70,18 +70,6 @@ export const ClusterOverview = compose( /> ), ), -)( - ({ - clusterEnabled, - isClusterRunning, - statusRunning, - }: ClusterOverviewState) => { - return ( - <> - {clusterEnabled && isClusterRunning ? ( - - ) : null} - - ); - }, -); +)(({ statusRunning }: ClusterOverviewState) => { + return ; +}); diff --git a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx index f6673d1957..100710184b 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx +++ b/plugins/main/public/controllers/management/components/management/configuration/alerts/alerts-configurations.test.tsx @@ -10,6 +10,11 @@ jest.mock('../../../../../../kibana-services', () => ({ return false; }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); const mockProps = { diff --git a/plugins/main/public/controllers/management/components/management/configuration/office365/office365.test.tsx b/plugins/main/public/controllers/management/components/management/configuration/office365/office365.test.tsx index 7ac86a41da..bf6f6c6773 100644 --- a/plugins/main/public/controllers/management/components/management/configuration/office365/office365.test.tsx +++ b/plugins/main/public/controllers/management/components/management/configuration/office365/office365.test.tsx @@ -24,6 +24,11 @@ jest.mock('../../../../../../kibana-services', () => ({ } }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); const mockStore = configureMockStore(); diff --git a/plugins/main/public/controllers/management/components/management/decoders/main-decoders.test.tsx b/plugins/main/public/controllers/management/components/management/decoders/main-decoders.test.tsx index d71e3963fb..a7fe641475 100644 --- a/plugins/main/public/controllers/management/components/management/decoders/main-decoders.test.tsx +++ b/plugins/main/public/controllers/management/components/management/decoders/main-decoders.test.tsx @@ -29,6 +29,11 @@ jest.mock('../../../../../kibana-services', () => ({ } }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); // the jest.mock of @osd/monaco is added due to a problem transcribing the files to run the tests. diff --git a/plugins/main/public/controllers/management/components/management/groups/groups-main.js b/plugins/main/public/controllers/management/components/management/groups/groups-main.js index 109ed61a48..86c541c10d 100644 --- a/plugins/main/public/controllers/management/components/management/groups/groups-main.js +++ b/plugins/main/public/controllers/management/components/management/groups/groups-main.js @@ -28,6 +28,7 @@ import { compose } from 'redux'; import { withGlobalBreadcrumb, withRouterSearch, + withUserAuthorizationPrompt, } from '../../../../../components/common/hocs'; import { endpointGroups } from '../../../../../utils/applications'; import { MultipleAgentSelector } from '../../../../../components/management/groups/multiple-agent-selector'; @@ -54,7 +55,7 @@ class WzGroups extends Component { const options = { context: `${WzGroups.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, + severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, error: { error: error, @@ -137,4 +138,7 @@ export default compose( : [{ text: endpointGroups.breadcrumbLabel }]; }), withRouterSearch, + withUserAuthorizationPrompt([ + { action: 'group:read', resource: 'group:id:*' }, + ]), )(WzGroups); diff --git a/plugins/main/public/controllers/management/components/management/groups/groups-main.test.tsx b/plugins/main/public/controllers/management/components/management/groups/groups-main.test.tsx index 3b62eca598..5b101aab92 100644 --- a/plugins/main/public/controllers/management/components/management/groups/groups-main.test.tsx +++ b/plugins/main/public/controllers/management/components/management/groups/groups-main.test.tsx @@ -23,6 +23,11 @@ jest.mock('../../../../../kibana-services', () => ({ prepend: str => str, }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); const mockProps = { diff --git a/plugins/main/public/controllers/management/components/management/management-main.js b/plugins/main/public/controllers/management/components/management/management-main.js index e114acf9ad..968638efe5 100644 --- a/plugins/main/public/controllers/management/components/management/management-main.js +++ b/plugins/main/public/controllers/management/components/management/management-main.js @@ -60,6 +60,7 @@ const WzManagementMain = props => ( + {/* TODO: research about 2 routes with same render */} diff --git a/plugins/main/public/controllers/management/components/management/mg-logs/logs.js b/plugins/main/public/controllers/management/components/management/mg-logs/logs.js index a0ceef21cf..4013621b26 100644 --- a/plugins/main/public/controllers/management/components/management/mg-logs/logs.js +++ b/plugins/main/public/controllers/management/components/management/mg-logs/logs.js @@ -111,7 +111,7 @@ export default compose( const options = { context: `${WzLogs.name}.componentDidMount`, level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, + severity: UI_ERROR_SEVERITIES.BUSINESS, store: true, error: { error: error, diff --git a/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx b/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx index a80f3269fb..080de01be8 100644 --- a/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx +++ b/plugins/main/public/controllers/management/components/management/reporting/reporting-main.test.tsx @@ -22,6 +22,11 @@ jest.mock('../../../../../kibana-services', () => ({ prepend: str => str, }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); jest.mock( diff --git a/plugins/main/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx b/plugins/main/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx index d542f08f18..b2df016984 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/main-ruleset.test.tsx @@ -29,6 +29,11 @@ jest.mock('../../../../../kibana-services', () => ({ } }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); // the jest.mock of @osd/monaco is added due to a problem transcribing the files to run the tests. diff --git a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx index 18268b4236..b680884e9b 100644 --- a/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx +++ b/plugins/main/public/controllers/management/components/management/ruleset/views/rule-info.tsx @@ -169,6 +169,7 @@ export default withRouterSearch( document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera const currentIndexPattern = await getDataPlugin().indexPatterns.get( + // TODO: this should use the selected index pattern without fallback AppState.getCurrentPattern() || getWazuhCorePlugin().configuration.getSettingValue('pattern'), ); @@ -791,6 +792,7 @@ export default withRouterSearch( FILTER_OPERATOR.IS, 'rule.id', id, + // TODO: this should validate the index pattern is selected this.state.currentIndexPattern, ), ], diff --git a/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js b/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js index cb941fd917..435f8b2e32 100644 --- a/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js +++ b/plugins/main/public/controllers/management/components/management/statistics/statistics-overview.js @@ -10,7 +10,7 @@ * * Find more information about this on the LICENSE file. */ -import React, { Component, useState, useEffect } from 'react'; +import React, { Component } from 'react'; import { EuiFlexItem, EuiFlexGroup, @@ -30,7 +30,6 @@ import { withGlobalBreadcrumb, withUserAuthorizationPrompt, } from '../../../../../components/common/hocs'; -import { PromptStatisticsNoIndices } from './prompt-statistics-no-indices'; import { UI_ERROR_SEVERITIES } from '../../../../../react-services/error-orchestrator/types'; import { UI_LOGGER_LEVELS, @@ -43,12 +42,6 @@ import { RedirectAppLinks } from '../../../../../../../../src/plugins/opensearch import { DashboardTabsPanels } from '../../../../../components/overview/server-management-statistics/dashboards/dashboardTabsPanels'; import { connect } from 'react-redux'; import NavigationService from '../../../../../react-services/navigation-service'; -import { - existsIndices, - existsIndexPattern, - createIndexPattern, -} from '../../../../../react-services'; -import { StatisticsDataSource } from '../../../../../components/common/data-source/pattern/statistics'; export class WzStatisticsOverview extends Component { _isMounted = false; diff --git a/plugins/main/public/controllers/management/components/management/status/status-main.test.tsx b/plugins/main/public/controllers/management/components/management/status/status-main.test.tsx index 2303ddc951..a881db0eeb 100644 --- a/plugins/main/public/controllers/management/components/management/status/status-main.test.tsx +++ b/plugins/main/public/controllers/management/components/management/status/status-main.test.tsx @@ -15,7 +15,6 @@ import React from 'react'; import WzStatus from './status-main'; import { renderWithProviders } from '../../../../../redux/render-with-redux-provider'; - jest.mock('../../../../../kibana-services', () => ({ getHttp: () => ({ basePath: { @@ -29,6 +28,11 @@ jest.mock('../../../../../kibana-services', () => ({ } }, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); // the jest.mock of @osd/monaco is added due to a problem transcribing the files to run the tests. diff --git a/plugins/main/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap b/plugins/main/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap index 25e4b14350..8d19801242 100644 --- a/plugins/main/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap +++ b/plugins/main/public/controllers/overview/components/__snapshots__/stats.test.tsx.snap @@ -22,43 +22,80 @@ exports[`Stats component renders correctly to match the snapshot 1`] = ` class="euiCard__children" >
-
-

- This instance has no agents registered. -
- Please deploy agents to begin monitoring your endpoints. -

+ + +
-
-
- +
  • + +
  • +
  • + +
  • + +
    +
    @@ -92,188 +129,11 @@ exports[`Stats component renders correctly to match the snapshot 1`] = ` class="euiCard__children" >
    - -
    -
    - -
    -
    - -
    -
    - -
    + class="euiProgress euiProgress--indeterminate euiProgress--xs euiProgress--primary" + />
    diff --git a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx index 0e0e4435f4..46b3ae4ebe 100644 --- a/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx +++ b/plugins/main/public/controllers/overview/components/last-alerts-stat/last-alerts-stat.tsx @@ -1,6 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { EuiStat, + EuiFlexGroup, EuiFlexItem, EuiLink, EuiToolTip, @@ -21,6 +22,18 @@ import { PatternDataSourceFilterManager, } from '../../../../components/common/data-source/pattern/pattern-data-source-filter-manager'; import { formatUINumber } from '../../../../react-services/format-number'; +import { compose } from 'redux'; +import { + withDataSource, + withDataSourceFetchOnStart, + withDataSourceInitiated, + withDataSourceLoading, +} from '../../../../components/common/hocs'; +import { + AlertsDataSourceRepository, + ThreatHuntingDataSource, +} from '../../../../components/common/data-source'; +import { LoadingSearchbarProgress } from '../../../../components/common/loading-searchbar-progress/loading-searchbar-progress'; type SeverityKey = 'low' | 'medium' | 'high' | 'critical'; @@ -59,6 +72,182 @@ export const severities = { }, } as const; +const discoverLocation = { + app: 'data-explorer', + basePath: 'discover', +}; + +export const LastAlertsSummaryBySeverity = compose( + withDataSource({ + DataSource: ThreatHuntingDataSource, + DataSourceRepositoryCreator: AlertsDataSourceRepository, + }), + withDataSourceLoading({ + isLoadingNameProp: 'dataSource.isLoading', + LoadingComponent: LoadingSearchbarProgress, + }), + withDataSourceInitiated({ + dataSourceErrorNameProp: 'dataSource.error', + dataSourceNameProp: 'dataSource.dataSource', + isLoadingNameProp: 'dataSource.isLoading', + }), +)(props => { + return ( + <> + + + + + + + + + + + + + + + + ); +}); + +export const LastAlertsCountBySeverity = withDataSourceFetchOnStart({ + nameProp: 'dataSource', + mapRequestParams: ({ dataSource, dependencies }) => { + const [, severityKey] = dependencies; + const { ruleLevelRange } = severities[severityKey]; + + return { + filters: [ + ...dataSource.fetchFilters, + PatternDataSourceFilterManager.createFilter( + FILTER_OPERATOR.IS_BETWEEN, + 'rule.level', + [ruleLevelRange.minRuleLevel, ruleLevelRange.maxRuleLevel], + dataSource.dataSource.indexPattern.id, + ), + ], + dateRange: { + from: 'now-24h', + to: 'now', + format: 'epoch_millis', + }, + pagination: { + pageIndex: 0, + pageSize: 1, // We only need the count, so we can limit the page size to 1 + }, + }; + }, + mapResponse: (response, props) => { + return { total: response.hits.total }; + }, + mapFetchActionDependencies(props) { + return [props.severity]; + }, +})( + ({ + severity: severityKey, + hideBottomText, + direction = 'row', + textAlign = 'center', + dataSource, + dataSourceAction, + }: { + severity: SeverityKey; + hideBottomText?: boolean; + direction: 'row' | 'column'; + textAlign?: EuiStatProps['textAlign']; + dataSource: any; + dataSourceAction: any; + }) => { + const severity = severities[severityKey]; + const ruleLevelRange = severity.ruleLevelRange; + const urlDiscover = useMemo(() => { + const indexPatternId = dataSource.dataSource.indexPattern.id; + const predefinedFilters = + PatternDataSourceFilterManager.filtersToURLFormat([ + ...dataSource.fetchFilters, + PatternDataSourceFilterManager.createFilter( + FILTER_OPERATOR.IS_BETWEEN, + 'rule.level', + [ruleLevelRange.minRuleLevel, ruleLevelRange.maxRuleLevel], + indexPatternId, + ), + ]); + + return getCore().application.getUrlForApp(discoverLocation.app, { + path: `${discoverLocation.basePath}#?_a=(discover:(columns:!(_source),isDirty:!f,sort:!()),metadata:(indexPattern:'${indexPatternId}',view:discover))&_g=${predefinedFilters}&_q=(filters:!(),query:(language:kuery,query:''))`, + }); + }, []); + + const { total } = dataSourceAction.data || {}; + + const statDescription = + direction === 'row' ? `${severity.label} severity` : ''; + const statValue = + direction === 'row' + ? `${total ?? '-'}` + : ` ${total ?? '-'} ${severity.label}`; + + return ( + + + + {formatUINumber(statValue)} + + + } + description={statDescription} + descriptionElement='h3' + titleColor={severity.color} + textAlign={textAlign} + /> + {hideBottomText ? null : ( + + {'Rule level ' + + ruleLevelRange.minRuleLevel + + (ruleLevelRange.maxRuleLevel + ? ' to ' + ruleLevelRange.maxRuleLevel + : ' or higher')} + + )} + + ); + }, +); + export function LastAlertsStat({ severity: severityKey, hideBottomText, diff --git a/plugins/main/public/controllers/overview/components/stats.js b/plugins/main/public/controllers/overview/components/stats.js index aec7fcbfba..8699bd2581 100644 --- a/plugins/main/public/controllers/overview/components/stats.js +++ b/plugins/main/public/controllers/overview/components/stats.js @@ -26,7 +26,7 @@ import { agentStatusColorByAgentStatus, } from '../../../../common/services/wz_agent_status'; import { endpointSummary } from '../../../utils/applications'; -import { LastAlertsStat } from './last-alerts-stat'; +import { LastAlertsSummaryBySeverity } from './last-alerts-stat'; import { VisualizationBasic } from '../../../components/common/charts/visualizations/basic'; import NavigationService from '../../../react-services/navigation-service'; import './stats.scss'; @@ -92,22 +92,22 @@ export const Stats = withErrorBoundary( window.innerWidth < 768 ? mobileLoadingSize : normalLoadingSize; const size = this.props.isAgentsLoading ? loadingSize - : { width: '100%', height: '150px' }; + : /* WORKAROUND: Increase the height if there is an error, to mitigate the overflow problems in the limited size.*/ + { width: '100%', height: this.props.error ? '250px' : '150px' }; return size; } render() { - const { isAgentsLoading } = this.props; + const { isAgentsLoading, error } = this.props; const hasResults = this.agentStatus.some( ({ status }) => this.props[status], ); - const showAgentsChart = isAgentsLoading || hasResults; return ( - {showAgentsChart ? ( + {hasResults || isAgentsLoading || error ? ( ) : ( - - - - - - +
    + +
    diff --git a/plugins/main/public/controllers/overview/components/stats.test.tsx b/plugins/main/public/controllers/overview/components/stats.test.tsx index 842ab74e67..67f8c5760a 100644 --- a/plugins/main/public/controllers/overview/components/stats.test.tsx +++ b/plugins/main/public/controllers/overview/components/stats.test.tsx @@ -16,7 +16,6 @@ import React from 'react'; import { render, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import { Stats } from './stats'; -import jsonBeautifier from '../../../utils/json-beautifier'; jest.mock('../../../react-services/navigation-service', () => { return { @@ -25,6 +24,9 @@ jest.mock('../../../react-services/navigation-service', () => { getUrlForApp() { return ''; }, + getHistory() { + return {}; + }, }; }, }; @@ -77,6 +79,16 @@ jest.mock('../../../kibana-services', () => ({ get: () => true, }, }), + getCookies: jest.fn(() => { + return { + get: () => 'test', + }; + }), + getUiSettings: jest.fn(() => { + return { + get: () => 'test', + }; + }), })); jest.mock('../../../react-services/common-services', () => ({ @@ -95,7 +107,17 @@ jest.mock('@osd/monaco', () => ({ describe('Stats component', () => { test('renders correctly to match the snapshot', async () => { await act(async () => { - const { container } = render(); + const { container } = render( + , + ); expect(container).toMatchSnapshot(); }); }); diff --git a/plugins/main/public/factories/misc.js b/plugins/main/public/factories/misc.js index 6e541e79c8..d56196e09b 100644 --- a/plugins/main/public/factories/misc.js +++ b/plugins/main/public/factories/misc.js @@ -21,8 +21,6 @@ export class WzMisc { this.state = { apiIsDown: false, comeFromWizard: false, - blankScreenError: false, - lastRestart: null }; WzMisc.instance = this; @@ -58,27 +56,4 @@ export class WzMisc { getWizard() { return this.state.comeFromWizard; } - - /** - * Set blank screen - * @param {String} value - */ - setBlankScr(value) { - this.state.blankScreenError = value; - } - - /** - * Get blank screen - */ - getBlankScr() { - return this.state.blankScreenError; - } - - /** - * Set last restart - * @param {String} value - */ - setLastRestart(value) { - this.state.lastRestart = value; - } } diff --git a/plugins/main/public/plugin.ts b/plugins/main/public/plugin.ts index 70f5ebb9ac..c4687f31dc 100644 --- a/plugins/main/public/plugin.ts +++ b/plugins/main/public/plugin.ts @@ -24,6 +24,7 @@ import { setWazuhCheckUpdatesPlugin, setHeaderActionMenuMounter, setWazuhCorePlugin, + getCookies, } from './kibana-services'; import { AppPluginStartDependencies, @@ -158,7 +159,13 @@ export class WazuhPlugin const { renderApp } = await import('./application'); setErrorOrchestrator(ErrorOrchestratorService); setHttp(core.http); - setCookies(new Cookies()); + // Set Cookies + try { + // WORKAOURND: the AppState can set the cookies, this check if they was not set before + getCookies(); + } catch { + setCookies(new Cookies()); + } if (!AppState.checkCookies()) { NavigationService.getInstance().reload(); } diff --git a/plugins/main/public/react-services/app-state.js b/plugins/main/public/react-services/app-state.js index 6703cceb1d..618de3b7a4 100644 --- a/plugins/main/public/react-services/app-state.js +++ b/plugins/main/public/react-services/app-state.js @@ -11,19 +11,36 @@ */ import store from '../redux/store'; -import { - updateCurrentApi, - updateShowMenu, -} from '../redux/actions/appStateActions'; +import { updateCurrentApi } from '../redux/actions/appStateActions'; import { CSVRequest } from '../services/csv-request'; -import { getToasts, getCookies } from '../kibana-services'; +import { getToasts, getCookies, setCookies } from '../kibana-services'; import * as FileSaver from '../services/file-saver'; import { WzAuthentication } from './wz-authentication'; import { UI_ERROR_SEVERITIES } from './error-orchestrator/types'; import { UI_LOGGER_LEVELS } from '../../common/constants'; import { getErrorOrchestrator } from './common-services'; +import { BehaviorSubject } from 'rxjs'; +import { distinctUntilChanged } from 'rxjs/operators'; +import { Cookies } from 'react-cookie'; +import { isEqual } from 'lodash'; + +/* WORKAROUND: this defines the cookies object in case it doesn't exist that is used by +the selectedServerAPI$ observable */ +try { + getCookies(); +} catch { + setCookies(new Cookies()); +} export class AppState { + static selectedServerAPI$ = new BehaviorSubject( + getCookies().get('currentApi') + ? decodeURI(getCookies().get('currentApi')) + : false, + ); + static selectedServerAPIChanged$ = this.selectedServerAPI$.pipe( + distinctUntilChanged(isEqual), + ); /** * Cluster setters and getters **/ @@ -138,7 +155,9 @@ export class AppState { static getCurrentAPI() { try { const currentAPI = getCookies().get('currentApi'); - return currentAPI ? decodeURI(currentAPI) : false; + const value = currentAPI ? decodeURI(currentAPI) : false; + this.selectedServerAPI$.next(value ? JSON.parse(value) : false); + return value; } catch (error) { throw error; } @@ -148,6 +167,7 @@ export class AppState { * Remove 'API' cookie */ static removeCurrentAPI() { + this.selectedServerAPI$.next(false); const updateApiMenu = updateCurrentApi(false); store.dispatch(updateApiMenu); return getCookies().remove('currentApi'); @@ -167,7 +187,9 @@ export class AppState { expires: exp, }); try { - const updateApiMenu = updateCurrentApi(JSON.parse(API).id); + const parsedApi = JSON.parse(API); + this.selectedServerAPI$.next(parsedApi); + const updateApiMenu = updateCurrentApi(parsedApi.id); store.dispatch(updateApiMenu); WzAuthentication.refresh(); } catch (error) { diff --git a/plugins/main/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx b/plugins/main/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx index 13ec30ecae..001ee1c767 100644 --- a/plugins/main/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx +++ b/plugins/main/public/react-services/error-management/docs/examples/error-handler-class-example.test.tsx @@ -23,6 +23,7 @@ jest.mock('../../../../kibana-services', () => ({ set: (name: string, value: string, options: any) => { return true; }, + get: () => 'test', }), })); diff --git a/plugins/main/public/react-services/error-management/error-handler/error-handler.test.ts b/plugins/main/public/react-services/error-management/error-handler/error-handler.test.ts index 06a96be49f..b512d066d0 100644 --- a/plugins/main/public/react-services/error-management/error-handler/error-handler.test.ts +++ b/plugins/main/public/react-services/error-management/error-handler/error-handler.test.ts @@ -21,6 +21,7 @@ jest.mock('../../../kibana-services', () => ({ set: (name: string, value: string, options: any) => { return true; }, + get: () => 'test', }), })); diff --git a/plugins/main/public/react-services/error-management/error-handler/error-handler.ts b/plugins/main/public/react-services/error-management/error-handler/error-handler.ts index bd66c2909c..616b4caccd 100644 --- a/plugins/main/public/react-services/error-management/error-handler/error-handler.ts +++ b/plugins/main/public/react-services/error-management/error-handler/error-handler.ts @@ -23,15 +23,17 @@ interface IUrlRequestedTypes { } export class ErrorHandler { - /** * Receives an error and create return a new error instance then treat the error - * + * * @param error error instance - * @param customLogOptions custom log options to show when the error is presented to the UI (toast|logs|blank-screen) - * @returns + * @param customLogOptions custom log options to show when the error is presented to the UI (toast|logs) + * @returns */ - static handleError(error: Error, customLogOptions?: ILogCustomOptions): Error | IWazuhError { + static handleError( + error: Error, + customLogOptions?: ILogCustomOptions, + ): Error | IWazuhError { if (!error) { throw Error('Error must be defined'); } @@ -68,7 +70,7 @@ export class ErrorHandler { ): IWazuhErrorConstructor | null { let errorType = null; // if is http error (axios error) then get new to create a new error instance - if(this.isHttpError(error)){ + if (this.isHttpError(error)) { errorType = this.getErrorTypeByConfig(error as AxiosError); } return errorType; @@ -76,24 +78,28 @@ export class ErrorHandler { /** * Check if the error received is an http error (axios error) - * @param error - * @returns + * @param error + * @returns */ - static isHttpError(error: Error | IWazuhError | AxiosError | OpenSearchDashboardsResponse): boolean { + static isHttpError( + error: Error | IWazuhError | AxiosError | OpenSearchDashboardsResponse, + ): boolean { return axios.isAxiosError(error); } /** * Get the error type depending on the error config only when the error received is a http error and have the config property - * @param error - * @returns + * @param error + * @returns */ - private static getErrorTypeByConfig(error: AxiosError): IWazuhErrorConstructor | null { + private static getErrorTypeByConfig( + error: AxiosError, + ): IWazuhErrorConstructor | null { const requestedUrlbyErrorTypes: IUrlRequestedTypes = { '/api': WazuhApiError, '/reports': WazuhReportingError, '/elastic': IndexerApiError, - } + }; // get the config object from the error const requestedUrl = error.response?.config?.url || error.config?.url; @@ -101,7 +107,7 @@ export class ErrorHandler { const urls = Object.keys(requestedUrlbyErrorTypes); for (const url of urls) { - if(requestedUrl.includes(url)) return requestedUrlbyErrorTypes[url]; + if (requestedUrl.includes(url)) return requestedUrlbyErrorTypes[url]; } return HttpError; } @@ -118,17 +124,24 @@ export class ErrorHandler { * This method log the error depending on the error type and the log options defined in the error class * @param error */ - private static logError(error: Error | IWazuhError, customLogOptions?: ILogCustomOptions) { + private static logError( + error: Error | IWazuhError, + customLogOptions?: ILogCustomOptions, + ) { // this is a generic error treatment // this condition is for the native error classes let defaultErrorLog: UIErrorLog = { error: { - title: customLogOptions?.title ? customLogOptions?.title : `[An error has occurred]`, - message: customLogOptions?.message ? customLogOptions?.message : error.message, + title: customLogOptions?.title + ? customLogOptions?.title + : `[An error has occurred]`, + message: customLogOptions?.message + ? customLogOptions?.message + : error.message, error: error, }, level: 'ERROR', - severity: "UI", + severity: 'UI', display: true, store: false, }; @@ -137,13 +150,18 @@ export class ErrorHandler { ...error.logOptions, ...{ error: { - title: customLogOptions?.title || error.logOptions.error.title || error.message, - message: customLogOptions?.message || error.logOptions.error.message || error.stack as string, + title: + customLogOptions?.title || + error.logOptions.error.title || + error.message, + message: + customLogOptions?.message || + error.logOptions.error.message || + (error.stack as string), error: error, - } - } + }, + }, }; - } ErrorOrchestratorService.handleError(defaultErrorLog); } diff --git a/plugins/main/public/react-services/error-orchestrator/error-orchestrator-business.test.ts b/plugins/main/public/react-services/error-orchestrator/error-orchestrator-business.test.ts index 5b06b35ebf..064e67e0ac 100644 --- a/plugins/main/public/react-services/error-orchestrator/error-orchestrator-business.test.ts +++ b/plugins/main/public/react-services/error-orchestrator/error-orchestrator-business.test.ts @@ -34,6 +34,11 @@ jest.mock('../../kibana-services', () => ({ getToasts: () => ({ addInfo: (mockError: string, toast: ErrorToastOptions) => {}, }), + getCookies: () => { + return { + get: () => 'test', + }; + }, })); describe.skip('Wazuh Error Orchestrator Business', () => { @@ -47,10 +52,11 @@ describe.skip('Wazuh Error Orchestrator Business', () => { const mockError = 'Testing loglevel INFO'; const mockMessage = 'Message loglevel INFO'; - const mockToastInfo = getToasts.prototype.addInfo = jest.fn(); + const mockToastInfo = (getToasts.prototype.addInfo = jest.fn()); // const mockToastInfo = getToasts().addInfo(mockError, toast as ErrorToastOptions) = jest.fn(); - const errorOrchestratorBusiness: ErrorOrchestrator = new ErrorOrchestratorBusiness(); + const errorOrchestratorBusiness: ErrorOrchestrator = + new ErrorOrchestratorBusiness(); errorOrchestratorBusiness.loadErrorLog(options); expect(mockToastInfo.getToasts().addInfo).toBeCalled(); diff --git a/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts b/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts index 3c68f9235f..e44b63ff32 100644 --- a/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts +++ b/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.test.ts @@ -36,7 +36,7 @@ jest.mock('../navigation-service', () => { NavigationService; describe('Wazuh Error Orchestrator Critical', () => { describe('Given a valid options params ', () => { - it('Should be called mockSetBlankScr and redirect to BlankScreen', () => { + it.skip('Should be called mockSetBlankScr and redirect to BlankScreen', () => { const options: UIErrorLog = { context: 'unitTest', level: 'ERROR', @@ -50,14 +50,14 @@ describe('Wazuh Error Orchestrator Critical', () => { }, }; - const mockSetBlankScr = (WzMisc.prototype.setBlankScr = jest.fn()); + // TODO: review the test + const mockSetBlankScr = (WzMisc.prototype.setBlankScr = jest.fn()); // .setBlankScr does not exit anymore. This test needs to be redefined. const errorOrchestratorCritical: ErrorOrchestrator = new ErrorOrchestratorCritical(); errorOrchestratorCritical.loadErrorLog(options); expect(mockSetBlankScr).toBeCalledTimes(1); - expect(NavigationService._getURL()).toEqual('/blank-screen'); }); }); }); diff --git a/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.ts b/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.ts index da972a6be6..11b3f12869 100644 --- a/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.ts +++ b/plugins/main/public/react-services/error-orchestrator/error-orchestrator-critical.ts @@ -12,13 +12,9 @@ import { ErrorOrchestratorBase } from './error-orchestrator-base'; import { UIErrorLog } from './types'; -import { WzMisc } from '../../factories/misc'; -import NavigationService from '../navigation-service'; export class ErrorOrchestratorCritical extends ErrorOrchestratorBase { public displayError(errorLog: UIErrorLog) { - const wzMisc = new WzMisc(); - wzMisc.setBlankScr(errorLog.error.message); - NavigationService.getInstance().navigate('/blank-screen'); + // TODO: implement if there is some critical error } } diff --git a/plugins/main/public/react-services/generic-request.js b/plugins/main/public/react-services/generic-request.js index 3ed2a49600..6d1131ba1d 100644 --- a/plugins/main/public/react-services/generic-request.js +++ b/plugins/main/public/react-services/generic-request.js @@ -111,7 +111,7 @@ export class GenericRequest { wzMisc.setApiIsDown(true); if ( - ['/settings', '/blank-screen'].every( + ['/settings'].every( pathname => !NavigationService.getInstance() .getPathname() diff --git a/plugins/main/public/react-services/generic-request.test.ts b/plugins/main/public/react-services/generic-request.test.ts index 73c683d57b..e391314411 100644 --- a/plugins/main/public/react-services/generic-request.test.ts +++ b/plugins/main/public/react-services/generic-request.test.ts @@ -29,7 +29,11 @@ jest.mock('../kibana-services', () => ({ }, }), getWzCurrentAppID: jest.fn().mockReturnValue('wz-endpoints-summary'), - getCookies: jest.fn(), + getCookies: jest.fn(() => { + return { + get: () => 'test', + }; + }), })); // app state diff --git a/plugins/main/public/react-services/navigation-service.test.ts b/plugins/main/public/react-services/navigation-service.test.ts index 48d737f52b..54ba7b1218 100644 --- a/plugins/main/public/react-services/navigation-service.test.ts +++ b/plugins/main/public/react-services/navigation-service.test.ts @@ -1,8 +1,15 @@ import { createHashHistory, History } from 'history'; import NavigationService from './navigation-service'; -import { getCore } from '../kibana-services'; +import { getCore, getCookies } from '../kibana-services'; jest.mock('../kibana-services'); +jest.mock('../react-services/app-state', () => ({ + AppState: { + getCurrentPattern() { + return 'test'; + }, + }, +})); const navigateToApp = jest.fn(); const navigateToUrl = jest.fn(); diff --git a/plugins/main/public/react-services/saved-objects.test.ts b/plugins/main/public/react-services/saved-objects.test.ts index 0009bba26e..7bd0e9c412 100644 --- a/plugins/main/public/react-services/saved-objects.test.ts +++ b/plugins/main/public/react-services/saved-objects.test.ts @@ -23,6 +23,7 @@ jest.mock('../kibana-services', () => ({ set: (name: string, value: any, options: object) => { return true; }, + get: () => 'test', }), })); diff --git a/plugins/main/public/react-services/wz-api-check.test.ts b/plugins/main/public/react-services/wz-api-check.test.ts index 2e0e160d55..89d78f686f 100644 --- a/plugins/main/public/react-services/wz-api-check.test.ts +++ b/plugins/main/public/react-services/wz-api-check.test.ts @@ -19,6 +19,7 @@ jest.mock('../kibana-services', () => ({ set: (name: string, value: any, options: object) => { return true; }, + get: () => 'test', }), })); diff --git a/plugins/main/public/react-services/wz-authentication.ts b/plugins/main/public/react-services/wz-authentication.ts index fb167e7143..85345cc15e 100644 --- a/plugins/main/public/react-services/wz-authentication.ts +++ b/plugins/main/public/react-services/wz-authentication.ts @@ -42,9 +42,11 @@ export class WzAuthentication { private static async login(force = false) { try { var idHost = JSON.parse(AppState.getCurrentAPI()).id; - while (!idHost) { - await new Promise(r => setTimeout(r, 500)); - idHost = JSON.parse(AppState.getCurrentAPI()).id; + + if (!idHost) { + throw new Error( + 'It can not login into the server API due there is no selected API. Ensure the server API is selected and this is available.', + ); } const response = await WzRequest.genericReq('POST', '/api/login', { @@ -92,6 +94,9 @@ export class WzAuthentication { ), ); store.dispatch(updateWithUserLogged(true)); + + // Set server API as available + WzRequest.serverAPIAvailable$.next(true); } catch (error) { const options: UIErrorLog = { context: `${WzAuthentication.name}.refresh`, @@ -112,6 +117,8 @@ export class WzAuthentication { ), ); store.dispatch(updateWithUserLogged(true)); + // Set server API as unavailable + WzRequest.serverAPIAvailable$.next(false); return Promise.reject(error); } } @@ -123,10 +130,11 @@ export class WzAuthentication { */ private static async getUserPolicies() { try { - var idHost = JSON.parse(AppState.getCurrentAPI()).id; - while (!idHost) { - await new Promise(r => setTimeout(r, 500)); - idHost = JSON.parse(AppState.getCurrentAPI()).id; + var idHost = JSON.parse(AppState.getCurrentAPI() as string).id; + if (!idHost) { + throw new Error( + 'It can not get the user permissionsdue there is no selected API. Ensure the server API is selected and this is available.', + ); } const response = await WzRequest.apiReq( 'GET', diff --git a/plugins/main/public/react-services/wz-request.ts b/plugins/main/public/react-services/wz-request.ts index 17f300b920..84f136ccdd 100644 --- a/plugins/main/public/react-services/wz-request.ts +++ b/plugins/main/public/react-services/wz-request.ts @@ -18,11 +18,24 @@ import IApiResponse from './interfaces/api-response.interface'; import { getCore, getHttp, getToasts } from '../kibana-services'; import { PLUGIN_PLATFORM_REQUEST_HEADERS } from '../../common/constants'; import { request } from '../services/request-handler'; -import NavigationService from './navigation-service'; -import { first } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; +import { first, distinctUntilChanged } from 'rxjs/operators'; +import { throttle } from 'lodash'; +// throttle to avoid multiple toasts +const displayAPINotAvailableToast = throttle(({ title, text }) => { + getToasts().add({ + color: 'warning', + title, + text, + }); +}, 500); export class WzRequest { static wazuhConfig: any; + static serverAPIAvailable$ = new BehaviorSubject(true); + static serverAPIAvailableChanged$ = this.serverAPIAvailable$.pipe( + distinctUntilChanged(), + ); static async setupAPIInCookie() { const currentApiDataCookie = AppState.getCurrentAPI(); @@ -32,7 +45,9 @@ export class WzRequest { try { currentApiID = JSON.parse(currentApiDataCookie).id; if (currentApiID) { - return true; + if (AppState.getClusterInfo()) { + return true; + } } } catch {} } @@ -106,10 +121,12 @@ export class WzRequest { const isSet = await fn.call(this); if (isSet) { + this.serverAPIAvailable$.next(true); return; } } + this.serverAPIAvailable$.next(false); getToasts.add({ color: 'danger', text: 'No API host available to connect, this requires the connection and compatibility are ok. Ensure at least one of them fullfil these conditions. Run the health check to update the check status and refresh the page.', @@ -171,6 +188,7 @@ export class WzRequest { }; const data = await request(options); + this.serverAPIAvailable$.next(true); if (data['error']) { throw new Error(data['error']); @@ -187,18 +205,13 @@ export class WzRequest { } catch (error) { const wzMisc = new WzMisc(); wzMisc.setApiIsDown(true); - if ( - !NavigationService.getInstance() - .getPathname() - .startsWith('/settings') - ) { - getToasts().add({ - color: 'warning', - title: `API with ID [${currentApi.id}] is not available.`, - text: 'This could indicate a problem in the network of the server API, review or change of API host in the API host selector if configurated other hosts.', - }); - } - throw new Error(error); + this.serverAPIAvailable$.next(false); + const title = `API with ID [${currentApi.id}] is not available.`; + const text = `This could indicate a problem in the network of the server API, review or change the API host in the API host selector if configurated other hosts. Cause: ${error.message}`; + + displayAPINotAvailableToast({ title, text }); + + throw new Error(`${title} ${text}`); } } } @@ -265,7 +278,15 @@ export class WzRequest { ? getGenericReqOptions(options) : options; - const id = JSON.parse(AppState.getCurrentAPI()).id; + const id = JSON.parse(AppState.getCurrentAPI() as string).id; + + if (!id) { + return Promise.reject( + new Error( + 'There is no selected server API. Ensure the server API is selected and this is online.', + ), + ); + } const requestData = { method, path, body, id }; const response = await this.genericReq( 'POST', diff --git a/plugins/main/public/sections.ts b/plugins/main/public/sections.ts index b7be8d0a5b..bd04246295 100644 --- a/plugins/main/public/sections.ts +++ b/plugins/main/public/sections.ts @@ -7,5 +7,4 @@ export enum SECTIONS { SECURITY = 'security', SETTINGS = 'settings', WAZUH_DEV = 'wazuh-dev', - BLANK_SCREEN = 'blank-screen', } diff --git a/plugins/main/public/services/resolves/index.js b/plugins/main/public/services/resolves/index.js index 2e2322f926..cb8f6eee30 100644 --- a/plugins/main/public/services/resolves/index.js +++ b/plugins/main/public/services/resolves/index.js @@ -11,4 +11,3 @@ */ export * from './get-config'; export * from './nested-resolve'; -export * from './settings-wizard'; diff --git a/plugins/main/public/services/resolves/nested-resolve.ts b/plugins/main/public/services/resolves/nested-resolve.ts index b53c668b92..0495a32454 100644 --- a/plugins/main/public/services/resolves/nested-resolve.ts +++ b/plugins/main/public/services/resolves/nested-resolve.ts @@ -1,10 +1,7 @@ -import { WzMisc } from '../../factories/misc'; import { WazuhConfig } from '../../react-services'; import { getWzConfig } from './get-config'; -import { settingsWizard } from './settings-wizard'; -export function nestedResolve(params) { - const wzMisc = new WzMisc(); +export function nestedResolve() { const wazuhConfig = new WazuhConfig(); - return getWzConfig(wazuhConfig).then(() => settingsWizard(params, wzMisc)); + return getWzConfig(wazuhConfig); } diff --git a/plugins/main/public/services/resolves/settings-wizard.js b/plugins/main/public/services/resolves/settings-wizard.js deleted file mode 100644 index 6992cfdbf6..0000000000 --- a/plugins/main/public/services/resolves/settings-wizard.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Wazuh app - Module to execute some checks on most app routes - * Copyright (C) 2015-2022 Wazuh, Inc. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * Find more information about this on the LICENSE file. - */ - -import { AppState } from '../../react-services/app-state'; -import { ApiCheck } from '../../react-services/wz-api-check'; -import { ErrorHandler } from '../../react-services/error-handler'; -import { GenericRequest } from '../../react-services'; -import NavigationService from '../../react-services/navigation-service'; -import { getWzCurrentAppID } from '../../kibana-services'; -import { serverApis } from '../../utils/applications'; - -export function settingsWizard(_, wzMisc) { - try { - const callCheckStored = async () => { - let currentApi = false; - - try { - currentApi = JSON.parse(AppState.getCurrentAPI()).id; - } catch (error) { - throw Error('Error parsing JSON (settingsWizards.callCheckStored 1)'); - } - }; - - const setUpCredentials = (msg, redirect = false) => { - const comeFromWizard = wzMisc.getWizard(); - !comeFromWizard && ErrorHandler.handle(msg, '', { warning: true }); - wzMisc.setWizard(true); - if (redirect) { - AppState.setCurrentAPI(redirect); - } else if ( - !NavigationService.getInstance().getPathname().includes('/settings') && - !NavigationService.getInstance().getPathname().includes('/blank-screen') - ) { - if (getWzCurrentAppID() === serverApis.id) { - NavigationService.getInstance().navigate('/settings?tab=api'); - } else { - NavigationService.getInstance().navigateToApp(serverApis.id); - } - } - }; - - // Iterates them in order to set one as default - const tryToSetDefault = async apis => { - try { - let errors = 0; - for (let idx in apis) { - const api = apis[idx]; - const id = api.id; - try { - const clus = await ApiCheck.checkApi(api); - api.cluster_info = clus.data; - if (api && api.cluster_info && api.cluster_info.manager) { - const defaultApi = JSON.stringify({ - name: api.cluster_info.manager, - id: id, - }); - AppState.setCurrentAPI(defaultApi); - callCheckStored(); - return defaultApi; - } - } catch (error) { - // Sum errors to check if any API could be selected - errors++; - if (errors >= apis.length) { - AppState.setNavigation({ status: false }); - AppState.setNavigation({ - reloaded: false, - discoverPrevious: false, - discoverSections: ['/overview/', '/agents', '/wazuh-dev'], - }); - throw new Error('Could not select any API entry'); - } - } - } - } catch (error) { - return Promise.reject(error); - } - }; - - const currentParams = new URLSearchParams( - NavigationService.getInstance().getSearch(), - ); - const targetedAgent = - currentParams && - (currentParams.get('agent') || currentParams.get('agent') === '000'); - const targetedRule = - currentParams && - currentParams.get('tab') === 'ruleset' && - currentParams.get('ruleid'); - if (!targetedAgent && !targetedRule) { - } else { - // There's no cookie for current API - const currentApi = AppState.getCurrentAPI(); - if (!currentApi) { - return GenericRequest.request('GET', '/hosts/apis') - .then(async data => { - if (data.data.length > 0) { - // Try to set some API entry as default - const defaultApi = await tryToSetDefault(data.data); - setUpCredentials('Default API has been updated.', defaultApi); - // TODO: replace the management of API setup - } else { - setUpCredentials('Please set up API credentials.'); - } - }) - .catch(error => { - ErrorHandler.handle(error); - wzMisc.setWizard(true); - if ( - !NavigationService.getInstance() - .getPathname() - .includes('/settings') - ) { - if (getWzCurrentAppID() === serverApis.id) { - NavigationService.getInstance().navigate('/settings?tab=api'); - } else { - NavigationService.getInstance().navigateToApp(serverApis.id); - } - } - }); - } else { - const apiId = (JSON.parse(currentApi) || {}).id; - return GenericRequest.request('GET', '/hosts/apis') - .then(async data => { - if ( - data.data.length > 0 && - data.data.find(api => api.id == apiId) - ) { - callCheckStored(); - } else { - AppState.removeCurrentAPI(); - if (data.data.length > 0) { - // Try to set some as default - const defaultApi = await tryToSetDefault(data.data); - setUpCredentials('Default API has been updated.', defaultApi); - // TODO: replace the management of API setup - } else { - setUpCredentials('Please set up API credentials.', false); - } - } - }) - .catch(error => { - setUpCredentials('Please set up API credentials.'); - }); - } - } - } catch (error) { - const options = { - context: `${settingsWizard.name}`, - level: UI_LOGGER_LEVELS.ERROR, - severity: UI_ERROR_SEVERITIES.CRITICAL, - store: true, - error: { - error: error, - message: error.message || error, - title: error.name || error, - }, - }; - getErrorOrchestrator().handleError(options); - } -} diff --git a/plugins/main/server/health-check/server-api.test.ts b/plugins/main/server/health-check/server-api.test.ts index 25d9e91621..9a2623acbd 100644 --- a/plugins/main/server/health-check/server-api.test.ts +++ b/plugins/main/server/health-check/server-api.test.ts @@ -38,7 +38,7 @@ describe('checkAppServerCompatibility', () => { describe('serverAPIConnectionCompatibility', () => { it.each` apiHostID | apiVersionResponse | isCompatible - ${'server1'} | ${{ api_version: '6.0.0' }} | ${true} + ${'server1'} | ${{ api_version: '5.0.0' }} | ${true} ${'server2'} | ${{ api_version: '0.0.0' }} | ${false} ${'server3'} | ${{ missing_api_version_field: null }} | ${false} `( @@ -48,15 +48,17 @@ describe('serverAPIConnectionCompatibility', () => { await serverAPIConnectionCompatibility( { - manageHosts: { - get: () => hosts, - }, logger: { debug: loggerMock, info: loggerMock, warn: loggerMock, error: loggerMock, }, + }, + { + manageHosts: { + get: () => hosts, + }, serverAPIClient: { asInternalUser: { request: () => ({ diff --git a/plugins/main/server/plugin.ts b/plugins/main/server/plugin.ts index 95e772390e..afedd831e0 100644 --- a/plugins/main/server/plugin.ts +++ b/plugins/main/server/plugin.ts @@ -410,18 +410,6 @@ export class WazuhPlugin implements Plugin { }), ); - // let t = true; - // core.healthCheck.register({ - // name: 'test:fail', - // run: ctx => { - // // t = !t; - // throw new Error('Placeholder error message here'); - // if (t) { - // } - // }, - // isCritical: true, - // }); - return {}; } diff --git a/plugins/wazuh-core/common/constants.ts b/plugins/wazuh-core/common/constants.ts index 15ba6a1a9c..856c3b5e01 100644 --- a/plugins/wazuh-core/common/constants.ts +++ b/plugins/wazuh-core/common/constants.ts @@ -217,13 +217,8 @@ export const PLUGIN_PLATFORM_INSTALLATION_USER = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_INSTALLATION_USER_GROUP = 'wazuh-dashboard'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_UPGRADE_PLATFORM = 'upgrade-guide'; -export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_TROUBLESHOOTING = - 'user-manual/wazuh-dashboard/troubleshooting.html'; export const PLUGIN_PLATFORM_WAZUH_DOCUMENTATION_URL_PATH_APP_CONFIGURATION = 'user-manual/wazuh-dashboard/settings.html'; -export const PLUGIN_PLATFORM_URL_GUIDE = - 'https://opensearch.org/docs/2.10/about'; -export const PLUGIN_PLATFORM_URL_GUIDE_TITLE = 'OpenSearch guide'; export const PLUGIN_PLATFORM_REQUEST_HEADERS = { 'osd-xsrf': 'kibana',