diff --git a/docs/entity/modifyFieldsOnValue.md b/docs/entity/modifyFieldsOnValue.md
index f379f1125..90c8abdb4 100644
--- a/docs/entity/modifyFieldsOnValue.md
+++ b/docs/entity/modifyFieldsOnValue.md
@@ -4,11 +4,11 @@ This feature allows to specify conditions to modify other fields based on curren
### Modification Object Properties
-| Property | Type | Description |
-| --------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------- |
-| fieldValue\* | string | Value that will trigger the update, put `[[any_other_value]]` to trigger update for any other values than specified |
-| mode | string | Mode that adds possibility to use modification only on certain mode |
-| fieldsToModify | array | List of fields modifications that will be applied after com ponent value will match |
+| Property | Type | Description |
+| --------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------- |
+| fieldValue\* | string | Value of current field that will trigger the update. Put `[[any_other_value]]` to make update for any other value than specified. |
+| mode | string | Mode that adds possibility to use modification only on certain mode. One of ( `create` / `edit` / `clone` / `config` ) |
+| fieldsToModify | array | List of fields modifications that will be applied after com ponent value will match. |
### fieldsToModify Properties
diff --git a/splunk_add_on_ucc_framework/global_config_validator.py b/splunk_add_on_ucc_framework/global_config_validator.py
index e50dc987a..b6ca1d87a 100644
--- a/splunk_add_on_ucc_framework/global_config_validator.py
+++ b/splunk_add_on_ucc_framework/global_config_validator.py
@@ -530,7 +530,7 @@ def _validate_groups(self) -> None:
f"Service {service['name']} uses group field {group_field} which is not defined in entity"
)
- def _is_circular(
+ def _is_circular_modification(
self,
mods: List[Any],
visited: Dict[str, str],
@@ -551,61 +551,73 @@ def _is_circular(
# no more dependent modification fields
visited[current_field] = DEAD_END
return visited
- else:
- for influenced_field in current_field_mods["influenced_fields"]:
- if influenced_field not in all_entity_fields:
- raise GlobalConfigValidatorException(
- f"""Modification in field '{current_field}' for not existing field '{influenced_field}'"""
- )
- if influenced_field == current_field:
- raise GlobalConfigValidatorException(
- f"""Field '{current_field}' tries to modify itself"""
- )
+
+ if current_field in current_field_mods["influenced_fields_value_change"]:
+ # field can modify itself except "value" property
+ raise GlobalConfigValidatorException(
+ f"""Field '{current_field}' tries to modify itself value"""
+ )
+
+ for influenced_field in current_field_mods["influenced_fields"]:
+ if influenced_field not in all_entity_fields:
+ raise GlobalConfigValidatorException(
+ f"""Modification in field '{current_field}' for not existing field '{influenced_field}'"""
+ )
+
+ if influenced_field in current_field_mods["influenced_fields_value_change"]:
if visited[influenced_field] == VISITING:
raise GlobalConfigValidatorException(
f"""Circular modifications for field '{influenced_field}' in field '{current_field}'"""
)
- else:
- visited = self._is_circular(
- mods, visited, all_entity_fields, influenced_field
- )
+ # check next influenced by value change field
+ visited = self._is_circular_modification(
+ mods, visited, all_entity_fields, influenced_field
+ )
+
# All dependent modifications fields are dead_end
visited[current_field] = DEAD_END
return visited
- def _check_if_circular(
+ def _check_if_circular_modification(
self,
all_entity_fields: List[Any],
fields_with_mods: List[Any],
modifications: List[Any],
) -> None:
visited = {field: "not_visited" for field in all_entity_fields}
-
for start_field in fields_with_mods:
# DFS algorithm for all fields with modifications
- visited = self._is_circular(
+ visited = self._is_circular_modification(
modifications, visited, all_entity_fields, start_field
)
@staticmethod
def _get_mods_data_for_single_entity(
- fields_with_mods: List[Any],
- all_modifications: List[Any],
entity: Dict[str, Any],
) -> List[Any]:
"""
- Add modification entity data to lists and returns them
+ Get modification entity data as lists
"""
+ entity_modifications = []
if "modifyFieldsOnValue" in entity:
+ influenced_fields_value_change = set()
influenced_fields = set()
- fields_with_mods.append(entity["field"])
for mods in entity["modifyFieldsOnValue"]:
for mod in mods["fieldsToModify"]:
influenced_fields.add(mod["fieldId"])
- all_modifications.append(
- {"fieldId": entity["field"], "influenced_fields": influenced_fields}
+
+ if (
+ mod.get("value") is not None
+ ): # circular deps are not a problem if not about value
+ influenced_fields_value_change.add(mod["fieldId"])
+ entity_modifications.append(
+ {
+ "fieldId": entity["field"],
+ "influenced_fields": influenced_fields,
+ "influenced_fields_value_change": influenced_fields_value_change,
+ }
)
- return [fields_with_mods, all_modifications]
+ return entity_modifications
@staticmethod
def _get_all_entities(
@@ -638,11 +650,13 @@ def _get_all_modification_data(
entities = self._get_all_entities(collections)
for entity in entities:
- self._get_mods_data_for_single_entity(
- fields_with_mods, all_modifications, entity
- )
all_fields.append(entity["field"])
+ if "modifyFieldsOnValue" in entity:
+ fields_with_mods.append(entity["field"])
+ entity_mods = self._get_mods_data_for_single_entity(entity)
+ all_modifications.extend(entity_mods)
+
return [fields_with_mods, all_modifications, all_fields]
def _validate_field_modifications(self) -> None:
@@ -664,7 +678,7 @@ def _validate_field_modifications(self) -> None:
all_fields_config,
) = self._get_all_modification_data(tabs)
- self._check_if_circular(
+ self._check_if_circular_modification(
all_fields_config, fields_with_mods_config, all_modifications_config
)
@@ -678,7 +692,7 @@ def _validate_field_modifications(self) -> None:
all_fields_inputs,
) = self._get_all_modification_data(services)
- self._check_if_circular(
+ self._check_if_circular_modification(
all_fields_inputs, fields_with_mods_inputs, all_modifications_inputs
)
diff --git a/tests/unit/test_global_config_validator.py b/tests/unit/test_global_config_validator.py
index 43dc3b9f2..185dc069e 100644
--- a/tests/unit/test_global_config_validator.py
+++ b/tests/unit/test_global_config_validator.py
@@ -337,7 +337,7 @@ def test_config_validation_modifications_on_change():
[
(
"invalid_config_with_modification_for_field_itself.json",
- "Field 'text1' tries to modify itself",
+ "Field 'text1' tries to modify itself value",
),
(
"invalid_config_with_modification_for_unexisiting_fields.json",
diff --git a/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json b/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json
index 3c8e0406f..e662dea54 100644
--- a/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json
+++ b/tests/unit/testdata/invalid_config_with_modification_circular_modifications.json
@@ -53,7 +53,7 @@
"fieldsToModify": [
{
"fieldId": "text2",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -62,7 +62,7 @@
"fieldsToModify": [
{
"fieldId": "text3",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -79,7 +79,7 @@
"fieldsToModify": [
{
"fieldId": "text3",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -88,7 +88,7 @@
"fieldsToModify": [
{
"fieldId": "text4",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -111,7 +111,7 @@
"fieldsToModify": [
{
"fieldId": "text5",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -120,7 +120,7 @@
"fieldsToModify": [
{
"fieldId": "text5",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -137,7 +137,7 @@
"fieldsToModify": [
{
"fieldId": "text6",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -146,7 +146,7 @@
"fieldsToModify": [
{
"fieldId": "text6",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -163,7 +163,7 @@
"fieldsToModify": [
{
"fieldId": "text7",
- "disabled": false
+ "value": "modification"
}
]
},
@@ -172,7 +172,7 @@
"fieldsToModify": [
{
"fieldId": "text7",
- "disabled": true
+ "value": "modification"
}
]
}
@@ -189,7 +189,7 @@
"fieldsToModify": [
{
"fieldId": "text1",
- "disabled": false
+ "value": "modification"
}
]
}
diff --git a/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json b/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json
index eb7145f94..a87aabccf 100644
--- a/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json
+++ b/tests/unit/testdata/invalid_config_with_modification_for_field_itself.json
@@ -53,7 +53,7 @@
"fieldsToModify": [
{
"fieldId": "text1",
- "disabled": false
+ "value": "value modification"
}
]
}
diff --git a/tests/unit/testdata/valid_config_with_modification_on_value_change.json b/tests/unit/testdata/valid_config_with_modification_on_value_change.json
index f39158c62..13b803d2c 100644
--- a/tests/unit/testdata/valid_config_with_modification_on_value_change.json
+++ b/tests/unit/testdata/valid_config_with_modification_on_value_change.json
@@ -54,6 +54,10 @@
{
"fieldId": "text2",
"disabled": false
+ },
+ {
+ "fieldId": "text1",
+ "label": "change label for itself"
}
]
},
diff --git a/ui/jest.setup.ts b/ui/jest.setup.ts
index 72eba6f54..54ea9a007 100644
--- a/ui/jest.setup.ts
+++ b/ui/jest.setup.ts
@@ -1,8 +1,9 @@
import '@testing-library/jest-dom';
import '@testing-library/jest-dom/jest-globals';
-
import { configure } from '@testing-library/react';
+
import { server } from './src/mocks/server';
+import './src/tests/expectExtenders';
/**
* Configure test attributes
diff --git a/ui/package.json b/ui/package.json
index 536bdd914..28d9f03cd 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -74,6 +74,7 @@
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.14",
+ "@types/jest-image-snapshot": "^6.4.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.17.6",
"@types/react": "^16.14.62",
diff --git a/ui/src/components/AcceptModal/AcceptModal.tsx b/ui/src/components/AcceptModal/AcceptModal.tsx
index 27b5f088e..675f697bd 100644
--- a/ui/src/components/AcceptModal/AcceptModal.tsx
+++ b/ui/src/components/AcceptModal/AcceptModal.tsx
@@ -2,7 +2,8 @@ import React from 'react';
import Modal from '@splunk/react-ui/Modal';
import Message from '@splunk/react-ui/Message';
import styled from 'styled-components';
-import { StyledButton } from '../../pages/EntryPageStyle';
+
+import { UCCButton } from '../UCCButton/UCCButton';
const ModalWrapper = styled(Modal)`
width: 600px;
@@ -30,13 +31,11 @@ function AcceptModal(props: AcceptModalProps) {
- props.handleRequestClose(false)}
label={props.declineBtnLabel || 'Cancel'}
/>
- props.handleRequestClose(true)}
label={props.acceptBtnLabel || 'OK'}
/>
diff --git a/ui/src/components/BaseFormView/BaseFormConfigMock.ts b/ui/src/components/BaseFormView/BaseFormConfigMock.ts
index 7571fbdbb..f5fd0f352 100644
--- a/ui/src/components/BaseFormView/BaseFormConfigMock.ts
+++ b/ui/src/components/BaseFormView/BaseFormConfigMock.ts
@@ -176,6 +176,115 @@ export function getGlobalConfigMockCustomControl() {
return GlobalConfigSchema.parse(globalConfigMockCustomControl);
}
+const getGlobalConfigMockGroups = ({
+ entitiesConfig,
+ entityGroupsConfig,
+ entitiesInputs,
+ entityGroupsInputs,
+}: {
+ entitiesConfig?: z.input[];
+ entityGroupsConfig?: typeof GROUPS_FOR_EXAMPLE_ENTITIES;
+ entitiesInputs?: z.input[];
+ entityGroupsInputs?: typeof GROUPS_FOR_EXAMPLE_ENTITIES;
+}) =>
+ ({
+ pages: {
+ configuration: {
+ tabs: [
+ {
+ name: 'account',
+ table: {
+ actions: ['edit', 'delete', 'clone'],
+ header: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ },
+ entity: [
+ {
+ type: 'text',
+ label: 'Name',
+ validators: [
+ {
+ type: 'regex',
+ errorMsg:
+ 'Account Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
+ pattern: '^[a-zA-Z]\\w*$',
+ },
+ ],
+ field: 'name',
+ help: 'A unique name for the account.',
+ required: true,
+ },
+ ...(entitiesConfig || []),
+ ],
+ groups: entityGroupsConfig,
+ title: 'Accounts',
+ },
+ ],
+ title: 'Configuration',
+ description: 'Set up your add-on',
+ },
+ inputs: {
+ services: [
+ {
+ name: 'demo_input',
+ entity: [
+ {
+ type: 'text',
+ label: 'Name',
+ validators: [
+ {
+ type: 'regex',
+ errorMsg:
+ 'Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
+ pattern: '^[a-zA-Z]\\w*$',
+ },
+ {
+ type: 'string',
+ errorMsg:
+ 'Length of input name should be between 1 and 100',
+ minLength: 1,
+ maxLength: 100,
+ },
+ ],
+ field: 'name',
+ help: 'A unique name for the data input.',
+ required: true,
+ encrypted: false,
+ },
+ ...(entitiesInputs || []),
+ ],
+ groups: entityGroupsInputs,
+ title: 'demo_input',
+ },
+ ],
+ title: 'Inputs',
+ description: 'Manage your data inputs',
+ table: {
+ actions: ['edit', 'delete', 'clone'],
+ header: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ },
+ },
+ },
+ meta: {
+ name: 'demo_addon_for_splunk',
+ restRoot: 'demo_addon_for_splunk',
+ version: '5.31.1R85f0e18e',
+ displayName: 'Demo Add-on for Splunk',
+ schemaVersion: '0.0.3',
+ checkForUpdates: false,
+ searchViewDefault: false,
+ },
+ } satisfies z.input);
+
const EXAMPLE_GROUPS_ENTITIES = [
{
type: 'text',
@@ -238,171 +347,105 @@ const GROUPS_FOR_EXAMPLE_ENTITIES = [
},
];
-const globalConfigMockGroupsForConfigPage = {
- pages: {
- configuration: {
- tabs: [
- {
- name: 'account',
- table: {
- actions: ['edit', 'delete', 'clone'],
- header: [
- {
- label: 'Name',
- field: 'name',
- },
- ],
- },
- entity: [
- {
- type: 'text',
- label: 'Name',
- validators: [
- {
- type: 'regex',
- errorMsg:
- 'Account Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
- pattern: '^[a-zA-Z]\\w*$',
- },
- ],
- field: 'name',
- help: 'A unique name for the account.',
- required: true,
- },
- ...EXAMPLE_GROUPS_ENTITIES,
- ],
- groups: GROUPS_FOR_EXAMPLE_ENTITIES,
- title: 'Accounts',
- },
- ],
- title: 'Configuration',
- description: 'Set up your add-on',
- },
- inputs: {
- services: [],
- title: 'Inputs',
- description: 'Manage your data inputs',
- table: {
- actions: ['edit', 'delete', 'clone'],
- header: [
- {
- label: 'Name',
- field: 'name',
- },
- ],
- },
- },
- },
- meta: {
- name: 'demo_addon_for_splunk',
- restRoot: 'demo_addon_for_splunk',
- version: '5.31.1R85f0e18e',
- displayName: 'Demo Add-on for Splunk',
- schemaVersion: '0.0.3',
- checkForUpdates: false,
- searchViewDefault: false,
- },
-} satisfies z.input;
-
export function getGlobalConfigMockGroupsForConfigPage(): GlobalConfig {
- return GlobalConfigSchema.parse(globalConfigMockGroupsForConfigPage);
+ return GlobalConfigSchema.parse(
+ getGlobalConfigMockGroups({
+ entitiesConfig: EXAMPLE_GROUPS_ENTITIES,
+ entityGroupsConfig: GROUPS_FOR_EXAMPLE_ENTITIES,
+ })
+ );
}
-const globalConfigMockGroupsForInputPage = {
- pages: {
- configuration: {
- tabs: [
- {
- name: 'account',
- table: {
- actions: ['edit', 'delete', 'clone'],
- header: [
- {
- label: 'Name',
- field: 'name',
- },
- ],
- },
- entity: [
- {
- type: 'text',
- label: 'Name',
- validators: [
- {
- type: 'regex',
- errorMsg:
- 'Account Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
- pattern: '^[a-zA-Z]\\w*$',
- },
- ],
- field: 'name',
- help: 'A unique name for the account.',
- required: true,
+export function getGlobalConfigMockGroupsForInputPage(): GlobalConfig {
+ return GlobalConfigSchema.parse(
+ getGlobalConfigMockGroups({
+ entitiesInputs: EXAMPLE_GROUPS_ENTITIES,
+ entityGroupsInputs: GROUPS_FOR_EXAMPLE_ENTITIES,
+ })
+ );
+}
+
+const GROUP_ENTITIES_MODIFICATIONS = [
+ {
+ type: 'text',
+ label: 'Text 1 Group 2',
+ field: 'text_field_1_group_2',
+ required: false,
+ modifyFieldsOnValue: [
+ {
+ fieldValue: '[[any_other_value]]',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_2_group_2',
+ disabled: false,
+ required: false,
+ help: 'help after mods 2-2',
+ label: 'label after mods 2-2',
+ markdownMessage: {
+ text: 'markdown message after mods 2-2',
},
- ],
- title: 'Accounts',
- },
- ],
- title: 'Configuration',
- description: 'Set up your add-on',
- },
- inputs: {
- services: [
- {
- name: 'demo_input',
- entity: [
- {
- type: 'text',
- label: 'Name',
- validators: [
- {
- type: 'regex',
- errorMsg:
- 'Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
- pattern: '^[a-zA-Z]\\w*$',
- },
- {
- type: 'string',
- errorMsg: 'Length of input name should be between 1 and 100',
- minLength: 1,
- maxLength: 100,
- },
- ],
- field: 'name',
- help: 'A unique name for the data input.',
- required: true,
- encrypted: false,
+ },
+ {
+ fieldId: 'text_field_2_group_1',
+ disabled: false,
+ required: true,
+ help: 'help after mods 2-1',
+ label: 'label after mods 2-1',
+ markdownMessage: {
+ text: 'markdown message after mods 2-1',
},
- ...EXAMPLE_GROUPS_ENTITIES,
- ],
- groups: GROUPS_FOR_EXAMPLE_ENTITIES,
- title: 'demo_input',
- },
- ],
- title: 'Inputs',
- description: 'Manage your data inputs',
- table: {
- actions: ['edit', 'delete', 'clone'],
- header: [
+ },
{
- label: 'Name',
- field: 'name',
+ fieldId: 'text_field_1_group_1',
+ disabled: true,
},
],
},
+ ],
+ },
+ {
+ type: 'text',
+ label: 'Text 2 Group 2',
+ field: 'text_field_2_group_2',
+ required: false,
+ },
+ {
+ type: 'text',
+ label: 'Text 1 Group 1',
+ field: 'text_field_1_group_1',
+ required: false,
+ },
+ {
+ type: 'text',
+ label: 'Text 2 Group 1',
+ field: 'text_field_2_group_1',
+ required: false,
+ options: {
+ enable: false,
},
},
- meta: {
- name: 'demo_addon_for_splunk',
- restRoot: 'demo_addon_for_splunk',
- version: '5.31.1R85f0e18e',
- displayName: 'Demo Add-on for Splunk',
- schemaVersion: '0.0.3',
- checkForUpdates: false,
- searchViewDefault: false,
+ {
+ type: 'text',
+ label: 'Text 1 Group 3',
+ field: 'text_field_1_group_3',
+ required: false,
+ options: {
+ enable: false,
+ },
},
-} satisfies z.input;
+ {
+ type: 'text',
+ label: 'Text 2 Group 3',
+ field: 'text_field_2_group_3',
+ required: false,
+ },
+] satisfies z.input[];
-export function getGlobalConfigMockGroupsFoInputPage(): GlobalConfig {
- return GlobalConfigSchema.parse(globalConfigMockGroupsForInputPage);
+export function getGlobalConfigMockModificationToGroupsConfig(): GlobalConfig {
+ return GlobalConfigSchema.parse(
+ getGlobalConfigMockGroups({
+ entitiesConfig: GROUP_ENTITIES_MODIFICATIONS,
+ entityGroupsConfig: GROUPS_FOR_EXAMPLE_ENTITIES,
+ })
+ );
}
diff --git a/ui/src/components/BaseFormView/BaseFormView.tsx b/ui/src/components/BaseFormView/BaseFormView.tsx
index b5189c01e..d74fd788e 100644
--- a/ui/src/components/BaseFormView/BaseFormView.tsx
+++ b/ui/src/components/BaseFormView/BaseFormView.tsx
@@ -1259,6 +1259,7 @@ class BaseFormView extends PureComponent {
this.entities?.map((e) => {
if (e.field === fieldName) {
const temState = this.state?.data?.[e.field];
+
return (
{
markdownMessage={temState?.markdownMessage}
dependencyValues={temState?.dependencyValues || null}
page={this.props.page}
+ fileNameToDisplay={temState.fileNameToDisplay}
+ modifiedEntitiesData={temState.modifiedEntitiesData}
/>
);
}
diff --git a/ui/src/components/BaseFormView/BaseFormViewGrups.test.tsx b/ui/src/components/BaseFormView/BaseFormViewGrups.test.tsx
new file mode 100644
index 000000000..bd7d0bea6
--- /dev/null
+++ b/ui/src/components/BaseFormView/BaseFormViewGrups.test.tsx
@@ -0,0 +1,66 @@
+import { render, screen, within } from '@testing-library/react';
+import React from 'react';
+
+import { setUnifiedConfig } from '../../util/util';
+import BaseFormView from './BaseFormView';
+import { getGlobalConfigMockModificationToGroupsConfig } from './BaseFormConfigMock';
+
+const handleFormSubmit = jest.fn();
+
+const PAGE_CONF = 'configuration';
+const SERVICE_NAME = 'account';
+const STANZA_NAME = 'stanzaName';
+
+it('should modify correctly all properties of field in groups', async () => {
+ const mockConfig = getGlobalConfigMockModificationToGroupsConfig();
+ setUnifiedConfig(mockConfig);
+ render(
+
+ );
+ await screen.findByText('Text 1 Group 2');
+
+ const getAndValidateGroupFieldLabels = (
+ fieldId: string,
+ label: string,
+ help: string,
+ markdownMsg: string
+ ) => {
+ const modifiedFieldSameGroup = document.querySelector(
+ `[data-name="${fieldId}"]`
+ ) as HTMLElement;
+
+ expect(modifiedFieldSameGroup).toBeInTheDocument();
+
+ expect(within(modifiedFieldSameGroup).getByTestId('help')).toHaveTextContent(label);
+ expect(within(modifiedFieldSameGroup).getByTestId('label')).toHaveTextContent(help);
+ expect(within(modifiedFieldSameGroup).getByTestId('msg-markdown')).toHaveTextContent(
+ markdownMsg
+ );
+ return modifiedFieldSameGroup;
+ };
+
+ const modifiedFieldSameGroup = getAndValidateGroupFieldLabels(
+ 'text_field_2_group_1',
+ 'help after mods 2-1',
+ 'label after mods 2-1',
+ 'markdown message after mods 2-1'
+ );
+
+ expect(within(modifiedFieldSameGroup).queryByText('*')).toBeInTheDocument();
+
+ const modifiedFieldDiffGroup = getAndValidateGroupFieldLabels(
+ 'text_field_2_group_2',
+ 'help after mods 2-2',
+ 'label after mods 2-2',
+ 'markdown message after mods 2-2'
+ );
+
+ expect(within(modifiedFieldDiffGroup).queryByText('*')).not.toBeInTheDocument();
+});
diff --git a/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx b/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx
index 5fdb655cb..b49967885 100644
--- a/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx
+++ b/ui/src/components/BaseFormView/stories/BaseFormView.stories.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
-import { fn } from '@storybook/test';
+import { fn, userEvent, within } from '@storybook/test';
+
import BaseFormView from '../BaseFormView';
import {
PAGE_CONFIG_BOTH_OAUTH,
@@ -13,9 +14,12 @@ import { Mode } from '../../../constants/modes';
import { BaseFormProps } from '../../../types/components/BaseFormTypes';
import { Platforms } from '../../../types/globalConfig/pages';
import {
- getGlobalConfigMockGroupsFoInputPage,
+ getGlobalConfigMockGroupsForInputPage,
getGlobalConfigMockGroupsForConfigPage,
+ getGlobalConfigMockModificationToGroupsConfig,
} from '../BaseFormConfigMock';
+import { getGlobalConfigMockModificationToFieldItself } from '../tests/configMocks';
+import { invariant } from '../../../util/invariant';
interface BaseFormStoriesProps extends BaseFormProps {
config: GlobalConfig;
@@ -125,7 +129,48 @@ export const InputPageGroups: Story = {
page: 'inputs',
stanzaName: 'unknownStanza',
handleFormSubmit: fn(),
- config: getGlobalConfigMockGroupsFoInputPage(),
+ config: getGlobalConfigMockGroupsForInputPage(),
+ platform: 'cloud',
+ },
+};
+
+export const GroupModificationsConfig: Story = {
+ args: {
+ currentServiceState: {},
+ serviceName: 'account',
+ mode: 'create' as Mode,
+ page: 'configuration',
+ stanzaName: 'unknownStanza',
+ handleFormSubmit: fn(),
+ config: getGlobalConfigMockModificationToGroupsConfig(),
platform: 'cloud',
},
};
+
+export const FieldModifyItself: Story = {
+ args: {
+ currentServiceState: {},
+ serviceName: 'account',
+ mode: 'create' as Mode,
+ page: 'configuration',
+ stanzaName: 'unknownStanza',
+ handleFormSubmit: fn(),
+ config: getGlobalConfigMockModificationToFieldItself(),
+ },
+};
+
+export const FieldModifyItselfAfterMods: Story = {
+ args: FieldModifyItself.args,
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ const modifyInputText = canvas
+ .getAllByRole('textbox')
+ .find((el) => el.getAttribute('value') === 'default value');
+
+ invariant(modifyInputText, 'modification input field should be defined');
+
+ await userEvent.clear(modifyInputText);
+ await userEvent.type(modifyInputText, 'modify itself');
+ },
+};
diff --git a/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-after-mods-chromium.png b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-after-mods-chromium.png
new file mode 100644
index 000000000..3b7febda1
--- /dev/null
+++ b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-after-mods-chromium.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7bb703699c8c301f552a38459fb9b237d1f7a4281ce011d3e3fa1ce4592917c0
+size 22951
diff --git a/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-chromium.png b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-chromium.png
new file mode 100644
index 000000000..1a20c14c2
--- /dev/null
+++ b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-field-modify-itself-chromium.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8f806fad0fff2f0417063c3d1638107f87f3edbcc373218c37ee413df522109
+size 19293
diff --git a/ui/src/components/BaseFormView/stories/__images__/BaseFormView-group-modifications-config-chromium.png b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-group-modifications-config-chromium.png
new file mode 100644
index 000000000..caeca1a1a
--- /dev/null
+++ b/ui/src/components/BaseFormView/stories/__images__/BaseFormView-group-modifications-config-chromium.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ba49279f39361f1c139fa1d0f041317a23bda3fb2199ba343d61c3f0393267c1
+size 29733
diff --git a/ui/src/components/BaseFormView/BaseFormView.test.tsx b/ui/src/components/BaseFormView/tests/BaseFormView.test.tsx
similarity index 90%
rename from ui/src/components/BaseFormView/BaseFormView.test.tsx
rename to ui/src/components/BaseFormView/tests/BaseFormView.test.tsx
index ac05b2ec3..63b31af09 100644
--- a/ui/src/components/BaseFormView/BaseFormView.test.tsx
+++ b/ui/src/components/BaseFormView/tests/BaseFormView.test.tsx
@@ -2,16 +2,16 @@ import { render, screen } from '@testing-library/react';
import React from 'react';
import userEvent from '@testing-library/user-event';
-import { getGlobalConfigMock } from '../../mocks/globalConfigMock';
-import { setUnifiedConfig } from '../../util/util';
-import BaseFormView from './BaseFormView';
-import { getBuildDirPath } from '../../util/script';
-import mockCustomControlMockForTest from '../CustomControl/CustomControlMockForTest';
+import { getGlobalConfigMock } from '../../../mocks/globalConfigMock';
+import { getBuildDirPath } from '../../../util/script';
+import { setUnifiedConfig } from '../../../util/util';
import {
getGlobalConfigMockCustomControl,
- getGlobalConfigMockGroupsFoInputPage,
+ getGlobalConfigMockGroupsForInputPage,
getGlobalConfigMockGroupsForConfigPage,
-} from './BaseFormConfigMock';
+} from '../BaseFormConfigMock';
+import mockCustomControlMockForTest from '../../CustomControl/CustomControlMockForTest';
+import BaseFormView from '../BaseFormView';
const handleFormSubmit = jest.fn();
@@ -97,7 +97,7 @@ it.each([
},
{
page: 'inputs' as const,
- config: getGlobalConfigMockGroupsFoInputPage(),
+ config: getGlobalConfigMockGroupsForInputPage(),
service: 'demo_input',
},
])('entities grouping for page works properly %s', async ({ config, page, service }) => {
diff --git a/ui/src/components/BaseFormView/tests/BaseFormViewModifications.test.tsx b/ui/src/components/BaseFormView/tests/BaseFormViewModifications.test.tsx
new file mode 100644
index 000000000..bdbcb4800
--- /dev/null
+++ b/ui/src/components/BaseFormView/tests/BaseFormViewModifications.test.tsx
@@ -0,0 +1,62 @@
+import { render, screen, within } from '@testing-library/react';
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+
+import { setUnifiedConfig } from '../../../util/util';
+import BaseFormView from '../BaseFormView';
+import { getGlobalConfigMockModificationToFieldItself } from './configMocks';
+import { invariant } from '../../../util/invariant';
+
+const handleFormSubmit = jest.fn();
+
+const PAGE_CONF = 'configuration';
+const SERVICE_NAME = 'account';
+const STANZA_NAME = 'stanzaName';
+
+it('should modify correctly all properties of field, self modification', async () => {
+ const mockConfig = getGlobalConfigMockModificationToFieldItself();
+ setUnifiedConfig(mockConfig);
+
+ render(
+
+ );
+
+ await screen.findByText('default label');
+
+ const modifyTextField = document.querySelector(
+ '[data-name="text_field_with_modifications"]'
+ ) as HTMLElement;
+
+ expect(modifyTextField).toBeInTheDocument();
+
+ invariant(modifyTextField, 'modification field should be defined');
+
+ expect(within(modifyTextField).getByTestId('help')).toHaveTextContent('default help');
+ expect(within(modifyTextField).getByTestId('label')).toHaveTextContent('default label');
+ expect(within(modifyTextField).getByTestId('msg-markdown')).toHaveTextContent(
+ 'default markdown message'
+ );
+ expect(within(modifyTextField).queryByText('*')).not.toBeInTheDocument();
+
+ const inputComponent = within(modifyTextField).getByRole('textbox');
+ await userEvent.clear(inputComponent);
+ await userEvent.type(inputComponent, 'modify itself');
+
+ expect(within(modifyTextField).getByTestId('help')).toHaveTextContent(
+ 'help after modification'
+ );
+ expect(within(modifyTextField).getByTestId('label')).toHaveTextContent(
+ 'label after modification'
+ );
+ expect(within(modifyTextField).getByTestId('msg-markdown')).toHaveTextContent(
+ 'markdown message after modification'
+ );
+ expect(within(modifyTextField).queryByText('*')).toBeInTheDocument();
+});
diff --git a/ui/src/components/BaseFormView/tests/configMocks.ts b/ui/src/components/BaseFormView/tests/configMocks.ts
new file mode 100644
index 000000000..c6c338e99
--- /dev/null
+++ b/ui/src/components/BaseFormView/tests/configMocks.ts
@@ -0,0 +1,169 @@
+import { z } from 'zod';
+import { GlobalConfigSchema } from '../../../types/globalConfig/globalConfig';
+
+const CONFIG_MOCK_MODIFICATION_ON_VALUE_CHANGE_CONFIG = {
+ pages: {
+ configuration: {
+ tabs: [
+ {
+ name: 'account',
+ table: {
+ actions: ['edit', 'delete', 'clone'],
+ header: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ },
+ entity: [
+ {
+ type: 'text',
+ label: 'Name',
+ validators: [
+ {
+ type: 'regex',
+ errorMsg:
+ 'Account Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
+ pattern: '^[a-zA-Z]\\w*$',
+ },
+ {
+ type: 'string',
+ errorMsg: 'Length of input name should be between 1 and 100',
+ minLength: 1,
+ maxLength: 100,
+ },
+ ],
+ field: 'name',
+ help: 'A unique name for the account.',
+ required: true,
+ },
+ {
+ type: 'text',
+ label: 'Example text field',
+ field: 'text_field_with_modifications',
+ help: 'Example text field with modification',
+ required: false,
+ defaultValue: 'default value',
+ modifyFieldsOnValue: [
+ {
+ fieldValue: 'default value',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_with_modifications',
+ disabled: false,
+ required: false,
+ help: 'default help',
+ label: 'default label',
+ markdownMessage: {
+ text: 'default markdown message',
+ },
+ },
+ ],
+ },
+ {
+ fieldValue: 'modify itself',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_with_modifications',
+ disabled: false,
+ required: true,
+ help: 'help after modification',
+ label: 'label after modification',
+ markdownMessage: {
+ text: 'markdown message after modification',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'text',
+ label: 'Example text field to be modified',
+ field: 'text_field_to_be_modified',
+ help: 'Example text field to be modified',
+ required: false,
+ modifyFieldsOnValue: [
+ {
+ fieldValue: '[[any_other_value]]',
+ fieldsToModify: [
+ {
+ fieldId: 'text_field_to_be_modified',
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ title: 'Accounts',
+ },
+ ],
+ title: 'Configuration',
+ description: 'Set up your add-on',
+ },
+ inputs: {
+ services: [
+ {
+ name: 'demo_input',
+ entity: [
+ {
+ type: 'text',
+ label: 'Name',
+ validators: [
+ {
+ type: 'regex',
+ errorMsg:
+ 'Input Name must begin with a letter and consist exclusively of alphanumeric characters and underscores.',
+ pattern: '^[a-zA-Z]\\w*$',
+ },
+ {
+ type: 'string',
+ errorMsg: 'Length of input name should be between 1 and 100',
+ minLength: 1,
+ maxLength: 100,
+ },
+ ],
+ field: 'name',
+ help: 'A unique name for the data input.',
+ required: true,
+ encrypted: false,
+ },
+ ],
+ title: 'demo_input',
+ },
+ ],
+ title: 'Inputs',
+ description: 'Manage your data inputs',
+ table: {
+ actions: ['edit', 'delete', 'clone'],
+ header: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ moreInfo: [
+ {
+ label: 'Name',
+ field: 'name',
+ },
+ ],
+ },
+ },
+ },
+ meta: {
+ name: 'demo_addon_for_splunk',
+ restRoot: 'demo_addon_for_splunk',
+ version: '5.31.1R85f0e18e',
+ displayName: 'Demo Add-on for Splunk',
+ schemaVersion: '0.0.3',
+ checkForUpdates: false,
+ searchViewDefault: false,
+ },
+} satisfies z.input;
+
+export function getGlobalConfigMockModificationToFieldItself() {
+ return GlobalConfigSchema.parse(CONFIG_MOCK_MODIFICATION_ON_VALUE_CHANGE_CONFIG);
+}
diff --git a/ui/src/components/ConfigurationFormView.jsx b/ui/src/components/ConfigurationFormView.jsx
index 8bab71111..99667e260 100644
--- a/ui/src/components/ConfigurationFormView.jsx
+++ b/ui/src/components/ConfigurationFormView.jsx
@@ -3,10 +3,9 @@ import PropTypes from 'prop-types';
import { _ } from '@splunk/ui-utils/i18n';
import styled from 'styled-components';
-import WaitSpinner from '@splunk/react-ui/WaitSpinner';
import BaseFormView from './BaseFormView/BaseFormView';
-import { StyledButton } from '../pages/EntryPageStyle';
+import { UCCButton } from './UCCButton/UCCButton';
import { getRequest, generateEndPointUrl } from '../util/api';
import { MODE_CONFIG } from '../constants/modes';
import { WaitSpinnerWrapper } from './table/CustomTableStyle';
@@ -88,12 +87,11 @@ function ConfigurationFormView({ serviceName }) {
)}
- : _('Save')}
+ label={_('Save')}
onClick={handleSubmit}
- disabled={isSubmitting}
+ loading={isSubmitting}
/>
>
diff --git a/ui/src/components/DashboardInfoModal/DashboardInfoModal.tsx b/ui/src/components/DashboardInfoModal/DashboardInfoModal.tsx
index 7745c79d6..ad0309033 100644
--- a/ui/src/components/DashboardInfoModal/DashboardInfoModal.tsx
+++ b/ui/src/components/DashboardInfoModal/DashboardInfoModal.tsx
@@ -6,7 +6,7 @@ import Heading from '@splunk/react-ui/Heading';
import P from '@splunk/react-ui/Paragraph';
import QuestionCircle from '@splunk/react-icons/QuestionCircle';
-import { StyledButton } from '../../pages/EntryPageStyle';
+import { UCCButton } from '../UCCButton/UCCButton';
const ModalWrapper = styled(Modal)`
width: 700px;
@@ -48,15 +48,14 @@ function DashboardInfoModal(props: DashboardInfoModalProps) {
{props?.troubleshootingButton?.link ? ( // to do change it into troubleshooting link
- }
to={props?.troubleshootingButton?.link}
label={props.troubleshootingButton?.label || 'Troubleshooting {add-on}'}
openInNewContext
/>
) : null}
- props.handleRequestClose()}
label={props.closeBtnLabel || 'Close'}
/>
diff --git a/ui/src/components/DeleteModal/DeleteModal.tsx b/ui/src/components/DeleteModal/DeleteModal.tsx
index d989d1ac2..c9487ecf0 100644
--- a/ui/src/components/DeleteModal/DeleteModal.tsx
+++ b/ui/src/components/DeleteModal/DeleteModal.tsx
@@ -2,17 +2,16 @@ import React, { Component } from 'react';
import Modal from '@splunk/react-ui/Modal';
import Message from '@splunk/react-ui/Message';
import styled from 'styled-components';
-import WaitSpinner from '@splunk/react-ui/WaitSpinner';
import update from 'immutability-helper';
import { _ } from '@splunk/ui-utils/i18n';
-import { generateToast } from '../../util/util';
-import { StyledButton } from '../../pages/EntryPageStyle';
+import { generateToast } from '../../util/util';
import { deleteRequest, generateEndPointUrl } from '../../util/api';
import TableContext from '../../context/TableContext';
import { parseErrorMsg, getFormattedMessage } from '../../util/messageUtil';
import { PAGE_INPUT } from '../../constants/pages';
import { StandardPages } from '../../types/components/shareableTypes';
+import { UCCButton } from '../UCCButton/UCCButton';
const ModalWrapper = styled(Modal)`
width: 800px;
@@ -109,17 +108,16 @@ class DeleteModal extends Component {
{deleteMsg}
-
- : _('Delete')}
+
diff --git a/ui/src/components/EntityModal/EntityModal.test.tsx b/ui/src/components/EntityModal/EntityModal.test.tsx
index c18ddf671..a43d8a556 100644
--- a/ui/src/components/EntityModal/EntityModal.test.tsx
+++ b/ui/src/components/EntityModal/EntityModal.test.tsx
@@ -62,7 +62,7 @@ describe('Oauth field disabled on edit - diableonEdit property', () => {
renderModalWithProps(props);
const oauthTextBox = getDisabledOauthField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).not.toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyEnabled();
});
it('Oauth Oauth - disableonEdit = true, oauth field disabled on edit', async () => {
@@ -82,7 +82,7 @@ describe('Oauth field disabled on edit - diableonEdit property', () => {
const oauthTextBox = getDisabledOauthField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyDisabled();
});
it('Oauth Basic - Enable field equal false, so field disabled', async () => {
@@ -102,7 +102,7 @@ describe('Oauth field disabled on edit - diableonEdit property', () => {
const oauthTextBox = getDisabledBasicField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyDisabled();
});
it('if oauth field not disabled with create after disableonEdit true', async () => {
@@ -120,7 +120,7 @@ describe('Oauth field disabled on edit - diableonEdit property', () => {
renderModalWithProps(props);
const oauthTextBox = getDisabledBasicField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).not.toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyEnabled();
});
});
@@ -163,7 +163,7 @@ describe('Options - Enable field property', () => {
renderModalWithProps(props);
const oauthTextBox = getDisabledOauthField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyDisabled();
});
it('Oauth Basic - Enable field equal false, so field disabled', async () => {
@@ -181,7 +181,7 @@ describe('Options - Enable field property', () => {
renderModalWithProps(props);
const oauthTextBox = getDisabledOauthField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyDisabled();
});
it('Oauth Basic - Fully enabled field, enabled: true, disableonEdit: false', async () => {
@@ -199,7 +199,7 @@ describe('Options - Enable field property', () => {
renderModalWithProps(props);
const oauthTextBox = getDisabledOauthField();
expect(oauthTextBox).toBeInTheDocument();
- expect(oauthTextBox).not.toHaveAttribute('disabled');
+ expect(oauthTextBox).toBeVisuallyEnabled();
});
});
diff --git a/ui/src/components/EntityModal/EntityModal.tsx b/ui/src/components/EntityModal/EntityModal.tsx
index dabe627e8..d7447b19d 100644
--- a/ui/src/components/EntityModal/EntityModal.tsx
+++ b/ui/src/components/EntityModal/EntityModal.tsx
@@ -1,15 +1,14 @@
-import React, { Component, ReactElement } from 'react';
+import React, { Component } from 'react';
import Modal from '@splunk/react-ui/Modal';
import styled from 'styled-components';
-import WaitSpinner from '@splunk/react-ui/WaitSpinner';
import { _ } from '@splunk/ui-utils/i18n';
-
import { ButtonClickHandler } from '@splunk/react-ui/Button';
+
import { Mode, MODE_CLONE, MODE_CREATE, MODE_EDIT } from '../../constants/modes';
-import { StyledButton } from '../../pages/EntryPageStyle';
import BaseFormView from '../BaseFormView/BaseFormView';
import { StandardPages } from '../../types/components/shareableTypes';
import PageContext from '../../context/PageContext';
+import { UCCButton } from '../UCCButton/UCCButton';
const ModalWrapper = styled(Modal)`
width: 800px;
@@ -33,7 +32,7 @@ interface EntityModalState {
class EntityModal extends Component {
form: React.RefObject;
- buttonText: string | ReactElement;
+ buttonText: string;
constructor(props: EntityModalProps) {
super(props);
@@ -98,18 +97,17 @@ class EntityModal extends Component {
-
- : this.buttonText}
+ label={this.buttonText}
+ loading={this.state.isSubmititng}
onClick={this.handleSubmit}
- disabled={this.state.isSubmititng}
/>
diff --git a/ui/src/components/EntityPage/EntityPage.tsx b/ui/src/components/EntityPage/EntityPage.tsx
index 181d6b0fd..dff3b1cd3 100644
--- a/ui/src/components/EntityPage/EntityPage.tsx
+++ b/ui/src/components/EntityPage/EntityPage.tsx
@@ -1,7 +1,6 @@
import React, { memo, useRef, useState } from 'react';
import Link from '@splunk/react-ui/Link';
-import WaitSpinner from '@splunk/react-ui/WaitSpinner';
import ColumnLayout from '@splunk/react-ui/ColumnLayout';
import { _ } from '@splunk/ui-utils/i18n';
import { variables } from '@splunk/themes';
@@ -9,13 +8,14 @@ import { variables } from '@splunk/themes';
import Heading from '@splunk/react-ui/Heading';
import styled from 'styled-components';
import { ButtonClickHandler } from '@splunk/react-ui/Button';
+
import { MODE_CLONE, MODE_CREATE, MODE_EDIT, Mode } from '../../constants/modes';
import BaseFormView from '../BaseFormView/BaseFormView';
import { SubTitleComponent } from '../../pages/Input/InputPageStyle';
import { PAGE_INPUT } from '../../constants/pages';
-import { StyledButton } from '../../pages/EntryPageStyle';
import { StandardPages } from '../../types/components/shareableTypes';
import PageContext from '../../context/PageContext';
+import { UCCButton } from '../UCCButton/UCCButton';
interface EntityPageProps {
handleRequestClose: () => void;
@@ -108,19 +108,18 @@ function EntityPage({
-
- : buttonText}
+ label={buttonText}
onClick={handleSubmit}
- disabled={isSubmitting}
+ loading={isSubmitting}
style={{ width: '80px' }}
/>
diff --git a/ui/src/components/ErrorModal/ErrorModal.tsx b/ui/src/components/ErrorModal/ErrorModal.tsx
index 6153609f9..e41fe2260 100644
--- a/ui/src/components/ErrorModal/ErrorModal.tsx
+++ b/ui/src/components/ErrorModal/ErrorModal.tsx
@@ -4,7 +4,7 @@ import Message from '@splunk/react-ui/Message';
import styled from 'styled-components';
import { getFormattedMessage } from '../../util/messageUtil';
-import { StyledButton } from '../../pages/EntryPageStyle';
+import { UCCButton } from '../UCCButton/UCCButton';
const ModalWrapper = styled(Modal)`
width: 600px;
@@ -31,7 +31,7 @@ function ErrorModal(props: ErrorModalProps) {
-
+
);
diff --git a/ui/src/components/FormModifications/FormModifications.test.tsx b/ui/src/components/FormModifications/FormModifications.test.tsx
index 5a0f21d12..5e139b23e 100644
--- a/ui/src/components/FormModifications/FormModifications.test.tsx
+++ b/ui/src/components/FormModifications/FormModifications.test.tsx
@@ -127,18 +127,22 @@ it('verify modification after text components change', async () => {
expect(parentElement).toHaveTextContent(mods.label);
};
- expect(componentInput).toBeDisabled();
+ expect(componentInput).toBeVisuallyDisabled();
+
verifyAllProps(componentParentElement, componentInput, mods1Field1);
- expect(component2Input).toBeDisabled();
+ expect(component2Input).toBeVisuallyDisabled();
+
verifyAllProps(component2ParentElement, component2Input, mods1Field2);
await userEvent.type(componentMakingModsTextBox1, secondValueToInput);
- expect(componentInput).toBeEnabled();
+ expect(component2Input).toBeVisuallyEnabled();
+
verifyAllProps(componentParentElement, componentInput, mods2Field1);
- expect(component2Input).toBeEnabled();
+ expect(component2Input).toBeVisuallyEnabled();
+
verifyAllProps(component2ParentElement, component2Input, mods2Field2);
});
diff --git a/ui/src/components/MenuInput/MenuInput.tsx b/ui/src/components/MenuInput/MenuInput.tsx
index 430cd5665..2d6b0442d 100644
--- a/ui/src/components/MenuInput/MenuInput.tsx
+++ b/ui/src/components/MenuInput/MenuInput.tsx
@@ -8,13 +8,14 @@ import ChevronLeft from '@splunk/react-icons/ChevronLeft';
import { _ as i18n } from '@splunk/ui-utils/i18n';
import styled from 'styled-components';
import { variables } from '@splunk/themes';
+
import { getFormattedMessage } from '../../util/messageUtil';
import { getUnifiedConfigs } from '../../util/util';
import CustomMenu from '../CustomMenu';
-import { StyledButton } from '../../pages/EntryPageStyle';
import { invariant } from '../../util/invariant';
import { usePageContext } from '../../context/usePageContext';
import { shouldHideForPlatform } from '../../util/pageContext';
+import { UCCButton } from '../UCCButton/UCCButton';
const CustomSubTitle = styled.span`
color: ${variables.brandColorD20};
@@ -67,14 +68,7 @@ function MenuInput({ handleRequestOpen }: MenuInputProps) {
}, [inputs.services, pageContext.platform]);
const closeReasons = ['clickAway', 'escapeKey', 'offScreen', 'toggleClick'];
- const toggle = (
-
- );
+ const toggle = ;
useEffect(() => {
if (!isSubMenu) {
@@ -211,9 +205,8 @@ function MenuInput({ handleRequestOpen }: MenuInputProps) {
// Making a dropdown if we have one service
const makeInputButton = () => (
- {
handleRequestOpen({ serviceName: services[0].name });
diff --git a/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx b/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx
index 30958e470..fed1af8a3 100644
--- a/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx
+++ b/ui/src/components/MultiInputComponent/MultiInputComponent.test.tsx
@@ -76,7 +76,7 @@ it('renders as disabled correctly', () => {
renderFeature({ disabled: true });
const inputComponent = screen.getByTestId('multiselect');
expect(inputComponent).toBeInTheDocument();
- expect(inputComponent.getAttribute('aria-disabled')).toEqual('true');
+ expect(inputComponent).toHaveAttribute('aria-disabled', 'true');
});
it.each(defaultInputProps.controlOptions.items)('handler called correctly', async (item) => {
diff --git a/ui/src/components/TextComponent/TextComponent.tsx b/ui/src/components/TextComponent/TextComponent.tsx
index ede7349ce..70e63e8f0 100755
--- a/ui/src/components/TextComponent/TextComponent.tsx
+++ b/ui/src/components/TextComponent/TextComponent.tsx
@@ -29,7 +29,7 @@ class TextComponent extends Component {
inline
error={this.props.error}
className={this.props.field}
- disabled={this.props.disabled}
+ disabled={this.props.disabled && 'dimmed'}
value={
this.props.value === null || typeof this.props.value === 'undefined'
? ''
diff --git a/ui/src/components/TextComponent/stories/TextComponent.stories.tsx b/ui/src/components/TextComponent/stories/TextComponent.stories.tsx
index c3eb48128..f6331a5bf 100644
--- a/ui/src/components/TextComponent/stories/TextComponent.stories.tsx
+++ b/ui/src/components/TextComponent/stories/TextComponent.stories.tsx
@@ -38,3 +38,14 @@ export const Base: Story = {
disabled: false,
},
};
+
+export const AllPropsTrue: Story = {
+ args: {
+ handleChange: fn(),
+ value: 'default value',
+ field: 'field',
+ error: true,
+ encrypted: true,
+ disabled: true,
+ },
+};
diff --git a/ui/src/components/TextComponent/stories/__images__/TextComponent-all-props-true-chromium.png b/ui/src/components/TextComponent/stories/__images__/TextComponent-all-props-true-chromium.png
new file mode 100644
index 000000000..3daaf24fb
--- /dev/null
+++ b/ui/src/components/TextComponent/stories/__images__/TextComponent-all-props-true-chromium.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d3f3fdd40742f1150cc38601eef1ebf351e47595c3a184834a246e5805ba6c24
+size 3722
diff --git a/ui/src/components/UCCButton/UCCButton.tsx b/ui/src/components/UCCButton/UCCButton.tsx
new file mode 100644
index 000000000..cf708ab2b
--- /dev/null
+++ b/ui/src/components/UCCButton/UCCButton.tsx
@@ -0,0 +1,29 @@
+import React, { ComponentProps } from 'react';
+
+import Button from '@splunk/react-ui/Button';
+import WaitSpinner from '@splunk/react-ui/WaitSpinner';
+import styled from 'styled-components';
+
+const StyledButton = styled(Button)`
+ min-width: 80px;
+`;
+
+type Props = {
+ label: string;
+ appearance?: 'default' | 'secondary' | 'primary' | 'destructive' | 'pill' | 'toggle' | 'flat';
+ disabled?: boolean;
+ loading?: boolean;
+} & Partial>;
+
+export const UCCButton = React.forwardRef(
+ ({ disabled, loading, appearance, ...rest }, ref) => (
+ : rest.icon}
+ label={loading ? '' : rest.label} // do not display text nor icon when loading
+ appearance={appearance || 'primary'}
+ disabled={(disabled || loading) && 'dimmed'}
+ /> // disable when loading
+ )
+);
diff --git a/ui/src/components/table/TableHeader.jsx b/ui/src/components/table/TableHeader.jsx
index ff3daf6c0..27bf140a8 100644
--- a/ui/src/components/table/TableHeader.jsx
+++ b/ui/src/components/table/TableHeader.jsx
@@ -7,10 +7,10 @@ import { Typography } from '@splunk/react-ui/Typography';
import styled from 'styled-components';
import { _ } from '@splunk/ui-utils/i18n';
+import { UCCButton } from '../UCCButton/UCCButton';
import TableFilter from './TableFilter';
import { TableSelectBoxWrapper } from './CustomTableStyle';
import { PAGE_INPUT } from '../../constants/pages';
-import { StyledButton } from '../../pages/EntryPageStyle';
import { InteractAllStatusButtons } from '../InteractAllStatusButton';
import { useTableContext } from '../../context/useTableContext';
@@ -124,13 +124,7 @@ function TableHeader({
alwaysShowLastPageLink
totalPages={Math.ceil(totalElement / pageSize)}
/>
- {isTabs && (
-
- )}
+ {isTabs && }
-
diff --git a/ui/src/tests/expectExtenders.ts b/ui/src/tests/expectExtenders.ts
new file mode 100644
index 000000000..05afce0f0
--- /dev/null
+++ b/ui/src/tests/expectExtenders.ts
@@ -0,0 +1,55 @@
+import { invariant } from '../util/invariant';
+
+expect.extend({
+ toBeVisuallyEnabled(field: HTMLElement | null | Element) {
+ invariant(field);
+
+ if (field.getAttribute('readonly')) {
+ return { pass: false, message: () => 'Field contains "readonly" attribute' };
+ }
+
+ const ariaDisabled = field.getAttribute('aria-disabled');
+
+ if (ariaDisabled === 'false') {
+ return { pass: true, message: () => 'Field is enabled' };
+ }
+
+ return {
+ pass: false,
+ message: () =>
+ `Attribute "aria-disabled" is incorrect expected "false", got "${ariaDisabled}"`,
+ };
+ },
+ toBeVisuallyDisabled(field: HTMLElement | null | Element) {
+ invariant(field);
+
+ if (field.getAttribute('readonly') === null) {
+ return {
+ pass: false,
+ message: () => `Field "readonly" attribute is null`,
+ };
+ }
+
+ const ariaDisabled = field.getAttribute('aria-disabled');
+
+ if (ariaDisabled === 'true') {
+ return { pass: true, message: () => 'Field is disabled' };
+ }
+
+ return {
+ pass: false,
+ message: () =>
+ `Attribute "aria-disabled" is incorrect expected "true", got ${ariaDisabled}`,
+ };
+ },
+});
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace jest {
+ interface Matchers {
+ toBeVisuallyDisabled(): R;
+ toBeVisuallyEnabled(): R;
+ }
+ }
+}
diff --git a/ui/src/types/modules.d.ts b/ui/src/types/modules.d.ts
index 16698a4a1..65d0329d1 100644
--- a/ui/src/types/modules.d.ts
+++ b/ui/src/types/modules.d.ts
@@ -84,11 +84,3 @@ declare module '@splunk/search-job';
declare module '@splunk/ui-utils/i18n';
declare module 'uuid';
-
-declare global {
- namespace jest {
- interface Matchers {
- toMatchImageSnapshot(): R;
- }
- }
-}
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 9ffbb60f7..21f4729ef 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -4511,7 +4511,16 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest@^29.5.14":
+"@types/jest-image-snapshot@^6.4.0":
+ version "6.4.0"
+ resolved "https://registry.npmjs.org/@types/jest-image-snapshot/-/jest-image-snapshot-6.4.0.tgz#641054d2fa2ff130a49c844ee7a9a68f281b6017"
+ integrity sha512-8TQ/EgqFCX0UWSpH488zAc21fCkJNpZPnnp3xWFMqElxApoJV5QOoqajnVRV7AhfF0rbQWTVyc04KG7tXnzCPA==
+ dependencies:
+ "@types/jest" "*"
+ "@types/pixelmatch" "*"
+ ssim.js "^3.1.1"
+
+"@types/jest@*", "@types/jest@^29.5.14":
version "29.5.14"
resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5"
integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==
@@ -4613,6 +4622,13 @@
resolved "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz#a9495a58d8c75be4ffe9a0bd749a307715c07404"
integrity sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==
+"@types/pixelmatch@*":
+ version "5.2.6"
+ resolved "https://registry.npmjs.org/@types/pixelmatch/-/pixelmatch-5.2.6.tgz#fba6de304ac958495f27d85989f5c6bb7499a686"
+ integrity sha512-wC83uexE5KGuUODn6zkm9gMzTwdY5L0chiK+VrKcDfEjzxh1uadlWTvOmAbCpnM9zx/Ww3f8uKlYQVnO/TrqVg==
+ dependencies:
+ "@types/node" "*"
+
"@types/prop-types@*":
version "15.7.13"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451"