Skip to content

Commit

Permalink
Method list dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffibm committed Feb 28, 2024
1 parent 9d906b9 commit ecffe86
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 0 deletions.
12 changes: 12 additions & 0 deletions app/controllers/miq_ae_class_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,18 @@ def namespace
render :json => find_record_with_rbac(MiqAeNamespace, params[:id]).attributes.slice('name', 'description', 'enabled')
end

def ae_domains
domains = MiqAeDomain.where("ancestry is null and enabled = ?", true).order("name").select("name,id")
render :json => {:domains => domains}
end

def ae_methods
methods = MiqAeMethod.all.order('name')
methods = methods.where('name ILIKE ? or relative_path ILIKE ?', "%#{params[:search]}%", "%#{params[:search]}%") if params[:search]
methods = methods.where('domain_id = ?', params[:domain_id]) if params[:domain_id]
render :json => {:methods => methods}
end

private

def feature_by_action
Expand Down
143 changes: 143 additions & 0 deletions app/javascript/components/AeInlineMethod/NamespaceSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import {
TextInput, Select, SelectItem, Loading,
} from 'carbon-components-react';
import MiqDataTable from '../miq-data-table';
import NotificationMessage from '../notification-message';
import './style.scss';

/** Component to search and select AeMethods. */
const NamespaceSelector = ({ onSelectMethod }) => {
const aeMethodsUrl = '/miq_ae_class/ae_methods';
const aeDomainsUrl = '/miq_ae_class/ae_domains';

const [data, setData] = useState({
loading: true,
domains: [],
searchText: undefined,
methods: [],
selectedDomain: undefined,
});

/** Function to format the method data needed for the data-table list. */
const formatMethods = (methods) => (methods.map((item) => ({
id: item.id.toString(),
name: { text: item.name, icon: 'icon node-icon fa-ruby' },
path: item.relative_path,
code: item.data,
})));

/** Loads the 'domains' and 'methods' from its respective URL's during the component's onLoad event. */
useEffect(() => {
Promise.all([
http.get(aeDomainsUrl),
http.get(aeMethodsUrl)])
.then(([{ domains }, { methods }]) => {
setData({
...data,
loading: false,
methods: formatMethods(methods),
domains,
});
});
}, []);

/** Headers needed for the data-table list. */
const miqHeaders = [
{
key: 'name',
header: 'Name',
},
{
key: 'path',
header: 'Relative path',
},
];

/** Function to return a conditional URL based on the selected filters. */
const searchUrl = (selectedDomain, text) => {
const queryParams = [];
if (selectedDomain) {
queryParams.push(`domain_id=${selectedDomain}`);
}
if (text) {
queryParams.push(`search=${text}`);
}
const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
return `${aeMethodsUrl}${queryString}`;
};

/** Function to handle search text and drop-down item onchange events. */
const handleInputChange = (text, selectedDomain) => {
const url = searchUrl(selectedDomain, text);
http.get(url)
.then(({ methods }) => {
setData({
...data,
methods: formatMethods(methods),
searchText: text,
selectedDomain,
});
});
};

/** Function to render the search text. */
const renderSearchText = () => (
<TextInput
id="search-method"
labelText={__('Search')}
placeholder={__('Search with Name or Relative path')}
onChange={(event) => handleInputChange(event.target.value, data.selectedDomain)}
/>
);

/** Function to render the domain items in a drop-down list. */
const renderDomainList = () => (
<Select
id="select-3"
labelText="Select an domain"
defaultValue="option-3"
onChange={(event) => handleInputChange(data.searchText, event.target.value)}
>
<SelectItem value="" text="None" />
{
data.domains.map((domain) => <SelectItem key={domain.id} value={domain.id} text={domain.name} />)
}
</Select>
);

/** Function to render the contents of the list. */
const renderContents = () => (data.methods && data.methods.length > 0
? (
<MiqDataTable
headers={miqHeaders}
rows={data.methods}
mode="miq-inline-method-list"
onCellClick={(selectedRow) => onSelectMethod(selectedRow)}
/>
)
: <NotificationMessage type="error" message={__('No methods available.')} />);

return (
<div className="inline-method-selector">
<div className="inline-filters">
{renderSearchText()}
{data.domains && renderDomainList()}
</div>
<div className="inline-contents-wrapper">
{
data.loading
? <Loading active small withOverlay={false} className="loading" />
: renderContents()
}
</div>
</div>
);
};

export default NamespaceSelector;

NamespaceSelector.propTypes = {
onSelectMethod: PropTypes.func.isRequired,
};
81 changes: 81 additions & 0 deletions app/javascript/components/AeInlineMethod/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Modal, Button, ModalBody } from 'carbon-components-react';
import NamespaceSelector from './NamespaceSelector';

/** Component to render a tree and to select an embedded method. */
const AeInlineMethod = ({ type }) => {
const [data, setData] = useState({
isModalOpen: false,
selectedNode: undefined,
list: [],
});

/** Function to show/hide the modal. */
const showModal = (status) => {
setData({
...data,
isModalOpen: status,
});
};

/** Function to render the Add method button. */
const renderAddButton = () => (
<Button
id="add-method"
kind="primary"
title={__('Add Method')}
onClick={() => showModal(true)}
size="sm"
>
{__('Add method')}
</Button>
);

return (
<div>
{renderAddButton()}
<Modal
primaryButtonDisabled={data.list.size > 0}
size="lg"
modalHeading={__('Select item')}
open={data.isModalOpen}
primaryButtonText={__('OK')}
secondaryButtonText={__('Cancel')}
onRequestClose={() => showModal(false)}
onRequestSubmit={() => {
console.log('on onRequestSubmit', data.list);
showModal(false);
}}
onSecondarySubmit={() => {
console.log('on onSecondarySubmit');
showModal(false);
}}
>
<ModalBody>
{
data.isModalOpen
&& (
<NamespaceSelector
onSelectMethod={(method) => {
data.list.push(method);
setData({
...data,
list: [...data.list],
});
}}
/>
)
}
</ModalBody>
</Modal>

</div>
);
};

export default AeInlineMethod;

AeInlineMethod.propTypes = {
type: PropTypes.string.isRequired,
};
25 changes: 25 additions & 0 deletions app/javascript/components/AeInlineMethod/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.inline-method-selector {
display: flex;
flex-direction: column;

.inline-filters {
display: flex;
flex-direction: row;
gap: 10px;
}

.inline-contents-wrapper {
display: flex;
flex-direction: column;
margin-top: 20px;

.miq-inline-method-list {
background: #FFF;
margin-top: 0;
}

.miq-notification-message-container {
margin: 0;
}
}
}
2 changes: 2 additions & 0 deletions app/javascript/packs/component-definitions-common.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Toolbar } from '../components/toolbar';
import ActionForm from '../components/action-form';
import AddRemoveHostAggregateForm from '../components/host-aggregate-form/add-remove-host-aggregate-form';
import AddRemoveSecurityGroupForm from '../components/vm-cloud-add-remove-security-group-form';
import AeInlineMethod from '../components/AeInlineMethod';
import AggregateStatusCard from '../components/aggregate_status_card';
import AnsibleCredentialsForm from '../components/ansible-credentials-form';
import AnsiblePlayBookEditCatalogForm from '../components/ansible-playbook-edit-catalog-form';
Expand Down Expand Up @@ -181,6 +182,7 @@ ManageIQ.component.addReact('ActionForm', ActionForm);
ManageIQ.component.addReact('AddRemoveHostAggregateForm', AddRemoveHostAggregateForm);
ManageIQ.component.addReact('AddRemoveSecurityGroupForm', AddRemoveSecurityGroupForm);
ManageIQ.component.addReact('AggregateStatusCard', AggregateStatusCard);
ManageIQ.component.addReact('AeInlineMethod', AeInlineMethod);
ManageIQ.component.addReact('AnsibleCredentialsForm', AnsibleCredentialsForm);
ManageIQ.component.addReact('AnsiblePlayBookEditCatalogForm', AnsiblePlayBookEditCatalogForm);
ManageIQ.component.addReact('AnsiblePlaybookWorkflow', AnsiblePlaybookWorkflow);
Expand Down
2 changes: 2 additions & 0 deletions app/views/miq_ae_class/_class_instances.html.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
= react('AeInlineMethod', {:type => "aeInlineMethod"})

#class_instances_div
- unless @angular_form
- if !@in_a_form
Expand Down
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,8 @@
explorer
method_form_fields
namespace
ae_domains
ae_methods
show
],
:post => %w[
Expand Down

0 comments on commit ecffe86

Please sign in to comment.