From 9ffed80d80d9ac4fbff22fa47d464391c1c14233 Mon Sep 17 00:00:00 2001
From: Tushar Balar <62089106+tbalar-splunk@users.noreply.github.com>
Date: Wed, 6 Sep 2023 20:09:09 +0530
Subject: [PATCH] feat: ADDON-61556 Introduced checkbox group component (#394)
* feat: ADDON-61556 Introduced checkbox group component
* feat: ADDON-61556 Developed checkbox groups component
* feat: ADDON-61556 Validation part has updated
* feat: ADDON-61556 Added select all and clear all buttons
* feat: ADDON-61556 Revert unwanted code
* feat: ADDON-61556 Fix minor css issue
* feat: ADDON-61556 Fix issue
* refactor: ADDON-61556 Addressed review comments
---
.../main/webapp/components/BaseFormView.jsx | 356 ++++++++++++++----
.../components/CheckboxGroupsComponent.jsx | 71 ++++
.../main/webapp/components/ControlWrapper.jsx | 38 +-
.../webapp/components/StyledComponent.jsx | 143 +++++++
.../main/webapp/constants/ControlTypeMap.js | 2 +
ui/src/main/webapp/schema/schema.json | 130 ++++---
ui/src/main/webapp/util/util.js | 13 +
7 files changed, 587 insertions(+), 166 deletions(-)
create mode 100644 ui/src/main/webapp/components/CheckboxGroupsComponent.jsx
create mode 100644 ui/src/main/webapp/components/StyledComponent.jsx
diff --git a/ui/src/main/webapp/components/BaseFormView.jsx b/ui/src/main/webapp/components/BaseFormView.jsx
index cc944c322..27b2b9216 100644
--- a/ui/src/main/webapp/components/BaseFormView.jsx
+++ b/ui/src/main/webapp/components/BaseFormView.jsx
@@ -1,15 +1,13 @@
import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import Message from '@splunk/react-ui/Message';
import update from 'immutability-helper';
+import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
-
-import CollapsiblePanel from '@splunk/react-ui/CollapsiblePanel';
-import Message from '@splunk/react-ui/Message';
-import styled from 'styled-components';
+import Link from '@splunk/react-ui/Link';
import ControlWrapper from './ControlWrapper';
import Validator, { SaveValidator } from '../util/Validator';
-import { getUnifiedConfigs, generateToast } from '../util/util';
+import { getUnifiedConfigs, generateToast, isTrue, populateKeyValueDict } from '../util/util';
import { MODE_CLONE, MODE_CREATE, MODE_EDIT, MODE_CONFIG } from '../constants/modes';
import { PAGE_INPUT, PAGE_CONF } from '../constants/pages';
import { axiosCallWrapper } from '../util/axiosCallWrapper';
@@ -24,30 +22,18 @@ import {
ERROR_STATE_MISSING_TRY_AGAIN,
} from '../constants/oAuthErrorMessage';
import TableContext from '../context/TableContext';
-
-const CollapsiblePanelWrapper = styled(CollapsiblePanel)`
- span {
- button {
- background-color: #f2f4f5;
- font-size: 16px;
- margin: 15px 0;
-
- &:hover:not([disabled]),
- &:focus:not([disabled]),
- &:active:not([disabled]) {
- background-color: #f2f4f5;
- box-shadow: none;
- }
- }
- }
-`;
-
-const CustomGroupLabel = styled.div`
- padding: 6px 10px;
- background-color: #f2f4f5;
- margin: 0 0 15px 0;
- font-size: 16px;
-`;
+import {
+ CollapsiblePanelWrapper,
+ CustomGroupLabel,
+ CheckboxLabelContainer,
+ CheckboxGroupPanelWrapper,
+ CustomCheckboxGroupsLabel,
+ CheckboxGroupContainer,
+ CheckboxGroupsToggleButtonWrapper,
+ StyledPadding4,
+} from './StyledComponent';
+
+const CHECKBOX_GROUPS = 'checkboxGroups';
class BaseFormView extends PureComponent {
static contextType = TableContext;
@@ -61,6 +47,7 @@ class BaseFormView extends PureComponent {
const globalConfig = getUnifiedConfigs();
this.appName = globalConfig.meta.name;
this.groupEntities = [];
+ this.checkboxEntities = [];
this.endpoint =
props.mode === MODE_EDIT || props.mode === MODE_CONFIG
? `${this.props.serviceName}/${encodeURIComponent(this.props.stanzaName)}`
@@ -87,7 +74,19 @@ class BaseFormView extends PureComponent {
globalConfig.pages.inputs.services.forEach((service) => {
if (service.name === props.serviceName) {
this.groups = service.groups;
+ this.checkboxGroupsMetadata = service.CheckboxGroupsMetadata;
+ this.checkboxGroups = service.CheckboxGroupsMetadata?.groups;
this.entities = service.entity;
+
+ // Removing entity based validation
+ // Validation will be done using main validator from CheckboxGroupsMetadata
+ this.entities.forEach((e) => {
+ if (e.type === CHECKBOX_GROUPS) {
+ delete e.validators;
+ this.checkboxEntities.push(e);
+ }
+ });
+
this.updateGroupEntities();
this.options = service.options;
if (service.hook) {
@@ -99,6 +98,20 @@ class BaseFormView extends PureComponent {
}
if (props.mode === MODE_EDIT || props.mode === MODE_CLONE) {
this.currentInput = context.rowData[props.serviceName][props.stanzaName];
+
+ const checkboxGroupFieldValue =
+ this.currentInput[this.checkboxGroupsMetadata?.field];
+
+ // This conversion is performed while reading from the conf file.
+ // Next time (without refreshing the page), it is expected that the data will already be in dictionary format,
+ // and this conversion step will not be necessary.
+ if (
+ this.checkboxGroupsMetadata?.field &&
+ typeof checkboxGroupFieldValue === 'string'
+ ) {
+ this.currentInput[this.checkboxGroupsMetadata.field] =
+ populateKeyValueDict(checkboxGroupFieldValue);
+ }
}
}
});
@@ -257,19 +270,40 @@ class BaseFormView extends PureComponent {
e.encrypted = typeof e.encrypted !== 'undefined' ? e.encrypted : false;
if (props.mode === MODE_CREATE) {
- tempEntity.value =
- typeof e.defaultValue !== 'undefined' ? e.defaultValue : null;
+ if (e.type === CHECKBOX_GROUPS) {
+ // - Set the checkboxTextFieldValue to the provided defaultValue if defined; otherwise, set to null.
+ // This value is used to populate the associated Text field.
+ // - Determine the value for the Checkbox field's enable/disable state:
+ // If the 'enable' option is explicitly defined in the options object, use that value;
+ // otherwise, default to enabling the Checkbox field (true).
+ tempEntity.checkboxTextFieldValue =
+ typeof e.defaultValue !== 'undefined' ? e.defaultValue : null;
+ tempEntity.value =
+ typeof e?.options?.enable !== 'undefined' ? e.options.enable : true;
+ } else {
+ tempEntity.value =
+ typeof e.defaultValue !== 'undefined' ? e.defaultValue : null;
+ }
tempEntity.display =
typeof e?.options?.display !== 'undefined' ? e.options.display : true;
tempEntity.error = false;
tempEntity.disabled = false;
temState[e.field] = tempEntity;
} else if (props.mode === MODE_EDIT) {
- tempEntity.value =
- typeof this.currentInput[e.field] !== 'undefined'
- ? this.currentInput[e.field]
- : null;
- tempEntity.value = e.encrypted ? '' : tempEntity.value;
+ if (e.type === CHECKBOX_GROUPS) {
+ const checkboxTextFieldValue =
+ this.currentInput[this.checkboxGroupsMetadata?.field]?.[e.field];
+ tempEntity.value = typeof checkboxTextFieldValue !== 'undefined';
+ tempEntity.checkboxTextFieldValue = tempEntity.value
+ ? checkboxTextFieldValue
+ : e.defaultValue;
+ } else {
+ tempEntity.value =
+ typeof this.currentInput[e.field] !== 'undefined'
+ ? this.currentInput[e.field]
+ : null;
+ tempEntity.value = e.encrypted ? '' : tempEntity.value;
+ }
tempEntity.display =
typeof e?.options?.display !== 'undefined' ? e.options.display : true;
tempEntity.error = false;
@@ -281,8 +315,17 @@ class BaseFormView extends PureComponent {
}
temState[e.field] = tempEntity;
} else if (props.mode === MODE_CLONE) {
- tempEntity.value =
- e.field === 'name' || e.encrypted ? '' : this.currentInput[e.field];
+ if (e.type === CHECKBOX_GROUPS) {
+ const checkboxTextFieldValue =
+ this.currentInput[this.checkboxGroupsMetadata?.field]?.[e.field];
+ tempEntity.value = typeof checkboxTextFieldValue !== 'undefined';
+ tempEntity.checkboxTextFieldValue = tempEntity.value
+ ? checkboxTextFieldValue
+ : e.defaultValue;
+ } else {
+ tempEntity.value =
+ e.field === 'name' || e.encrypted ? '' : this.currentInput[e.field];
+ }
tempEntity.display =
typeof e?.options?.display !== 'undefined' ? e.options.display : true;
tempEntity.error = false;
@@ -399,9 +442,41 @@ class BaseFormView extends PureComponent {
this.datadict = {};
- Object.keys(this.state.data).forEach((field) => {
- this.datadict[field] = this.state.data[field].value;
- });
+ const updateDataDict = () => {
+ // If checkboxGroupsMetadata is defined, set an empty value in the datadict
+ // using the field name specified in checkboxGroupsMetadata.
+ // This key is used to store the selected values for the checkbox group.
+ if (this.checkboxGroupsMetadata) {
+ this.datadict[this.checkboxGroupsMetadata.field] = '';
+ }
+
+ // Iterate through each field in the state's data object
+ Object.keys(this.state.data).forEach((field) => {
+ const fieldData = this.state.data[field];
+
+ // Check if the field contains 'checkboxTextFieldValue' key, indicating a checkboxGroups component
+ if (fieldData.checkboxTextFieldValue) {
+ // For selected checkboxGroups components, append the field-value pair to the datadict
+ if (fieldData.value) {
+ this.datadict[
+ this.checkboxGroupsMetadata?.field
+ ] += `${field}/${fieldData.checkboxTextFieldValue},`;
+ }
+ } else {
+ // For non-checkboxGroups components, update datadict with the field's value
+ this.datadict[field] = fieldData.value;
+ }
+ });
+
+ // If there are checkboxGroups selections in datadict, remove trailing comma
+ if (this.datadict[this.checkboxGroupsMetadata?.field]) {
+ this.datadict[this.checkboxGroupsMetadata.field] = this.datadict[
+ this.checkboxGroupsMetadata.field
+ ].slice(0, -1);
+ }
+ };
+
+ updateDataDict();
if (this.hook && typeof this.hook.onSave === 'function') {
const validationPass = this.hook.onSave(this.datadict);
@@ -410,10 +485,9 @@ class BaseFormView extends PureComponent {
return;
}
}
+
const executeValidationSubmit = () => {
- Object.keys(this.state.data).forEach((field) => {
- this.datadict[field] = this.state.data[field].value;
- });
+ updateDataDict();
// validation for unique name
if ([MODE_CREATE, MODE_CLONE].includes(this.props.mode)) {
@@ -453,13 +527,33 @@ class BaseFormView extends PureComponent {
});
} else {
temEntities = this.entities;
+
+ if (this.checkboxGroupsMetadata?.validators) {
+ const checkboxGroupField = {
+ type: 'text',
+ field: this.checkboxGroupsMetadata.field,
+ label: this.checkboxGroupsMetadata.label,
+ validators: this.checkboxGroupsMetadata.validators,
+ };
+ temEntities.push(checkboxGroupField);
+ }
}
// Validation of form fields on Submit
const validator = new Validator(temEntities);
let error = validator.doValidation(this.datadict);
+
+ // - If the error pertains to a checkboxGroups component, display the error message at the top of the form.
+ // - For other validation errors, display the error message and highlight the corresponding field in the form.
+ // If no validation error occurs, and a saveValidator is specified in the options:
+ // - Apply the saveValidator to validate the entire dataset before saving.
+ // - If an error occurs, display the associated error message.
if (error) {
- this.setErrorFieldMsg(error.errorField, error.errorMsg);
+ if (error.errorField === this.checkboxGroupsMetadata?.field) {
+ this.setErrorMsg(error.errorMsg);
+ } else {
+ this.setErrorFieldMsg(error.errorField, error.errorMsg);
+ }
} else if (this.options && this.options.saveValidator) {
error = SaveValidator(this.options.saveValidator, this.datadict);
if (error) {
@@ -669,7 +763,7 @@ class BaseFormView extends PureComponent {
});
};
- handleChange = (field, targetValue) => {
+ handleChange = (field, targetValue, componentType = null) => {
const changes = {};
if (field === 'auth_type') {
Object.keys(this.authMap).forEach((type) => {
@@ -713,7 +807,14 @@ class BaseFormView extends PureComponent {
});
}
- changes[field] = { value: { $set: targetValue } };
+ // If the component type is a checkboxGroups component, update the 'checkboxTextFieldValue' field
+ // to reflect the user's typed input, ensuring real-time synchronization with the textbox content.
+ // For other component types, update the 'value' field with the new target value.
+ if (componentType === CHECKBOX_GROUPS) {
+ changes[field] = { checkboxTextFieldValue: { $set: targetValue } };
+ } else {
+ changes[field] = { value: { $set: targetValue } };
+ }
const newFields = update(this.state, { data: changes });
const tempState = this.clearAllErrorMsg(newFields);
@@ -728,6 +829,22 @@ class BaseFormView extends PureComponent {
}
};
+ handleCheckboxToggleAll = (selectAll) => {
+ const allFields = [];
+ const changes = {};
+
+ this.checkboxEntities.forEach((item) => {
+ allFields.push(item.field);
+ });
+
+ allFields.forEach((field) => {
+ changes[field] = { value: { $set: selectAll } };
+ });
+
+ const newFields = update(this.state, { data: changes });
+ this.setState(newFields);
+ };
+
addCustomValidator = (field, validatorFunc) => {
const index = this.entities.findIndex((x) => x.field === field);
const validator = [{ type: 'custom', validatorFunc }];
@@ -969,54 +1086,139 @@ class BaseFormView extends PureComponent {
// eslint-disable-next-line class-methods-use-this
timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // eslint-disable-line no-promise-executor-return
- renderGroupElements = () => {
+ getCheckedCheckboxCount = (fields) => {
+ let count = 0;
+ const entitiesList = this.entities.filter((e) => fields.includes(e.field));
+ entitiesList.forEach((e) => {
+ const temState = this.state.data[e.field];
+ if (isTrue(temState.value)) {
+ count += 1;
+ }
+ });
+ return count;
+ };
+
+ renderCheckboxGroupElements = (group, collpsibleElement) => {
+ const checkboxGroupTitle = (
+