diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.test.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.test.tsx index ed5183d5e4e35..3d7a430cc1185 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.test.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.test.tsx @@ -65,6 +65,13 @@ describe('AddRemoteInstanceService:: ', () => { replication_set: 'test', cluster: 'test', custom_labels: 'test:test', + pmm_agent_id: { + value: 'pmm-server', + }, + node: { + value: 'node1', + label: 'node1', + }, }; const payload = { @@ -84,16 +91,12 @@ describe('AddRemoteInstanceService:: ', () => { custom_labels: { test: 'test', }, - add_node: { - node_name: 'localhost', - node_type: 'NODE_TYPE_REMOTE_NODE', - }, pmm_agent_id: 'pmm-server', port: '80', qan_postgresql_pgstatements_agent: true, metrics_mode: 1, + node_id: 'node1', }; - expect(toPayload(data)).toStrictEqual(payload); }); }); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.tsx index 01b755575a7fb..5d48b4f9cec81 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.service.tsx @@ -1,5 +1,6 @@ import { CancelToken } from 'axios'; +import { PMM_SERVER_NODE_AGENT_ID } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.constants'; import { Databases } from 'app/percona/shared/core'; import { apiManagement } from 'app/percona/shared/helpers/api'; @@ -142,7 +143,9 @@ export const toPayload = (values: any, discoverName?: string, type?: InstanceAva data.service_name = data.address; } - if (!values.isAzure && data.add_node === undefined) { + if (data.address === '127.0.0.1' || data.address === 'localhost') { + data.node_id = data.node.value; + } else if (!values.isAzure && data.add_node === undefined) { data.add_node = { node_name: data.service_name, node_type: 'NODE_TYPE_REMOTE_NODE', @@ -179,8 +182,15 @@ export const toPayload = (values: any, discoverName?: string, type?: InstanceAva } } - data.metrics_mode = 1; + data.pmm_agent_id = values.pmm_agent_id.value; + + if (data.pmm_agent_id === PMM_SERVER_NODE_AGENT_ID) { + data.metrics_mode = 1; + } else { + data.metrics_mode = 2; + } delete data.tracking; + delete data.node; return data; }; diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.test.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.test.tsx index 93dd7c141af00..9cef7b6233ea8 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.test.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.test.tsx @@ -1,7 +1,9 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import { Provider } from 'react-redux'; import { Databases } from 'app/percona/shared/core'; +import { configureStore } from 'app/store/configureStore'; import { InstanceTypesExtra } from '../../panel.types'; @@ -20,7 +22,11 @@ jest.mock('app/percona/shared/helpers/logger', () => { describe('Add remote instance:: ', () => { it('should render correct for mysql and postgres and highlight empty mandatory fields on submit', async () => { const type = Databases.mysql; - render(); + render( + + + + ); expect(screen.getByTestId('address-text-input').classList.contains('invalid')).toBe(false); expect(screen.getByTestId('username-text-input').classList.contains('invalid')).toBe(false); @@ -35,7 +41,11 @@ describe('Add remote instance:: ', () => { it('should render for external service and highlight empty mandatory fields on submit', async () => { const type = InstanceTypesExtra.external; - render(); + render( + + + + ); expect(screen.getByTestId('address-text-input').classList.contains('invalid')).toBe(false); expect(screen.getByTestId('metrics_path-text-input').classList.contains('invalid')).toBe(false); @@ -55,7 +65,11 @@ describe('Add remote instance:: ', () => { it('should render correct for HAProxy and highlight empty mandatory fields on submit', async () => { const type = Databases.haproxy; - render(); + render( + + + + ); expect(screen.getByTestId('address-text-input').classList.contains('invalid')).toBe(false); expect(screen.getByTestId('username-text-input').classList.contains('invalid')).toBe(false); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.test.ts b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.test.ts index 0c9ff829f3c02..007f22a2a44b9 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.test.ts +++ b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.test.ts @@ -23,6 +23,7 @@ describe('Get instance data:: ', () => { port: '5432', metricsParameters: 'manually', schema: 'https', + pmm_agent_id: '', }, }; @@ -77,6 +78,7 @@ describe('Get instance data:: ', () => { port: '27017', metricsParameters: 'manually', schema: 'https', + pmm_agent_id: '', }, }; @@ -100,6 +102,7 @@ describe('Get instance data:: ', () => { port: '3306', metricsParameters: 'manually', schema: 'https', + pmm_agent_id: '', }, }; @@ -123,6 +126,7 @@ describe('Get instance data:: ', () => { port: '6032', metricsParameters: 'manually', schema: 'https', + pmm_agent_id: '', }, }; diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.tsx index 9d00a10c1a547..c087c2626632e 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/AddRemoteInstance.tools.tsx @@ -117,6 +117,7 @@ export const getInstanceData = (instanceType: InstanceAvailableType, credentials remoteInstanceCredentials: { metricsParameters: MetricsParameters.manually, schema: Schema.HTTPS, + pmm_agent_id: '', }, }; diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.test.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.test.tsx index f5decfb2fa137..cb1524fb44dae 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.test.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.test.tsx @@ -1,21 +1,26 @@ import { render, fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { Form } from 'react-final-form'; +import { Provider } from 'react-redux'; + +import { configureStore } from 'app/store/configureStore'; import { ExternalServiceConnectionDetails } from './ExternalServiceConnectionDetails'; describe('Add remote instance:: ', () => { it('should render correct for mysql and postgres and highlight empty mandatory fields on submit', async () => { render( -
{ - changeValue(state, field, () => value); - }, - }} - render={({ form }) => } - /> + + { + changeValue(state, field, () => value); + }, + }} + render={({ form }) => } + /> + ); const metricsParametrsRadioState = screen.getByTestId('metricsParameters-radio-state'); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.tsx index b80ce5548b2d2..59a8703512e3d 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/ExternalServiceConnectionDetails/ExternalServiceConnectionDetails.tsx @@ -1,6 +1,7 @@ import React, { FC, useCallback, useEffect, useMemo } from 'react'; import { useStyles2 } from '@grafana/ui'; +import { NodesAgents } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents'; import { PasswordInputField } from 'app/percona/shared/components/Form/PasswordInput'; import { RadioButtonGroupField } from 'app/percona/shared/components/Form/RadioButtonGroup'; import { TextInputField } from 'app/percona/shared/components/Form/TextInput'; @@ -96,6 +97,7 @@ export const ExternalServiceConnectionDetails: FC = ({ form }) => />
+
({ width: 100%; margin-right: 5px; `, + selectFieldWrapper: css` + width: 100%; + `, + selectField: css` + height: 38px; + `, group: css` display: flex; flex-direction: row; diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.test.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.test.tsx index 64ab6b28cf813..f7050cf756c07 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.test.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.test.tsx @@ -2,8 +2,10 @@ import { render, screen } from '@testing-library/react'; import { FormApi, FormState } from 'final-form'; import React from 'react'; import { Form } from 'react-final-form'; +import { Provider } from 'react-redux'; import { Databases } from 'app/percona/shared/core'; +import { configureStore } from 'app/store/configureStore'; import { AdditionalOptionsFormPart, getAdditionalOptions } from './AdditionalOptions/AdditionalOptions'; import { ExternalServiceConnectionDetails } from './ExternalServiceConnectionDetails/ExternalServiceConnectionDetails'; @@ -20,14 +22,16 @@ const form: Partial = { describe('MainDetailsFormPart ::', () => { it('should disable fields with sat isRDS flag', async () => { const { container } = render( - } - /> + + } + /> + ); const fields = container.querySelectorAll('input'); - expect(fields.length).toBe(5); + expect(fields.length).toBe(8); expect(screen.getByTestId('address-text-input')).toBeDisabled(); expect(screen.getByTestId('serviceName-text-input')).not.toBeDisabled(); @@ -38,14 +42,16 @@ describe('MainDetailsFormPart ::', () => { it('should disable fields with not sat isRDS flag', async () => { const { container } = render( - } - /> + + } + /> + ); const fields = container.querySelectorAll('input'); - expect(fields.length).toBe(5); + expect(fields.length).toBe(8); expect(screen.getByTestId('address-text-input')).not.toBeDisabled(); expect(screen.getByTestId('serviceName-text-input')).not.toBeDisabled(); @@ -58,10 +64,12 @@ describe('MainDetailsFormPart ::', () => { describe('ExternalServiceConnectionDetails ::', () => { it('should render', async () => { const { container } = render( - } - /> + + } + /> + ); const fields = container.querySelectorAll('input'); @@ -89,17 +97,19 @@ describe('AdditionalOptionsFormPart ::', () => { }; render( - ( - - )} - /> + + ( + + )} + /> + ); expect(screen.getByTestId('skip_connection_check-checkbox-input')).toBeInTheDocument(); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.test.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.test.tsx index d33f2db214fef..54ee600051076 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.test.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.test.tsx @@ -1,12 +1,19 @@ import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; import { Form } from 'react-final-form'; +import { Provider } from 'react-redux'; + +import { configureStore } from 'app/store/configureStore'; import { HAProxyConnectionDetails } from './HAProxyConnectionDetails'; describe('HAProxy connection details:: ', () => { it('should trim username and password values right', () => { - render( } />); + render( + + } /> + + ); const userNameTextInput = screen.getByTestId('username-text-input'); fireEvent.change(userNameTextInput, { target: { value: ' test ' } }); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.tsx index 05d22e9379b50..d4f687c9c8678 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/HAProxyConnectionDetails/HAProxyConnectionDetails.tsx @@ -1,6 +1,7 @@ import React, { FC, useCallback, useMemo } from 'react'; import { useStyles2 } from '@grafana/ui'; +import { NodesAgents } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents'; import { PasswordInputField } from 'app/percona/shared/components/Form/PasswordInput'; import { TextInputField } from 'app/percona/shared/components/Form/TextInput'; import Validators from 'app/percona/shared/helpers/validators'; @@ -10,7 +11,7 @@ import { Messages } from '../FormParts.messages'; import { getStyles } from '../FormParts.styles'; import { MainDetailsFormPartProps } from '../FormParts.types'; -export const HAProxyConnectionDetails: FC = ({ remoteInstanceCredentials }) => { +export const HAProxyConnectionDetails: FC = ({ form, remoteInstanceCredentials }) => { const styles = useStyles2(getStyles); const portValidators = useMemo(() => [validators.required, Validators.validatePort], []); @@ -29,6 +30,7 @@ export const HAProxyConnectionDetails: FC = ({ remoteI />
+
= ({ form, remote />
+
{ it('should have max query length attribute', () => { - render( } />); + render( + + } /> + + ); const textInput = screen.getByTestId('maxQueryLength-text-input'); fireEvent.change(textInput, { target: { value: '1000' } }); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MongoDBConnectionDetails/MongoDBConnectionDetails.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MongoDBConnectionDetails/MongoDBConnectionDetails.tsx index 35acf13958896..5aa423d14f7ac 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MongoDBConnectionDetails/MongoDBConnectionDetails.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MongoDBConnectionDetails/MongoDBConnectionDetails.tsx @@ -1,6 +1,7 @@ import React, { FC, useMemo } from 'react'; import { useStyles2 } from '@grafana/ui'; +import { NodesAgents } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents'; import { PasswordInputField } from 'app/percona/shared/components/Form/PasswordInput'; import { TextInputField } from 'app/percona/shared/components/Form/TextInput'; import Validators from 'app/percona/shared/helpers/validators'; @@ -31,6 +32,7 @@ export const MongoDBConnectionDetails: FC = ({ form, r />
+
{ it('should have max query length attribute', () => { - render( } />); + render( + + } /> + + ); const textInput = screen.getByTestId('maxQueryLength-text-input'); fireEvent.change(textInput, { target: { value: '1000' } }); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MySQLConnectionDetails/MySQLConnectionDetails.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MySQLConnectionDetails/MySQLConnectionDetails.tsx index 1c6ea246b0374..ac3b5e5d10f4a 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MySQLConnectionDetails/MySQLConnectionDetails.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/MySQLConnectionDetails/MySQLConnectionDetails.tsx @@ -9,6 +9,7 @@ import { validators } from 'app/percona/shared/helpers/validatorsForm'; import { Messages } from '../FormParts.messages'; import { getStyles } from '../FormParts.styles'; import { MainDetailsFormPartProps } from '../FormParts.types'; +import { NodesAgents } from '../NodesAgents/NodesAgents'; export const MySQLConnectionDetails: FC = ({ form, remoteInstanceCredentials }) => { const styles = useStyles2(getStyles); @@ -32,6 +33,7 @@ export const MySQLConnectionDetails: FC = ({ form, rem />
+
{ + const submitMock = jest.fn(); + + function setup() { + render( + + ( + + +

{values.node?.value}

+

{values.pmm_agent_id?.value}

+

{values.address}

+ + )} + /> +
+ ); + } + + beforeEach(() => { + submitMock.mockClear(); + }); + + it('should not pick any agent when the selected node is not pmm-server', async () => { + jest + .spyOn(InventoryService, 'getNodes') + .mockReturnValue(Promise.resolve({ nodes: nodesMockMultipleAgentsNoPMMServer })); + setup(); + await waitFor(() => { + expect(fetchNodesActionActionSpy).toHaveBeenCalled(); + }); + + const nodesSelect = screen.getByLabelText('Nodes'); + await waitFor(() => + selectEvent.select(nodesSelect, [nodesMockMultipleAgentsNoPMMServer[0].node_name], { + container: document.body, + }) + ); + + expect(screen.getByTestId('agent')).toHaveTextContent(''); + expect(screen.getByTestId('node')).toHaveTextContent(nodesMockMultipleAgentsNoPMMServer[0].node_id); + }); + + it('should pick the pmm-server from the list of agents when pmm-server node is chosen', async () => { + jest.spyOn(InventoryService, 'getNodes').mockReturnValue(Promise.resolve({ nodes: nodesMock })); + setup(); + await waitFor(() => { + expect(fetchNodesActionActionSpy).toHaveBeenCalled(); + }); + const nodesSelect = screen.getByLabelText('Nodes'); + + await waitFor(() => + selectEvent.select(nodesSelect, ['pmm-server'], { + container: document.body, + }) + ); + + expect(screen.getByTestId('agent')).toHaveTextContent('pmm-server'); + expect(screen.getByTestId('node')).toHaveTextContent('pmm-server'); + }); + + it('should change the address to localhost when the agent id is not pmmServer', async () => { + jest.spyOn(InventoryService, 'getNodes').mockReturnValue(Promise.resolve({ nodes: nodesMockOneAgentNoPMMServer })); + setup(); + await waitFor(() => { + expect(fetchNodesActionActionSpy).toHaveBeenCalled(); + }); + const nodesSelect = screen.getByLabelText('Nodes'); + + await waitFor(() => + selectEvent.select(nodesSelect, [nodesMockOneAgentNoPMMServer[0].node_name], { + container: document.body, + }) + ); + await waitFor(() => + selectEvent.select(nodesSelect, [nodesMockOneAgentNoPMMServer[0].agents[0].agent_id], { + container: document.body, + }) + ); + + expect(screen.getByTestId('address')).toHaveTextContent('localhost'); + }); + + it('should have the node/agent selected values when submitted', async () => { + jest.spyOn(InventoryService, 'getNodes').mockReturnValue(Promise.resolve({ nodes: nodesMock })); + + setup(); + await waitFor(() => { + expect(fetchNodesActionActionSpy).toHaveBeenCalled(); + }); + + const form = screen.getByTestId('node-agents-form'); + + const nodesSelect = screen.getByLabelText('Nodes'); + await waitFor(() => + selectEvent.select(nodesSelect, 'pmm-server', { + container: document.body, + }) + ); + + fireEvent.submit(form); + + expect(submitMock).toHaveBeenCalledWith( + expect.objectContaining({ + node: expect.objectContaining({ + value: 'pmm-server', + }), + pmm_agent_id: expect.objectContaining({ + value: 'pmm-server', + }), + }), + expect.anything(), + expect.anything() + ); + }); +}); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.tsx new file mode 100644 index 0000000000000..59a8dc550de77 --- /dev/null +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.tsx @@ -0,0 +1,108 @@ +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; + +import { useStyles2 } from '@grafana/ui'; +import { Messages } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.messages'; +import { getStyles } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/FormParts.styles'; +import { PMM_SERVER_NODE_AGENT_ID } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.constants'; +import { NodesAgentsProps } from 'app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.types'; +import { GET_NODES_CANCEL_TOKEN } from 'app/percona/inventory/Inventory.constants'; +import { AgentsOption, NodesOption } from 'app/percona/inventory/Inventory.types'; +import { SelectField } from 'app/percona/shared/components/Form/SelectFieldCore'; +import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook'; +import { nodesOptionsMapper } from 'app/percona/shared/core/reducers/nodes'; +import { fetchNodesAction } from 'app/percona/shared/core/reducers/nodes/nodes'; +import { getNodes } from 'app/percona/shared/core/selectors'; +import { isApiCancelError } from 'app/percona/shared/helpers/api'; +import { logger } from 'app/percona/shared/helpers/logger'; +import { useAppDispatch } from 'app/store/store'; +import { useSelector } from 'app/types'; + +export const NodesAgents: FC = ({ form }) => { + const styles = useStyles2(getStyles); + const dispatch = useAppDispatch(); + const [generateToken] = useCancelToken(); + const [selectedNode, setSelectedNode] = useState(); + const { nodes } = useSelector(getNodes); + + const nodesOptions = useMemo(() => nodesOptionsMapper(nodes), [nodes]); + + const loadData = useCallback(async () => { + try { + await dispatch(fetchNodesAction({ token: generateToken(GET_NODES_CANCEL_TOKEN) })).unwrap(); + } catch (e) { + if (isApiCancelError(e)) { + return; + } + logger.error(e); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const changeAgentValue = (value: AgentsOption) => { + if (value.label !== PMM_SERVER_NODE_AGENT_ID) { + form?.change('address', 'localhost'); + } else { + form?.change('address', ''); + } + }; + + const setNodeAndAgent = (value: NodesOption) => { + setSelectedNode(value); + + let selectedAgent: AgentsOption | undefined; + if (value.agents && value.agents?.length > 1) { + selectedAgent = value.agents.find((item) => item.value === PMM_SERVER_NODE_AGENT_ID); + } else if (value.agents && value.agents?.length === 1) { + selectedAgent = value.agents[0]; + } + if (selectedAgent) { + form?.change('pmm_agent_id', selectedAgent); + + if (selectedAgent.value !== PMM_SERVER_NODE_AGENT_ID) { + form?.change('address', 'localhost'); + } + } else { + form?.change('pmm_agent_id', undefined); + form?.change('address', ''); + } + }; + + useEffect(() => { + if (nodesOptions.length === 0) { + loadData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [nodesOptions]); + + return ( +
+
+ setNodeAndAgent(event as NodesOption)} + className={styles.selectField} + value={selectedNode} + aria-label={Messages.form.labels.nodesAgents.nodes} + /> +
+
+ changeAgentValue(event as AgentsOption)} + className={styles.selectField} + aria-label={Messages.form.labels.nodesAgents.agents} + /> +
+
+ ); +}; diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.types.ts b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.types.ts new file mode 100644 index 0000000000000..865e97935d888 --- /dev/null +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/NodesAgents/NodesAgents.types.ts @@ -0,0 +1,5 @@ +import { FormApi } from 'final-form'; + +export interface NodesAgentsProps { + form?: FormApi; +} diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.test.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.test.tsx index c169872521614..4b817753588a9 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.test.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.test.tsx @@ -1,12 +1,19 @@ import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; import { Form } from 'react-final-form'; +import { Provider } from 'react-redux'; + +import { configureStore } from 'app/store/configureStore'; import { PostgreSQLConnectionDetails } from './PostgreSQLConnectionDetails'; describe('PostgreSQL connection details:: ', () => { it('should have database attribute', () => { - render(
} />); + render( + + } /> + + ); const textInput = screen.getByTestId('database-text-input'); fireEvent.change(textInput, { target: { value: 'db1' } }); @@ -15,7 +22,11 @@ describe('PostgreSQL connection details:: ', () => { }); it('should have max query length attribute', () => { - render( } />); + render( + + } /> + + ); const textInput = screen.getByTestId('maxQueryLength-text-input'); fireEvent.change(textInput, { target: { value: '1000' } }); diff --git a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.tsx b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.tsx index 1ae6fbc86512d..1ae0318217c47 100644 --- a/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.tsx +++ b/public/app/percona/add-instance/components/AddRemoteInstance/FormParts/PostgreSQLConnectionDetails/PostgreSQLConnectionDetails.tsx @@ -9,6 +9,7 @@ import { validators } from 'app/percona/shared/helpers/validatorsForm'; import { Messages } from '../FormParts.messages'; import { getStyles } from '../FormParts.styles'; import { MainDetailsFormPartProps } from '../FormParts.types'; +import { NodesAgents } from '../NodesAgents/NodesAgents'; export const PostgreSQLConnectionDetails: FC = ({ form, remoteInstanceCredentials }) => { const styles = useStyles2(getStyles); @@ -31,6 +32,7 @@ export const PostgreSQLConnectionDetails: FC = ({ form />
+
nodeFromDbMapper(nodes).sort((a, b) => a.nodeName.localeCompare(b.nodeName)), + [nodes] + ); + const service = services.find((s) => s.params.serviceId === match.params.serviceId); - const node = nodes.find((s) => s.nodeId === nodeId); + const node = mappedNodes.find((s) => s.nodeId === nodeId); const flattenAgents = useMemo(() => data.map((value) => ({ type: value.type, ...value.params })), [data]); const columns = useMemo( diff --git a/public/app/percona/inventory/Tabs/Nodes.tsx b/public/app/percona/inventory/Tabs/Nodes.tsx index 52a1ec7a38e0f..95efa549fd074 100644 --- a/public/app/percona/inventory/Tabs/Nodes.tsx +++ b/public/app/percona/inventory/Tabs/Nodes.tsx @@ -14,7 +14,7 @@ import { FormElement } from 'app/percona/shared/components/Form'; import { TabbedPage, TabbedPageContents } from 'app/percona/shared/components/TabbedPage'; import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook'; import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel'; -import { RemoveNodeParams } from 'app/percona/shared/core/reducers/nodes'; +import { nodeFromDbMapper, RemoveNodeParams } from 'app/percona/shared/core/reducers/nodes'; import { fetchNodesAction, removeNodesAction } from 'app/percona/shared/core/reducers/nodes/nodes'; import { getNodes } from 'app/percona/shared/core/selectors'; import { isApiCancelError } from 'app/percona/shared/helpers/api'; @@ -51,6 +51,11 @@ export const NodesTab = () => { const styles = useStyles2(getStyles); const dispatch = useAppDispatch(); + const mappedNodes = useMemo( + () => nodeFromDbMapper(nodes).sort((a, b) => a.nodeName.localeCompare(b.nodeName)), + [nodes] + ); + const getActions = useCallback( (row: Row): Action[] => [ { @@ -350,8 +355,8 @@ export const NodesTab = () => { ( 'app/percona/inventory/Inventory.service' ).InventoryService; @@ -39,3 +169,8 @@ InventoryService.getService = () => Promise.resolve({ mysql: stubWithLabels, }); + +InventoryService.getNodes = () => + Promise.resolve({ + nodes: nodesMock, + }); diff --git a/public/app/percona/shared/core/reducers/nodes/nodes.ts b/public/app/percona/shared/core/reducers/nodes/nodes.ts index 6a2099e9b6b11..8c5c7dedf0fec 100644 --- a/public/app/percona/shared/core/reducers/nodes/nodes.ts +++ b/public/app/percona/shared/core/reducers/nodes/nodes.ts @@ -3,11 +3,10 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; import { CancelToken } from 'axios'; import { InventoryService } from 'app/percona/inventory/Inventory.service'; -import { Node, RemoveNodeBody } from 'app/percona/inventory/Inventory.types'; +import { NodeDB, RemoveNodeBody } from 'app/percona/inventory/Inventory.types'; import { filterFulfilled, processPromiseResults } from 'app/percona/shared/helpers/promises'; import { NodesState, RemoveNodesParams } from './nodes.types'; -import { nodeFromDbMapper } from './nodes.utils'; const initialState: NodesState = { nodes: [], @@ -35,12 +34,11 @@ const nodesSlice = createSlice({ }, }); -export const fetchNodesAction = createAsyncThunk( +export const fetchNodesAction = createAsyncThunk( 'percona/fetchNodes', async (params = {}) => { const { nodes } = await InventoryService.getNodes(params.token); - const mappedNodes = nodeFromDbMapper(nodes); - return mappedNodes.sort((a, b) => a.nodeName.localeCompare(b.nodeName)); + return nodes; } ); diff --git a/public/app/percona/shared/core/reducers/nodes/nodes.types.ts b/public/app/percona/shared/core/reducers/nodes/nodes.types.ts index 76610b40756f9..aeece8752072e 100644 --- a/public/app/percona/shared/core/reducers/nodes/nodes.types.ts +++ b/public/app/percona/shared/core/reducers/nodes/nodes.types.ts @@ -1,9 +1,9 @@ import { CancelToken } from 'axios'; -import { Node } from 'app/percona/inventory/Inventory.types'; +import { NodeDB } from 'app/percona/inventory/Inventory.types'; export interface NodesState { - nodes: Node[]; + nodes: NodeDB[]; isLoading: boolean; } diff --git a/public/app/percona/shared/core/reducers/nodes/nodes.utils.ts b/public/app/percona/shared/core/reducers/nodes/nodes.utils.ts index 3b7193cd27adf..19817dfd11a8b 100644 --- a/public/app/percona/shared/core/reducers/nodes/nodes.utils.ts +++ b/public/app/percona/shared/core/reducers/nodes/nodes.utils.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions */ -import { Node, NodeDB } from 'app/percona/inventory/Inventory.types'; +import { AgentsOption, AgentType, Node, NodeDB, NodesOption } from 'app/percona/inventory/Inventory.types'; import { getAgentsMonitoringStatus } from 'app/percona/inventory/Tabs/Services.utils'; import { DbAgent } from 'app/percona/shared/services/services/Services.types'; const MAIN_COLUMNS = ['address', 'services', 'agents', 'node_type', 'node_id', 'node_name', 'status', 'custom_labels']; -export const nodeFromDbMapper = (nodeFromDb: NodeDB[]) => { +export const nodeFromDbMapper = (nodeFromDb: NodeDB[]): Node[] => { return nodeFromDb.map((node) => { const properties: Record = {}; @@ -51,6 +51,19 @@ export const nodeFromDbMapper = (nodeFromDb: NodeDB[]) => { })), properties: properties, agentsStatus: getAgentsMonitoringStatus(agents ?? []), - } as Node; + }; }); }; + +export const nodesOptionsMapper = (nodeFromDb: NodeDB[]): NodesOption[] => + nodeFromDb.map((node) => { + const agents = (node.agents || []) + .filter((agent) => agent.agent_type === AgentType.pmmAgent) + .map((agent) => ({ value: agent.agent_id, label: agent.agent_id, key: agent.agent_type })); + + return { + value: node.node_id, + label: node.node_name, + agents: agents, + }; + });