Skip to content

Commit

Permalink
Order Service Form conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffibm authored and jeffbonson committed Jul 12, 2024
1 parent f9d74a6 commit 2009602
Show file tree
Hide file tree
Showing 44 changed files with 17,381 additions and 171 deletions.
1 change: 1 addition & 0 deletions app/controllers/catalog_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ def svc_catalog_provision
ra, st, svc_catalog_provision_finish_submit_endpoint
)
@in_a_form = true
@dialog_locals = options[:dialog_locals]
replace_right_cell(:action => "dialog_provision", :dialog_locals => options[:dialog_locals])
else
# if catalog item has no dialog and provision button was pressed from list view
Expand Down
1 change: 1 addition & 0 deletions app/helpers/catalog_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module CatalogHelper
include RequestInfoHelper
include Mixins::AutomationMixin
include OrchestrationTemplateHelper
include OrderServiceHelper

def miq_catalog_resource(resources)
headers = ["", _("Name"), _("Description"), _("Action Order"), _("Provision Order"), _("Action Start"), _("Action Stop"), _("Delay (mins) Start"), _("Delay (mins) Stop")]
Expand Down
44 changes: 43 additions & 1 deletion app/helpers/miq_request_helper.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
require 'byebug'
module MiqRequestHelper
include RequestInfoHelper
include RequestDetailsHelper
include OrderServiceHelper

def row_data(label, value)
{:cells => {:label => label, :value => value}}
end

def request_task_configuration_script_ids(miq_request)
miq_request.miq_request_tasks.map { |task| task.options&.dig(:configuration_script_id) }.compact
miq_request.miq_request_tasks.filter_map { |task| task.options&.dig(:configuration_script_id) }
end

def select_box_options(options)
options.map do |item|
if /Classification::(\d+)/.match?(item)
classification_id = item.match(/Classification::(\d+)/)[1]
classification = Classification.find_by(:id => classification_id)
if classification
{:label => classification.description, :value => classification.id.to_s}
end
else
classification = Classification.find_by(:id => item)
if classification
{:label => classification.description, :value => classification.id.to_s}
end
end
end
end

def dialog_field_values(dialog, wf)

transformed_data = dialog.transform_keys { |key| key.sub('Array::', '') }
transformed_data.transform_values do |value|
if value.to_s.include?("\u001F")
select_box_options(value.split("\u001F"))
elsif value.to_s.include?("::")
model, id = value.split("::")
record = model.constantize.find_by(:id => id)
record ? [{:label => record.description, :value => record.id}] : value
else
value
end
end
end

def service_request_data(request_options, wf)
{
:dialogId => request_options[:workflow_settings][:dialog_id],
:requestDialogOptions => dialog_field_values(request_options[:dialog], wf),
}
end
end
20 changes: 20 additions & 0 deletions app/helpers/order_service_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module OrderServiceHelper
def order_service_data(dialog)
{
:dialogId => dialog[:dialog_id],
:params => {
:resourceActionId => dialog[:resource_action_id],
:targetId => dialog[:target_id],
:targetType => dialog[:target_type],
:realTargetType => dialog[:real_target_type],
},
:urls => {
:apiSubmitEndpoint => dialog[:api_submit_endpoint],
:apiAction => dialog[:api_action],
:cancelEndPoint => dialog[:cancel_endpoint],
:finishSubmitEndpoint => dialog[:finish_submit_endpoint],
:openUrl => dialog[:open_url],
}
}
end
end
77 changes: 77 additions & 0 deletions app/javascript/components/service/DialogFields.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { DIALOG_FIELD_TYPES } from './constants';
import CheckboxField from './dialogFieldItems/CheckboxField';
import DateField from './dialogFieldItems/DateField';
import DateTimeField from './dialogFieldItems/DateTimeField';
import DropDownField from './dialogFieldItems/DropDownField';
import RadioField from './dialogFieldItems/RadioField';
import RefreshField from './RefreshField';
import ServiceContext from './ServiceContext';
import TagField from './dialogFieldItems/TagField';
import TextInputField from './dialogFieldItems/TextInputField';
import TextAreaField from './dialogFieldItems/TextAreaField';

/** Function to render fields based on type */
const renderFieldContent = (field) => {
switch (field.type) {
case DIALOG_FIELD_TYPES.checkBox:
return <CheckboxField field={field} />;
case DIALOG_FIELD_TYPES.date:
return <DateField field={field} />;
case DIALOG_FIELD_TYPES.dateTime:
return <DateTimeField field={field} />;
case DIALOG_FIELD_TYPES.dropDown:
return <DropDownField field={field} />;
case DIALOG_FIELD_TYPES.radio:
return <RadioField field={field} />;
case DIALOG_FIELD_TYPES.tag:
return <TagField field={field} />;
case DIALOG_FIELD_TYPES.textBox:
return <TextInputField field={field} />;
case DIALOG_FIELD_TYPES.textArea:
return <TextAreaField field={field} />;
default:
return <>{__('Field not supported')}</>;
}
};

/** Function to render a field. */
const renderFieldItem = (field, data) => {
const isRefreshing = data.fieldsToRefresh.includes(field.name);
return (
<div
className={classNames('section-field-row', isRefreshing && 'field-refresh-in-progress')}
key={field.id.toString()}
id={`section-field-row-${field.name}`}
>
<div className="field-item">
{
renderFieldContent(field)
}
</div>
<RefreshField field={field} />
</div>
);
};

/** Component to render the Fields in the Service/DialogTabs/DialogGroups component */
const DialogFields = ({ dialogFields }) => {
const { data } = useContext(ServiceContext);
return (
<>
{
dialogFields.map((field) => (
field.visible ? renderFieldItem(field, data) : <span key={field.id.toString()} />
))
}
</>
);
};

DialogFields.propTypes = {
dialogFields: PropTypes.arrayOf(PropTypes.any).isRequired,
};

export default DialogFields;
27 changes: 27 additions & 0 deletions app/javascript/components/service/DialogGroups.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import DialogFields from './DialogFields';

/** Component to render the Groups in the Service/DialogTabs component */
const DialogGroups = ({ dialogGroups }) => (
<>
{
dialogGroups.map((item) => (
<div className="section" key={item.id.toString()}>
<div className="section-label">
{item.label}
</div>
<div className="section-fields">
<DialogFields dialogFields={item.dialog_fields} />
</div>
</div>
))
}
</>
);

DialogGroups.propTypes = {
dialogGroups: PropTypes.arrayOf(PropTypes.any).isRequired,
};

export default DialogGroups;
40 changes: 40 additions & 0 deletions app/javascript/components/service/DialogTabs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useContext } from 'react';
import { Tabs, Tab, Loading } from 'carbon-components-react';
import DialogGroups from './DialogGroups';
import ServiceContext from './ServiceContext';
import { extractDialogTabs } from './helper';

/** Component to render the Tabs in the Service component */
const DialogTabs = () => {
const { data } = useContext(ServiceContext);
const dialogTabs = extractDialogTabs(data.apiResponse);

const tabLabel = (label, tabIndex) => {
const { fieldsToRefresh, groupFieldsByTab } = data;
const refreshInProgress = fieldsToRefresh.some((field) => groupFieldsByTab[tabIndex].includes(field));
return refreshInProgress
? (
<div className="tab-label">
{label}
<Loading active small withOverlay={false} className="loading" />
</div>
)
: label;
};

return (
<Tabs className="miq_custom_tabs">
{
dialogTabs.map((tab, tabIndex) => (
<Tab key={tab.id.toString()} label={tabLabel(tab.label, tabIndex)}>
<div className="tabs">
<DialogGroups dialogGroups={tab.dialog_groups} />
</div>
</Tab>
))
}
</Tabs>
);
};

export default DialogTabs;
62 changes: 62 additions & 0 deletions app/javascript/components/service/RefreshField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Button, Loading } from 'carbon-components-react';
import { Renew16 } from '@carbon/icons-react';
import ServiceContext from './ServiceContext';
import ServiceValidator from './ServiceValidator';
import { defaultFieldValue } from './helper';
import { fieldProperties } from './helper.field';

/** Function to reset the dialogField data when the field refresh button is clicked. */
const resetDialogField = (dialogFields, field) => {
const { value, valid } = ServiceValidator.validateField({ field, value: defaultFieldValue(field) });
dialogFields[field.name] = { value, valid };
return { ...dialogFields };
};

const RefreshField = ({ field }) => {
const { data, setData } = useContext(ServiceContext);
const { isDisabled } = fieldProperties(field, data);

const { fieldsToRefresh } = data;
const inProgress = fieldsToRefresh.includes(field.name);
return (
<div className="refresh-field-item">
{
!!(field.dynamic && field.show_refresh_button) && !inProgress && (
<Button
hasIconOnly
disabled={isDisabled}
className="refresh-field-button"
onClick={() => {
setData({
...data,
fieldsToRefresh: [field.name],
dialogFields: resetDialogField(data.dialogFields, field),
});
}}
iconDescription={__(`Refresh ${field.label}`)}
tooltipAlignment="start"
tooltipPosition="left"
renderIcon={Renew16}
/>
)
}
{
inProgress && <Loading active small withOverlay={false} className="loading" />
}
</div>
);
};

RefreshField.propTypes = {
field: PropTypes.shape({
label: PropTypes.string,
dynamic: PropTypes.bool,
show_refresh_button: PropTypes.bool,
dialog_field_responders: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string,
}).isRequired,
};

export default RefreshField;
84 changes: 84 additions & 0 deletions app/javascript/components/service/ServiceButtons.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useContext, useEffect } from 'react';
import { Button } from 'carbon-components-react';
import ServiceContext from './ServiceContext';
import { omitValidation } from './helper';
import miqRedirectBack from '../../helpers/miq-redirect-back';

const ServiceButtons = React.memo(() => {
const { data, setData } = useContext(ServiceContext);
const {
apiAction, apiSubmitEndpoint, openUrl, finishSubmitEndpoint,
} = data.urls;

useEffect(() => {
if (data.locked) {
const handleSubmission = async() => {
const values = omitValidation(data.dialogFields);
let submitData = { action: 'order', ...values };

if (apiSubmitEndpoint.includes('/generic_objects/')) {
submitData = { action: apiAction, parameters: values };
} else if (apiAction === 'reconfigure') {
submitData = { action: apiAction, resource: values };
}

try {
const response = await API.post(apiSubmitEndpoint, submitData, { skipErrors: [400] });
if (openUrl === 'true') {
const taskResponse = await API.wait_for_task(response)
.then(() =>
// eslint-disable-next-line no-undef
$http.post('open_url_after_dialog', { targetId, realTargetType }));

if (taskResponse.data.open_url) {
window.open(response.data.open_url);
miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint);
} else {
add_flash(__('Automate failed to obtain URL.'), 'error');
miqSparkleOff();
}
} else {
miqRedirectBack(__('Order Request was Submitted'), 'success', finishSubmitEndpoint);
}
} catch (_error) {
// Handle error if needed
}
};

handleSubmission();
}
}, [data.locked]);

const formValid = data.dialogFields ? Object.values(data.dialogFields).every((field) => field.valid) : false;

const submitForm = () => {
miqSparkleOn();
setData({
...data,
locked: true,
});
};

return (
<div className="service-action-buttons">
<Button
disabled={!formValid || data.locked}
onClick={submitForm}
>
{
data.locked ? __('Submitting...') : __('Submit')
}
</Button>

<Button
kind="secondary"
disabled={data.locked || data.fieldsToRefresh.length > 0}
onClick={() => miqRedirectBack(__('Dialog Cancelled'), 'warning', '/catalog')}
>
{__('Cancel')}
</Button>
</div>
);
});

export default ServiceButtons;
4 changes: 4 additions & 0 deletions app/javascript/components/service/ServiceContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createContext } from 'react';

const ServiceContext = createContext();
export default ServiceContext;
Loading

0 comments on commit 2009602

Please sign in to comment.