diff options
author | David O Neill <dmz.oneill@gmail.com> | 2024-01-04 18:04:56 +0100 |
---|---|---|
committer | Seth Foster <fosterseth@users.noreply.github.com> | 2024-02-02 16:37:41 +0100 |
commit | 82ad7dcf40185f886f83619f59060666fcb6bf07 (patch) | |
tree | 6891a7a0c82f6cb05eb86fcb5e6c7a31f1c270b1 | |
parent | Fix lint trailing whitespace (diff) | |
download | awx-82ad7dcf40185f886f83619f59060666fcb6bf07.tar.xz awx-82ad7dcf40185f886f83619f59060666fcb6bf07.zip |
Mesh UI support
- add endpoint
- delete endpoint (wip)
- associate
- disassociate
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`, }); }, []); |