diff --git a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/BaseFormView.jsx b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/BaseFormView.jsx index 654edd3b6..395b6b55e 100644 --- a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/BaseFormView.jsx +++ b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/BaseFormView.jsx @@ -201,21 +201,21 @@ class BaseFormView extends PureComponent { } if (!error) { - const params = new URLSearchParams(); + const body = new URLSearchParams(); Object.keys(datadict).forEach((key) => { - if (datadict[key]) { - params.append(key, datadict[key]); + if (datadict[key] != null) { + body.append(key, datadict[key]); } }); if (this.props.mode === MODE_EDIT) { - params.delete('name'); + body.delete('name'); } axiosCallWrapper({ serviceName: this.endpoint, - params, + body, customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'post', handleError: false, @@ -256,7 +256,7 @@ class BaseFormView extends PureComponent { const changes = {}; if (this.dependencyMap.has(field)) { const value = this.dependencyMap.get(field); - for (const loadField in value) { + Object.keys(value).forEach((loadField) => { const data = {}; let load = true; @@ -265,19 +265,23 @@ class BaseFormView extends PureComponent { return e.field === dependency; }).required; - const value = - dependency == field ? targetValue : this.state.data[dependency]['value']; - if (required && !value) { + const currentValue = + dependency === field ? targetValue : this.state.data[dependency].value; + if (required && !currentValue) { load = false; + data[dependency] = null; } else { - data[dependency] = value; + data[dependency] = currentValue; } }); if (load) { - changes[loadField] = { dependencyValues: { $set: data } }; + changes[loadField] = { + dependencyValues: { $set: data }, + value: { $set: null }, + }; } - } + }); } changes[field] = { value: { $set: targetValue } }; @@ -296,7 +300,7 @@ class BaseFormView extends PureComponent { addCustomValidator = (field, validatorFunc) => { const index = this.entities.findIndex((x) => x.field === field); - const validator = [{ type: 'custom', validatorFunc: validatorFunc }]; + const validator = [{ type: 'custom', validatorFunc }]; this.entities[index].validators = validator; }; diff --git a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/MultiInputComponent.jsx b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/MultiInputComponent.jsx index 18bf45be8..f727163f8 100644 --- a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/MultiInputComponent.jsx +++ b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/MultiInputComponent.jsx @@ -1,29 +1,108 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import Multiselect from '@splunk/react-ui/Multiselect'; +import { _ } from '@splunk/ui-utils/i18n'; +import axios from 'axios'; +import { axiosCallWrapper } from '../util/axiosCallWrapper'; +import { filterResponse } from '../util/util'; function MultiInputComponent(props) { - const { field, disabled = false, value, controlOptions, ...restProps } = props; - const { items, placeholder, createSearchChoice, delimiter = ',' } = controlOptions; + const { + field, + disabled = false, + error = false, + value, + controlOptions, + dependencyValues, + ...restProps + } = props; + const { + endpointUrl, + denyList, + allowList, + items, + dependencies, + referenceName, + placeholder, + createSearchChoice, + labelField, + delimiter = ',', + } = controlOptions; function handleChange(e, { values }) { restProps.handleChange(field, values.join(delimiter)); } + function generateOptions(items) { + return items.map((item) => ( + + )); + } + + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState(null); + + useEffect(() => { + if (items) { + setOptions(generateOptions(items)); + return; + } + + let current = true; + const source = axios.CancelToken.source(); + + const options = { CancelToken: source.token, handleError: true }; + if (referenceName) { + options.serviceName = referenceName; + } else if (endpointUrl) { + options.endpointUrl = endpointUrl; + } + + if (dependencyValues) { + options.params = dependencyValues; + } + if (!dependencies || dependencyValues) { + setLoading(true); + axiosCallWrapper(options) + .then((response) => { + if (current) { + setOptions( + generateOptions( + filterResponse(response.data.entry, labelField, allowList, denyList) + ) + ); + setLoading(false); + } + }) + .catch((error) => { + if (current) { + setLoading(false); + } + }); + } + return () => { + source.cancel('Operation canceled.'); + current = false; + }; + }, [dependencyValues]); + + const effectiveDisabled = loading ? true : disabled; + const effectivePlaceholder = loading ? _('Loading') : placeholder; + const valueList = value ? value.split(delimiter) : []; return ( - {items.map((item) => ( - - ))} + {options && options.length > 0 && options} ); } @@ -31,18 +110,26 @@ function MultiInputComponent(props) { MultiInputComponent.propTypes = { disabled: PropTypes.bool, value: PropTypes.string, + error: PropTypes.bool, handleChange: PropTypes.func.isRequired, field: PropTypes.string, + dependencyValues: PropTypes.object, controlOptions: PropTypes.shape({ delimiter: PropTypes.string, placeholder: PropTypes.string, createSearchChoice: PropTypes.bool, + referenceName: PropTypes.string, + dependencies: PropTypes.array, + endpointUrl: PropTypes.string, + denyList: PropTypes.string, + allowList: PropTypes.string, + labelField: PropTypes.string, items: PropTypes.arrayOf( PropTypes.shape({ label: PropTypes.string.isRequired, value: PropTypes.string.isRequired, }) - ).isRequired, + ), }), }; diff --git a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/SingleInputComponent.jsx b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/SingleInputComponent.jsx index c0d06727a..44228d1f2 100644 --- a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/SingleInputComponent.jsx +++ b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/SingleInputComponent.jsx @@ -1,18 +1,41 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import Select from '@splunk/react-ui/Select'; +import { _ } from '@splunk/ui-utils/i18n'; +import axios from 'axios'; +import { axiosCallWrapper } from '../util/axiosCallWrapper'; +import { filterResponse } from '../util/util'; function SingleInputComponent(props) { - const { field, disabled = false, value, controlOptions, ...restProps } = props; - const { autoCompleteFields=[] } = controlOptions; + const { + field, + disabled = false, + error = false, + value, + controlOptions, + dependencyValues, + ...restProps + } = props; + const { + endpointUrl, + denyList, + allowList, + placeholder = _('Select a value'), + dependencies, + createSearchChoice, + referenceName, + disableSearch, + labelField, + autoCompleteFields, + } = controlOptions; function handleChange(e, { value }) { restProps.handleChange(field, value); } - function generateOptions() { + function generateOptions(items) { const data = []; - autoCompleteFields.forEach((item) => { + items.forEach((item) => { if (item.value && item.label) { data.push(); } @@ -28,9 +51,67 @@ function SingleInputComponent(props) { return data; } + const [loading, setLoading] = useState(false); + const [options, setOptions] = useState(null); + + useEffect(() => { + if (autoCompleteFields) { + setOptions(generateOptions(autoCompleteFields)); + return; + } + + let current = true; + const source = axios.CancelToken.source(); + + const options = { CancelToken: source.token, handleError: true }; + if (referenceName) { + options.serviceName = referenceName; + } else if (endpointUrl) { + options.endpointUrl = endpointUrl; + } + + if (dependencyValues) { + options.params = dependencyValues; + } + if (!dependencies || dependencyValues) { + setLoading(true); + axiosCallWrapper(options) + .then((response) => { + if (current) { + setOptions( + generateOptions( + filterResponse(response.data.entry, labelField, allowList, denyList) + ) + ); + setLoading(false); + } + }) + .catch((error) => { + if (current) { + setLoading(false); + } + }); + } + return () => { + source.cancel('Operation canceled.'); + current = false; + }; + }, [dependencyValues]); + + const effectiveDisabled = loading ? true : disabled; + const effectivePlaceholder = loading ? _('Loading') : placeholder; + return ( - + {options && options.length > 0 && options} ); } @@ -38,10 +119,21 @@ function SingleInputComponent(props) { SingleInputComponent.propTypes = { disabled: PropTypes.bool, value: PropTypes.string, + error: PropTypes.bool, handleChange: PropTypes.func.isRequired, field: PropTypes.string, + dependencyValues: PropTypes.object, controlOptions: PropTypes.shape({ - autoCompleteFields: PropTypes.array.isRequired, + autoCompleteFields: PropTypes.array, + endpointUrl: PropTypes.string, + denyList: PropTypes.string, + allowList: PropTypes.string, + placeholder: PropTypes.string, + dependencies: PropTypes.array, + createSearchChoice: PropTypes.bool, // TODO: Not supported yet + referenceName: PropTypes.string, + disableSearch: PropTypes.bool, // TODO: Not supported yet + labelField: PropTypes.string, }), }; diff --git a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/table/TableWrapper.jsx b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/table/TableWrapper.jsx index 07200a347..e71897ce3 100644 --- a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/table/TableWrapper.jsx +++ b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/components/table/TableWrapper.jsx @@ -104,12 +104,12 @@ function TableWrapper({ page, serviceName, handleRequestModalOpen }) { }, }); }); - const params = new URLSearchParams(); - params.append('disabled', !row.disabled); + const body = new URLSearchParams(); + body.append('disabled', !row.disabled); axiosCallWrapper({ serviceName: `${row.serviceName}/${row.name}`, - params, + body, customHeaders: { 'Content-Type': 'application/x-www-form-urlencoded' }, method: 'post', handleError: true, diff --git a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/axiosCallWrapper.js b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/axiosCallWrapper.js index 4e80487d1..9f7879c58 100644 --- a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/axiosCallWrapper.js +++ b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/axiosCallWrapper.js @@ -9,32 +9,24 @@ import { generateEndPointUrl, generateToast } from './util'; * @param {string} data.serviceName service name which is input name or tab name based on the page * @param {string} data.endpointUrl rest endpoint path * @param {object} data.params object with params as key value pairs + * @param {object} data.body object with body as key value pairs for post request * @param {object} data.customHeaders extra headers as key value pair * @param {string} data.method rest method type * @param {string} data.handleError whether or not show toast notifications on failure * @param {string} data.callbackOnError callback function to execute after handling error. Only executed when handleError is set to true * @returns */ -const axiosCallWrapper = ( - data = { - serviceName: null, - endpointUrl: null, - params: {}, - customHeaders: {}, - method: 'get', - handleError: false, - callbackOnError: () => {}, - } -) => { - const { - serviceName, - endpointUrl, - params, - customHeaders, - method, - handleError, - callbackOnError, - } = data; +const axiosCallWrapper = ({ + serviceName, + endpointUrl, + params, + body, + cancelToken, + customHeaders = {}, + method = 'get', + handleError = false, + callbackOnError = () => {}, +}) => { const endpoint = serviceName ? generateEndPointUrl(serviceName) : endpointUrl; const appData = { app, @@ -46,24 +38,32 @@ const axiosCallWrapper = ( 'Content-Type': 'application/json', }; const headers = Object.assign(baseHeaders, customHeaders); - const url = `${createRESTURL(endpoint, appData)}?output_mode=json`; + const url = createRESTURL(endpoint, appData); + + let newParams = { output_mode: 'json' }; + if (params) { + newParams = { ...newParams, ...params }; + } const options = { + params: newParams, method, url, credentials: 'include', headers, + cancelToken, }; if (method === 'post') { - options.data = params; - } else { - options.params = params; + options.data = body; } return handleError ? axios(options).catch((error) => { let message = ''; + if (axios.isCancel(error)) { + return Promise.reject(error); + } if (error.response) { // The request was made and the server responded with a status code message = `Error response received from server: ${error.response.data.messages[0].text}`; diff --git a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/util.js b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/util.js index 83a80a3ac..173b1b3ec 100644 --- a/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/util.js +++ b/splunk_add_on_ucc_framework/ucc_ui_lib/src/main/webapp/util/util.js @@ -10,7 +10,7 @@ export function setMetaInfo(data) { export function getMetaInfo() { return { - appData: appData, + appData, }; } @@ -34,6 +34,32 @@ export const generateToast = (message, action = undefined) => { autoDismiss: true, dismissOnActionClick: true, showAction: Boolean(action), - action: action ? action : undefined, + action: action || undefined, }); }; + +export function filterByAllowList(fields, allowList) { + const allowRegex = new RegExp(allowList); + return fields.filter((item) => allowRegex.test(item.value)); +} + +export function filterByDenyList(fields, denyList) { + const denyRegex = new RegExp(denyList); + return fields.filter((item) => !denyRegex.test(item.value)); +} + +export function filterResponse(items, labelField, allowList, denyList) { + let newItems = items.map((item) => { + return { label: labelField ? item[labelField] : item.name, value: item.name }; + }); + + if (allowList) { + newItems = filterByAllowList(newItems, allowList); + } + + if (denyList) { + newItems = filterByDenyList(newItems, denyList); + } + + return newItems; +}