diff --git a/app/controllers/miq_ae_class_controller.rb b/app/controllers/miq_ae_class_controller.rb index 70c945a8ef0..d001fb9ecde 100644 --- a/app/controllers/miq_ae_class_controller.rb +++ b/app/controllers/miq_ae_class_controller.rb @@ -501,6 +501,7 @@ def edit_fields @in_a_form = true @in_a_form_fields = true session[:changed] = @changed = false + @hide_bottom_bar = true replace_right_cell end @@ -881,19 +882,11 @@ def fields_form_field_changed fields_get_form_vars @changed = (@edit[:new] != @edit[:current]) - render :update do |page| - page << javascript_prologue unless %w[up down].include?(params[:button]) if params[:field_datatype] == "password" - page << javascript_hide("field_default_value") - page << javascript_show("field_password_value") - page << "$('#field_password_value').val('');" session[:field_data][:default_value] = @edit[:new_field][:default_value] = '' elsif params[:field_datatype] - page << javascript_hide("field_password_value") - page << javascript_show("field_default_value") - page << "$('#field_default_value').val('');" session[:field_data][:default_value] = @edit[:new_field][:default_value] = '' end @@ -904,19 +897,12 @@ def fields_form_field_changed def_field = "fields_default_value_" << f[1].to_s pwd_field = "fields_password_value_" << f[1].to_s if @edit[:new][:fields][f[1].to_i]['datatype'] == "password" - page << javascript_hide(def_field) - page << javascript_show(pwd_field) - page << "$('##{pwd_field}').val('');" else - page << javascript_hide(pwd_field) - page << javascript_show(def_field) - page << "$('##{def_field}').val('');" end @edit[:new][:fields][f[1].to_i]['default_value'] = nil end end - page << javascript_for_miq_button_visibility_changed(@changed) - end + render :json => {:message => 'Field updated successfully'}, :status => 200 end # AJAX driven routine to check for changes in ANY field on the form @@ -1140,13 +1126,12 @@ def update_fields return unless load_edit("aefields_edit__#{params[:id]}", "replace_cell__explorer") fields_get_form_vars - @changed = (@edit[:new] != @edit[:current]) case params[:button] when "cancel" @sb[:action] = session[:edit] = nil # clean out the saved info - add_flash(_("Edit of schema for Automate Class \"%{name}\" was cancelled by the user") % {:name => @ae_class.name}) @in_a_form = false - replace_right_cell + message = _("Edit of schema for Automate Class \"%{name}\" was cancelled by the user") % {:name => @ae_class.name} + render :json => {:status => 200, :message => message} when "save" ae_class = find_record_with_rbac(MiqAeClass, params[:id]) begin @@ -1157,27 +1142,26 @@ def update_fields ae_class.save! end rescue StandardError => bang - add_flash(_("Error during 'save': %{error_message}") % {:error_message => bang.message}, :error) session[:changed] = @changed = true - javascript_flash + error_message = _("Error during 'save': %{error_message}") % {:error_message => bang.message}, :error + render :json => {:status => 500, :error => error_message} else - add_flash(_("Schema for Automate Class \"%{name}\" was saved") % {:name => ae_class.name}) AuditEvent.success(build_saved_audit(ae_class, @edit)) @sb[:action] = session[:edit] = nil # clean out the saved info - @in_a_form = false - replace_right_cell(:replace_trees => [:ae]) - nil + success_message = _("Schema for Automate Class \"%{name}\" was saved") % {:name => ae_class.name} + render :json => {:status => 200, :message => success_message} end when "reset" fields_set_form_vars - session[:changed] = @changed = false + session[:changed] = false add_flash(_("All changes have been reset"), :warning) @button = "reset" @in_a_form = true - replace_right_cell + success_message = _("All changes have been reset") + render :json => {:status => 200, :message => success_message} else @changed = session[:changed] = (@edit[:new] != @edit[:current]) - replace_right_cell(:replace_trees => [:ae]) + render :json => {:status => 200} end end @@ -1440,17 +1424,9 @@ def create_namespace def field_select assert_privileges('miq_ae_field_edit') fields_get_form_vars - @combo_xml = build_type_options - @dtype_combo_xml = build_dtype_options session[:field_data] = {} @edit[:new_field][:substitute] = session[:field_data][:substitute] = true - @changed = (@edit[:new] != @edit[:current]) - render :update do |page| - page << javascript_prologue - page.replace("class_fields_div", :partial => "class_fields") - page << javascript_for_miq_button_visibility(@changed) - page << "miqSparkle(false);" - end + render :json => {:status => 200} end # AJAX driven routine to select a classification entry @@ -1458,35 +1434,22 @@ def field_accept assert_privileges('miq_ae_field_edit') fields_get_form_vars @changed = (@edit[:new] != @edit[:current]) - @combo_xml = build_type_options - @dtype_combo_xml = build_dtype_options - render :update do |page| - page << javascript_prologue - page.replace("class_fields_div", :partial => "class_fields") - page << javascript_for_miq_button_visibility(@changed) - page << "miqSparkle(false);" - end + render :json => { + :message => 'Accepted', + :status => 200, + } end # AJAX driven routine to delete a classification entry def field_delete assert_privileges('miq_ae_field_edit') fields_get_form_vars - @combo_xml = build_type_options - @dtype_combo_xml = build_dtype_options if params.key?(:id) && @edit[:fields_to_delete].exclude?(params[:id]) @edit[:fields_to_delete].push(params[:id]) end - @edit[:new][:fields].delete_at(params[:arr_id].to_i) - @changed = (@edit[:new] != @edit[:current]) - render :update do |page| - page << javascript_prologue - page.replace("class_fields_div", :partial => "class_fields") - page << javascript_for_miq_button_visibility(@changed) - page << "miqSparkle(false);" - end + render :json => {:status => 200} end # AJAX driven routine to select a classification entry @@ -2352,12 +2315,11 @@ def fields_get_form_vars if params[:item].blank? && !%w[accept save].include?(params[:button]) && params["action"] != "field_delete" field_data = session[:field_data] new_field = @edit[:new_field] - field_attributes.each do |field| field_name = "field_#{field}".to_sym field_sym = field.to_sym if field == "substitute" - field_data[field_sym] = new_field[field_sym] = params[field_name] == "1" if params[field_name] + field_data[field_sym] = new_field[field_sym] = params[field_name] if params.key?(field_name) elsif params[field_name] field_data[field_sym] = new_field[field_sym] = params[field_name] end @@ -2376,7 +2338,7 @@ def fields_get_form_vars field_attributes.each do |field| field_name = "fields_#{field}_#{i}" if field == "substitute" - fld[field] = params[field_name] == "1" if params[field_name] + fld[field] = params[field_name] if params.key?(field_name) elsif %w[aetype datatype].include?(field) var_name = "fields_#{field}#{i}" fld[field] = params[var_name.to_sym] if params[var_name.to_sym] @@ -2389,18 +2351,17 @@ def fields_get_form_vars end end elsif params[:button] == "accept" - if session[:field_data][:name].blank? || session[:field_data][:aetype].blank? - field = session[:field_data][:name].blank? ? "Name" : "Type" - field += " and Type" if field == "Name" && session[:field_data][:aetype].blank? + if params[:name].blank? || params[:aetype].blank? + field = params[:name].blank? ? "Name" : "Type" + field += " and Type" if field == "Name" && params[:aetype].blank? add_flash(_("%{field} is required") % {:field => field}, :error) return end new_fields = {} field_attributes.each do |field_attribute| - new_fields[field_attribute] = @edit[:new_field][field_attribute.to_sym] + new_fields[field_attribute] = params[field_attribute.to_sym] end @edit[:new][:fields].push(new_fields) - @edit[:new_field] = session[:field_data] = {} end end diff --git a/app/helpers/miq_ae_class_helper.rb b/app/helpers/miq_ae_class_helper.rb index 96c2c2544f9..dfa5855b874 100644 --- a/app/helpers/miq_ae_class_helper.rb +++ b/app/helpers/miq_ae_class_helper.rb @@ -249,7 +249,7 @@ def schema_data(schema_data) cells.push({:text => ae_field.send(fname)}) end end - push_data({:id => index.to_s, :clickable => false, :cells => cells}) + push_data({:id => index.to_s, :field_id => ae_field.id, :clickable => false, :cells => cells}) end end diff --git a/app/javascript/components/data-tables/datastore/helper.js b/app/javascript/components/data-tables/datastore/helper.js index bb29e80f740..d21623b545f 100644 --- a/app/javascript/components/data-tables/datastore/helper.js +++ b/app/javascript/components/data-tables/datastore/helper.js @@ -26,18 +26,29 @@ const commonHeaders = () => [ const domainOverridesHeaders = () => [{ text: 'defaultKey_0', header_text: __('Domain') }]; /** Function which returns the header data for table with type class_fields schema. */ -const schemaHeaders = () => [ - { text: 'Name', header_text: __('Name') }, - { text: 'Description', header_text: __('Description') }, - { text: 'DefaultValue', header_text: __('Default Value') }, - { text: 'Collect', header_text: __('Collect') }, - { text: 'Message', header_text: __('Message') }, - { text: 'OnEntry', header_text: __('On Entry') }, - { text: 'OnExit', header_text: __('On Exit') }, - { text: 'OnError', header_text: __('On Error') }, - { text: 'MaxRetries', header_text: __('Max Retries') }, - { text: 'MaxTime', header_text: __('Max Time') }, -]; +export const schemaHeaders = (isEdit = false) => { + const headers = [ + { name: 'name', text: 'Name', header_text: __('Name') }, + { name: 'description', text: 'Description', header_text: __('Description') }, + { name: 'default_value', text: 'DefaultValue', header_text: __('Default Value') }, + { name: 'collect', text: 'Collect', header_text: __('Collect') }, + { name: 'message', text: 'Message', header_text: __('Message') }, + { name: 'on_entry', text: 'OnEntry', header_text: __('On Entry') }, + { name: 'on_exit', text: 'OnExit', header_text: __('On Exit') }, + { name: 'on_error', text: 'OnError', header_text: __('On Error') }, + { name: 'max_retries', text: 'MaxRetries', header_text: __('Max Retries') }, + { name: 'max_time', text: 'MaxTime', header_text: __('Max Time') }, + ]; + + if (isEdit) { + headers.push( + { name: 'edit', text: 'Edit', header_text: __('Edit') }, + { name: 'delete', text: 'Delete', header_text: __('Delete') } + ); + } + + return headers; +}; /** Function which returns the header data for table with type instant_fields. */ const instantFieldHeaders = (hasOptions) => { @@ -58,7 +69,7 @@ const instantFieldHeaders = (hasOptions) => { /** Function which returns the header items based on its type. */ const datastoreHeaders = (type, hasOptions, { list, details, instances, methods, domain, schema, fields, -}) => { +}, isEdit) => { switch (type) { case list: return nsListHeaders(hasOptions); @@ -69,7 +80,7 @@ const datastoreHeaders = (type, hasOptions, { case domain: return domainOverridesHeaders(); case schema: - return schemaHeaders(); + return schemaHeaders(isEdit); case fields: return instantFieldHeaders(hasOptions); default: @@ -77,11 +88,41 @@ const datastoreHeaders = (type, hasOptions, { } }; +export const createEditableRows = (data) => { + const rowItems = Array.isArray(data) ? data.map((item) => { + const updatedCells = [ + ...item.cells, + { + is_button: true, + text: __('Update'), + kind: 'tertiary', + size: 'md', + callback: 'editClassField', + }, + { + is_button: true, + text: __('Delete'), + kind: 'danger', + size: 'md', + callback: 'deleteClassField', + }, + ]; + + return { + ...item, + cells: updatedCells, + }; + }) + : []; + + return rowItems; +}; + /** Function which returns the data needed for table. */ -export const tableData = (type, hasOptions, initialData, datastoreTypes) => { +export const tableData = (type, hasOptions, initialData, datastoreTypes, isEdit) => { const cBox = hasCheckbox(type, datastoreTypes); const nodeTree = type === datastoreTypes.domain ? 'x_show' : 'tree_select'; - const columns = datastoreHeaders(type, hasOptions, datastoreTypes); + const columns = datastoreHeaders(type, hasOptions, datastoreTypes, isEdit); const { headerKeys, headerItems } = headerData(columns, cBox); const miqRows = rowData(headerKeys, initialData, true); return { @@ -105,3 +146,10 @@ export const removeSelected = (array, item) => { } return array; }; + +export const transformSelectOptions = (array) => + array.map(([label, value, extraProps]) => ({ + label, + value, + ...extraProps, + })); diff --git a/app/javascript/components/data-tables/datastore/index.jsx b/app/javascript/components/data-tables/datastore/index.jsx index be867c13c19..b4e43ee8324 100644 --- a/app/javascript/components/data-tables/datastore/index.jsx +++ b/app/javascript/components/data-tables/datastore/index.jsx @@ -4,19 +4,25 @@ import PropTypes from 'prop-types'; import { tableData, addSelected, removeSelected, } from './helper'; +import { ClassFieldsEditor } from './schema/class-fields-editor'; import MiqDataTable from '../../miq-data-table'; import { CellAction } from '../../miq-data-table/helper'; const Datastore = ({ - type, initialData, hasOptions, datastoreTypes, + type, initialData, hasOptions, datastoreTypes, isEdit, aeTypeOptions, dTypeOptions, aeClassId, }) => { const { miqHeaders, miqRows, hasCheckbox, nodeTree, - } = tableData(type, hasOptions, initialData, datastoreTypes); + } = tableData(type, hasOptions, initialData, datastoreTypes, isEdit); + + const [state, setState] = useState({ + schemaRecords: miqRows.rowItems, + }); if (miqRows.merged) { miqHeaders.splice(0, 1); } + /** Function to find an item from initialData. */ const findItem = (item) => initialData.find((row) => row.id.toString() === item.id.toString()); @@ -84,6 +90,7 @@ const Datastore = ({ /** Function to handle the cell event actions. */ const onCellClick = (selectedRow, cellType, event) => { + setState((state) => ({ ...state, selectedRowId: selectedRow.id })); switch (cellType) { case CellAction.selectAll: onSelectAll(event); break; case CellAction.itemSelect: onItemSelect(findItem(selectedRow), event.target); break; @@ -92,15 +99,41 @@ const Datastore = ({ } }; + const renderEditView = () => { + switch (type) { + case 'class_fields': + return ( + + ); + + default: + return null; + } + }; + return ( - onCellClick(selectedRow, cellType, event)} - rowCheckBox={hasCheckbox} - mode={`datastore-list ${type}`} - gridChecks={selectionIds} - /> + <> + {isEdit ? ( + renderEditView() + ) : ( + <> + + onCellClick(selectedRow, cellType, event)} + rowCheckBox={hasCheckbox} + mode={`datastore-list ${type}`} + gridChecks={selectionIds} + /> + + )} + ); }; @@ -111,8 +144,14 @@ Datastore.propTypes = { initialData: PropTypes.arrayOf(PropTypes.any).isRequired, hasOptions: PropTypes.bool, datastoreTypes: PropTypes.shape({}).isRequired, + isEdit: PropTypes.bool.isRequired, + aeTypeOptions: PropTypes.arrayOf(PropTypes.any), + dTypeOptions: PropTypes.arrayOf(PropTypes.any), + aeClassId: PropTypes.number.isRequired, }; Datastore.defaultProps = { hasOptions: false, + aeTypeOptions: [], + dTypeOptions: [], }; diff --git a/app/javascript/components/data-tables/datastore/schema/class-fields-editor.jsx b/app/javascript/components/data-tables/datastore/schema/class-fields-editor.jsx new file mode 100644 index 00000000000..7d651f8b344 --- /dev/null +++ b/app/javascript/components/data-tables/datastore/schema/class-fields-editor.jsx @@ -0,0 +1,297 @@ +import React, { + useState, useEffect, useCallback, +} from 'react'; +import PropTypes from 'prop-types'; +import { Modal } from 'carbon-components-react'; +import MiqFormRenderer from '@@ddf'; +import debounce from 'lodash/debounce'; +import { schemaHeaders, createEditableRows } from '../helper'; +import createClassFieldsSchema from './modal-form.schema'; +import createSchemaEditSchema from './class-fields-schema'; +import mapper from '../../../../forms/mappers/componentMapper'; +import { SchemaTableComponent } from './schema-table'; +import miqRedirectBack from '../../../../helpers/miq-redirect-back'; +import miqFlash from '../../../../helpers/miq-flash'; + +export const ClassFieldsEditor = (props) => { + const { + aeClassId, initialData, aeTypeOptions, dTypeOptions, + } = props; + + const componentMapper = { + ...mapper, + 'schema-table': SchemaTableComponent, + }; + + const fieldData = createEditableRows(initialData); + + const transformedRows = () => { + const rowItems = []; + const headers = schemaHeaders(true); + fieldData.forEach(({ + // eslint-disable-next-line camelcase + id, field_id, cells, clickable, + }) => { + // eslint-disable-next-line camelcase + const fieldId = field_id; + const reducedItems = cells.reduce((result, item, index) => { + result[headers[index].name] = item; + result.id = fieldId.toString(); + result.field_id = fieldId; + result.clickable = clickable; + return result; + }, {}); + rowItems.push(reducedItems); + }); + + return rowItems; + }; + + const [state, setState] = useState({ + isModalOpen: false, + selectedRowId: undefined, + rows: transformedRows(), + formKey: true, // for remounting + isSchemaModified: false, + }); + + useEffect(() => { + setState((state) => ({ ...state, isSchemaModified: !state.isSchemaModified })); + }, [state.rows]); + + const handleModalClose = () => { + setState((state) => ({ ...state, isModalOpen: false, selectedRowId: undefined })); + }; + + const formatFieldValues = (field, rowId) => { + if (!field || typeof field !== 'object') return []; + + const getFieldName = () => ((field.display_name) ? `${field.display_name} (${field.name})` : `${field.name}`); + + const getIconForValue = () => { + const aeMatch = aeTypeOptions.find((option) => option[1] === field.aetype); + const aeTypeIcon = (aeMatch && aeMatch[2] && aeMatch[2]['data-icon']) || ''; + + const dtypeMatch = dTypeOptions.find((option) => option[1] === field.datatype); + const dTypeIcon = (dtypeMatch && dtypeMatch[2] && dtypeMatch[2]['data-icon']) || ''; + + const subIcon = (field.substitute) ? 'pficon pficon-ok' : 'pficon pficon-close'; + + return [aeTypeIcon, dTypeIcon, subIcon]; + }; + + const row = { + id: rowId.toString(), + field_id: field.id, + name: { + text: getFieldName(), + icon: getIconForValue() || [], + }, + aetype: { text: field.aetype }, + datatype: { text: field.datatype }, + default_value: { text: field.default_value || '' }, + display_name: { text: field.display_name || '' }, + description: { text: field.description || '' }, + substitute: { text: field.substitute }, + collect: { text: field.collect || '' }, + message: { text: field.message || '' }, + on_entry: { text: field.on_entry || '' }, + on_exit: { text: field.on_exit || '' }, + on_error: { text: field.on_error || '' }, + max_retries: { text: field.max_retries || '' }, + max_time: { text: field.max_time || '' }, + edit: { + is_button: true, + text: __('Update'), + kind: 'tertiary', + size: 'md', + callback: 'editClassField', + }, + delete: { + is_button: true, + text: __('Delete'), + kind: 'danger', + size: 'md', + callback: 'deleteClassField', + }, + }; + + return row; + }; + + const onModalSubmit = (values) => { + const isEdit = state.selectedRowId !== undefined; + + const updateState = (newData) => { + setState((prevState) => ({ + ...prevState, + rows: isEdit + ? prevState.rows.map((field) => (field.id === newData.id ? newData : field)) + : [...prevState.rows, newData], + })); + handleModalClose(); + }; + + if (isEdit) { + const data = formatFieldValues(values, state.selectedRowId); + updateState(data); + } else { + http.post(`/miq_ae_class/field_accept?button=accept`, values, { skipErrors: [400] }) + .then(() => { + const data = formatFieldValues(values, state.rows.length); + updateState(data); + }) + .catch((error) => { + // console.error('Failed to save new field:', error); + }); + } + }; + + const updateFieldValueInState = (fieldName, newValue) => { + // Update existing field in state attr - rows + setState((prevState) => { + const updatedRow = prevState.rows.map((row, index) => { + if (index === prevState.selectedRowId) { + return { + ...row, + [fieldName]: { + ...row[fieldName], + text: newValue, + }, + }; + } + return row; + }); + + return { + ...prevState, + rows: updatedRow, + }; + }); + }; + + const handleSchemaFieldChange = useCallback( + debounce((aeClassId, val, fieldName) => { + let fname; + + if (state.selectedRowId) { + if (fieldName === 'datatype' || fieldName === 'aetype') { + fname = `fields_${fieldName}${state.selectedRowId}`; + } else { + fname = `fields_${fieldName}_${state.selectedRowId}`; + } + } else { + fname = `field_${fieldName}`; + } + + const data = { + [fname]: val, + id: aeClassId, + }; + + http.post(`/miq_ae_class/fields_form_field_changed/${aeClassId}`, data, { + skipErrors: [400], + }).then((response) => { + console.log(response); + }).catch((error) => { + console.error('Error:', error); + console.error('Response:', error.response); + console.log('Something went wrong'); + }); + }, 500), + [aeClassId, state.selectedRowId] + ); + + const onSchemaReset = () => { + http.post(`/miq_ae_class/update_fields/${aeClassId}?button=reset`, { skipErrors: [400] }) + .then((response) => { + if (response.status === 200) { + miqRedirectBack(__(response.message), 'success', '/miq_ae_class/explorer'); + } else { + miqSparkleOff(); + miqFlash('error', response.error); + } + }) + .catch(miqSparkleOff); + }; + + const onSchemaSave = () => { + http.post(`/miq_ae_class/update_fields/${aeClassId}?button=save`, { skipErrors: [400] }) + .then((response) => { + if (response.status === 200) { + miqRedirectBack(__(response.message), 'success', '/miq_ae_class/explorer'); + } else { + miqSparkleOff(); + miqFlash('error', response.error); + } + }) + .catch(miqSparkleOff); + }; + + // On cancelling the edit schema action + const onCancel = () => { + http.post(`/miq_ae_class/update_fields/${aeClassId}?button=cancel`, { skipErrors: [400] }) + .then((response) => { + if (response.status === 200) { + miqRedirectBack(__(response.message), 'success', '/miq_ae_class/explorer'); + } else { + miqSparkleOff(); + miqFlash('error', response.error); + } + }) + .catch(miqSparkleOff); + }; + + return ( + <> + + + + + row.id === state.selectedRowId), + handleSchemaFieldChange, + updateFieldValueInState, + )} + onSubmit={onModalSubmit} + onCancel={handleModalClose} + canReset + disableSubmit={['invalid']} + buttonsLabels={{ submitLabel: __('Save') }} + /> + + + ); +}; + +ClassFieldsEditor.propTypes = { + aeClassId: PropTypes.number.isRequired, + initialData: PropTypes.arrayOf(PropTypes.any).isRequired, + aeTypeOptions: PropTypes.arrayOf(PropTypes.any), + dTypeOptions: PropTypes.arrayOf(PropTypes.any), +}; + +ClassFieldsEditor.defaultProps = { + aeTypeOptions: [], + dTypeOptions: [], +}; diff --git a/app/javascript/components/data-tables/datastore/schema/class-fields-schema.js b/app/javascript/components/data-tables/datastore/schema/class-fields-schema.js new file mode 100644 index 00000000000..373e5c3642a --- /dev/null +++ b/app/javascript/components/data-tables/datastore/schema/class-fields-schema.js @@ -0,0 +1,77 @@ +import { componentTypes } from '@@ddf'; +import { schemaHeaders } from '../helper'; + +const createSchemaEditSchema = (rows, setState, isSchemaModified) => { + const handleAddField = () => { + http.post(`/miq_ae_class/field_select?add=new&item=field`, { skipErrors: [400] }) + .then(() => { + setState((prev) => ({ + ...prev, + selectedRowId: undefined, + isModalOpen: true, + formKey: !prev.formKey, + })); + }) + .catch((error) => { + // console.error('Failed to add new field:', error); + }); + }; + + const handleFieldDelete = (rowId) => { + const field = rows.find((field) => field.id === rowId); + const arrId = rows.findIndex((field) => field.id === rowId); + const fId = field.field_id; + const url = `/miq_ae_class/field_delete${fId ? `/${fId}` : ''}?arr_id=${arrId}`; + http.post(url, { skipErrors: [400] }) + .then(() => { + setState((prev) => ({ + ...prev, + selectedRowId: undefined, + rows: prev.rows.filter((field) => field.id !== rowId), + })); + }) + .catch((error) => { + // console.error('Failed to delete field:', error); + }); + }; + + return { + fields: [ + { + component: componentTypes.SUB_FORM, + name: 'schema_editor_section', + id: 'schema_editor_section', + key: isSchemaModified, + fields: [ + { + component: 'schema-table', + name: 'schema-table', + id: 'schema-table', + rows, + headers: schemaHeaders(true), + onCellClick: (selectedRow) => { + switch (selectedRow.callbackAction) { + case 'editClassField': + setState((prev) => ({ + ...prev, + // selectedRowId: selectedRow.id, + selectedRowId: rows.findIndex((row) => row.id === selectedRow.id), + isModalOpen: true, + formKey: !prev.formKey, + })); + break; + case 'deleteClassField': + handleFieldDelete(selectedRow.id); + break; + default: + break; + } + }, + onButtonClick: handleAddField, + }, + ], + }, + ], + }; +}; +export default createSchemaEditSchema; diff --git a/app/javascript/components/data-tables/datastore/schema/modal-form.schema.js b/app/javascript/components/data-tables/datastore/schema/modal-form.schema.js new file mode 100644 index 00000000000..28ddce7957f --- /dev/null +++ b/app/javascript/components/data-tables/datastore/schema/modal-form.schema.js @@ -0,0 +1,294 @@ +import { componentTypes, validatorTypes } from '@@ddf'; +import { transformSelectOptions } from '../helper'; + +const createClassFieldsSchema = (aeClassId, selectedRowId, aeTypeOptions, + dTypeOptions, schemaField = {}, handleSchemaFieldChange, updateFieldValueInState) => { + const classField = schemaField; + + const formatName = () => { + const fullName = classField.name.text; + const match = fullName.match(/^(.+?)\s*\(([^)]+)\)$/); + + if (classField.display_name && 'text' in classField.display_name) { + return { + display_name: classField.display_name.text, + name: (match && match[2]) || fullName, + }; + } + return { + display_name: (match && match[1]) || '', + name: (match && match[2]) || fullName, + }; + }; + + const getIcons = (index) => { + if ( + classField + && typeof classField === 'object' + && Object.keys(classField).length > 0 + && classField.name + && 'icon' in classField.name + ) { + let icons = classField.name.icon; + if (icons.length === 2) { + // icons come in the order [aetype, dtype, substitute] + // aetype and substitute will be present always; so rearrange index 1 and 2 + icons = [icons[0], '', icons[1]]; + } + return icons[index]; + } + return ''; + }; + + const getInitialValue = (field, defaultVal = '') => { + if ( + classField + && typeof classField === 'object' + && Object.keys(classField).length > 0 + ) { + if (selectedRowId) { + if (field === 'name' || field === 'display_name') { + const formatted = formatName(); + return formatted[field] || defaultVal; + } + if (field === 'substitute') { + if (classField[field] && 'text' in classField[field]) { + return classField[field].text !== undefined && classField[field].text !== null + ? classField[field].text + : defaultVal; + } + const icon = getIcons(2); + return icon === 'pficon pficon-ok'; + } + if (field === 'message') { + return classField[field].text; + } + return classField[field].text || defaultVal; + } + + if (classField[field] && 'text' in classField[field]) { + return classField[field].text || defaultVal; + } + } + + return defaultVal; + }; + + const getType = (options, icon) => { + const match = options.find( + (item) => item[2] && item[2]['data-icon'] === icon + ); + + return match ? match[1] : null; + }; + + return { + fields: [ + { + component: componentTypes.TEXT_FIELD, + name: 'name', + id: 'name', + label: __('Name'), + isRequired: true, + validate: [{ type: validatorTypes.REQUIRED }], + initialValue: getInitialValue('name'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('name', val); + handleSchemaFieldChange(aeClassId, val, 'name'); + }, + }), + }, + { + component: componentTypes.SELECT, + name: 'aetype', + id: 'aetype', + label: __('Type'), + isRequired: true, + validate: [{ type: validatorTypes.REQUIRED }], + placeholder: __(''), + options: transformSelectOptions(aeTypeOptions), + includeEmpty: true, + initialValue: getType(aeTypeOptions, getIcons(0)), + ...(selectedRowId && { + onChange: (val) => { + updateFieldValueInState('aetype', val); + handleSchemaFieldChange(aeClassId, val, 'aetype'); + }, + }), + }, + { + component: componentTypes.SELECT, + name: 'datatype', + id: 'datatype', + label: __('Data Type'), + placeholder: __(''), + includeEmpty: true, + options: transformSelectOptions(dTypeOptions), + initialValue: getType(dTypeOptions, getIcons(1)), + ...(selectedRowId && { + onChange: (val) => { + updateFieldValueInState('datatype', val); + handleSchemaFieldChange(aeClassId, val, 'datatype'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'default_value', + id: 'default_value', + label: __('Default Value'), + initialValue: getInitialValue('default_value'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('default_value', val); + handleSchemaFieldChange(aeClassId, val, 'default_value'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'display_name', + id: 'display_name', + label: __('Display Name'), + initialValue: getInitialValue('display_name'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('display_name', val); + handleSchemaFieldChange(aeClassId, val, 'display_name'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'description', + id: 'description', + label: __('Description'), + initialValue: getInitialValue('description'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('description', val); + handleSchemaFieldChange(aeClassId, val, 'description'); + }, + }), + }, + { + component: componentTypes.CHECKBOX, + name: 'substitute', + id: 'substitute', + label: __('Sub'), + initialValue: getInitialValue('substitute', true), + ...(selectedRowId && { + onChange: (val) => { + updateFieldValueInState('substitute', val); + handleSchemaFieldChange(aeClassId, val, 'substitute'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'collect', + id: 'collect', + label: __('Collect'), + initialValue: getInitialValue('collect'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('collect', val); + handleSchemaFieldChange(aeClassId, val, 'collect'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'message', + id: 'message', + label: __('Message'), + initialValue: getInitialValue('message', 'create'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('message', val); + handleSchemaFieldChange(aeClassId, val, 'message'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'on_entry', + id: 'on_entry', + label: __('On Entry'), + initialValue: getInitialValue('on_entry'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('on_entry', val); + handleSchemaFieldChange(aeClassId, val, 'on_entry'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'on_exit', + id: 'on_exit', + label: __('On Exit'), + initialValue: getInitialValue('on_exit'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('on_exit', val); + handleSchemaFieldChange(aeClassId, val, 'on_exit'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'on_error', + id: 'on_error', + label: __('On Error'), + initialValue: getInitialValue('on_error'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('on_error', val); + handleSchemaFieldChange(aeClassId, val, 'on_error'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'max_retries', + id: 'max_retries', + label: __('Max Retries'), + initialValue: getInitialValue('max_retries'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('max_retries', val); + handleSchemaFieldChange(aeClassId, val, 'max_retries'); + }, + }), + }, + { + component: componentTypes.TEXT_FIELD, + name: 'max_time', + id: 'max_time', + label: __('Max Time'), + initialValue: getInitialValue('max_time'), + ...(selectedRowId && { + onChange: (e) => { + const val = e.target.value; + updateFieldValueInState('max_time', val); + handleSchemaFieldChange(aeClassId, val, 'max_time'); + }, + }), + }, + ], + }; +}; + +export default createClassFieldsSchema; diff --git a/app/javascript/components/data-tables/datastore/schema/schema-table.jsx b/app/javascript/components/data-tables/datastore/schema/schema-table.jsx new file mode 100644 index 00000000000..b629f9d943d --- /dev/null +++ b/app/javascript/components/data-tables/datastore/schema/schema-table.jsx @@ -0,0 +1,54 @@ +// import React from 'react'; +import React, { useEffect } from 'react'; +import { useFieldApi, useFormApi } from '@@ddf'; +import { Button } from 'carbon-components-react'; +import MiqDataTable from '../../../miq-data-table'; + +export const SchemaTableComponent = (props) => { + const { + input, rows, onCellClick, onButtonClick, + } = useFieldApi(props); + const formOptions = useFormApi(); + + useEffect(() => { + input.onChange(rows); + }, [rows]); + + return ( +
+
+ +
+
+ onCellClick(selectedRow, cellType, formOptions)} + /> +
+
+ ); +}; diff --git a/app/stylesheet/miq-data-table.scss b/app/stylesheet/miq-data-table.scss index 2097fba9820..7f2015e174e 100644 --- a/app/stylesheet/miq-data-table.scss +++ b/app/stylesheet/miq-data-table.scss @@ -418,3 +418,9 @@ table.miq_preview { color: var(--red); } } + +.schema-table { + .schema-add { + margin-bottom: 16px; + } +} diff --git a/app/views/miq_ae_class/_class_fields.html.haml b/app/views/miq_ae_class/_class_fields.html.haml index e6bcc90cc8b..a4d684fd817 100644 --- a/app/views/miq_ae_class/_class_fields.html.haml +++ b/app/views/miq_ae_class/_class_fields.html.haml @@ -10,108 +10,4 @@ :locals => {:message => _("No schema found")} - else - - url = url_for_only_path(:action => 'fields_form_field_changed', :id => (@ae_class.id || 'new')) - - obs = {:interval => '.5', :url => url}.to_json - / Edit Schema - .form_div - %h3= _('Schema') - %table.table.table-striped.table-bordered - %thead - %tr - %th.table-view-pf-select - - [_('Name'), _('Type'), _('Data Type'), _('Default Value'), _('Display Name'), _('Description'), _('Sub'), _('Collect'), - _('Message'), _('On Entry'), _('On Exit'), _('On Error'), _('Max Retries'), _('Max Time')].each do |title| - %th - = title - %tbody - - @edit[:new][:fields].each_with_index do |field, i| - - unless @edit[:fields_to_delete].include?(field["id"]) - %tr - %td - = link_to({:action => "field_delete", :id => field["id"].to_s, :arr_id => i}, - "data-miq_sparkle_on" => true, - "data-miq_sparkle_off" => true, - :remote => true, - "data-method" => :post, - :class => 'btn btn-default', - :confirm => _('Are you sure you want to delete field from schema?'), - :title => _('Click to delete this field from schema')) do - %i.pficon.pficon-delete - - %w(name aetype datatype default_value display_name description substitute collect message on_entry on_exit on_error max_retries max_time).each do |fname| - %td - - if %w(aetype datatype).include?(fname.to_s) - - combo_name = "fields_#{fname}#{i}" - - combo_options = (fname == "aetype" ? @combo_xml : @dtype_combo_xml) - - combo_url = "/miq_ae_class/fields_form_field_changed/#{@ae_class.id || 'new'}" - .form-group - = select_tag(combo_name, - options_for_select(combo_options, field[fname]), - "title" => "Choose", - :class => "selectpicker") - :javascript - miqSelectPickerEvent("#{combo_name}", "#{combo_url}") - - elsif fname == "substitute" - = check_box_tag("fields_#{fname}_#{i}", "1", field["substitute"], - "data-miq_observe_checkbox" => {:url => url}.to_json) - - elsif fname == "default_value" - - default_value = field["default_value"] - = text_field_tag("fields_default_value_#{i}", default_value, - :style => field['datatype'] == "password" ? "display:none" : "", - "data-miq_observe" => obs) - = password_field_tag("fields_password_value_#{i}", '', - :placeholder => placeholder_if_present(default_value), - :style => field['datatype'] == "password" ? "" : "display:none", - :autocomplete => "new-password", - "data-miq_observe" => obs) - - else - = text_field_tag("fields_#{fname}_#{i}", field[fname], - "data-miq_observe" => obs) - - if !params[:add] && params[:add] != "new" && session[:field_data].blank? - %tr{:onclick => remote_function(:url => {:action => 'field_select', :add => 'new', :item => "field"})} - %td - %button.btn.btn-default - %i.fa.fa-plus - %td - = h("<#{_('New Field')}>") - - 13.times do - %td - - else - %tr - %td - = link_to({:action => "field_accept", :button => "accept"}, - "data-miq_sparkle_on" => true, - "data-miq_sparkle_off" => true, - :remote => true, - :class => 'btn btn-default', - "data-method" => :post, - :title => _("Add this entry")) do - %i.pficon.pficon-save - - %w(name aetype datatype default_value display_name description substitute collect message on_entry on_exit on_error max_retries max_time).each do |fname| - %td - - if %w(aetype datatype).include?(fname) - - combo_name = "field_#{fname}" - - combo_options = @edit[:new]["#{fname}s".to_sym] - - combo_url = "/miq_ae_class/fields_form_field_changed/#{@ae_class.id || 'new'}" - .form-group - = select_tag(combo_name, - options_for_select(combo_options, session[:field_data][fname]), - "title" => "Choose", - :class => "selectpicker") - :javascript - miqSelectPickerEvent("#{combo_name}", "#{combo_url}") - - elsif fname == "substitute" - - checked = !session[:field_data].blank? && session[:field_data][:substitute] - = check_box_tag("field_#{fname}", "1", checked, "data-miq_observe_checkbox" => {:url => url}.to_json) - - elsif fname == "default_value" - = text_field_tag("field_default_value", session[:field_data][:default_value], - :style => session[:field_data][:datatype] == "password" ? "display:none" : "", - "data-miq_observe" => obs) - = password_field_tag("field_password_value", '', - :placeholder => placeholder_if_present(session[:field_data][:default_value]), - :style => session[:field_data][:datatype] == "password" ? "" : "display:none", - "data-miq_observe" => obs) - - else - = text_field_tag("field_#{fname}", session[:field_data][fname.to_sym], - "data-miq_observe" => obs) -:javascript - miqInitSelectPicker(); + = render :partial => 'datastore_list', :locals => {:type => MiqAeClassHelper::DATASTORE_TYPES[:schema], :data => @ae_class.ae_fields} diff --git a/app/views/miq_ae_class/_datastore_list.haml b/app/views/miq_ae_class/_datastore_list.haml index 09970df9588..11311dd39d5 100644 --- a/app/views/miq_ae_class/_datastore_list.haml +++ b/app/views/miq_ae_class/_datastore_list.haml @@ -1,2 +1,7 @@ - datastore_data(type, data) -= react('Datastore', {:type => type, :initialData => @initial_data, :hasOptions => @has_options, :datastoreTypes => MiqAeClassHelper::DATASTORE_TYPES}) += react('Datastore', {:type => type, :initialData => @initial_data, :hasOptions => @has_options, + :datastoreTypes => MiqAeClassHelper::DATASTORE_TYPES, + :isEdit => !!@in_a_form_fields, + :aeTypeOptions => @combo_xml, + :dTypeOptions => @dtype_combo_xml, + :aeClassId => @ae_class&.id})