From 10a9d9dbe045ccc18078b64fed10c1914039fde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chantal=20Bel=C3=A9n=20kelm?= <99441266+chantal-kelm@users.noreply.github.com> Date: Fri, 23 Jun 2023 09:53:25 -0300 Subject: [PATCH] 5518 inputs logic server address name password and group (#5554) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add useForm hook types * Add custom field use in useForm hook * Add some code redeability fixes * Refactored useForm types and unit tests * Move types to types file * reuse of common form on the card * Card with logic * CheckboxGroup component logic update * CheckboxGroup component logic update * Adding card icons * update checkbox logic, styles, and card styles * clean code * clean code * gitignore Mac files * updating checkbox logic, styles, and card styles * step component * Passing interfaces to a separate file, updating styles, and component logic * Update interfaces and clean up code * update of folder structure and step logic * tcp, udp, protocols, password, groups, logics * input logic server address name password groups and styles * group input logic * oscards input logic * oscards input logic * styles * regex * styles and settings * styles * various adjustments * cleaning up code and changing some styles * cleaning up code * cleaning code * update password * gitignore * gitignore * correcting validation text in input agent name * correcting validation text in input agent name * corrección de validación de input de nombre del agente * cleaning code * cleaning code * regex that differentiates between FQDN and IP * Use of PLUGIN_VERSION_SHORT * Use of PLUGIN_VERSION_SHORT * link * Revert "Merge branch '4205-redesign-add-agent-page' into 5518-inputs-logic-server-address-name-password-and-group" This reverts commit a4c6fb5d24a482e80f9595a879d141ff2d7fa5bb, reversing changes made to 5a0d2cb0e71972eb8f68b16f035ebc977220379f. * link and revert * characteres valid * correction of styles when bringing changes from parent branch * change tooltip to popover * moving validations to a separate file with their tests * corrections and cleaning of comments * camel case * change in function * type * remove type * fullWidth * type * change * conditional * change label a to Euilink * change label a to Euilink * conditional * delete usePrevious * delete usePrevious * deleted files ds store * test correction and placeholder * show architecture instead of id * removing console css warnings * fixed regex fqdn * fixed regex fqdn * data * changelog * changelog --------- Co-authored-by: Maximiliano Ibarra Co-authored-by: Maximiliano Ibarra <6089438+Machi3mfl@users.noreply.github.com> --- CHANGELOG.md | 10 + public/components/common/form/hooks.tsx | 2 +- public/components/common/form/index.tsx | 9 +- .../components/common/form/input_select.tsx | 30 ++- public/components/common/form/input_text.tsx | 27 +- public/components/common/form/types.ts | 3 +- public/controllers/agent/index.js | 2 +- .../steps/wz-manager-address.tsx | 6 +- .../components/os-card/os-card.tsx | 56 ----- .../checkbox-group/checkbox-group.scss | 19 +- .../checkbox-group/checkbox-group.test.tsx | 8 +- .../checkbox-group/checkbox-group.tsx | 19 +- .../{ => step-one}/os-card/os-card.scss | 4 + .../{ => step-one}/os-card/os-card.test.tsx | 2 +- .../components/step-one/os-card/os-card.tsx | 73 ++++++ .../components/steps-three/group-input.scss | 8 + .../components/steps-three/group-input.tsx | 96 +++++++ .../container/register-agent.tsx | 23 -- .../register-agent}/register-agent.scss | 6 +- .../register-agent}/register-agent.test.tsx | 4 +- .../register-agent/register-agent.tsx | 238 ++++++++++++++++++ .../containers/steps/steps.scss | 56 +++++ .../register-agent/containers/steps/steps.tsx | 234 +++++++++++++++++ public/controllers/register-agent/index.tsx | 2 +- .../register-agent/interfaces/types.ts | 16 ++ .../services/register-agent-services.tsx | 238 ++++++++++++++++++ .../utils/register-agent-data.tsx | 20 +- .../register-agent/utils/validations.test.tsx | 68 +++++ .../register-agent/utils/validations.tsx | 50 ++++ 29 files changed, 1185 insertions(+), 144 deletions(-) delete mode 100644 public/controllers/register-agent/components/os-card/os-card.tsx rename public/controllers/register-agent/components/{ => step-one}/checkbox-group/checkbox-group.scss (77%) rename public/controllers/register-agent/components/{ => step-one}/checkbox-group/checkbox-group.test.tsx (87%) rename public/controllers/register-agent/components/{ => step-one}/checkbox-group/checkbox-group.tsx (77%) rename public/controllers/register-agent/components/{ => step-one}/os-card/os-card.scss (94%) rename public/controllers/register-agent/components/{ => step-one}/os-card/os-card.test.tsx (90%) create mode 100644 public/controllers/register-agent/components/step-one/os-card/os-card.tsx create mode 100644 public/controllers/register-agent/components/steps-three/group-input.scss create mode 100644 public/controllers/register-agent/components/steps-three/group-input.tsx delete mode 100644 public/controllers/register-agent/container/register-agent.tsx rename public/controllers/register-agent/{container => containers/register-agent}/register-agent.scss (84%) rename public/controllers/register-agent/{container => containers/register-agent}/register-agent.test.tsx (85%) create mode 100644 public/controllers/register-agent/containers/register-agent/register-agent.tsx create mode 100644 public/controllers/register-agent/containers/steps/steps.scss create mode 100644 public/controllers/register-agent/containers/steps/steps.tsx create mode 100644 public/controllers/register-agent/interfaces/types.ts create mode 100644 public/controllers/register-agent/services/register-agent-services.tsx create mode 100644 public/controllers/register-agent/utils/validations.test.tsx create mode 100644 public/controllers/register-agent/utils/validations.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index bb65a829e1..c0d9e0b9fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to the Wazuh app project will be documented in this file. +## Wazuh v4.6.0 - OpenSearch Dashboards 2.6.0 - Revision 4500 + +### Added + +### Changed + +- Changed the deploy a new agent page from step one to step three. [#5554](https://github.com/wazuh/wazuh-kibana-app/pull/5554) [5462](https://github.com/wazuh/wazuh-kibana-app/pull/5462) + +### Fixed + ## Wazuh v4.5.0 - OpenSearch Dashboards 2.6.0 - Revision 4500 ### Added diff --git a/public/components/common/form/hooks.tsx b/public/components/common/form/hooks.tsx index be7e1a8fa8..63ff8fdb72 100644 --- a/public/components/common/form/hooks.tsx +++ b/public/components/common/form/hooks.tsx @@ -103,7 +103,7 @@ export const useForm = (fields: FormConfiguration): UseFormReturn => { Object.entries(enhanceFields as EnhancedFields) .filter(([, { error }]) => error) .map(([fieldKey, { error }]) => [fieldKey, error]), - ) as { [key: string]: string }; + ); function undoChanges() { setFormFields(state => diff --git a/public/components/common/form/index.tsx b/public/components/common/form/index.tsx index 0f267589c1..2b7cb7ec0f 100644 --- a/public/components/common/form/index.tsx +++ b/public/components/common/form/index.tsx @@ -7,10 +7,9 @@ import { InputFormSwitch } from './input_switch'; import { InputFormFilePicker } from './input_filepicker'; import { InputFormTextArea } from './input_text_area'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; -import { OsCard } from '../../../controllers/register-agent/components/os-card/os-card'; import { SettingTypes } from './types'; -interface InputFormProps { +export interface InputFormProps { type: SettingTypes; value: any; onChange: (event: React.ChangeEvent) => void; @@ -65,10 +64,6 @@ export const InputForm = ({ /> ); - if (type === 'custom') { - return ; - } - return label ? ( <> @@ -98,5 +93,5 @@ const Input = { select: InputFormSelect, text: InputFormText, textarea: InputFormTextArea, - custom: OsCard, + custom: ({ component, ...rest }) => component(rest), }; diff --git a/public/components/common/form/input_select.tsx b/public/components/common/form/input_select.tsx index a8f02e99d7..b212f3f068 100644 --- a/public/components/common/form/input_select.tsx +++ b/public/components/common/form/input_select.tsx @@ -2,12 +2,26 @@ import React from 'react'; import { EuiSelect } from '@elastic/eui'; import { IInputFormType } from './types'; -export const InputFormSelect = ({ options, value, onChange }: IInputFormType) => { - return ( - - ) +export const InputFormSelect = ({ + options, + value, + onChange, + placeholder, + selectedOptions, + isDisabled, + isClearable, + dataTestSubj, +}: IInputFormType) => { + return ( + + ); }; diff --git a/public/components/common/form/input_text.tsx b/public/components/common/form/input_text.tsx index feb0d218ee..c8e3d730d4 100644 --- a/public/components/common/form/input_text.tsx +++ b/public/components/common/form/input_text.tsx @@ -1,14 +1,21 @@ import React from 'react'; import { EuiFieldText } from '@elastic/eui'; -import { IInputFormType } from "./types"; +import { IInputFormType } from './types'; -export const InputFormText = ({ value, isInvalid, onChange }: IInputFormType) => { - return ( - - ); +export const InputFormText = ({ + value, + isInvalid, + onChange, + placeholder, + fullWidth, +}: IInputFormType) => { + return ( + + ); }; diff --git a/public/components/common/form/types.ts b/public/components/common/form/types.ts index 737c0c8ab9..301914479c 100644 --- a/public/components/common/form/types.ts +++ b/public/components/common/form/types.ts @@ -19,6 +19,7 @@ export interface IInputForm { } /// use form hook types + export type SettingTypes = | 'text' | 'textarea' @@ -54,7 +55,7 @@ interface EnhancedField { initialValue: any; value: any; changed: boolean; - error: string | null; + error: string | null | undefined; setInputRef: (reference: any) => void; inputRef: any; onChange: (event: any) => void; diff --git a/public/controllers/agent/index.js b/public/controllers/agent/index.js index c9e06604aa..51a445bb00 100644 --- a/public/controllers/agent/index.js +++ b/public/controllers/agent/index.js @@ -11,7 +11,7 @@ */ import { AgentsPreviewController } from './agents-preview'; import { AgentsController } from './agents'; -import { RegisterAgent } from '../register-agent/container/register-agent'; +import { RegisterAgent } from '../../controllers/register-agent/containers/register-agent/register-agent'; import { ExportConfiguration } from './components/export-configuration'; import { AgentsWelcome } from '../../components/common/welcome/agents-welcome'; import { Mitre } from '../../components/overview'; diff --git a/public/controllers/agent/register-agent/steps/wz-manager-address.tsx b/public/controllers/agent/register-agent/steps/wz-manager-address.tsx index 0c46c70676..8bfd679e2f 100644 --- a/public/controllers/agent/register-agent/steps/wz-manager-address.tsx +++ b/public/controllers/agent/register-agent/steps/wz-manager-address.tsx @@ -11,14 +11,14 @@ const WzManagerAddressInput = (props: Props) => { const [value, setValue] = useState(''); useEffect(() => { - if(defaultValue){ + if (defaultValue) { setValue(defaultValue); onChange(defaultValue); - }else{ + } else { setValue(''); onChange(''); } - },[]) + }, []); /** * Handles the change of the selected node IP * @param value diff --git a/public/controllers/register-agent/components/os-card/os-card.tsx b/public/controllers/register-agent/components/os-card/os-card.tsx deleted file mode 100644 index d9c0f2f00b..0000000000 --- a/public/controllers/register-agent/components/os-card/os-card.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useState } from 'react'; -import { - EuiCard, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiSpacer, - EuiCheckbox, -} from '@elastic/eui'; -import { REGISTER_AGENT_DATA } from '../../utils/register-agent-data'; -import { CheckboxGroupComponent } from '../checkbox-group/checkbox-group'; -import './os-card.scss'; - -export const OsCard = () => { - const [selectedOption, setSelectedOption] = useState( - undefined, - ); - - const handleOptionChange = (optionId: string) => { - setSelectedOption(optionId); - }; - - return ( -
- - {REGISTER_AGENT_DATA.map((data, index) => ( - - - Icon - {data.title} -
- } - display='plain' - hasBorder - onClick={() => {}} - > - {data.hr &&
} - {/* */} - - - - - ))} - - - ); -}; diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss similarity index 77% rename from public/controllers/register-agent/components/checkbox-group/checkbox-group.scss rename to public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss index 89e0e58513..ce3cc09745 100644 --- a/public/controllers/register-agent/components/checkbox-group/checkbox-group.scss +++ b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.scss @@ -1,14 +1,14 @@ .checkbox-group-container { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: 1fr 1fr; margin-top: 26px; - margin-bottom: 11px; + justify-content: center; } .checkbox-item { - width: 50%; display: flex; flex-direction: row-reverse; + align-items: center; justify-content: center; } @@ -28,7 +28,8 @@ .checkbox-item { display: flex; flex-direction: row-reverse; - justify-content: start; + // justify-content: start; + align-self: baseline; } } @@ -36,16 +37,12 @@ margin-left: 8px; font-style: normal; font-weight: 400; - font-size: 14px; + font-size: 12px; color: #343741; } - .first-card-four-items { .checkbox-item:nth-child(n + 3) { padding-top: 16px; + justify-content: center; } } - -.first-of-row { - padding-right: 17px; -} diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx similarity index 87% rename from public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx rename to public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx index c2fa3c3fb0..e2dec80399 100644 --- a/public/controllers/register-agent/components/checkbox-group/checkbox-group.test.tsx +++ b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { CheckboxGroupComponent } from './checkbox-group'; +import { CheckboxGroupComponent } from '../../step-one/checkbox-group/checkbox-group'; describe('CheckboxGroupComponent', () => { const data = ['Option 1', 'Option 2', 'Option 3']; @@ -50,6 +50,10 @@ describe('CheckboxGroupComponent', () => { fireEvent.click(checkboxItems[1]); expect(onOptionChange).toHaveBeenCalledTimes(1); - expect(onOptionChange).toHaveBeenCalledWith('option-0-1'); + expect(onOptionChange).toHaveBeenCalledWith( + expect.objectContaining({ + target: { value: `option-${cardIndex}-1` }, + }), + ); }); }); diff --git a/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx similarity index 77% rename from public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx rename to public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx index 160bcfd66a..09043dea50 100644 --- a/public/controllers/register-agent/components/checkbox-group/checkbox-group.tsx +++ b/public/controllers/register-agent/components/step-one/checkbox-group/checkbox-group.tsx @@ -2,13 +2,6 @@ import React from 'react'; import { EuiRadioGroup } from '@elastic/eui'; import './checkbox-group.scss'; -interface RegisterAgentData { - icon: string; - title: string; - hr: boolean; - architecture: string[]; -} - interface Props { data: string[]; cardIndex: number; @@ -23,14 +16,9 @@ const CheckboxGroupComponent: React.FC = ({ selectedOption, onOptionChange, }) => { - const handleOptionChange = (optionId: string) => { - onOptionChange(optionId); - }; - const isSingleArchitecture = data.length === 1; const isDoubleArchitecture = data.length === 2; const isFirstCardWithFourItems = cardIndex === 0 && data.length === 4; - return (
= ({ > {arch} handleOptionChange(id)} + onChange={(id: string) => { + onOptionChange({ target: { value: id } }); + }} />
))} @@ -59,4 +49,3 @@ const CheckboxGroupComponent: React.FC = ({ }; export { CheckboxGroupComponent }; -export type { RegisterAgentData }; diff --git a/public/controllers/register-agent/components/os-card/os-card.scss b/public/controllers/register-agent/components/step-one/os-card/os-card.scss similarity index 94% rename from public/controllers/register-agent/components/os-card/os-card.scss rename to public/controllers/register-agent/components/step-one/os-card/os-card.scss index d4d3b41649..364734dc6b 100644 --- a/public/controllers/register-agent/components/os-card/os-card.scss +++ b/public/controllers/register-agent/components/step-one/os-card/os-card.scss @@ -48,3 +48,7 @@ .last-card { margin-right: 63px; } + +.cardsCallOut { + margin-top: 16px; +} diff --git a/public/controllers/register-agent/components/os-card/os-card.test.tsx b/public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx similarity index 90% rename from public/controllers/register-agent/components/os-card/os-card.test.tsx rename to public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx index da1a61c120..ebb927853d 100644 --- a/public/controllers/register-agent/components/os-card/os-card.test.tsx +++ b/public/controllers/register-agent/components/step-one/os-card/os-card.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import { OsCard } from './os-card'; +import { OsCard } from '../../step-one/os-card/os-card'; describe('OsCard', () => { test('renders three cards with different titles', () => { diff --git a/public/controllers/register-agent/components/step-one/os-card/os-card.tsx b/public/controllers/register-agent/components/step-one/os-card/os-card.tsx new file mode 100644 index 0000000000..c1db7610a3 --- /dev/null +++ b/public/controllers/register-agent/components/step-one/os-card/os-card.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiLink, + EuiCheckbox, +} from '@elastic/eui'; +import { REGISTER_AGENT_DATA_STEP_ONE } from '../../../utils/register-agent-data'; +import { CheckboxGroupComponent } from '../../step-one/checkbox-group/checkbox-group'; +import './os-card.scss'; +import { webDocumentationLink } from '../../../../../../common/services/web_documentation'; + +interface Props { + setStatusCheck: string; + onChange: React.ChangeEventHandler; +} + +export const OsCard = ({ onChange, value }: Props) => { + return ( +
+ + {REGISTER_AGENT_DATA_STEP_ONE.map((data, index) => ( + + + Icon + {data.title} +
+ } + display='plain' + hasBorder + onClick={() => {}} + className='card' + > + {data.hr &&
} + + + + ))} + + + For additional systems and architectures, please check our{' '} + + steps + + . + + } + > + + ); +}; diff --git a/public/controllers/register-agent/components/steps-three/group-input.scss b/public/controllers/register-agent/components/steps-three/group-input.scss new file mode 100644 index 0000000000..e69348c07b --- /dev/null +++ b/public/controllers/register-agent/components/steps-three/group-input.scss @@ -0,0 +1,8 @@ +.groupTitle { + margin-top: '32px'; + flex-direction: 'row'; + font-style: normal; + font-weight: 700; + font-size: 12px; + line-height: 20px; +} diff --git a/public/controllers/register-agent/components/steps-three/group-input.tsx b/public/controllers/register-agent/components/steps-three/group-input.tsx new file mode 100644 index 0000000000..2878738617 --- /dev/null +++ b/public/controllers/register-agent/components/steps-three/group-input.tsx @@ -0,0 +1,96 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiComboBox, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; + +const popoverAgentGroup = ( + + Learn about{' '} + + Select a group. + + +); + +const GroupInput = ({ value, options, onChange }) => { + const [isPopoverAgentGroup, setIsPopoverAgentGroup] = useState(false); + + const onButtonAgentGroup = () => + setIsPopoverAgentGroup(isPopoverAgentGroup => !isPopoverAgentGroup); + const closeAgentGroup = () => setIsPopoverAgentGroup(false); + return ( + <> + + + + Select one or more existing groups + + } + isOpen={isPopoverAgentGroup} + closePopover={closeAgentGroup} + anchorPosition='rightCenter' + > + {popoverAgentGroup} + + + + { + onChange({ + target: { value: group }, + }); + }} + isDisabled={!options?.groups.length} + isClearable={true} + data-test-subj='demoComboBox' + data-testid='group-input-combobox' + /> + {!options?.groups.length && ( + <> + + + )} + + ); +}; + +export default GroupInput; diff --git a/public/controllers/register-agent/container/register-agent.tsx b/public/controllers/register-agent/container/register-agent.tsx deleted file mode 100644 index 5617bf3606..0000000000 --- a/public/controllers/register-agent/container/register-agent.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, { ChangeEvent } from 'react'; -import { InputForm } from '../../../components/common/form'; -import './register-agent.scss'; - -export const RegisterAgent: React.FC = () => { - const handleChange = (event: ChangeEvent) => { - // ver - }; - - return ( -
-
Deploy new agent
- -
- ); -}; diff --git a/public/controllers/register-agent/container/register-agent.scss b/public/controllers/register-agent/containers/register-agent/register-agent.scss similarity index 84% rename from public/controllers/register-agent/container/register-agent.scss rename to public/controllers/register-agent/containers/register-agent/register-agent.scss index c127bd0e9c..a0d9bd03d7 100644 --- a/public/controllers/register-agent/container/register-agent.scss +++ b/public/controllers/register-agent/containers/register-agent/register-agent.scss @@ -1,6 +1,6 @@ .container { box-sizing: border-box; - height: 1271px; + max-height: 1271px; margin-top: 44px; background: #ffffff; border: 1px solid rgba(52, 55, 65, 0.2); @@ -20,3 +20,7 @@ display: flex; justify-content: center; } +.close { + display: flex; + margin-top: 17px; +} diff --git a/public/controllers/register-agent/container/register-agent.test.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx similarity index 85% rename from public/controllers/register-agent/container/register-agent.test.tsx rename to public/controllers/register-agent/containers/register-agent/register-agent.test.tsx index 061960686a..5f4dd452be 100644 --- a/public/controllers/register-agent/container/register-agent.test.tsx +++ b/public/controllers/register-agent/containers/register-agent/register-agent.test.tsx @@ -5,7 +5,9 @@ import { RegisterAgent } from './register-agent'; describe('RegisterAgent', () => { test('renders the component', () => { - render(); + const mockHasAgents = jest.fn(); + + render(); // Verifica que el título esté presente const titleElement = screen.getByText('Deploy new agent'); diff --git a/public/controllers/register-agent/containers/register-agent/register-agent.tsx b/public/controllers/register-agent/containers/register-agent/register-agent.tsx new file mode 100644 index 0000000000..0b89f4f450 --- /dev/null +++ b/public/controllers/register-agent/containers/register-agent/register-agent.tsx @@ -0,0 +1,238 @@ +import React, { useState, useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiTitle, + EuiButtonEmpty, + EuiPage, + EuiPageBody, + EuiSpacer, + EuiProgress, +} from '@elastic/eui'; +import { WzRequest } from '../../../../react-services/wz-request'; +import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; +import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types'; +import { ErrorHandler } from '../../../../react-services/error-management'; +import { getMasterRemoteConfiguration } from '../../../agent/components/register-agent-service'; +import './register-agent.scss'; +import { Steps } from '../steps/steps'; +import { InputForm } from '../../../../components/common/form'; +import { getGroups } from '../../services/register-agent-services'; +import { useForm } from '../../../../components/common/form/hooks'; +import { FormConfiguration } from '../../../../components/common/form/types'; +import { useSelector } from 'react-redux'; +import { withReduxProvider } from '../../../../components/common/hocs'; +import GroupInput from '../../components/steps-three/group-input'; +import { OsCard } from '../../components/step-one/os-card/os-card'; +import { + validateServerAddress, + validateAgentName, +} from '../../utils/validations'; + +export const RegisterAgent = withReduxProvider( + ({ getWazuhVersion, hasAgents, addNewAgent, reload }) => { + const configuration = useSelector( + (state: { appConfig: { data: any } }) => state.appConfig.data, + ); + + const [wazuhVersion, setWazuhVersion] = useState(''); + const [udpProtocol, setUdpProtocol] = useState(false); + const [connectionSecure, setConnectionSecure] = useState( + true, + ); + const [haveUdpProtocol, setHaveUdpProtocol] = useState( + false, + ); + const [haveConnectionSecure, setHaveConnectionSecure] = useState< + boolean | null + >(false); + const [loading, setLoading] = useState(false); + const [wazuhPassword, setWazuhPassword] = useState(''); + const [groups, setGroups] = useState([]); + const [needsPassword, setNeedsPassword] = useState(false); + const [hideTextPassword, setHideTextPassword] = useState( + false, + ); + + const initialFields: FormConfiguration = { + operatingSystemSelection: { + type: 'custom', + initialValue: '', + component: props => { + return ; + }, + options: { + groups, + }, + }, + + //IP: This is a set of four numbers, for example, 192.158.1.38. Each number in the set can range from 0 to 255. Therefore, the full range of IP addresses goes from 0.0.0.0 to 255.255.255.255 + // O ipv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 + + // FQDN: Maximum of 63 characters per label. + // Can only contain numbers, letters and hyphens (-) + // Labels cannot begin or end with a hyphen + // Currently supports multilingual characters, i.e. letters not included in the English alphabet: e.g. á é í ó ú ü ñ. + // Minimum 3 labels + + serverAddress: { + type: 'text', + initialValue: configuration['enrollment.dns'] || '', + validate: validateServerAddress, + }, + agentName: { + type: 'text', + initialValue: '', + validate: validateAgentName, + }, + + agentGroups: { + type: 'custom', + initialValue: [], + component: props => { + return ; + }, + options: { + groups, + }, + }, + }; + + const form = useForm(initialFields); + + const getRemoteConfig = async () => { + const remoteConfig = await getMasterRemoteConfiguration(); + if (remoteConfig) { + setHaveUdpProtocol(remoteConfig.isUdp); + setHaveConnectionSecure(remoteConfig.haveSecureConnection); + setUdpProtocol(remoteConfig.isUdp); + setConnectionSecure(remoteConfig.haveSecureConnection); + } + }; + + const getAuthInfo = async () => { + try { + const result = await WzRequest.apiReq( + 'GET', + '/agents/000/config/auth/auth', + {}, + ); + return (result.data || {}).data || {}; + } catch (error) { + ErrorHandler.handleError(error); + } + }; + + useEffect(() => { + const fetchData = async () => { + try { + const wazuhVersion = await getWazuhVersion(); + let wazuhPassword = ''; + let hideTextPassword = false; + await getRemoteConfig(); + const authInfo = await getAuthInfo(); + const needsPassword = (authInfo.auth || {}).use_password === 'yes'; + if (needsPassword) { + wazuhPassword = + configuration['enrollment.password'] || + authInfo['authd.pass'] || + ''; + if (wazuhPassword) { + hideTextPassword = true; + } + } + const groups = await getGroups(); + + setNeedsPassword(needsPassword); + setHideTextPassword(hideTextPassword); + setWazuhPassword(wazuhPassword); + setWazuhVersion(wazuhVersion); + setGroups(groups); + setLoading(false); + } catch (error) { + setWazuhVersion(wazuhVersion); + setLoading(false); + const options = { + context: 'RegisterAgent', + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + display: true, + store: false, + error: { + error: error, + message: error.message || error, + title: error.name || error, + }, + }; + ErrorHandler.handleError(error, options); + } + }; + + fetchData(); + }, []); + + const agentGroup = ; + const osCard = ( + + ); + + return ( +
+ + + + + +
+ {hasAgents() ? ( + addNewAgent(false)} + iconType='cross' + > + ) : ( + reload()} + iconType='refresh' + > + Refresh + + )} +
+ + + +

Deploy new agent

+
+
+
+ + {loading ? ( + <> + + + + + + ) : ( + + + + )} +
+
+
+
+
+
+ ); + }, +); diff --git a/public/controllers/register-agent/containers/steps/steps.scss b/public/controllers/register-agent/containers/steps/steps.scss new file mode 100644 index 0000000000..d18ea5c07e --- /dev/null +++ b/public/controllers/register-agent/containers/steps/steps.scss @@ -0,0 +1,56 @@ +.stepTitle { + font-style: normal; + font-weight: 700; + font-size: 16px; + letter-spacing: 0.6px; + color: #343741; + flex-direction: row; +} + +.stepSubtitleServerAddress { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + color: #343741; + margin-bottom: 9px; +} + +.stepSubtitle { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 24px; + color: #343741; + margin-bottom: 20px; +} + +.titleAndIcon { + display: flex; + flex-direction: row; +} + +.warningForAgentName { + margin-top: 10px; +} + +.euiToolTipAnchor { + margin-left: 7px; +} + +.subtitleAgentName { + flex-direction: 'row'; + font-style: 'normal'; + font-weight: 700; + font-size: '12px'; + line-height: '20px'; + color: '#343741'; +} + +.euiStep__titleWrapper { + align-items: center; +} + +.euiButtonEmpty .euiButtonEmpty__content { + padding: 0; +} diff --git a/public/controllers/register-agent/containers/steps/steps.tsx b/public/controllers/register-agent/containers/steps/steps.tsx new file mode 100644 index 0000000000..bf456eff0e --- /dev/null +++ b/public/controllers/register-agent/containers/steps/steps.tsx @@ -0,0 +1,234 @@ +import React, { Fragment, useState } from 'react'; +import { + EuiSteps, + EuiText, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiCallOut, + EuiPopover, + EuiButtonEmpty, + EuiLink, +} from '@elastic/eui'; +import { InputForm } from '../../../../components/common/form'; +import './steps.scss'; +import { + REGISTER_AGENT_DATA_STEP_THREE, + REGISTER_AGENT_DATA_STEP_TWO, +} from '../../utils/register-agent-data'; +import { webDocumentationLink } from '../../../../../common/services/web_documentation'; +import { PLUGIN_VERSION_SHORT } from '../../../../../common/constants'; + +const popoverServerAddress = ( + + Learn about{' '} + + Server address. + + +); + +const popoverAgentName = ( + + Learn about{' '} + + Assigning an agent name. + + +); + +const warningForAgentName = + 'The agent name must be unique. It can’t be changed once the agent has been enrolled.'; + +export const Steps = ({ + needsPassword, + hideTextPassword, + agentGroup, + form, + osCard, +}) => { + const [isPopoverServerAddress, setIsPopoverServerAddress] = useState(false); + const [isPopoverAgentName, setIsPopoverAgentName] = useState(false); + + const onButtonServerAddress = () => + setIsPopoverServerAddress( + isPopoverServerAddress => !isPopoverServerAddress, + ); + const closeServerAddress = () => setIsPopoverServerAddress(false); + + const onButtonAgentName = () => + setIsPopoverAgentName(isPopoverAgentName => !isPopoverAgentName); + const closeAgentName = () => setIsPopoverAgentName(false); + + const firstSetOfSteps = [ + { + title: ( + +

Select the package to download and install on your system:

+
+ ), + children: osCard, + status: form.fields.operatingSystemSelection.value + ? 'complete' + : 'current', + }, + { + title: ( + + + + Server address + + } + isOpen={isPopoverServerAddress} + closePopover={closeServerAddress} + anchorPosition='rightCenter' + > + {popoverServerAddress} + + + + ), + children: ( + + + {REGISTER_AGENT_DATA_STEP_TWO.map((data, index) => ( + + + {data.subtitle} + + + ))} + + } + fullWidth={false} + placeholder='Server address' + /> + + ), + status: !form.fields.operatingSystemSelection.value + ? 'disabled' + : !form.fields.serverAddress.value && + form.fields.operatingSystemSelection.value + ? 'current' + : form.fields.operatingSystemSelection.value && + form.fields.serverAddress.value + ? 'complete' + : '', + }, + ...(!(!needsPassword || hideTextPassword) + ? [ + { + title: ( + +

Wazuh password

+
+ ), + children: ( + + { + 'No ha establecido una contraseña. Se le asigno una por defecto' + } + + ), + }, + ] + : []), + { + title: ( + +

Optional settings

+
+ ), + children: ( + + + {REGISTER_AGENT_DATA_STEP_THREE.map((data, index) => ( + + {data.subtitle} + + ))} + + + + + + Assign an agent name + + } + isOpen={isPopoverAgentName} + closePopover={closeAgentName} + anchorPosition='rightCenter' + > + {popoverAgentName} + + + + + } + placeholder='Agent name' + /> + + {agentGroup} + + ), + status: + !form.fields.operatingSystemSelection.value || + !form.fields.serverAddress.value + ? 'disabled' + : form.fields.serverAddress.value !== '' + ? 'current' + : form.fields.agentGroups.value.length > 0 + ? 'complete' + : '', + }, + ]; + + return ; +}; diff --git a/public/controllers/register-agent/index.tsx b/public/controllers/register-agent/index.tsx index d516e34a58..146589950a 100644 --- a/public/controllers/register-agent/index.tsx +++ b/public/controllers/register-agent/index.tsx @@ -1 +1 @@ -export { RegisterAgent } from './container/register-agent'; +export { RegisterAgent } from './containers/register-agent/register-agent'; diff --git a/public/controllers/register-agent/interfaces/types.ts b/public/controllers/register-agent/interfaces/types.ts new file mode 100644 index 0000000000..06de5db134 --- /dev/null +++ b/public/controllers/register-agent/interfaces/types.ts @@ -0,0 +1,16 @@ +interface RegisterAgentData { + icon: string; + title: string; + hr: boolean; + architecture: string[]; +} + +interface CheckboxGroupComponentProps { + data: string[]; + cardIndex: number; + selectedOption: string | undefined; + onOptionChange: (optionId: string) => void; + onChange: (id: string) => void; +} + +export type { RegisterAgentData, CheckboxGroupComponentProps }; diff --git a/public/controllers/register-agent/services/register-agent-services.tsx b/public/controllers/register-agent/services/register-agent-services.tsx new file mode 100644 index 0000000000..262afedd64 --- /dev/null +++ b/public/controllers/register-agent/services/register-agent-services.tsx @@ -0,0 +1,238 @@ +import { WzRequest } from '../../../react-services/wz-request'; +import { ServerAddressOptions } from '../register-agent/steps'; + +type Protocol = 'TCP' | 'UDP'; + +type RemoteItem = { + connection: 'syslog' | 'secure'; + ipv6: 'yes' | 'no'; + protocol: Protocol[]; + allowed_ips?: string[]; + queue_size?: string; +}; + +type RemoteConfig = { + name: string; + isUdp: boolean | null; + haveSecureConnection: boolean | null; +}; + +/** + * Get the cluster status + */ +export const clusterStatusResponse = async (): Promise => { + const clusterStatus = await WzRequest.apiReq('GET', '/cluster/status', {}); + if ( + clusterStatus.data.data.enabled === 'yes' && + clusterStatus.data.data.running === 'yes' + ) { + // Cluster mode + return true; + } else { + // Manager mode + return false; + } +}; + +/** + * Get the remote configuration from api + */ +async function getRemoteConfiguration(nodeName: string): Promise { + let config: RemoteConfig = { + name: nodeName, + isUdp: false, + haveSecureConnection: false, + }; + + try { + const clusterStatus = await clusterStatusResponse(); + let result; + if (clusterStatus) { + result = await WzRequest.apiReq( + 'GET', + `/cluster/${nodeName}/configuration/request/remote`, + {}, + ); + } else { + result = await WzRequest.apiReq( + 'GET', + '/manager/configuration/request/remote', + {}, + ); + } + const items = ((result.data || {}).data || {}).affected_items || []; + const remote = items[0]?.remote; + if (remote) { + const remoteFiltered = remote.filter((item: RemoteItem) => { + return item.connection === 'secure'; + }); + + remoteFiltered.length > 0 + ? (config.haveSecureConnection = true) + : (config.haveSecureConnection = false); + + let protocolsAvailable: Protocol[] = []; + remote.forEach((item: RemoteItem) => { + // get all protocols available + item.protocol.forEach(protocol => { + protocolsAvailable = protocolsAvailable.concat(protocol); + }); + }); + + config.isUdp = + getRemoteProtocol(protocolsAvailable) === 'UDP' ? true : false; + } + return config; + } catch (error) { + return config; + } +} + +/** + * Get the remote protocol available from list of protocols + * @param protocols + */ +function getRemoteProtocol(protocols: Protocol[]) { + if (protocols.length === 1) { + return protocols[0]; + } else { + return !protocols.includes('TCP') ? 'UDP' : 'TCP'; + } +} + +/** + * Get the remote configuration from nodes registered in the cluster and decide the protocol to setting up in deploy agent param + * @param nodeSelected + * @param defaultServerAddress + */ +async function getConnectionConfig( + nodeSelected: ServerAddressOptions, + defaultServerAddress?: string, +) { + const nodeName = nodeSelected?.label; + const nodeIp = nodeSelected?.value; + if (!defaultServerAddress) { + if (nodeSelected.nodetype !== 'custom') { + const remoteConfig = await getRemoteConfiguration(nodeName); + return { + serverAddress: nodeIp, + udpProtocol: remoteConfig.isUdp, + connectionSecure: remoteConfig.haveSecureConnection, + }; + } else { + return { + serverAddress: nodeName, + udpProtocol: false, + connectionSecure: true, + }; + } + } else { + return { + serverAddress: defaultServerAddress, + udpProtocol: false, + connectionSecure: true, + }; + } +} + +type NodeItem = { + name: string; + ip: string; + type: string; +}; + +type NodeResponse = { + data: { + data: { + affected_items: NodeItem[]; + }; + }; +}; + +/** + * Get the list of the cluster nodes and parse it into a list of options + */ +export const getNodeIPs = async (): Promise => { + return await WzRequest.apiReq('GET', '/cluster/nodes', {}); +}; + +/** + * Get the list of the manager and parse it into a list of options + */ +export const getManagerNode = async (): Promise => { + const managerNode = await WzRequest.apiReq('GET', '/manager/api/config', {}); + return ( + managerNode?.data?.data?.affected_items?.map(item => ({ + label: item.node_name, + value: item.node_api_config.host, + nodetype: 'master', + })) || [] + ); +}; + +/** + * Parse the nodes list from the API response to a format that can be used by the EuiComboBox + * @param nodes + */ +export const parseNodesInOptions = ( + nodes: NodeResponse, +): ServerAddressOptions[] => { + return nodes.data.data.affected_items.map((item: NodeItem) => ({ + label: item.name, + value: item.ip, + nodetype: item.type, + })); +}; + +/** + * Get the list of the cluster nodes from API and parse it into a list of options + */ +export const fetchClusterNodesOptions = async (): Promise< + ServerAddressOptions[] +> => { + const clusterStatus = await clusterStatusResponse(); + if (clusterStatus) { + // Cluster mode + // Get the cluster nodes + const nodes = await getNodeIPs(); + return parseNodesInOptions(nodes); + } else { + // Manager mode + // Get the manager node + return await getManagerNode(); + } +}; + +/** + * Get the master node data from the list of cluster nodes + * @param nodeIps + */ +export const getMasterNode = ( + nodeIps: ServerAddressOptions[], +): ServerAddressOptions[] => { + return nodeIps.filter(nodeIp => nodeIp.nodetype === 'master'); +}; + +/** + * Get the remote configuration from manager + * This function get the config from manager mode or cluster mode + */ +export const getMasterRemoteConfiguration = async () => { + const nodes = await fetchClusterNodesOptions(); + const masterNode = getMasterNode(nodes); + return await getRemoteConfiguration(masterNode[0].label); +}; + +export { getConnectionConfig, getRemoteConfiguration }; + +export const getGroups = async () => { + try { + const result = await WzRequest.apiReq('GET', '/groups', {}); + return result.data.data.affected_items.map(item => ({ + label: item.name, + id: item.name, + })); + } catch (error) { + throw new Error(error); + } +}; diff --git a/public/controllers/register-agent/utils/register-agent-data.tsx b/public/controllers/register-agent/utils/register-agent-data.tsx index a32b9464b0..c0e4e79a94 100644 --- a/public/controllers/register-agent/utils/register-agent-data.tsx +++ b/public/controllers/register-agent/utils/register-agent-data.tsx @@ -1,9 +1,9 @@ -import { RegisterAgentData } from '../components/checkbox-group/checkbox-group'; +import { RegisterAgentData } from '../interfaces/types'; import LinuxIcon from '../../../../public/assets/images/icons/linux-icon.svg'; import WindowsIcon from '../../../../public/assets/images/icons/windows-icon.svg'; import MacIcon from '../../../../public/assets/images/icons/mac-icon.svg'; -export const REGISTER_AGENT_DATA: RegisterAgentData[] = [ +export const REGISTER_AGENT_DATA_STEP_ONE: RegisterAgentData[] = [ { icon: LinuxIcon, title: 'LINUX', @@ -23,3 +23,19 @@ export const REGISTER_AGENT_DATA: RegisterAgentData[] = [ architecture: ['Intel', 'Apple Silicon'], }, ]; + +export const REGISTER_AGENT_DATA_STEP_TWO = [ + { + title: 'Server address', + subtitle: + 'This is the address the agent uses to communicate with the Wazuh server. Enter an IP address or a fully qualified domain name (FDQN).', + }, +]; + +export const REGISTER_AGENT_DATA_STEP_THREE = [ + { + title: 'Optional settings', + subtitle: + 'The deployment sets the endpoint hostname as the agent name by default. Optionally, you can set your own name in the field below.', + }, +]; diff --git a/public/controllers/register-agent/utils/validations.test.tsx b/public/controllers/register-agent/utils/validations.test.tsx new file mode 100644 index 0000000000..e51dd972fd --- /dev/null +++ b/public/controllers/register-agent/utils/validations.test.tsx @@ -0,0 +1,68 @@ +import { validateServerAddress, validateAgentName } from './validations'; + +describe('Validations', () => { + it('should return undefined for an empty value', () => { + const result = validateServerAddress(''); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid FQDN', () => { + const validFQDN = 'example.fqdn.valid'; + const result = validateServerAddress(validFQDN); + expect(result).toBeUndefined(); + }); + + it('should return undefined for a valid IP', () => { + const validIP = '192.168.1.1'; + const result = validateServerAddress(validIP); + expect(result).toBeUndefined(); + }); + + it('should return an error message for an invalid FQDN', () => { + const invalidFQDN = 'example.fqdn'; + const result = validateServerAddress(invalidFQDN); + expect(result).toBe( + 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.', + ); + }); + + test('should return an error message for an invalid IP', () => { + const invalidIP = '999.999.999.999.999'; + const result = validateServerAddress(invalidIP); + expect(result).toBe('Not a valid IP'); + }); + + test('should return undefined for an empty value', () => { + const emptyValue = ''; + const result = validateAgentName(emptyValue); + expect(result).toBeUndefined(); + }); + + test('should return an error message for invalid format and length', () => { + const invalidAgentName = '?'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe( + 'The minimum length is 2 characters. The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid format', () => { + const invalidAgentName = 'agent$name'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe( + 'The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"', + ); + }); + + test('should return an error message for invalid length', () => { + const invalidAgentName = 'a'; + const result = validateAgentName(invalidAgentName); + expect(result).toBe('The minimum length is 2 characters.'); + }); + + test('should return an empty string for a valid agent name', () => { + const validAgentName = 'agent_name'; + const result = validateAgentName(validAgentName); + expect(result).toBe(''); + }); +}); diff --git a/public/controllers/register-agent/utils/validations.tsx b/public/controllers/register-agent/utils/validations.tsx new file mode 100644 index 0000000000..6b5a17978f --- /dev/null +++ b/public/controllers/register-agent/utils/validations.tsx @@ -0,0 +1,50 @@ +export const validateServerAddress = value => { + const isFQDN = + /^(?!-)(?!.*--)[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*(?:\.[a-zA-Z0-9áéíóúüñ]{1,63}(?:-[a-zA-Z0-9áéíóúüñ]{1,63})*){2,}$/; + + const isIP = + /^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}|(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})$/; + const numbersAndPoints = /^[0-9.]+$/; + const areLettersNumbersAndColons = /^[a-zA-Z0-9:]+$/; + const letters = /[a-zA-Z]/; + const isFQDNFormatValid = isFQDN.test(value); + const isIPFormatValid = isIP.test(value); + const areNumbersAndPoints = numbersAndPoints.test(value); + const hasLetters = letters.test(value); + const hasPoints = value.includes('.'); + + let validation = undefined; + if (value.length === 0) { + return validation; + } else if (isFQDNFormatValid && value !== '') { + return validation; // FQDN valid + } else if (isIPFormatValid && value !== '') { + return validation; // IP valid + } else if (hasPoints && hasLetters && !isFQDNFormatValid) { + return (validation = + 'Each label must have a letter or number at the beginning. The maximum length is 63 characters.'); // FQDN invalid + } else if ( + (areNumbersAndPoints || areLettersNumbersAndColons) && + !isIPFormatValid + ) { + return (validation = 'Not a valid IP'); // IP invalid + } +}; + +export const validateAgentName = value => { + if (value.length === 0) { + return undefined; + } + const regex = /^[A-Za-z.\-_,]+$/; + + const isLengthValid = value.length >= 2 && value.length <= 63; + const isFormatValid = regex.test(value); + if (!isFormatValid && !isLengthValid) { + return 'The minimum length is 2 characters. The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"'; + } else if (!isLengthValid) { + return 'The minimum length is 2 characters.'; + } else if (!isFormatValid) { + return 'The character is not valid. Allowed characters are A-Z, a-z, ".", "-", "_"'; + } + return ''; +};