diff --git a/docs/custom_ui_extensions/custom_row.md b/docs/custom_ui_extensions/custom_row.md index a421e19a3..cac848dbe 100644 --- a/docs/custom_ui_extensions/custom_row.md +++ b/docs/custom_ui_extensions/custom_row.md @@ -2,18 +2,24 @@ When a row is expanded on the Inputs table, Custom Row is utilized to incorporat ### Properties -| Property | Description | -| ----------------- | ----------- | -| globalConfig | is a hierarchical object that contains the globalConfig file's properties and values. | -| el | is used to render a customized element on the Inputs table when a row is expanded. | -| serviceName | is the name of the service/tab specified in the globalConfig file. | -| row | it the object of the record for which the CustomRowInput constructor is called. | +| Property | Description | +| ------------ | ------------------------------------------------------------------------------------- | +| globalConfig | is a hierarchical object that contains the globalConfig file's properties and values. | +| el | is used to render a customized element on the Inputs table when a row is expanded. | +| serviceName | is the name of the service/tab specified in the globalConfig file. | +| row | is the object of the record for which the CustomRowInput constructor is called. | ### Methods -| Property | Description | -| ----------------- | ----------- | -| render | is a method which should have logic for the custom row component, and it will be executed automatically when the create, edit, or clone actions are performed. | +| Property | Description | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| render | is a method which contains the logic to display the custom row component. This method is automatically executed when the row is expanded | +| getDLRows | is a method which contains the logic to update the custom row values, return a key-value pair. | + +> Note + +> - Atleast one method should be present +> - If both method is present then the getDLRows method have the high priority. ### Usage @@ -35,34 +41,14 @@ When a row is expanded on the Inputs table, Custom Row is utilized to incorporat ### Example +```js +--8<-- "tests/testdata/test_addons/package_global_config_everything/package/appserver/static/js/build/custom/custom_input_row.js" ``` -class CustomInputRow { - /** - * Custom Row Cell - * @constructor - * @param {Object} globalConfig - Global configuration. - * @param {string} serviceName - Input service name. - * @param {element} el - The element of the custom cell. - * @param {Object} row - custom row object. - */ - constructor(globalConfig, serviceName, el, row) { - this.globalConfig = globalConfig; - this.serviceName = serviceName; - this.el = el; - this.row = row; - } - render() { - const content_html_template = 'Custom Input Row'; - this.el.innerHTML = content_html_template; - return this; - } -} - -export default CustomInputRow; -``` +> Note: -> Note: The Javascript file for the custom control should be saved in the custom folder at `appserver/static/js/build/custom/`. +> - The content should be included in the JavaScript file named by customRow.src property in globalConfig (see usage for details). +> - The Javascript file for the custom control should be saved in the custom folder at `appserver/static/js/build/custom/`. ### Output diff --git a/docs/images/custom_ui_extensions/Custom_Row_Output.png b/docs/images/custom_ui_extensions/Custom_Row_Output.png index c2a08c38a..477c7487e 100644 Binary files a/docs/images/custom_ui_extensions/Custom_Row_Output.png and b/docs/images/custom_ui_extensions/Custom_Row_Output.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 4158401d5..119178b99 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ markdown_extensions: - sane_lists - codehilite - pymdownx.superfences + - pymdownx.snippets theme: name: "material" @@ -26,18 +27,18 @@ theme: - navigation.indexes extra_css: - - css/extra.css + - css/extra.css nav: - Home: "index.md" - Quickstart: "quickstart.md" - ".conf files": "dot_conf_files.md" - Inputs: - - "inputs/index.md" - - Introduction: "inputs/index.md" - - Tabs: "inputs/tabs.md" - - Multi-level Menu: "inputs/multilevel_menu.md" - - Helper Module: "inputs/helper.md" + - "inputs/index.md" + - Introduction: "inputs/index.md" + - Tabs: "inputs/tabs.md" + - Multi-level Menu: "inputs/multilevel_menu.md" + - Helper Module: "inputs/helper.md" - Configuration: - "configurations/index.md" - Introduction: "configurations/index.md" @@ -45,15 +46,15 @@ nav: - Proxy: "configurations/proxy.md" - Dashboard: "dashboard.md" - Alert Actions: - - "alert_actions/index.md" - - Alert Action Scripts: "alert_actions/alert_scripts.md" - - Adaptive Response: "alert_actions/adaptive_response.md" + - "alert_actions/index.md" + - Alert Action Scripts: "alert_actions/alert_scripts.md" + - Adaptive Response: "alert_actions/adaptive_response.md" - Entity: - - "entity/index.md" - - Introduction: "entity/index.md" - - Components: "entity/components.md" - - Validators: "entity/validators.md" - - Modify Fields On Change: "entity/modifyFieldsOnValue.md" + - "entity/index.md" + - Introduction: "entity/index.md" + - Components: "entity/components.md" + - Validators: "entity/validators.md" + - Modify Fields On Change: "entity/modifyFieldsOnValue.md" - Table: "table.md" - Additional packaging: "additional_packaging.md" @@ -61,23 +62,23 @@ nav: - OpenAPI: "openapi.md" - UCC-related libraries: "ucc_related_libraries.md" - Custom UI Extensions: - - Overview: "custom_ui_extensions/overview.md" - - Custom Hook: "custom_ui_extensions/custom_hook.md" - - Custom Control: "custom_ui_extensions/custom_control.md" - - Custom Row: "custom_ui_extensions/custom_row.md" - - Custom Cell: "custom_ui_extensions/custom_cell.md" - - Custom Menu: "custom_ui_extensions/custom_menu.md" - - Custom Tab: "custom_ui_extensions/custom_tab.md" + - Overview: "custom_ui_extensions/overview.md" + - Custom Hook: "custom_ui_extensions/custom_hook.md" + - Custom Control: "custom_ui_extensions/custom_control.md" + - Custom Row: "custom_ui_extensions/custom_row.md" + - Custom Cell: "custom_ui_extensions/custom_cell.md" + - Custom Menu: "custom_ui_extensions/custom_menu.md" + - Custom Tab: "custom_ui_extensions/custom_tab.md" - Advanced: - - Custom Mapping: "advanced/custom_mapping.md" - - Dependent Dropdown: "advanced/dependent_dropdown.md" - - OAuth Support: "advanced/oauth_support.md" - - Custom REST Handler: "advanced/custom_rest_handler.md" - - Groups Feature: "advanced/groups_feature.md" - - Save Validator: "advanced/save_validator.md" - - OS-dependent libraries: "advanced/os-dependent_libraries.md" - - Sub Description: "advanced/sub_description.md" - - Custom Warning: "advanced/custom_warning.md" + - Custom Mapping: "advanced/custom_mapping.md" + - Dependent Dropdown: "advanced/dependent_dropdown.md" + - OAuth Support: "advanced/oauth_support.md" + - Custom REST Handler: "advanced/custom_rest_handler.md" + - Groups Feature: "advanced/groups_feature.md" + - Save Validator: "advanced/save_validator.md" + - OS-dependent libraries: "advanced/os-dependent_libraries.md" + - Sub Description: "advanced/sub_description.md" + - Custom Warning: "advanced/custom_warning.md" - Troubleshooting: "troubleshooting.md" - Contributing: "contributing.md" - Changelog: "CHANGELOG.md" diff --git a/tests/testdata/test_addons/package_global_config_everything/globalConfig.json b/tests/testdata/test_addons/package_global_config_everything/globalConfig.json index 01cd0271b..d8e5f704b 100644 --- a/tests/testdata/test_addons/package_global_config_everything/globalConfig.json +++ b/tests/testdata/test_addons/package_global_config_everything/globalConfig.json @@ -1376,6 +1376,10 @@ "field": "disabled" } ], + "customRow": { + "src": "custom_input_row", + "type": "external" + }, "moreInfo": [ { "label": "Name", @@ -1550,9 +1554,9 @@ "meta": { "name": "Splunk_TA_UCCExample", "restRoot": "splunk_ta_uccexample", - "version": "5.44.0R7f88cfdd", + "version": "5.45.0R08c37572", "displayName": "Splunk UCC test Add-on", "schemaVersion": "0.0.7", - "_uccVersion": "5.44.0" + "_uccVersion": "5.45.0" } } diff --git a/tests/testdata/test_addons/package_global_config_everything/package/appserver/static/js/build/custom/custom_input_row.js b/tests/testdata/test_addons/package_global_config_everything/package/appserver/static/js/build/custom/custom_input_row.js new file mode 100644 index 000000000..4d5bc8627 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/appserver/static/js/build/custom/custom_input_row.js @@ -0,0 +1,34 @@ +class CustomInputRow { + /** + * Custom Row Cell + * @constructor + * @param {Object} globalConfig - Global configuration. + * @param {string} serviceName - Input service name. + * @param {element} el - The element of the custom cell. + * @param {Object} row - custom row object, + * use this.row., where is a field name + */ + constructor(globalConfig, serviceName, el, row) { + this.globalConfig = globalConfig; + this.serviceName = serviceName; + this.el = el; + this.row = row; + } + + getDLRows() { + return Object.fromEntries( + Object.entries(this.row).map(([key, value]) => [ + key, + key === "interval" ? `${value} sec` : value, + ]) + ); + } + + render() { + const content_html_template = "Custom Input Row"; + this.el.innerHTML = content_html_template; + return this; + } +} + +export default CustomInputRow; diff --git a/tests/ui/test_input_page.py b/tests/ui/test_input_page.py index 8f122b7ce..65df07f4e 100644 --- a/tests/ui/test_input_page.py +++ b/tests/ui/test_input_page.py @@ -293,11 +293,12 @@ def test_inputs_more_info( ): """Verifies the expand functionality of the inputs table""" input_page = InputPage(ucc_smartx_selenium_helper, ucc_smartx_rest_helper) + interval = "90" self.assert_util( input_page.table.get_more_info, { "Name": "dummy_input_one", - "Interval": "90", + "Interval": f"{interval} sec", "Index": "default", "Status": "Enabled", "Example Account": "test_input", @@ -309,6 +310,11 @@ def test_inputs_more_info( }, left_args={"name": "dummy_input_one"}, ) + backend_stanza = input_page.backend_conf.get_stanza( + "example_input_one://dummy_input_one" + ) + # we verify that the conf value is `interval` and only the UI has changed + assert backend_stanza.get("interval") == interval @pytest.mark.execute_enterprise_cloud_true @pytest.mark.forwarder diff --git a/ui/src/components/table/CustomTableControl.jsx b/ui/src/components/table/CustomTableControl.jsx index 149adf40d..544dd32f2 100644 --- a/ui/src/components/table/CustomTableControl.jsx +++ b/ui/src/components/table/CustomTableControl.jsx @@ -1,9 +1,12 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import DL from '@splunk/react-ui/DefinitionList'; import { _ } from '@splunk/ui-utils/i18n'; +import ExclamationTriangle from '@splunk/react-icons/ExclamationTriangle'; import { getUnifiedConfigs } from '../../util/util'; import { getBuildDirPath } from '../../util/script'; +import { getExpansionRowData } from './TableExpansionRowData'; function onCustomControlError(params) { // eslint-disable-next-line no-console @@ -17,23 +20,63 @@ class CustomTableControl extends Component { super(props); this.state = { loading: true, + row: { ...props.row }, + checkMethodIsPresent: false, + methodNotPresentError: '', }; this.shouldRender = true; } componentDidMount() { const globalConfig = getUnifiedConfigs(); - this.setState({ loading: true }); - this.loadCustomControl().then((Control) => { - this.customControl = new Control( - globalConfig, - this.props.serviceName, - this.el, - this.props.row, - this.props.field + this.loadCustomControl() + .then(async (Control) => { + if (typeof Control !== 'function') { + this.setState({ + loading: false, + methodNotPresentError: 'Loaded module is not a constructor function', + }); + return; + } + this.customControl = new Control( + globalConfig, + this.props.serviceName, + this.el, + this.state.row, + this.props.field + ); + + const result = await this.callCustomMethod('getDLRows'); + try { + // check if getDLRow is exist in the custom input row file + if (result && typeof result === 'object' && !Array.isArray(result)) { + this.setState({ + row: { ...result }, + checkMethodIsPresent: true, + loading: false, + }); + } else if (result !== null) { + // check if getDLRow return invalid object + this.setState({ + loading: false, + checkMethodIsPresent: true, + methodNotPresentError: 'getDLRows method did not return a valid object', + }); + } else { + // if getDLRow is not present then check render method is present or not + this.handleNoGetDLRows(); + } + } catch (error) { + onCustomControlError({ methodName: 'getDLRows', error }); + this.handleNoGetDLRows(); + } + }) + .catch(() => + this.setState({ + loading: false, + methodNotPresentError: 'Error loading custom control', + }) ); - this.setState({ loading: false }); - }); } shouldComponentUpdate(nextProps, nextState) { @@ -48,45 +91,100 @@ class CustomTableControl extends Component { } loadCustomControl = () => - new Promise((resolve) => { - if (this.props.type === 'external') { - import( - /* webpackIgnore: true */ `${getBuildDirPath()}/custom/${ - this.props.fileName - }.js` - ).then((external) => { - const Control = external.default; - resolve(Control); - }); + new Promise((resolve, reject) => { + const { type, fileName } = this.props; + const globalConfig = getUnifiedConfigs(); + + if (type === 'external') { + import(/* webpackIgnore: true */ `${getBuildDirPath()}/custom/${fileName}.js`) + .then((external) => resolve(external.default)) + .catch((error) => reject(error)); } else { - const globalConfig = getUnifiedConfigs(); const appName = globalConfig.meta.name; __non_webpack_require__( - [`app/${appName}/js/build/custom/${this.props.fileName}`], - (Control) => resolve(Control) + [`app/${appName}/js/build/custom/${fileName}`], + (Control) => resolve(Control), + (error) => reject(error) ); } }); + /** + * Calls a method on the customControl instance, if it exists, with the provided arguments. + * + * @param {string} methodName - The name of the method to call on the customControl class instance. + * @param {...unknown} args - Any arguments that should be passed to the method. + * @returns {*} - The response from the custom control method, or null if the method does not exist or an error occurs. + */ + callCustomMethod = async (methodName, ...args) => { + try { + if (typeof this.customControl[methodName] === 'function') { + return this.customControl[methodName](...args); + } + return null; + } catch (error) { + onCustomControlError({ methodName, error }); + return null; + } + }; + + handleNoGetDLRows = () => { + if (!this.customControl || typeof this.customControl.render !== 'function') { + this.setState((prevState) => ({ + ...prevState, + methodNotPresentError: + 'At least "render" either "getDLRows" method should be present.', + })); + } + this.setState((prevState) => ({ + ...prevState, + loading: false, + })); + }; + render() { - if (!this.state.loading) { + const { row, loading, checkMethodIsPresent, methodNotPresentError } = this.state; + const { moreInfo } = this.props; + + if ( + !loading && + !checkMethodIsPresent && + this.customControl && + typeof this.customControl.render === 'function' + ) { try { - this.customControl.render(this.props.row, this.props.field); + this.customControl.render(row, moreInfo); } catch (error) { onCustomControlError({ methodName: 'render', error }); } } + + let content; + + if (methodNotPresentError) { + content = ( + + + {methodNotPresentError} + + ); + } else if (checkMethodIsPresent) { + content =
{getExpansionRowData(row, moreInfo)}
; + } else { + content = ( + { + this.el = el; + }} + style={{ visibility: loading ? 'hidden' : 'visible' }} + /> + ); + } + return ( <> - {this.state.loading && _('Loading...')} - { - { - this.el = el; - }} - style={{ visibility: this.state.loading ? 'hidden' : 'visible' }} - /> - } + {loading && _('Loading...')} + {content} ); } @@ -98,6 +196,7 @@ CustomTableControl.propTypes = { field: PropTypes.string, fileName: PropTypes.string.isRequired, type: PropTypes.string, + moreInfo: PropTypes.array.isRequired, }; export default CustomTableControl; diff --git a/ui/src/components/table/TableExpansionRow.jsx b/ui/src/components/table/TableExpansionRow.jsx index 0e401e4fd..a2e4cb7c8 100644 --- a/ui/src/components/table/TableExpansionRow.jsx +++ b/ui/src/components/table/TableExpansionRow.jsx @@ -2,37 +2,15 @@ import React from 'react'; import DL from '@splunk/react-ui/DefinitionList'; import Table from '@splunk/react-ui/Table'; import styled from 'styled-components'; -import { _ } from '@splunk/ui-utils/i18n'; import CustomTableControl from './CustomTableControl'; import { getUnifiedConfigs } from '../../util/util'; +import { getExpansionRowData } from './TableExpansionRowData'; const TableCellWrapper = styled(Table.Cell)` border-top: none; `; -function getExpansionRowData(row, moreInfo) { - const DefinitionLists = []; - - if (moreInfo?.length) { - moreInfo.forEach((val) => { - const label = _(val.label); - // remove extra rows which are empty in moreInfo - if (val.field in row && row[val.field] !== null && row[val.field] !== '') { - DefinitionLists.push({label}); - DefinitionLists.push( - - {val.mapping && val.mapping[row[val.field]] - ? val.mapping[row[val.field]] - : String(row[val.field])} - - ); - } - }); - } - return DefinitionLists; -} - export function getExpansionRow(colSpan, row, moreInfo) { const inputs = getUnifiedConfigs().pages?.inputs; @@ -50,6 +28,7 @@ export function getExpansionRow(colSpan, row, moreInfo) { row, fileName: customRow.src, type: customRow.type, + moreInfo, })} ) : ( diff --git a/ui/src/components/table/TableExpansionRowData.jsx b/ui/src/components/table/TableExpansionRowData.jsx new file mode 100644 index 000000000..adcae33e4 --- /dev/null +++ b/ui/src/components/table/TableExpansionRowData.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import DL from '@splunk/react-ui/DefinitionList'; +import { _ } from '@splunk/ui-utils/i18n'; + +/** + * Generates the definition list rows for the expansion view based on the provided row data and moreInfo configuration. + * + * @param {Object} row - The data object containing the row information. + * @param {Array} moreInfo - An array of objects containing configuration for each field to display. + * @returns {Array} - An array of React elements representing the definition list rows. + */ +export function getExpansionRowData(row, moreInfo) { + const DefinitionLists = []; + + if (moreInfo?.length) { + moreInfo.forEach((val) => { + const label = _(val.label); + // Remove extra rows which are empty in moreInfo + if (val.field in row && row[val.field] !== null && row[val.field] !== '') { + DefinitionLists.push({label}); + DefinitionLists.push( + + {val.mapping && val.mapping[row[val.field]] + ? val.mapping[row[val.field]] + : String(row[val.field])} + + ); + } + }); + } + return DefinitionLists; +}