Skip to content

Commit

Permalink
Add frontend state service (#7037)
Browse files Browse the repository at this point in the history
* feat(state): create state core service

- Create state service based on AppState from main plugin
- Create state containers
  - server_host
  - server_host_cluster_info
  - data_source_alerts
- Add documentation for state service

* chore: remove unused methods and no-effect functions

- Remove unused methods:
  - AppState.setCreatedAt
  - AppState.getCreatedAt
  - AppState.getAPISelector
  - AppState.getPatternSelector
  - AppState.setPatternSelector (no-effect)

- Remove usage of AppState.setPatternSelector because this has no
  effect in the current application

* feat(state): add tests

* feat(state): wrap the subscrition handler to allow they can unsubscribed when the plugin stops

- Remove console.log
- Dispatch new values when remove from the state containers

* fix(state): enhance README.md examples

* Apply suggestions from code review

Co-authored-by: Guido Modarelli <[email protected]>

* fix(state): typos

* fix(state): minor fixes

* fix(state): unsubscribe from state container

* fix: upgrade react-cookie dependency to remove vulnerability in package

* Apply suggestions from code review

Co-authored-by: Guido Modarelli <[email protected]>

* fix(state): lint and prettier

* fix(state): lint and prettier

* fix(core): dependencies

* fix(settings): improve types for SettingsComponent props

* fix(eslint): disable unicorn/no-static-only-class rule

* fix(types): improve type annotations for decoratorCheckIsEnabled callback function in index-patterns.ts

* fix(eslint): remove redundant eslint directive for unicorn/no-static-only-class in app-state.js

* fix(types): set default type parameters for State and LifecycleService interfaces in state and services types files

* fix(eslint): remove unnecessary eslint directive for unicorn/no-static-only-class in wz-api-check.js

* fix(eslint): add consistent-function-scoping rule to enforce function scoping standards across the project

* fix(eslint): replace Promise.reject with throw for error handling in wz-api-check.js to improve code readability and flow

* fix(eslint): update type definition for WrappedComponent in createHOCs for better clarity and type safety in creator.tsx

* fix(types): update remove method type in StateContainer for improved type safety in state management functionalities

* fix(types): update createHooks to handle optional updater$ for better robustness in state management functionality

* fix(logging): improve error handling by explicitly casting error to Error for better clarity in data source alerts handling

* fix(logging): enhance error logging by explicitly casting to Error for clearer messages in server host cluster info handling

* fix(types): improve typing for set method and enhance error logging clarity with explicit Error casting in server host state management

* fix: optimizing error due to usage of number separator

* feat(state): replace state getters in http server

* feat(state): enhance state containers emitting the errors

* feat(state): enhance types

* fix: typo

* fix(initialization): get username in user endpoint

---------

Co-authored-by: Guido Modarelli <[email protected]>
Co-authored-by: Guido Modarelli <[email protected]>
  • Loading branch information
3 people authored Jan 7, 2025
1 parent 09dd139 commit 4aa5ab8
Show file tree
Hide file tree
Showing 24 changed files with 1,492 additions and 322 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ module.exports = {
/* -------------------------------------------------------------------------- */
/* unicorn */
/* -------------------------------------------------------------------------- */
'unicorn/consistent-function-scoping': [
'error',
{
checkArrowFunctions: false,
},
],
'unicorn/no-static-only-class': 'off',
'unicorn/prefer-module': 'off',
'unicorn/prefer-ternary': 'off',
'unicorn/numeric-separators-style': 'off',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,22 @@ jest.mock('../../../components/common/hooks', () => ({
}));

jest.mock('../services', () => ({
checkPatternService: appInfo => () => undefined,
checkTemplateService: appInfo => () => undefined,
checkApiService: appInfo => () => undefined,
checkSetupService: appInfo => () => undefined,
checkFieldsService: appInfo => () => undefined,
checkPluginPlatformSettings: appInfo => () => undefined,
checkPatternSupportService: appInfo => () => undefined,
checkIndexPatternService: appInfo => () => undefined,
checkPatternService: appInfo => () => {},
checkTemplateService: appInfo => () => {},
checkApiService: appInfo => () => {},
checkSetupService: appInfo => () => {},
checkFieldsService: appInfo => () => {},
checkPluginPlatformSettings: appInfo => () => {},
checkPatternSupportService: appInfo => () => {},
checkIndexPatternService: appInfo => () => {},
}));

jest.mock('../components/check-result', () => ({
CheckResult: () => <></>,
}));

jest.mock('../../../react-services', () => ({
AppState: {
setPatternSelector: () => {},
},
AppState: {},
ErrorHandler: {
handle: error => error,
},
Expand Down Expand Up @@ -123,6 +121,7 @@ describe('Health Check container', () => {
]); // invoke is wrapped with act to await for setState

const callOutError = component.find('EuiCallOut');

expect(callOutError.text()).toBe('[API version] Test error');
});

Expand All @@ -134,6 +133,7 @@ describe('Health Check container', () => {
]);

const callOutWarning = component.find('EuiCallOut');

expect(callOutWarning.text()).toBe('[API version] Test warning');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import {
EuiCallOut,
EuiDescriptionList,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import React, { Fragment, useState, useEffect, useRef } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { AppState, ErrorHandler } from '../../../react-services';
import { compose } from 'redux';
import { ErrorHandler } from '../../../react-services';
import {
useAppConfig,
useRouterSearch,
Expand All @@ -33,7 +35,7 @@ import {
} from '../services';
import { CheckResult } from '../components/check-result';
import { withErrorBoundary, withRouteResolvers } from '../../common/hocs';
import { getCore, getHttp, getWzCurrentAppID } from '../../../kibana-services';
import { getCore, getHttp } from '../../../kibana-services';
import {
HEALTH_CHECK_REDIRECTION_TIME,
WAZUH_INDEX_TYPE_MONITORING,
Expand All @@ -43,7 +45,6 @@ import { getThemeAssetURL, getAssetURL } from '../../../utils/assets';
import { serverApis } from '../../../utils/applications';
import { RedirectAppLinks } from '../../../../../../src/plugins/opensearch_dashboards_react/public';
import { ip, wzConfig } from '../../../services/resolves';
import { compose } from 'redux';
import NavigationService from '../../../react-services/navigation-service';

const checks = {
Expand Down Expand Up @@ -94,12 +95,51 @@ const checks = {
},
};

const addTagsToUrl = (error: string) => {
const words = error.split(' ');

for (const [index, word] of words.entries()) {
if (word.includes('http://') || word.includes('https://')) {
if (words[index - 1] === 'guide:') {
if (word.endsWith('.') || word.endsWith(',')) {
words[index - 2] = `<a href="${word.slice(
0,
-1,
)}" rel="noopener noreferrer" target="_blank">${
words[index - 2]
} ${words[index - 1].slice(0, -1)}</a>${word.slice(-1)}`;
} else {
words[index - 2] =
`<a href="${word}" rel="noopener noreferrer" target="_blank">${
words[index - 2]
} ${words[index - 1].slice(0, -1)}</a> `;
}

words.splice(index - 1, 2);
} else {
if (word.endsWith('.') || word.endsWith(',')) {
words[index] = `<a href="${word.slice(
0,
-1,
)}" rel="noopener noreferrer" target="_blank">${word.slice(
0,
-1,
)}</a>${word.slice(-1)}`;
} else {
words[index] =
`<a href="${word}" rel="noopener noreferrer" target="_blank">${word}</a>`;
}
}
}
}

return words.join(' ');
};

function HealthCheckComponent() {
const [checkWarnings, setCheckWarnings] = useState<{ [key: string]: [] }>({});
const [checkErrors, setCheckErrors] = useState<{ [key: string]: [] }>({});
const [checksReady, setChecksReady] = useState<{ [key: string]: boolean }>(
{},
);
const [checkWarnings, setCheckWarnings] = useState<Record<string, []>>({});
const [checkErrors, setCheckErrors] = useState<Record<string, []>>({});
const [checksReady, setChecksReady] = useState<Record<string, boolean>>({});
const [isDebugMode, setIsDebugMode] = useState<boolean>(false);
const appConfig = useAppConfig();
const checksInitiated = useRef(false);
Expand All @@ -119,6 +159,7 @@ function HealthCheckComponent() {
.pathname +
'?' +
searchParams;

NavigationService.getInstance().navigate(relativePath);
} else {
NavigationService.getInstance().navigate('/');
Expand All @@ -131,25 +172,23 @@ function HealthCheckComponent() {
useEffect(() => {
if (appConfig.isReady && !checksInitiated.current) {
checksInitiated.current = true;
AppState.setPatternSelector(appConfig.data['ip.selector']);
}
}, [appConfig]);

useEffect(() => {
// Redirect to app when all checks are ready
Object.keys(checks).every(check => checksReady[check]) &&
if (
Object.keys(checks).every(check => checksReady[check]) &&
!isDebugMode &&
!thereAreWarnings &&
(() =>
setTimeout(
redirectionPassHealthcheck,
HEALTH_CHECK_REDIRECTION_TIME,
))();
!thereAreWarnings
) {
setTimeout(redirectionPassHealthcheck, HEALTH_CHECK_REDIRECTION_TIME);
}
}, [checksReady]);

useEffect(() => {
// Check if Health should not redirect automatically (Debug mode)
setIsDebugMode(typeof search.debug !== 'undefined');
setIsDebugMode(search.debug !== undefined);
}, []);

const handleWarnings = (checkID, warnings, parsed) => {
Expand All @@ -161,6 +200,7 @@ function HealthCheckComponent() {
}),
)
: warnings;

setCheckWarnings(prev => ({ ...prev, [checkID]: newWarnings }));
};

Expand All @@ -173,6 +213,7 @@ function HealthCheckComponent() {
}),
)
: errors;

setCheckErrors(prev => ({ ...prev, [checkID]: newErrors }));
};

Expand All @@ -199,73 +240,32 @@ function HealthCheckComponent() {

const renderChecks = () => {
const showLogButton = thereAreErrors || thereAreWarnings || isDebugMode;
return Object.keys(checks).map((check, index) => {
return (
<CheckResult
showLogButton={showLogButton}
key={`health_check_check_${check}`}
name={check}
title={checks[check].title}
awaitFor={checks[check].awaitFor}
shouldCheck={
checks[check].shouldCheck || appConfig.data[`checks.${check}`]
}
validationService={checks[check].validator(appConfig)}
handleWarnings={handleWarnings}
handleErrors={handleErrors}
cleanWarnings={cleanWarnings}
cleanErrors={cleanErrors}
isLoading={appConfig.isLoading}
handleCheckReady={handleCheckReady}
checksReady={checksReady}
canRetry={checks[check].canRetry}
/>
);
});
};

const addTagsToUrl = error => {
const words = error.split(' ');
words.forEach((word, index) => {
if (word.includes('http://') || word.includes('https://')) {
if (words[index - 1] === 'guide:') {
if (word.endsWith('.') || word.endsWith(',')) {
words[index - 2] = `<a href="${word.slice(
0,
-1,
)}" rel="noopener noreferrer" target="_blank">${
words[index - 2]
} ${words[index - 1].slice(0, -1)}</a>${word.slice(-1)}`;
} else {
words[
index - 2
] = `<a href="${word}" rel="noopener noreferrer" target="_blank">${
words[index - 2]
} ${words[index - 1].slice(0, -1)}</a> `;
}
words.splice(index - 1, 2);
} else {
if (word.endsWith('.') || word.endsWith(',')) {
words[index] = `<a href="${word.slice(
0,
-1,
)}" rel="noopener noreferrer" target="_blank">${word.slice(
0,
-1,
)}</a>${word.slice(-1)}`;
} else {
words[
index
] = `<a href="${word}" rel="noopener noreferrer" target="_blank">${word}</a>`;
}
return Object.keys(checks).map(check => (
<CheckResult
showLogButton={showLogButton}
key={`health_check_check_${check}`}
name={check}
title={checks[check].title}
awaitFor={checks[check].awaitFor}
shouldCheck={
checks[check].shouldCheck || appConfig.data[`checks.${check}`]
}
}
});
return words.join(' ');
validationService={checks[check].validator(appConfig)}
handleWarnings={handleWarnings}
handleErrors={handleErrors}
cleanWarnings={cleanWarnings}
cleanErrors={cleanErrors}
isLoading={appConfig.isLoading}
handleCheckReady={handleCheckReady}
checksReady={checksReady}
canRetry={checks[check].canRetry}
/>
));
};

const renderWarnings = () => {
return Object.keys(checkWarnings).map(checkID =>
const renderWarnings = () =>
Object.keys(checkWarnings).map(checkID =>
checkWarnings[checkID].map((warning, index) => (
<Fragment key={index}>
<EuiCallOut
Expand All @@ -286,10 +286,8 @@ function HealthCheckComponent() {
</Fragment>
)),
);
};

const renderErrors = () => {
return Object.keys(checkErrors).map(checkID =>
const renderErrors = () =>
Object.keys(checkErrors).map(checkID =>
checkErrors[checkID].map((error, index) => (
<Fragment key={index}>
<EuiCallOut
Expand All @@ -310,7 +308,6 @@ function HealthCheckComponent() {
</Fragment>
)),
);
};

return (
<div className='health-check'>
Expand Down
Loading

0 comments on commit 4aa5ab8

Please sign in to comment.