summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid O Neill <dmz.oneill@gmail.com>2024-01-04 18:04:56 +0100
committerSeth Foster <fosterseth@users.noreply.github.com>2024-02-02 16:37:41 +0100
commit82ad7dcf40185f886f83619f59060666fcb6bf07 (patch)
tree6891a7a0c82f6cb05eb86fcb5e6c7a31f1c270b1
parentFix lint trailing whitespace (diff)
downloadawx-82ad7dcf40185f886f83619f59060666fcb6bf07.tar.xz
awx-82ad7dcf40185f886f83619f59060666fcb6bf07.zip
Mesh UI support
- add endpoint - delete endpoint (wip) - associate - disassociate
-rw-r--r--awx/ui/src/api/models/Instances.js4
-rw-r--r--awx/ui/src/api/models/Receptor.js4
-rw-r--r--awx/ui/src/components/AddEndpointModal/AddEndpointModal.js97
-rw-r--r--awx/ui/src/components/AddEndpointModal/AddEndpointModal.test.js84
-rw-r--r--awx/ui/src/components/AddEndpointModal/data.hosts.json393
-rw-r--r--awx/ui/src/components/AddEndpointModal/index.js1
-rw-r--r--awx/ui/src/screens/Instances/Instance.js9
-rw-r--r--awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointList.js188
-rw-r--r--awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointListItem.js54
-rw-r--r--awx/ui/src/screens/Instances/InstanceEndPointList/index.js1
-rw-r--r--awx/ui/src/screens/Instances/InstancePeers/InstancePeerList.js6
-rw-r--r--awx/ui/src/screens/Instances/InstancePeers/InstancePeerListItem.js1
-rw-r--r--awx/ui/src/screens/Instances/Instances.js1
13 files changed, 838 insertions, 5 deletions
diff --git a/awx/ui/src/api/models/Instances.js b/awx/ui/src/api/models/Instances.js
index f04d98ceff..64a7e70c40 100644
--- a/awx/ui/src/api/models/Instances.js
+++ b/awx/ui/src/api/models/Instances.js
@@ -32,6 +32,10 @@ class Instances extends Base {
return this.http.get(`${this.baseUrl}${instanceId}/receptor_addresses/`);
}
+ updateReceptorAddresses(instanceId, data) {
+ return this.http.post(`${this.baseUrl}${instanceId}/receptor_addresses/`, data);
+ }
+
deprovisionInstance(instanceId) {
return this.http.patch(`${this.baseUrl}${instanceId}/`, {
node_state: 'deprovisioning',
diff --git a/awx/ui/src/api/models/Receptor.js b/awx/ui/src/api/models/Receptor.js
index c20ca73a89..fd63d4cf74 100644
--- a/awx/ui/src/api/models/Receptor.js
+++ b/awx/ui/src/api/models/Receptor.js
@@ -5,6 +5,10 @@ class ReceptorAddresses extends Base {
super(http);
this.baseUrl = 'api/v2/receptor_addresses/';
}
+
+ updateReceptorAddresses(instanceId, data) {
+ return this.http.post(`${this.baseUrl}`, data);
+ }
}
export default ReceptorAddresses;
diff --git a/awx/ui/src/components/AddEndpointModal/AddEndpointModal.js b/awx/ui/src/components/AddEndpointModal/AddEndpointModal.js
new file mode 100644
index 0000000000..a102b88c92
--- /dev/null
+++ b/awx/ui/src/components/AddEndpointModal/AddEndpointModal.js
@@ -0,0 +1,97 @@
+import React from 'react';
+
+import { t } from '@lingui/macro';
+import { Form, FormGroup, Modal } from '@patternfly/react-core';
+import { InstancesAPI } from 'api';
+import { Formik } from 'formik';
+import { FormColumnLayout } from 'components/FormLayout';
+import FormField, {
+ CheckboxField,
+} from 'components/FormField';
+import FormActionGroup from '../FormActionGroup/FormActionGroup';
+
+function AddEndpointModal({
+ title = t`Add endpoint`,
+ onClose,
+ isAddEndpointModalOpen = false,
+ instance,
+ ouiaId,
+}) {
+
+ const handleClose = () => {
+ onClose();
+ };
+
+ const handleEndpointAdd = async (values) => {
+ try {
+ values.id = instance.id;
+ InstancesAPI.updateReceptorAddresses(instance.id, values);
+ onClose();
+ } catch (error) {
+ // do nothing
+ }
+ };
+
+ return (
+ <Modal
+ ouiaId={ouiaId}
+ variant="large"
+ title={title}
+ aria-label={t`Add Endpoint modal`}
+ isOpen={isAddEndpointModalOpen}
+ onClose={handleClose}
+ actions={[]}
+ >
+ <Formik
+ initialValues={{
+ listener_port: 1001
+ }}
+ onSubmit={handleEndpointAdd}
+ >
+ {(formik) => (
+ <Form autoComplete="off" onSubmit={formik.handleSubmit}>
+ <FormColumnLayout>
+ <FormField
+ id="address"
+ label={t`Address`}
+ name="address"
+ type="text"
+ />
+
+ <FormField
+ id="websocket_path"
+ label={t`Websocket path`}
+ name="websocket path"
+ type="text"
+ />
+
+ <FormField
+ id="listener_port"
+ label={t`Listener Port`}
+ name="listener_port"
+ type="number"
+ tooltip={t`Select the port that Receptor will listen on for incoming connections, e.g. 27199.`}
+ />
+
+ <FormGroup fieldId="endpoint" label={t`Options`}>
+ <CheckboxField
+ id="peers_from_control_nodes"
+ name="peers_from_control_nodes"
+ label={t`Peers from control nodes`}
+ tooltip={t`If enabled, control nodes will peer to this instance automatically. If disabled, instance will be connected only to associated peers.`}
+ />
+ </FormGroup>
+
+ <FormActionGroup
+ onCancel={handleClose}
+ onSubmit={formik.handleSubmit}
+ />
+ </FormColumnLayout>
+ </Form>
+ )}
+ </Formik>
+ </Modal>
+ );
+}
+
+export default AddEndpointModal; \ No newline at end of file
diff --git a/awx/ui/src/components/AddEndpointModal/AddEndpointModal.test.js b/awx/ui/src/components/AddEndpointModal/AddEndpointModal.test.js
new file mode 100644
index 0000000000..8c4a309d6b
--- /dev/null
+++ b/awx/ui/src/components/AddEndpointModal/AddEndpointModal.test.js
@@ -0,0 +1,84 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+
+import {
+ mountWithContexts,
+ waitForElement,
+} from '../../../testUtils/enzymeHelpers';
+import AssociateModal from './AddEndpointModal';
+import mockHosts from './data.hosts.json';
+
+jest.mock('../../api');
+
+describe('<AssociateModal />', () => {
+ let wrapper;
+ let onClose;
+ let onAssociate;
+ let fetchRequest;
+ let optionsRequest;
+
+ beforeEach(async () => {
+ onClose = jest.fn();
+ onAssociate = jest.fn().mockResolvedValue();
+ fetchRequest = jest.fn().mockReturnValue({ data: { ...mockHosts } });
+ optionsRequest = jest.fn().mockResolvedValue({
+ data: {
+ actions: {
+ GET: {},
+ POST: {},
+ },
+ related_search_fields: [],
+ },
+ });
+ await act(async () => {
+ wrapper = mountWithContexts(
+ <AssociateModal
+ onClose={onClose}
+ onAssociate={onAssociate}
+ fetchRequest={fetchRequest}
+ optionsRequest={optionsRequest}
+ isModalOpen
+ />
+ );
+ });
+ await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('should render successfully', () => {
+ expect(wrapper.find('AssociateModal').length).toBe(1);
+ });
+
+ test('should fetch and render list items', () => {
+ expect(fetchRequest).toHaveBeenCalledTimes(1);
+ expect(optionsRequest).toHaveBeenCalledTimes(1);
+ expect(wrapper.find('CheckboxListItem').length).toBe(3);
+ });
+
+ test('should update selected list chips when items are selected', () => {
+ expect(wrapper.find('SelectedList Chip')).toHaveLength(0);
+ act(() => {
+ wrapper.find('CheckboxListItem').first().invoke('onSelect')();
+ });
+ wrapper.update();
+ expect(wrapper.find('SelectedList Chip')).toHaveLength(1);
+ wrapper.find('SelectedList Chip button').simulate('click');
+ expect(wrapper.find('SelectedList Chip')).toHaveLength(0);
+ });
+
+ test('save button should call onAssociate', () => {
+ act(() => {
+ wrapper.find('CheckboxListItem').first().invoke('onSelect')();
+ });
+ wrapper.find('button[aria-label="Save"]').simulate('click');
+ expect(onAssociate).toHaveBeenCalledTimes(1);
+ });
+
+ test('cancel button should call onClose', () => {
+ wrapper.find('button[aria-label="Cancel"]').simulate('click');
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/awx/ui/src/components/AddEndpointModal/data.hosts.json b/awx/ui/src/components/AddEndpointModal/data.hosts.json
new file mode 100644
index 0000000000..07c6ef7d9f
--- /dev/null
+++ b/awx/ui/src/components/AddEndpointModal/data.hosts.json
@@ -0,0 +1,393 @@
+
+{
+ "count": 3,
+ "results": [
+ {
+ "id": 2,
+ "type": "host",
+ "url": "/api/v2/hosts/2/",
+ "related": {
+ "created_by": "/api/v2/users/10/",
+ "modified_by": "/api/v2/users/19/",
+ "variable_data": "/api/v2/hosts/2/variable_data/",
+ "groups": "/api/v2/hosts/2/groups/",
+ "all_groups": "/api/v2/hosts/2/all_groups/",
+ "job_events": "/api/v2/hosts/2/job_events/",
+ "job_host_summaries": "/api/v2/hosts/2/job_host_summaries/",
+ "activity_stream": "/api/v2/hosts/2/activity_stream/",
+ "inventory_sources": "/api/v2/hosts/2/inventory_sources/",
+ "smart_inventories": "/api/v2/hosts/2/smart_inventories/",
+ "ad_hoc_commands": "/api/v2/hosts/2/ad_hoc_commands/",
+ "ad_hoc_command_events": "/api/v2/hosts/2/ad_hoc_command_events/",
+ "insights": "/api/v2/hosts/2/insights/",
+ "ansible_facts": "/api/v2/hosts/2/ansible_facts/",
+ "inventory": "/api/v2/inventories/2/",
+ "last_job": "/api/v2/jobs/236/",
+ "last_job_host_summary": "/api/v2/job_host_summaries/2202/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 2,
+ "name": " Inventory 1 Org 0",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 33,
+ "hosts_with_active_failures": 0,
+ "total_groups": 4,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 2,
+ "kind": ""
+ },
+ "last_job": {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "description": "",
+ "finished": "2020-02-26T03:15:21.471439Z",
+ "status": "successful",
+ "failed": false,
+ "job_template_id": 18,
+ "job_template_name": " Job Template 1 Project 0"
+ },
+ "last_job_host_summary": {
+ "id": 2202,
+ "failed": false
+ },
+ "created_by": {
+ "id": 10,
+ "username": "user-3",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 19,
+ "username": "all",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "groups": {
+ "count": 2,
+ "results": [
+ {
+ "id": 1,
+ "name": " Group 1 Inventory 0"
+ },
+ {
+ "id": 2,
+ "name": " Group 2 Inventory 0"
+ }
+ ]
+ },
+ "recent_jobs": [
+ {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-26T03:15:21.471439Z"
+ },
+ {
+ "id": 232,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T21:20:33.593789Z"
+ },
+ {
+ "id": 229,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:19:46.364134Z"
+ },
+ {
+ "id": 228,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:18:54.138363Z"
+ },
+ {
+ "id": 225,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T15:55:32.247652Z"
+ }
+ ]
+ },
+ "created": "2020-02-24T15:10:58.922179Z",
+ "modified": "2020-02-26T21:52:43.428530Z",
+ "name": ".host-000001.group-00000.dummy",
+ "description": "",
+ "inventory": 2,
+ "enabled": false,
+ "instance_id": "",
+ "variables": "",
+ "has_active_failures": false,
+ "has_inventory_sources": false,
+ "last_job": 236,
+ "last_job_host_summary": 2202,
+ "insights_system_id": null,
+ "ansible_facts_modified": null
+ },
+ {
+ "id": 3,
+ "type": "host",
+ "url": "/api/v2/hosts/3/",
+ "related": {
+ "created_by": "/api/v2/users/11/",
+ "modified_by": "/api/v2/users/1/",
+ "variable_data": "/api/v2/hosts/3/variable_data/",
+ "groups": "/api/v2/hosts/3/groups/",
+ "all_groups": "/api/v2/hosts/3/all_groups/",
+ "job_events": "/api/v2/hosts/3/job_events/",
+ "job_host_summaries": "/api/v2/hosts/3/job_host_summaries/",
+ "activity_stream": "/api/v2/hosts/3/activity_stream/",
+ "inventory_sources": "/api/v2/hosts/3/inventory_sources/",
+ "smart_inventories": "/api/v2/hosts/3/smart_inventories/",
+ "ad_hoc_commands": "/api/v2/hosts/3/ad_hoc_commands/",
+ "ad_hoc_command_events": "/api/v2/hosts/3/ad_hoc_command_events/",
+ "insights": "/api/v2/hosts/3/insights/",
+ "ansible_facts": "/api/v2/hosts/3/ansible_facts/",
+ "inventory": "/api/v2/inventories/2/",
+ "last_job": "/api/v2/jobs/236/",
+ "last_job_host_summary": "/api/v2/job_host_summaries/2195/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 2,
+ "name": " Inventory 1 Org 0",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 33,
+ "hosts_with_active_failures": 0,
+ "total_groups": 4,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 2,
+ "kind": ""
+ },
+ "last_job": {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "description": "",
+ "finished": "2020-02-26T03:15:21.471439Z",
+ "status": "successful",
+ "failed": false,
+ "job_template_id": 18,
+ "job_template_name": " Job Template 1 Project 0"
+ },
+ "last_job_host_summary": {
+ "id": 2195,
+ "failed": false
+ },
+ "created_by": {
+ "id": 11,
+ "username": "user-4",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "groups": {
+ "count": 2,
+ "results": [
+ {
+ "id": 1,
+ "name": " Group 1 Inventory 0"
+ },
+ {
+ "id": 2,
+ "name": " Group 2 Inventory 0"
+ }
+ ]
+ },
+ "recent_jobs": [
+ {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-26T03:15:21.471439Z"
+ },
+ {
+ "id": 232,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T21:20:33.593789Z"
+ },
+ {
+ "id": 229,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:19:46.364134Z"
+ },
+ {
+ "id": 228,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:18:54.138363Z"
+ },
+ {
+ "id": 225,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T15:55:32.247652Z"
+ }
+ ]
+ },
+ "created": "2020-02-24T15:10:58.945113Z",
+ "modified": "2020-02-27T03:43:43.635871Z",
+ "name": ".host-000002.group-00000.dummy",
+ "description": "",
+ "inventory": 2,
+ "enabled": false,
+ "instance_id": "",
+ "variables": "",
+ "has_active_failures": false,
+ "has_inventory_sources": false,
+ "last_job": 236,
+ "last_job_host_summary": 2195,
+ "insights_system_id": null,
+ "ansible_facts_modified": null
+ },
+ {
+ "id": 4,
+ "type": "host",
+ "url": "/api/v2/hosts/4/",
+ "related": {
+ "created_by": "/api/v2/users/12/",
+ "modified_by": "/api/v2/users/1/",
+ "variable_data": "/api/v2/hosts/4/variable_data/",
+ "groups": "/api/v2/hosts/4/groups/",
+ "all_groups": "/api/v2/hosts/4/all_groups/",
+ "job_events": "/api/v2/hosts/4/job_events/",
+ "job_host_summaries": "/api/v2/hosts/4/job_host_summaries/",
+ "activity_stream": "/api/v2/hosts/4/activity_stream/",
+ "inventory_sources": "/api/v2/hosts/4/inventory_sources/",
+ "smart_inventories": "/api/v2/hosts/4/smart_inventories/",
+ "ad_hoc_commands": "/api/v2/hosts/4/ad_hoc_commands/",
+ "ad_hoc_command_events": "/api/v2/hosts/4/ad_hoc_command_events/",
+ "insights": "/api/v2/hosts/4/insights/",
+ "ansible_facts": "/api/v2/hosts/4/ansible_facts/",
+ "inventory": "/api/v2/inventories/2/",
+ "last_job": "/api/v2/jobs/236/",
+ "last_job_host_summary": "/api/v2/job_host_summaries/2192/"
+ },
+ "summary_fields": {
+ "inventory": {
+ "id": 2,
+ "name": " Inventory 1 Org 0",
+ "description": "",
+ "has_active_failures": false,
+ "total_hosts": 33,
+ "hosts_with_active_failures": 0,
+ "total_groups": 4,
+ "has_inventory_sources": false,
+ "total_inventory_sources": 0,
+ "inventory_sources_with_failures": 0,
+ "organization_id": 2,
+ "kind": ""
+ },
+ "last_job": {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "description": "",
+ "finished": "2020-02-26T03:15:21.471439Z",
+ "status": "successful",
+ "failed": false,
+ "job_template_id": 18,
+ "job_template_name": " Job Template 1 Project 0"
+ },
+ "last_job_host_summary": {
+ "id": 2192,
+ "failed": false
+ },
+ "created_by": {
+ "id": 12,
+ "username": "user-5",
+ "first_name": "",
+ "last_name": ""
+ },
+ "modified_by": {
+ "id": 1,
+ "username": "admin",
+ "first_name": "",
+ "last_name": ""
+ },
+ "user_capabilities": {
+ "edit": true,
+ "delete": true
+ },
+ "groups": {
+ "count": 2,
+ "results": [
+ {
+ "id": 1,
+ "name": " Group 1 Inventory 0"
+ },
+ {
+ "id": 2,
+ "name": " Group 2 Inventory 0"
+ }
+ ]
+ },
+ "recent_jobs": [
+ {
+ "id": 236,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-26T03:15:21.471439Z"
+ },
+ {
+ "id": 232,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T21:20:33.593789Z"
+ },
+ {
+ "id": 229,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:19:46.364134Z"
+ },
+ {
+ "id": 228,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T16:18:54.138363Z"
+ },
+ {
+ "id": 225,
+ "name": " Job Template 1 Project 0",
+ "status": "successful",
+ "finished": "2020-02-25T15:55:32.247652Z"
+ }
+ ]
+ },
+ "created": "2020-02-24T15:10:58.962312Z",
+ "modified": "2020-02-27T03:43:45.528882Z",
+ "name": ".host-000003.group-00000.dummy",
+ "description": "",
+ "inventory": 2,
+ "enabled": false,
+ "instance_id": "",
+ "variables": "",
+ "has_active_failures": false,
+ "has_inventory_sources": false,
+ "last_job": 236,
+ "last_job_host_summary": 2192,
+ "insights_system_id": null,
+ "ansible_facts_modified": null
+ }
+ ]
+}
diff --git a/awx/ui/src/components/AddEndpointModal/index.js b/awx/ui/src/components/AddEndpointModal/index.js
new file mode 100644
index 0000000000..ff04ac09af
--- /dev/null
+++ b/awx/ui/src/components/AddEndpointModal/index.js
@@ -0,0 +1 @@
+export { default } from './AddEndpointModal';
diff --git a/awx/ui/src/screens/Instances/Instance.js b/awx/ui/src/screens/Instances/Instance.js
index 6d0d1e8004..1a0f5fd76d 100644
--- a/awx/ui/src/screens/Instances/Instance.js
+++ b/awx/ui/src/screens/Instances/Instance.js
@@ -12,6 +12,7 @@ import { SettingsAPI } from 'api';
import ContentLoading from 'components/ContentLoading';
import InstanceDetail from './InstanceDetail';
import InstancePeerList from './InstancePeers';
+import InstanceEndPointList from './InstanceEndPointList';
function Instance({ setBreadcrumb }) {
const { me } = useConfig();
@@ -54,7 +55,8 @@ function Instance({ setBreadcrumb }) {
}, [request]);
if (isK8s) {
- tabsArray.push({ name: t`Peers`, link: `${match.url}/peers`, id: 1 });
+ tabsArray.push({ name: t`Endpoints`, link: `${match.url}/endpoints`, id: 1 });
+ tabsArray.push({ name: t`Peers`, link: `${match.url}/peers`, id: 2 });
}
if (isLoading) {
return <ContentLoading />;
@@ -73,6 +75,11 @@ function Instance({ setBreadcrumb }) {
<InstanceDetail isK8s={isK8s} setBreadcrumb={setBreadcrumb} />
</Route>
{isK8s && (
+ <Route path="/instances/:id/endpoints" key="endpoints">
+ <InstanceEndPointList setBreadcrumb={setBreadcrumb} />
+ </Route>
+ )}
+ {isK8s && (
<Route path="/instances/:id/peers" key="peers">
<InstancePeerList setBreadcrumb={setBreadcrumb} />
</Route>
diff --git a/awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointList.js b/awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointList.js
new file mode 100644
index 0000000000..e3d1131941
--- /dev/null
+++ b/awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointList.js
@@ -0,0 +1,188 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { t } from '@lingui/macro';
+import { CardBody } from 'components/Card';
+import PaginatedTable, {
+ getSearchableKeys,
+ HeaderCell,
+ HeaderRow,
+ ToolbarAddButton,
+} from 'components/PaginatedTable';
+import AddEndpointModal from 'components/AddEndpointModal';
+import useToast from 'hooks/useToast';
+import { getQSConfig } from 'util/qs';
+import { useParams } from 'react-router-dom';
+import useRequest from 'hooks/useRequest';
+import DataListToolbar from 'components/DataListToolbar';
+import { InstancesAPI, ReceptorAPI } from 'api';
+import useExpanded from 'hooks/useExpanded';
+import useSelected from 'hooks/useSelected';
+import InstanceEndPointListItem from './InstanceEndPointListItem';
+
+const QS_CONFIG = getQSConfig('peer', {
+ page: 1,
+ page_size: 20,
+ order_by: 'pk',
+});
+
+function InstanceEndPointList({ setBreadcrumb }) {
+ const { id } = useParams();
+ const [isAddEndpointModalOpen, setisAddEndpointModalOpen] = useState(false);
+ const { Toast, toastProps } = useToast();
+ const {
+ isLoading,
+ error: contentError,
+ request: fetchEndpoints,
+ result: { instance, endpoints, count, relatedSearchableKeys, searchableKeys },
+ } = useRequest(
+ useCallback(async () => {
+ const [
+ { data: detail },
+ {
+ data: { results },
+ },
+ actions,
+ ] = await Promise.all([
+ InstancesAPI.readDetail(id),
+ ReceptorAPI.read(),
+ InstancesAPI.readOptions(),
+ ]);
+
+ const endpoint_list = []
+
+ for(let q = 0; q < results.length; q++) {
+ const receptor = results[q];
+ if(id.toString() === receptor.instance.toString()) {
+ endpoint_list.push(receptor);
+ }
+ }
+
+ return {
+ instance: detail,
+ endpoints: endpoint_list,
+ count: endpoint_list.length,
+ relatedSearchableKeys: (actions?.data?.related_search_fields || []).map(
+ (val) => val.slice(0, -8)
+ ),
+ searchableKeys: getSearchableKeys(actions.data.actions?.GET),
+ };
+ }, [id]),
+ {
+ instance: {},
+ endpoints: [],
+ count: 0,
+ relatedSearchableKeys: [],
+ searchableKeys: [],
+ }
+ );
+
+ useEffect(() => {
+ fetchEndpoints();
+ }, [fetchEndpoints]);
+
+ useEffect(() => {
+ if (instance) {
+ setBreadcrumb(instance);
+ }
+ }, [instance, setBreadcrumb]);
+
+ const { expanded, isAllExpanded, handleExpand, expandAll } =
+ useExpanded(endpoints);
+ const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
+ useSelected(endpoints);
+
+ const handleEndpointDelete = async () => {
+ // console.log(selected)
+ // InstancesAPI.updateReceptorAddresses(instance.id, values);
+ }
+
+ const isHopNode = instance.node_type === 'hop';
+ const isExecutionNode = instance.node_type === 'execution';
+
+ return (
+ <CardBody>
+ <PaginatedTable
+ contentError={contentError}
+ hasContentLoading={
+ isLoading
+ }
+ items={endpoints}
+ itemCount={count}
+ pluralizedItemName={t`Endpoints`}
+ qsConfig={QS_CONFIG}
+ onRowClick={handleSelect}
+ clearSelected={clearSelected}
+ toolbarSearchableKeys={searchableKeys}
+ toolbarRelatedSearchableKeys={relatedSearchableKeys}
+ toolbarSearchColumns={[
+ {
+ name: t`Name`,
+ key: 'hostname__icontains',
+ isDefault: true,
+ },
+ ]}
+ toolbarSortColumns={[
+ {
+ name: t`Name`,
+ key: 'hostname',
+ },
+ ]}
+ headerRow={
+ <HeaderRow qsConfig={QS_CONFIG} isExpandable>
+ <HeaderCell sortKey="address">{t`Address`}</HeaderCell>
+ <HeaderCell sortKey="port">{t`Port`}</HeaderCell>
+ </HeaderRow>
+ }
+ renderToolbar={(props) => (
+ <DataListToolbar
+ {...props}
+ isAllSelected={isAllSelected}
+ onSelectAll={selectAll}
+ isAllExpanded={isAllExpanded}
+ onExpandAll={expandAll}
+ qsConfig={QS_CONFIG}
+ additionalControls={[
+ (isExecutionNode || isHopNode) && (
+ <ToolbarAddButton
+ ouiaId="add-endpoint-button"
+ key="add-endpoint"
+ defaultLabel={t`Add`}
+ onClick={() => setisAddEndpointModalOpen(true)}
+ />
+ ),
+ (isExecutionNode || isHopNode) && (
+ <ToolbarAddButton
+ ouiaId="delete-endpoint-button"
+ key="delete-endpoint"
+ defaultLabel={t`Delete`}
+ onClick={() => handleEndpointDelete()}
+ />
+ ),
+ ]}
+ />
+ )}
+ renderRow={(endpoint, index) => (
+ <InstanceEndPointListItem
+ isSelected={selected.some((row) => row.id === endpoint.id)}
+ onSelect={() => handleSelect(endpoint)}
+ isExpanded={expanded.some((row) => row.id === endpoint.id)}
+ onExpand={() => handleExpand(endpoint)}
+ key={endpoint.id}
+ peerInstance={endpoint}
+ rowIndex={index}
+ />
+ )}
+ />
+ {isAddEndpointModalOpen && (
+ <AddEndpointModal
+ isAddEndpointModalOpen={isAddEndpointModalOpen}
+ onClose={() => setisAddEndpointModalOpen(false)}
+ title={t`New endpoint`}
+ instance={instance}
+ />
+ )}
+ <Toast {...toastProps} />
+ </CardBody>
+ );
+}
+
+export default InstanceEndPointList;
diff --git a/awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointListItem.js b/awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointListItem.js
new file mode 100644
index 0000000000..2a1fd5dddb
--- /dev/null
+++ b/awx/ui/src/screens/Instances/InstanceEndPointList/InstanceEndPointListItem.js
@@ -0,0 +1,54 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import { t } from '@lingui/macro';
+import 'styled-components/macro';
+import { Tr, Td } from '@patternfly/react-table';
+
+function InstanceEndPointListItem({
+ peerInstance,
+ isSelected,
+ onSelect,
+ isExpanded,
+ onExpand,
+ rowIndex,
+}) {
+ const labelId = `check-action-${peerInstance.id}`;
+ return (
+ <Tr
+ id={`peerInstance-row-${peerInstance.id}`}
+ ouiaId={`peerInstance-row-${peerInstance.id}`}
+ >
+ <Td
+ expand={{
+ rowIndex,
+ isExpanded,
+ onToggle: onExpand,
+ }}
+ />
+
+ <Td
+ select={{
+ rowIndex,
+ isSelected,
+ onSelect,
+ }}
+ dataLabel={t`Selected`}
+ />
+
+ <Td id={labelId} dataLabel={t`Address`}>
+ <Link to={`/instances/${peerInstance.instance}/details`}>
+ <b>{peerInstance.address}</b>
+ </Link>
+ </Td>
+
+ <Td id={labelId} dataLabel={t`Port`}>
+ <Link to={`/instances/${peerInstance.instance}/details`}>
+ <b>{peerInstance.port}</b>
+ </Link>
+ </Td>
+
+ </Tr>
+ );
+}
+
+export default InstanceEndPointListItem;
diff --git a/awx/ui/src/screens/Instances/InstanceEndPointList/index.js b/awx/ui/src/screens/Instances/InstanceEndPointList/index.js
new file mode 100644
index 0000000000..9e5f69dd34
--- /dev/null
+++ b/awx/ui/src/screens/Instances/InstanceEndPointList/index.js
@@ -0,0 +1 @@
+export { default } from './InstanceEndPointList';
diff --git a/awx/ui/src/screens/Instances/InstancePeers/InstancePeerList.js b/awx/ui/src/screens/Instances/InstancePeers/InstancePeerList.js
index 2af9d3d43b..b56be6ba0d 100644
--- a/awx/ui/src/screens/Instances/InstancePeers/InstancePeerList.js
+++ b/awx/ui/src/screens/Instances/InstancePeers/InstancePeerList.js
@@ -47,7 +47,7 @@ function InstancePeerList({ setBreadcrumb }) {
const [
{ data: detail },
{
- data: { results, count: itemNumber },
+ data: { results },
},
actions,
instances,
@@ -72,7 +72,7 @@ function InstancePeerList({ setBreadcrumb }) {
return {
instance: detail,
peers: address_list,
- count: itemNumber,
+ count: address_list.length,
relatedSearchableKeys: (actions?.data?.related_search_fields || []).map(
(val) => val.slice(0, -8)
),
@@ -283,7 +283,7 @@ function InstancePeerList({ setBreadcrumb }) {
key="disassociate"
onDisassociate={handlePeersDiassociate}
itemsToDisassociate={selected}
- modalTitle={t`Remove instance from peers?`}
+ modalTitle={t`Remove peers?`}
/>
),
]}
diff --git a/awx/ui/src/screens/Instances/InstancePeers/InstancePeerListItem.js b/awx/ui/src/screens/Instances/InstancePeers/InstancePeerListItem.js
index 0cd6e7e6da..796f20572a 100644
--- a/awx/ui/src/screens/Instances/InstancePeers/InstancePeerListItem.js
+++ b/awx/ui/src/screens/Instances/InstancePeers/InstancePeerListItem.js
@@ -43,7 +43,6 @@ function InstancePeerListItem({
}}
dataLabel={t`Selected`}
/>
-
<Td id={labelId} dataLabel={t`Address`}>
<Link to={`/instances/${peerInstance.instance}/details`}>
<b>{peerInstance.address}</b>
diff --git a/awx/ui/src/screens/Instances/Instances.js b/awx/ui/src/screens/Instances/Instances.js
index eefc7d2716..9836ffa290 100644
--- a/awx/ui/src/screens/Instances/Instances.js
+++ b/awx/ui/src/screens/Instances/Instances.js
@@ -25,6 +25,7 @@ function Instances() {
[`/instances/${instance.id}`]: `${instance.hostname}`,
[`/instances/${instance.id}/details`]: t`Details`,
[`/instances/${instance.id}/peers`]: t`Peers`,
+ [`/instances/${instance.id}/endpoints`]: t`Endpoints`,
[`/instances/${instance.id}/edit`]: t`Edit Instance`,
});
}, []);