diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2eb2371c9..f3fe5a64ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ All notable changes to the Wazuh app project will be documented in this file.
- Updated and added operating systems, versions, architectures commands of Install and enroll the agent and
commands of Start the agent in the deploy new agent section [#4458](https://github.com/wazuh/wazuh-kibana-app/pull/4458)
- Makes Agents Overview loading icons independent [#4363](https://github.com/wazuh/wazuh-kibana-app/pull/4363)
+- Added cluster's IP and protocol as suggestions in the agent deployment wizard. [#4776](https://github.com/wazuh/wazuh-kibana-app/pull/4776)
### Fixed
diff --git a/public/controllers/agent/components/register-agent-service.test.ts b/public/controllers/agent/components/register-agent-service.test.ts
new file mode 100644
index 0000000000..589a3f8dbb
--- /dev/null
+++ b/public/controllers/agent/components/register-agent-service.test.ts
@@ -0,0 +1,272 @@
+import * as RegisterAgentService from './register-agent-service';
+import { WzRequest } from '../../../react-services/wz-request';
+import { ServerAddressOptions } from '../register-agent/steps';
+
+jest.mock('../../../react-services', () => ({
+ ...jest.requireActual('../../../react-services') as object,
+ WzRequest: () => ({
+ apiReq: jest.fn(),
+ }),
+}));
+
+
+describe('Register agent service', () => {
+ beforeEach(() => jest.clearAllMocks());
+ describe('getRemoteConfiguration', () => {
+ it('should return secure connection = TRUE when have connection secure', async () => {
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ {
+ connection: 'secure',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '1514',
+ queue_size: '131072',
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+ const nodeName = 'example-node';
+ const res = await RegisterAgentService.getRemoteConfiguration('example-node');
+ expect(res.name).toBe(nodeName);
+ expect(res.haveSecureConnection).toBe(true);
+ });
+
+ it('should return secure connection = FALSE available when dont have connection secure', async () => {
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['UDP', 'TCP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+ const nodeName = 'example-node';
+ const res = await RegisterAgentService.getRemoteConfiguration('example-node');
+ expect(res.name).toBe(nodeName);
+ expect(res.haveSecureConnection).toBe(false);
+ });
+
+ it('should return protocols UDP when is the only connection protocol available', async () => {
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ {
+ connection: 'secure',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '1514',
+ queue_size: '131072',
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+ const nodeName = 'example-node';
+ const res = await RegisterAgentService.getRemoteConfiguration('example-node');
+ expect(res.name).toBe(nodeName);
+ expect(res.isUdp).toEqual(true);
+ });
+
+ it('should return protocols TCP when is the only connection protocol available', async () => {
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['TCP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ {
+ connection: 'secure',
+ ipv6: 'no',
+ protocol: ['TCP'],
+ port: '1514',
+ queue_size: '131072',
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+ const nodeName = 'example-node';
+ const res = await RegisterAgentService.getRemoteConfiguration('example-node');
+ expect(res.name).toBe(nodeName);
+ expect(res.isUdp).toEqual(false);
+ });
+
+ it('should return is not UDP when have UDP and TCP protocols available', async () => {
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['TCP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ {
+ connection: 'secure',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '1514',
+ queue_size: '131072',
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+ const nodeName = 'example-node';
+ const res = await RegisterAgentService.getRemoteConfiguration('example-node');
+ expect(res.name).toBe(nodeName);
+ expect(res.isUdp).toEqual(false);
+ });
+ });
+
+ describe('getConnectionConfig', () => {
+
+ beforeAll(() => {
+ jest.clearAllMocks();
+ })
+
+ it('should return UDP when the server address is typed manually (custom)', async () => {
+ const nodeSelected: ServerAddressOptions = {
+ label: 'node-selected',
+ value: 'node-selected',
+ nodetype: 'master'
+ };
+
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ {
+ connection: 'secure',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '1514',
+ queue_size: '131072',
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+
+ const config = await RegisterAgentService.getConnectionConfig(nodeSelected, 'default-dns-address');
+ expect(config.udpProtocol).toEqual(true);
+ expect(config.serverAddress).toBe('default-dns-address');
+ })
+
+ it('should return UDP when the server address is received like default server address dns (custom)', async () => {
+ const nodeSelected: ServerAddressOptions = {
+ label: 'node-selected',
+ value: 'node-selected',
+ nodetype: 'master'
+ };
+
+ const remoteWithSecureAndNoSecure = [
+ {
+ connection: 'syslog',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '514',
+ 'allowed-ips': ['0.0.0.0/0'],
+ },
+ {
+ connection: 'secure',
+ ipv6: 'no',
+ protocol: ['UDP'],
+ port: '1514',
+ queue_size: '131072',
+ },
+ ];
+ const mockedResponse = {
+ data: {
+ data: {
+ affected_items: [
+ {
+ remote: remoteWithSecureAndNoSecure,
+ },
+ ],
+ },
+ },
+ };
+ WzRequest.apiReq = jest.fn().mockResolvedValueOnce(mockedResponse);
+
+ const config = await RegisterAgentService.getConnectionConfig(nodeSelected);
+ expect(config.udpProtocol).toEqual(true);
+ })
+ })
+});
diff --git a/public/controllers/agent/components/register-agent-service.ts b/public/controllers/agent/components/register-agent-service.ts
new file mode 100644
index 0000000000..80e1e1ecbe
--- /dev/null
+++ b/public/controllers/agent/components/register-agent-service.ts
@@ -0,0 +1,147 @@
+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 remote configuration from api
+ */
+async function getRemoteConfiguration(
+ nodeName: string,
+): Promise{
+ let config: RemoteConfig = {
+ name: nodeName,
+ isUdp: null,
+ haveSecureConnection: null,
+ };
+ const result = await WzRequest.apiReq(
+ 'GET',
+ `/cluster/${nodeName}/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;
+};
+
+/**
+ * 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;
+ if(!defaultServerAddress){
+ if(nodeSelected.nodetype !== 'custom'){
+ const remoteConfig = await getRemoteConfiguration(nodeName);
+ return { serverAddress: remoteConfig.name, udpProtocol: remoteConfig.isUdp, connectionSecure: remoteConfig.haveSecureConnection };
+ }else{
+ return { serverAddress: nodeName, udpProtocol: true, connectionSecure: true };
+ }
+ }else{
+ return { serverAddress: defaultServerAddress, udpProtocol: true, 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', {});
+};
+
+/**
+ * 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 => {
+ const nodes = await getNodeIPs();
+ return parseNodesInOptions(nodes);
+}
+
+/**
+ * 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');
+};
+
+
+
+export {
+ getConnectionConfig,
+ getRemoteConfiguration,
+}
\ No newline at end of file
diff --git a/public/controllers/agent/components/register-agent.js b/public/controllers/agent/components/register-agent.js
index 1458f199be..833103cf87 100644
--- a/public/controllers/agent/components/register-agent.js
+++ b/public/controllers/agent/components/register-agent.js
@@ -43,6 +43,8 @@ import { getErrorOrchestrator } from '../../../react-services/common-services';
import { webDocumentationLink } from '../../../../common/services/web_documentation';
import { architectureButtons, architectureButtonsi386, architecturei386Andx86_64, versionButtonsRaspbian, versionButtonsSuse, versionButtonsOracleLinux, versionButtonFedora, architectureButtonsSolaris, architectureButtonsWithPPC64LE, architectureButtonsOpenSuse, architectureButtonsAix, architectureButtonsHpUx, versionButtonAmazonLinux, versionButtonsRedHat, versionButtonsCentos, architectureButtonsMacos, osButtons, versionButtonsDebian, versionButtonsUbuntu, versionButtonsWindows, versionButtonsMacOS, versionButtonsOpenSuse, versionButtonsSolaris, versionButtonsAix, versionButtonsHPUX } from '../wazuh-config'
import './register-agent.scss'
+import ServerAddress from '../register-agent/steps/server-address';
+import { getConnectionConfig, fetchClusterNodesOptions } from './register-agent-service'
export const RegisterAgent = withErrorBoundary(
@@ -65,9 +67,11 @@ export const RegisterAgent = withErrorBoundary(
wazuhPassword: '',
groups: [],
selectedGroup: [],
+ defaultServerAddress: '',
udpProtocol: false,
showPassword: false,
showProtocol: true,
+ connectionSecure: true
};
this.restartAgentCommand = {
rpm: this.systemSelector(),
@@ -84,13 +88,9 @@ export const RegisterAgent = withErrorBoundary(
try {
this.setState({ loading: true });
const wazuhVersion = await this.props.getWazuhVersion();
- let serverAddress = false;
let wazuhPassword = '';
let hidePasswordInput = false;
- serverAddress = this.configuration['enrollment.dns'] || false;
- if (!serverAddress) {
- serverAddress = await this.props.getCurrentApiAddress();
- }
+ this.getEnrollDNSConfig();
let authInfo = await this.getAuthInfo();
const needsPassword = (authInfo.auth || {}).use_password === 'yes';
if (needsPassword) {
@@ -99,11 +99,8 @@ export const RegisterAgent = withErrorBoundary(
hidePasswordInput = true;
}
}
-
- const udpProtocol = await this.getRemoteInfo();
const groups = await this.getGroups();
this.setState({
- serverAddress,
needsPassword,
hidePasswordInput,
versionButtonsRedHat,
@@ -143,7 +140,7 @@ export const RegisterAgent = withErrorBoundary(
context: `${RegisterAgent.name}.componentDidMount`,
level: UI_LOGGER_LEVELS.ERROR,
severity: UI_ERROR_SEVERITIES.BUSINESS,
- display: false,
+ display: true,
store: false,
error: {
error: error,
@@ -155,6 +152,16 @@ export const RegisterAgent = withErrorBoundary(
}
}
+ getEnrollDNSConfig = () => {
+ let serverAddress = this.configuration['enrollment.dns'] || '';
+ this.setState({ defaultServerAddress: serverAddress });
+ if(serverAddress){
+ this.setState({ udpProtocol: true });
+ }else{
+ this.setState({ udpProtocol: false });
+ }
+ }
+
async getAuthInfo() {
try {
const result = await WzRequest.apiReq('GET', '/agents/000/config/auth/auth', {});
@@ -165,18 +172,6 @@ export const RegisterAgent = withErrorBoundary(
}
}
- async getRemoteInfo() {
- try {
- const result = await WzRequest.apiReq('GET', '/agents/000/config/request/remote', {});
- const remote = ((result.data || {}).data || {}).remote || {};
- if (remote.length === 2) {
- this.setState({ udpProtocol: true })
- }
- } catch (error) {
- throw new Error(error);
- }
- }
-
selectOS(os) {
this.setState({
selectedOS: os,
@@ -216,8 +211,12 @@ export const RegisterAgent = withErrorBoundary(
this.setState({ selectedSYS: sys });
}
- setServerAddress(event) {
- this.setState({ serverAddress: event.target.value });
+ setServerAddress(serverAddress) {
+ this.setState({ serverAddress });
+ }
+
+ setAgentName(event) {
+ this.setState({ agentName: event.target.value });
}
setAgentName(event) {
@@ -266,7 +265,9 @@ export const RegisterAgent = withErrorBoundary(
}
optionalDeploymentVariables() {
- let deployment = `WAZUH_MANAGER='${this.state.serverAddress}' `;
+
+ let deployment = this.state.serverAddress && `WAZUH_MANAGER='${this.state.serverAddress}' `;
+ const protocol = false
if (this.state.selectedOS == 'win') {
deployment += `WAZUH_REGISTRATION_SERVER='${this.state.serverAddress}' `;
}
@@ -275,7 +276,7 @@ export const RegisterAgent = withErrorBoundary(
deployment += `WAZUH_REGISTRATION_PASSWORD='${this.state.wazuhPassword}' `;
}
- if (!this.state.udpProtocol == true) {
+ if (this.state.udpProtocol) {
deployment += `WAZUH_PROTOCOL='UDP' `;
}
@@ -295,8 +296,7 @@ export const RegisterAgent = withErrorBoundary(
agentNameVariable() {
let agentName = `WAZUH_AGENT_NAME='${this.state.agentName}' `;
-
- if (this.state.selectedArchitecture && this.state.agentName !== '') {
+ if(this.state.selectedArchitecture && this.state.agentName !== '') {
return agentName;
} else {
return '';
@@ -719,18 +719,7 @@ export const RegisterAgent = withErrorBoundary(
);
const missingOSSelection = this.checkMissingOSSelection();
- const ipInput = (
-
-
- This is the address the agent uses to communicate with the Wazuh server. It can be an IP address or a fully qualified domain name (FQDN).
-
- this.setServerAddress(event)}
- />
-
- );
+
const agentName = (
);
-
+
const agentGroup = (
-
- Select one or more existing groups
- {
- this.setGroupName(group);
- }}
- isDisabled={!this.state.groups.length}
- isClearable={true}
- data-test-subj="demoComboBox"
- />
-
+
+ Select one or more existing groups
+ {
+ this.setGroupName(group);
+ }}
+ isDisabled={!this.state.groups.length}
+ isClearable={true}
+ data-test-subj="demoComboBox"
+ />
+
)
const passwordInput = (
- ) :
- this.state.selectedOS && (
+ ) : (this.state.connectionSecure === true && this.state.udpProtocol === false) ? (
+
+
+ You can use this command to install and enroll the Wazuh agent in one or more hosts.
+
+
+
+ {windowsAdvice}
+
+
+ {this.state.wazuhPassword && !this.state.showPassword ? this.obfuscatePassword(text) : text}
+
+
+ {(copy) => (
+
+ )}
+
+
+ {this.state.needsPassword && (
+ this.setShowPassword(active)}
+ />
+ )}
+
+ ) : (this.state.connectionSecure === false) ?
+ (
+
+
+ You can use this command to install and enroll the Wazuh agent in one or more hosts.
+
+
+
+
+ Warning: there's no secure protocol configured and agents will not be able to communicate with the manager.
+ >
+ }
+ iconType="iInCircle"
+ />
+
+ {windowsAdvice}
+
+
+ {this.state.wazuhPassword && !this.state.showPassword ? this.obfuscatePassword(text) : text}
+
+
+ {(copy) => (
+
+ )}
+
+
+ {this.state.needsPassword && (
+ this.setShowPassword(active)}
+ />
+ )}
+
+ ) : (
You can use this command to install and enroll the Wazuh agent in one or more hosts.
- If the installer finds another Wazuh agent in the system, it will upgrade it preserving the configuration.
- >
- }
+ title={warningUpgrade}
iconType="iInCircle"
/>
@@ -871,7 +931,7 @@ export const RegisterAgent = withErrorBoundary(
{this.state.wazuhPassword && !this.state.showPassword ? this.obfuscatePassword(text) : text}
-
+
{(copy) => (
Copy command
@@ -1092,6 +1152,47 @@ export const RegisterAgent = withErrorBoundary(
)
}
+ const onChangeServerAddress = async (selectedNodes) => {
+ if(selectedNodes.length === 0){
+ this.setState({
+ serverAddress: '',
+ udpProtocol: false,
+ connectionSecure: null
+ })
+ }else{
+ const nodeSelected = selectedNodes[0];
+ try {
+ const remoteConfig = await getConnectionConfig(nodeSelected);
+ this.setState({
+ serverAddress: remoteConfig.serverAddress,
+ udpProtocol: remoteConfig.udpProtocol,
+ connectionSecure: remoteConfig.connectionSecure
+ })
+ }catch(error){
+ const options = {
+ context: `${RegisterAgent.name}.onChangeServerAddress`,
+ 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,
+ },
+ };
+ getErrorOrchestrator().handleError(options);
+ this.setState({
+ serverAddress: nodeSelected.label,
+ udpProtocol: false,
+ connectionSecure: false
+ })
+ }
+ }
+
+ }
+
+
const steps = [
{
title: 'Choose the operating system',
@@ -1245,7 +1346,6 @@ export const RegisterAgent = withErrorBoundary(
title: 'Choose the version',
children: (
this.state.selectedVersion == '11.31' ? buttonGroupWithMessage("Choose the version", versionButtonsHPUX, this.state.selectedVersion, (version) => this.setVersion(version)) : buttonGroup("Choose the version", versionButtonsHPUX, this.state.selectedVersion, (version) => this.setVersion(version))
-
),
},
]
@@ -1342,7 +1442,12 @@ export const RegisterAgent = withErrorBoundary(
: []),
{
title: 'Wazuh server address',
- children:
{ipInput},
+ children:
+
+ ,
},
...(!(!this.state.needsPassword || this.state.hidePasswordInput)
? [
@@ -1437,6 +1542,7 @@ export const RegisterAgent = withErrorBoundary(
]
: []),
];
+
return (
@@ -1471,7 +1577,7 @@ export const RegisterAgent = withErrorBoundary(
)}
-
+
{this.state.loading && (
<>
diff --git a/public/controllers/agent/register-agent/steps/__snapshots__/server-address.test.tsx.snap b/public/controllers/agent/register-agent/steps/__snapshots__/server-address.test.tsx.snap
new file mode 100644
index 0000000000..d41e54d637
--- /dev/null
+++ b/public/controllers/agent/register-agent/steps/__snapshots__/server-address.test.tsx.snap
@@ -0,0 +1,84 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Server Address Combobox should match snapshot 1`] = `
+
+
+
+ This is the address the agent uses to communicate with the Wazuh server. It can be an IP address or a fully qualified domain name (FQDN).
+
+
+
+
+`;
diff --git a/public/controllers/agent/register-agent/steps/index.ts b/public/controllers/agent/register-agent/steps/index.ts
new file mode 100644
index 0000000000..5732aed9ae
--- /dev/null
+++ b/public/controllers/agent/register-agent/steps/index.ts
@@ -0,0 +1 @@
+export * from './server-address';
\ No newline at end of file
diff --git a/public/controllers/agent/register-agent/steps/server-address.test.tsx b/public/controllers/agent/register-agent/steps/server-address.test.tsx
new file mode 100644
index 0000000000..86dd698c7f
--- /dev/null
+++ b/public/controllers/agent/register-agent/steps/server-address.test.tsx
@@ -0,0 +1,195 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { act } from 'react-dom/test-utils';
+import ServerAddress from './server-address';
+import * as registerAgentsUtils from '../../components/register-agent-service';
+
+const mockedNodesIps = [
+ {
+ name: 'master-node',
+ type: 'master',
+ version: '4.x',
+ ip: 'wazuh-master',
+ },
+ {
+ name: 'worker1',
+ type: 'worker',
+ version: '4.x',
+ ip: '172.26.0.7',
+ },
+ {
+ name: 'worker2',
+ type: 'worker',
+ version: '4.x',
+ ip: '172.26.0.6',
+ },
+];
+
+const mockedClusterNodes = {
+ data: {
+ data: {
+ affected_items: mockedNodesIps,
+ total_affected_items: mockedNodesIps.length,
+ total_failed_items: 0,
+ failed_items: [],
+ },
+ message: 'All selected nodes information was returned',
+ error: 0,
+ },
+};
+
+const promiseFetchOptions = Promise.resolve(
+ registerAgentsUtils.parseNodesInOptions(mockedClusterNodes),
+);
+const mockedFetchOptions = () => promiseFetchOptions;
+
+describe('Server Address Combobox', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render correctly', () => {
+ const { container } = render(
+ {}}
+ fetchOptions={() =>
+ Promise.resolve(
+ registerAgentsUtils.parseNodesInOptions(mockedClusterNodes),
+ )
+ }
+ />,
+ );
+ expect(container).toBeInTheDocument();
+ });
+
+ it('should match snapshot', () => {
+ const { container } = render(
+ {}} fetchOptions={mockedFetchOptions} />,
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should set default combobox value and disable input when defaultValue is defined', async () => {
+ const onChangeMocked = jest.fn();
+ const { container, getByText, getByRole } = render(
+ ,
+ );
+
+ await act(async () => {
+ await promiseFetchOptions;
+ expect(onChangeMocked).toBeCalledTimes(1);
+ expect(onChangeMocked).toBeCalledWith([
+ { label: 'default-dns', value: 'default-dns', nodetype: 'custom' }
+ ]);
+ expect(getByText('default-dns')).toBeInTheDocument();
+ expect(getByRole('textbox')).toHaveAttribute('disabled');
+ expect(container).toBeInTheDocument();
+ });
+ });
+
+ it('should set node type master like default value when combobox is initiliazed and not have defaultValue', async () => {
+ const { container, getByText } = render(
+ {}} fetchOptions={mockedFetchOptions} />,
+ );
+
+ await act(async () => {
+ await promiseFetchOptions; // waiting for the combobox items are loaded
+ expect(getByText('master-node')).toBeInTheDocument();
+ expect(container).toBeInTheDocument();
+ });
+ });
+
+ it('should render the correct number of options', async () => {
+ const { getByRole, findByText } = render(
+ {}} fetchOptions={mockedFetchOptions} />,
+ );
+
+ await act(async () => {
+ await promiseFetchOptions; // waiting for the combobox items are loaded
+ fireEvent.click(getByRole('button', { name: 'Clear input' }));
+ await findByText(`${mockedNodesIps[0].name}:${mockedNodesIps[0].ip}`);
+ await findByText(`${mockedNodesIps[1].name}:${mockedNodesIps[1].ip}`);
+ await findByText(`${mockedNodesIps[2].name}:${mockedNodesIps[2].ip}`);
+ });
+
+ });
+
+ it('should allow only single selection', async () => {
+ const onChangeMocked = jest.fn();
+ const { getByRole, getByText,findByText } = render(
+ ,
+ );
+ await act(async () => {
+ await promiseFetchOptions; // waiting for the combobox items are loaded
+ fireEvent.click(getByRole('button', { name: 'Clear input' }));
+ const serverAddresInput = getByRole('textbox');
+ fireEvent.change(serverAddresInput, { target: { value: 'first-typed' } });
+ fireEvent.keyDown(serverAddresInput, { key: 'Enter', code: 'Enter' });
+ fireEvent.change(serverAddresInput, { target: { value: 'last-typed' } });
+ fireEvent.keyDown(serverAddresInput, { key: 'Enter', code: 'Enter' });
+ expect(onChangeMocked).toHaveBeenLastCalledWith([{ label: 'last-typed', value: 'last-typed', nodetype: 'custom' }]);
+ expect(getByText('last-typed')).toBeInTheDocument();
+ });
+ });
+
+ it('should return EMPTY parsed Node IPs when options are not selected', async () => {
+ const onChangeMocked = jest.fn();
+ const { getByRole, container } = render(
+ ,
+ );
+ await act(async () => {
+ await promiseFetchOptions; // waiting for the combobox items are loaded
+ fireEvent.click(getByRole('button', { name: 'Clear input' }));
+ expect(onChangeMocked).toBeCalledTimes(2);
+ expect(onChangeMocked).toBeCalledWith([]);
+ expect(container).toBeInTheDocument();
+ });
+ });
+
+ it('should allow create customs options when user type and trigger enter key', async () => {
+ const onChangeMocked = jest.fn();
+
+ const { getByRole } = render(
+ ,
+ );
+ await act(async () => {
+ await promiseFetchOptions; // waiting for the combobox items are loaded
+ fireEvent.change(getByRole('textbox'), {
+ target: { value: 'custom-ip-dns' },
+ });
+ fireEvent.keyDown(getByRole('textbox'), { key: 'Enter', code: 'Enter' });
+ expect(onChangeMocked).toBeCalledTimes(2);
+ expect(onChangeMocked).toHaveBeenNthCalledWith(2,[{ label: 'custom-ip-dns', value: 'custom-ip-dns', nodetype: 'custom' }]);
+ });
+ });
+
+ it('should show "node.name:node.ip" in the combobox options', async () => {
+ const { getByRole, getByText } = render(
+ {}} fetchOptions={mockedFetchOptions} />,
+ );
+ await act(async () => {
+ await promiseFetchOptions; // waiting for the combobox items are loaded
+ fireEvent.click(getByRole('button', { name: 'Clear input' }));
+ });
+
+ mockedNodesIps.forEach(nodeItem => {
+ expect(
+ getByText(`${nodeItem.name}:${nodeItem.ip}`),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/public/controllers/agent/register-agent/steps/server-address.tsx b/public/controllers/agent/register-agent/steps/server-address.tsx
new file mode 100644
index 0000000000..8c23073f36
--- /dev/null
+++ b/public/controllers/agent/register-agent/steps/server-address.tsx
@@ -0,0 +1,170 @@
+import {
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+ EuiHighlight,
+ EuiText,
+} from '@elastic/eui';
+import React, { useEffect, useState } from 'react';
+import { UI_LOGGER_LEVELS } from '../../../../../common/constants';
+import { getErrorOrchestrator } from '../../../../react-services/common-services';
+import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types';
+import { getMasterNode } from '../../components/register-agent-service';
+
+type Props = {
+ onChange: (value: EuiComboBoxOptionOption[]) => void;
+ fetchOptions: () => Promise[]>;
+ defaultValue?: string;
+};
+
+export type ServerAddressOptions = EuiComboBoxOptionOption & {
+ nodetype?: string;
+};
+
+const ServerAddress = (props: Props) => {
+ const { onChange, fetchOptions, defaultValue } = props;
+ const [nodeIPs, setNodeIPs] = useState([]);
+ const [selectedNodeIPs, setSelectedNodeIPs] = useState<
+ ServerAddressOptions[]
+ >([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ useEffect(() => {
+ initialize();
+ }, []);
+
+ /**
+ * Fetches the node IPs (options) and sets the state
+ */
+ const initialize = async () => {
+ if (!fetchOptions) {
+ throw new Error('fetchOptions is required');
+ }
+ try {
+ setIsLoading(true);
+ await setDefaultValue();
+ setIsLoading(false);
+ } catch (error) {
+ setIsLoading(false);
+ const options = {
+ context: `${ServerAddress.name}.initialize`,
+ 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,
+ },
+ };
+ getErrorOrchestrator().handleError(options);
+ }
+ };
+
+ /**
+ * Sets the default value of server address
+ */
+ const setDefaultValue = async () => {
+ if (defaultValue) {
+ const defaultNode = [{ label: defaultValue, value: defaultValue, nodetype: 'custom' }];
+ handleOnChange(defaultNode);
+ setIsDisabled(true);
+ } else {
+ setIsDisabled(false);
+ const nodeIps = await fetchOptions();
+ setNodeIPs(nodeIps);
+ const defaultNode = getMasterNode(nodeIps);
+ if (defaultNode.length > 0) {
+ handleOnChange(defaultNode);
+ }
+ }
+ };
+
+ /**
+ * Handles the change of the selected node IP
+ * @param value
+ */
+ const handleOnChange = (value: EuiComboBoxOptionOption[]) => {
+ setSelectedNodeIPs(value);
+ onChange(value);
+ };
+
+ /**
+ * Handle the render of the custom options in the combobox list
+ * @param option
+ * @param searchValue
+ * @param contentClassName
+ */
+ const handleRenderOption = (
+ option: EuiComboBoxOptionOption,
+ inputValue: string,
+ contentClassName: string,
+ ) => {
+ const { label, value } = option;
+ return (
+
+ {`${label}:${value}`}
+
+ );
+ };
+
+ /**
+ * Handle the interaction when the user enter a option that is not in the list
+ * Creating new options in the list and selecting it
+ * @param inputValue
+ * @param options
+ */
+ const handleOnCreateOption = (
+ inputValue: string,
+ options: ServerAddressOptions[] = [],
+ ) => {
+ if (!inputValue) {
+ return;
+ }
+
+ const normalizedSearchValue = inputValue.trim().toLowerCase();
+ if (!normalizedSearchValue) {
+ return;
+ }
+
+ const newOption = {
+ value: inputValue,
+ label: inputValue,
+ nodetype: 'custom',
+ };
+ // Create the option if it doesn't exist.
+ if (
+ options.findIndex(
+ (option: ServerAddressOptions) =>
+ option.label.trim().toLowerCase() === normalizedSearchValue,
+ ) === -1
+ ) {
+ setNodeIPs([...nodeIPs, newOption]);
+ }
+ // Select the option.
+ handleOnChange([newOption]);
+ };
+
+ return (
+
+
+ This is the address the agent uses to communicate with the Wazuh server. It can be an IP address or a fully qualified domain name (FQDN).
+
+ handleOnCreateOption(sv, fo)}
+ />
+
+ );
+};
+
+export default ServerAddress;