feat: add support for JSON flattening in pipeline processor create and edit (#8331)

* feat: add support for JSON flattening in pipeline processor create and edit

* chore: add fallback value for mapping state

* feat: improve the UI of json flattening form and show path_prefix if enable_path is true

* fix: improve the state management

* chore: get enablePaths state using useWatch

* chore: json flattening tests

* chore: improve the test descriptions

* fix: update snapshot and adjust the existing failing test

* chore: overall improvements

* fix: update the JSON flattening keys map

* fix: destroy the new processor modal on closing to fix the unintended persistent mapping state

* chore: log event on saving pipeline if json_parser processors get modified

* chore: fix the alignment of json flattening switch

* chore: overall improvement

* refactor: improve the mapping comparison by using lodash's isEqual

* chore: update the snapshot

* refactor: improve the pipeline json_parser processor filtering logic

---------

Co-authored-by: Aditya Singh <adityasinghssj1@gmail.com>
Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
This commit is contained in:
Shaheer Kochai 2025-07-14 10:34:04 +04:30 committed by GitHub
parent 893b11c4a0
commit ddb08b3883
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 590 additions and 73 deletions

View File

@ -0,0 +1,6 @@
.json-flattening-form {
margin-top: 16px;
&__item {
margin-bottom: 12px;
}
}

View File

@ -0,0 +1,110 @@
import './JsonFlattening.styles.scss';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Form, Input, Space, Switch, Tooltip } from 'antd';
import { useEffect, useState } from 'react';
import { ProcessorData } from 'types/api/pipeline/def';
import { PREDEFINED_MAPPING } from '../config';
import KeyValueList from './KeyValueList';
interface JsonFlatteningProps {
selectedProcessorData?: ProcessorData;
isAdd: boolean;
}
function JsonFlattening({
selectedProcessorData,
isAdd,
}: JsonFlatteningProps): JSX.Element | null {
const form = Form.useFormInstance();
const mappingValue = selectedProcessorData?.mapping || {};
const enableFlattening = Form.useWatch('enable_flattening', form);
const enablePaths = Form.useWatch('enable_paths', form);
const [enableMapping, setEnableMapping] = useState(
!!mappingValue && Object.keys(mappingValue).length > 0,
);
const selectedMapping = selectedProcessorData?.mapping;
useEffect(() => {
if (!enableMapping) {
form.setFieldsValue({ mapping: undefined });
} else if (form.getFieldValue('mapping') === undefined) {
form.setFieldsValue({
mapping: selectedMapping || PREDEFINED_MAPPING,
});
}
}, [enableMapping, form, selectedMapping]);
const handleEnableMappingChange = (checked: boolean): void => {
setEnableMapping(checked);
};
const handleEnablePathsChange = (checked: boolean): void => {
form.setFieldValue('enable_paths', checked);
};
if (!enableFlattening) {
return null;
}
return (
<div className="json-flattening-form">
<Form.Item
className="json-flattening-form__item"
name="enable_paths"
valuePropName="checked"
initialValue={isAdd ? true : selectedProcessorData?.enable_paths}
>
<Space>
<Switch
size="small"
checked={enablePaths}
onChange={handleEnablePathsChange}
/>
Enable Paths
</Space>
</Form.Item>
{enablePaths && (
<Form.Item
name="path_prefix"
label="Path Prefix"
initialValue={selectedProcessorData?.path_prefix}
>
<Input placeholder="Path Prefix" />
</Form.Item>
)}
<Form.Item className="json-flattening-form__item">
<Space>
<Switch
size="small"
checked={enableMapping}
onChange={handleEnableMappingChange}
/>
Enable Mapping
<Tooltip title="The order of filled keys will determine the priority of keys i.e. earlier keys have higher precedence">
<InfoCircleOutlined />
</Tooltip>
</Space>
</Form.Item>
{enableMapping && (
<Form.Item
name="mapping"
initialValue={selectedProcessorData?.mapping || PREDEFINED_MAPPING}
>
<KeyValueList />
</Form.Item>
)}
</div>
);
}
JsonFlattening.defaultProps = {
selectedProcessorData: undefined,
};
export default JsonFlattening;

View File

@ -0,0 +1,47 @@
import { Form, Select } from 'antd';
import { PREDEFINED_MAPPING } from '../config';
interface KeyValueListProps {
value?: Record<string, string[]>;
onChange?: (value: Record<string, string[]>) => void;
}
function KeyValueList({
value = PREDEFINED_MAPPING,
onChange,
}: KeyValueListProps): JSX.Element {
const handleValueChange = (key: string, newValue: string[]): void => {
const newMapping = {
...value,
[key]: newValue,
};
if (onChange) {
onChange(newMapping);
}
};
return (
<div>
{Object.keys(value).map((key) => (
<Form.Item key={key} label={key}>
<Select
mode="tags"
style={{ width: '100%' }}
placeholder="Values"
onChange={(newValue: string[]): void => handleValueChange(key, newValue)}
value={value[key]}
tokenSeparators={[',']}
/>
</Form.Item>
))}
</div>
);
}
KeyValueList.defaultProps = {
value: PREDEFINED_MAPPING,
onChange: (): void => {},
};
export default KeyValueList;

View File

@ -1,16 +1,21 @@
import './styles.scss'; import './styles.scss';
import { Form, Input, Select } from 'antd'; import { Form, Input, Select, Space, Switch } from 'antd';
import { ModalFooterTitle } from 'container/PipelinePage/styles'; import { ModalFooterTitle } from 'container/PipelinePage/styles';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ProcessorData } from 'types/api/pipeline/def';
import { formValidationRules } from '../config'; import { formValidationRules } from '../config';
import { processorFields, ProcessorFormField } from './config'; import { processorFields, ProcessorFormField } from './config';
import CSVInput from './FormFields/CSVInput'; import CSVInput from './FormFields/CSVInput';
import JsonFlattening from './FormFields/JsonFlattening';
import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles'; import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles';
// eslint-disable-next-line sonarjs/cognitive-complexity
function ProcessorFieldInput({ function ProcessorFieldInput({
fieldData, fieldData,
selectedProcessorData,
isAdd,
}: ProcessorFieldInputProps): JSX.Element | null { }: ProcessorFieldInputProps): JSX.Element | null {
const { t } = useTranslation('pipeline'); const { t } = useTranslation('pipeline');
@ -50,6 +55,13 @@ function ProcessorFieldInput({
); );
} else if (Array.isArray(fieldData?.initialValue)) { } else if (Array.isArray(fieldData?.initialValue)) {
inputField = <CSVInput placeholder={t(fieldData.placeholder)} />; inputField = <CSVInput placeholder={t(fieldData.placeholder)} />;
} else if (fieldData?.name === 'enable_flattening') {
inputField = (
<JsonFlattening
selectedProcessorData={selectedProcessorData}
isAdd={isAdd}
/>
);
} else { } else {
inputField = <Input placeholder={t(fieldData.placeholder)} />; inputField = <Input placeholder={t(fieldData.placeholder)} />;
} }
@ -68,6 +80,28 @@ function ProcessorFieldInput({
</PipelineIndexIcon> </PipelineIndexIcon>
)} )}
<FormWrapper> <FormWrapper>
{fieldData.name === 'enable_flattening' ? (
<Form.Item
required={false}
name="enable_flattening"
initialValue={
selectedProcessorData?.enable_flattening ?? fieldData.initialValue
}
valuePropName="checked"
className="enable-flattening-switch"
>
<Space>
<Switch
size="small"
checked={form.getFieldValue('enable_flattening')}
onChange={(checked: boolean): void => {
form.setFieldValue('enable_flattening', checked);
}}
/>
<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>
</Space>
</Form.Item>
) : (
<Form.Item <Form.Item
required={false} required={false}
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>} label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
@ -78,30 +112,50 @@ function ProcessorFieldInput({
> >
{inputField} {inputField}
</Form.Item> </Form.Item>
)}
{fieldData.name === 'enable_flattening' && inputField}
</FormWrapper> </FormWrapper>
</div> </div>
); );
} }
ProcessorFieldInput.defaultProps = {
selectedProcessorData: undefined,
};
interface ProcessorFieldInputProps { interface ProcessorFieldInputProps {
fieldData: ProcessorFormField; fieldData: ProcessorFormField;
selectedProcessorData?: ProcessorData;
isAdd: boolean;
} }
function ProcessorForm({ processorType }: ProcessorFormProps): JSX.Element { function ProcessorForm({
processorType,
selectedProcessorData,
isAdd,
}: ProcessorFormProps): JSX.Element {
return ( return (
<div className="processor-form-container"> <div className="processor-form-container">
{processorFields[processorType]?.map((fieldData: ProcessorFormField) => ( {processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
<ProcessorFieldInput <ProcessorFieldInput
key={fieldData.name + String(fieldData.initialValue)} key={fieldData.name + String(fieldData.initialValue)}
fieldData={fieldData} fieldData={fieldData}
selectedProcessorData={selectedProcessorData}
isAdd={isAdd}
/> />
))} ))}
</div> </div>
); );
} }
ProcessorForm.defaultProps = {
selectedProcessorData: undefined,
};
interface ProcessorFormProps { interface ProcessorFormProps {
processorType: string; processorType: string;
selectedProcessorData?: ProcessorData;
isAdd: boolean;
} }
export default ProcessorForm; export default ProcessorForm;

View File

@ -136,6 +136,13 @@ export const processorFields: { [key: string]: Array<ProcessorFormField> } = {
name: 'parse_to', name: 'parse_to',
initialValue: 'attributes', initialValue: 'attributes',
}, },
{
id: 4,
fieldName: 'Enable Flattening',
placeholder: '',
name: 'enable_flattening',
initialValue: false,
},
], ],
regex_parser: [ regex_parser: [
{ {
@ -458,3 +465,14 @@ export const processorFields: { [key: string]: Array<ProcessorFormField> } = {
}, },
], ],
}; };
export const PREDEFINED_MAPPING = {
environment: ['service.env', 'environment', 'env'],
host: ['host', 'hostname', 'host.name'],
message: ['message', 'msg', 'log'],
service: ['service', 'appname'],
severity: ['status', 'severity', 'level'],
span_id: ['span_id', 'span.id'],
trace_flags: ['flags'],
trace_id: ['trace_id', 'trace.id'],
};

View File

@ -160,6 +160,7 @@ function AddNewProcessor({
width={800} width={800}
footer={null} footer={null}
onCancel={onCancelModal} onCancel={onCancelModal}
destroyOnClose
> >
<Divider plain /> <Divider plain />
<Form <Form
@ -171,7 +172,11 @@ function AddNewProcessor({
onValuesChange={onFormValuesChanged} onValuesChange={onFormValuesChanged}
> >
<TypeSelect value={processorType} onChange={handleProcessorType} /> <TypeSelect value={processorType} onChange={handleProcessorType} />
<ProcessorForm processorType={processorType} /> <ProcessorForm
processorType={processorType}
selectedProcessorData={selectedProcessorData}
isAdd={isAdd}
/>
<Divider plain /> <Divider plain />
<Form.Item> <Form.Item>
<ModalButtonWrapper> <ModalButtonWrapper>

View File

@ -24,3 +24,7 @@
flex-grow: 1; flex-grow: 1;
margin-left: 2.5rem; margin-left: 2.5rem;
} }
.enable-flattening-switch .ant-form-item-control-input {
min-height: unset !important;
}

View File

@ -219,21 +219,6 @@ function PipelineExpandView({
moveRow: moveProcessorRow, moveRow: moveProcessorRow,
} as React.HTMLAttributes<unknown>); } as React.HTMLAttributes<unknown>);
const processorData = useMemo(
() =>
expandedPipelineData?.config &&
expandedPipelineData?.config.map(
(item: ProcessorData): ProcessorData => ({
id: item.id,
orderId: item.orderId,
type: item.type,
name: item.name,
enabled: item.enabled,
}),
),
[expandedPipelineData],
);
const getLocales = (): TableLocale => ({ const getLocales = (): TableLocale => ({
emptyText: <span />, emptyText: <span />,
}); });
@ -248,7 +233,7 @@ function PipelineExpandView({
rowKey="id" rowKey="id"
size="small" size="small"
components={tableComponents} components={tableComponents}
dataSource={processorData} dataSource={expandedPipelineData?.config}
pagination={false} pagination={false}
onRow={onRowHandler} onRow={onRowHandler}
footer={footer} footer={footer}

View File

@ -6,7 +6,7 @@ import { ExpandableConfig } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import savePipeline from 'api/pipeline/post'; import savePipeline from 'api/pipeline/post';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { isUndefined } from 'lodash-es'; import { isEqual, isUndefined } from 'lodash-es';
import cloneDeep from 'lodash-es/cloneDeep'; import cloneDeep from 'lodash-es/cloneDeep';
import React, { import React, {
useCallback, useCallback,
@ -75,7 +75,7 @@ function PipelinesListEmptyState(): JSX.Element {
<a <a
href="https://signoz.io/docs/logs-pipelines/introduction/?utm_source=product&utm_medium=pipelines-tab" href="https://signoz.io/docs/logs-pipelines/introduction/?utm_source=product&utm_medium=pipelines-tab"
target="_blank" target="_blank"
rel="noreferrer" rel="noopener noreferrer"
> >
here here
</a> </a>
@ -407,15 +407,46 @@ function PipelineListsView({
return undefined; return undefined;
}, [isEditingActionMode, addNewPipelineHandler, t]); }, [isEditingActionMode, addNewPipelineHandler, t]);
const getModifiedJsonFlatteningConfigs = useCallback(
() =>
currPipelineData.flatMap((pipeline) => {
const prevPipeline = prevPipelineData.find((p) => p.name === pipeline.name);
return (pipeline.config || [])
.filter((processor) => {
const prevProcessor = prevPipeline?.config?.find(
(p) => p.name === processor.name,
);
return (
processor.type === 'json_parser' &&
(!prevProcessor ||
prevProcessor.enable_flattening !== processor.enable_flattening ||
prevProcessor.enable_paths !== processor.enable_paths ||
prevProcessor.path_prefix !== processor.path_prefix ||
!isEqual(prevProcessor.mapping, processor.mapping))
);
})
.map((processor) => ({
enableFlattening: !!processor.enable_flattening,
enablePaths: !!processor.enable_paths,
pathPrefix: processor.path_prefix || '',
mapping: processor.mapping || {},
}));
}),
[currPipelineData, prevPipelineData],
);
const onSaveConfigurationHandler = useCallback(async () => { const onSaveConfigurationHandler = useCallback(async () => {
const modifiedPipelineData = currPipelineData.map((item: PipelineData) => { const modifiedPipelineData = currPipelineData.map((item: PipelineData) => {
const pipelineData = { ...item }; const pipelineData = { ...item };
delete pipelineData?.id; delete pipelineData?.id;
return pipelineData; return pipelineData;
}); });
const response = await savePipeline({ const response = await savePipeline({
data: { pipelines: modifiedPipelineData }, data: { pipelines: modifiedPipelineData },
}); });
if (response.statusCode === 200) { if (response.statusCode === 200) {
refetchPipelineLists(); refetchPipelineLists();
setActionMode(ActionMode.Viewing); setActionMode(ActionMode.Viewing);
@ -425,6 +456,15 @@ function PipelineListsView({
setCurrPipelineData(pipelinesInDB); setCurrPipelineData(pipelinesInDB);
setPrevPipelineData(pipelinesInDB); setPrevPipelineData(pipelinesInDB);
// Log modified JSON flattening configurations
const modifiedConfigs = getModifiedJsonFlatteningConfigs();
if (modifiedConfigs.length > 0) {
logEvent('Logs pipeline: Saved JSON Flattening Configuration', {
count: modifiedConfigs.length,
configurations: modifiedConfigs,
});
}
logEvent('Logs: Pipelines: Saved Pipelines', { logEvent('Logs: Pipelines: Saved Pipelines', {
count: pipelinesInDB.length, count: pipelinesInDB.length,
enabled: pipelinesInDB.filter((p) => p.enabled).length, enabled: pipelinesInDB.filter((p) => p.enabled).length,
@ -446,7 +486,14 @@ function PipelineListsView({
setPrevPipelineData(modifiedPipelineData); setPrevPipelineData(modifiedPipelineData);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currPipelineData, notifications, refetchPipelineLists, setActionMode, t]); }, [
currPipelineData,
notifications,
refetchPipelineLists,
setActionMode,
t,
getModifiedJsonFlatteningConfigs,
]);
const onCancelConfigurationHandler = useCallback((): void => { const onCancelConfigurationHandler = useCallback((): void => {
setActionMode(ActionMode.Viewing); setActionMode(ActionMode.Viewing);

View File

@ -58,6 +58,15 @@ export const pipelineMockData: Array<PipelineData> = [
from: 'attributes.auth', from: 'attributes.auth',
to: 'attributes.username', to: 'attributes.username',
}, },
{
orderId: 3,
enabled: true,
type: 'json_parser',
id: 'jsonparser',
name: 'json parser',
from: 'attributes.auth',
to: 'attributes.username',
},
], ],
createdBy: 'nityananda@signoz.io', createdBy: 'nityananda@signoz.io',
createdAt: '2023-03-07T16:56:53.36071141Z', createdAt: '2023-03-07T16:56:53.36071141Z',

View File

@ -1,8 +1,16 @@
import { render } from 'tests/test-utils'; import { fireEvent, screen, waitFor } from '@testing-library/react';
import { render as customRender } from 'tests/test-utils';
import { ProcessorData } from 'types/api/pipeline/def';
import { pipelineMockData } from '../mocks/pipeline'; import { pipelineMockData } from '../mocks/pipeline';
import AddNewProcessor from '../PipelineListsView/AddNewProcessor'; import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
// Mock the config module to set JSON parser as default
jest.mock('../PipelineListsView/AddNewProcessor/config', () => ({
...jest.requireActual('../PipelineListsView/AddNewProcessor/config'),
DEFAULT_PROCESSOR_TYPE: 'json_parser',
}));
jest.mock('uplot', () => { jest.mock('uplot', () => {
const paths = { const paths = {
spline: jest.fn(), spline: jest.fn(),
@ -17,44 +25,233 @@ jest.mock('uplot', () => {
}; };
}); });
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
});
const selectedProcessorData = { const selectedProcessorData = {
id: '1', id: '1',
orderId: 1, orderId: 1,
type: 'grok_parser', type: 'json_parser',
name: 'grok use common', name: 'json parser',
output: 'grokusecommon', output: 'jsonparser',
}; };
describe('PipelinePage container test', () => {
it('should render AddNewProcessor section', () => {
const setActionType = jest.fn();
const isActionType = 'add-processor';
const { asFragment } = render( // Constants for repeated text
<AddNewProcessor const ENABLE_PATHS_TEXT = 'Enable Paths';
isActionType={isActionType} const ENABLE_MAPPING_TEXT = 'Enable Mapping';
setActionType={setActionType} const PATH_PREFIX_LABEL = 'Path Prefix';
selectedProcessorData={selectedProcessorData}
setShowSaveButton={jest.fn()} // Helper function to render AddNewProcessor with JSON parser type
expandedPipelineData={pipelineMockData[0]} const renderJsonProcessor = ({
setExpandedPipelineData={jest.fn()} selectedProcessorData: processorData = selectedProcessorData,
/>, isActionType = 'add-processor',
); }: {
expect(asFragment()).toMatchSnapshot(); selectedProcessorData?: ProcessorData;
isActionType?: 'add-processor' | 'edit-processor';
}): ReturnType<typeof customRender> => {
const defaultProps = {
isActionType,
setActionType: jest.fn(),
selectedProcessorData: processorData,
setShowSaveButton: jest.fn(),
expandedPipelineData: pipelineMockData[2],
setExpandedPipelineData: jest.fn(),
};
// eslint-disable-next-line react/jsx-props-no-spreading
return customRender(<AddNewProcessor {...defaultProps} />);
};
describe('JSON Flattening Processor Tests', () => {
describe('Enable/Disable Flattening', () => {
it('should display the form when enable flattening is turned on', async () => {
renderJsonProcessor({
selectedProcessorData: {
...selectedProcessorData,
enable_flattening: true,
},
});
// Verify the JSON flattening form is displayed
expect(screen.queryByText(ENABLE_PATHS_TEXT)).toBeInTheDocument();
expect(screen.queryByText(ENABLE_MAPPING_TEXT)).toBeInTheDocument();
});
it('should not display the form when enable flattening is turned off', async () => {
renderJsonProcessor({
selectedProcessorData: {
...selectedProcessorData,
enable_flattening: false,
},
});
// Verify the JSON flattening form is not displayed
expect(screen.queryByText(ENABLE_PATHS_TEXT)).not.toBeInTheDocument();
expect(screen.queryByText(ENABLE_MAPPING_TEXT)).not.toBeInTheDocument();
});
it('should display the form when enable flattening switch is toggled on', async () => {
renderJsonProcessor({});
// Wait for the component to render and find the enable flattening switch
await waitFor(() => {
expect(screen.getByRole('switch')).toBeInTheDocument();
});
// Find the enable flattening switch
const enableFlatteningSwitch = screen.getByRole('switch');
// Turn on the switch
fireEvent.click(enableFlatteningSwitch);
// Verify the JSON flattening form is displayed
await waitFor(() => {
expect(screen.getByText(ENABLE_PATHS_TEXT)).toBeInTheDocument();
expect(screen.getByText(ENABLE_MAPPING_TEXT)).toBeInTheDocument();
});
});
it('should hide the form when enable flattening switch is toggled off', async () => {
renderJsonProcessor({
selectedProcessorData: {
...selectedProcessorData,
enable_flattening: true,
},
});
// Wait for the component to render and find the switches
await waitFor(() => {
expect(screen.getAllByRole('switch')[0]).toBeInTheDocument();
});
// Find the enable flattening switch
const enableFlatteningSwitch = screen.getAllByRole('switch')[0];
// Turn off the switch
fireEvent.click(enableFlatteningSwitch);
await waitFor(() => {
expect(screen.queryByText(ENABLE_PATHS_TEXT)).not.toBeInTheDocument();
expect(screen.queryByText(ENABLE_MAPPING_TEXT)).not.toBeInTheDocument();
});
});
});
describe('Enable/Disable Paths', () => {
it('should toggle path prefix visibility when enable paths switch is toggled', async () => {
renderJsonProcessor({
selectedProcessorData: {
...selectedProcessorData,
enable_flattening: true,
enable_paths: false,
},
});
// Wait for the component to render and find the switches
await waitFor(() => {
expect(screen.getAllByRole('switch')[1]).toBeInTheDocument();
});
// In add mode, enable_paths is always true initially, so the path prefix should be visible
await waitFor(() => {
expect(screen.getByLabelText(PATH_PREFIX_LABEL)).toBeInTheDocument();
});
// Find the enable paths switch (second switch in the form) and turn it off
const enablePathsSwitch = screen.getAllByRole('switch')[1];
fireEvent.click(enablePathsSwitch);
// Verify the path prefix field is now hidden
await waitFor(() => {
expect(screen.queryByLabelText(PATH_PREFIX_LABEL)).not.toBeInTheDocument();
});
// Turn the paths switch back on
fireEvent.click(enablePathsSwitch);
// Verify the path prefix field is displayed again
await waitFor(() => {
expect(screen.getByLabelText(PATH_PREFIX_LABEL)).toBeInTheDocument();
});
});
it('should hide path prefix when enable paths switch is turned off', async () => {
renderJsonProcessor({
selectedProcessorData: {
...selectedProcessorData,
enable_flattening: true,
enable_paths: true,
},
});
// Wait for the component to render and find the switches
await waitFor(() => {
expect(screen.getAllByRole('switch')[1]).toBeInTheDocument();
});
// Verify the path prefix is initially visible
await waitFor(() => {
expect(screen.getByLabelText(PATH_PREFIX_LABEL)).toBeInTheDocument();
});
// Find the enable paths switch and turn it off
const enablePathsSwitch = screen.getAllByRole('switch')[1];
fireEvent.click(enablePathsSwitch);
// Verify the path prefix field is now hidden
await waitFor(() => {
expect(screen.queryByLabelText(PATH_PREFIX_LABEL)).not.toBeInTheDocument();
});
});
});
describe('Enable/Disable Mapping', () => {
it('should display the mapping fields when enable mapping is turned on', async () => {
renderJsonProcessor({
selectedProcessorData: {
...selectedProcessorData,
enable_flattening: true,
enable_paths: true,
mapping: {
environment: ['existing.env'],
host: ['existing.host'],
},
},
});
// Verify the mapping fields are displayed
await waitFor(() => {
expect(screen.getByText('environment')).toBeInTheDocument();
expect(screen.getByText('host')).toBeInTheDocument();
});
});
});
describe('Edit Processor Flow', () => {
it('should load existing processor data correctly when editing', async () => {
const existingProcessorData = {
id: '1',
orderId: 1,
type: 'json_parser',
name: 'test json parser',
output: 'testoutput',
enable_flattening: true,
enable_paths: true,
path_prefix: 'existing.prefix',
enable_mapping: true,
mapping: {
environment: ['existing.env'],
host: ['existing.host'],
},
};
renderJsonProcessor({
selectedProcessorData: existingProcessorData,
isActionType: 'edit-processor',
});
// Verify the form is displayed with existing data
await waitFor(() => {
expect(screen.getByDisplayValue('existing.prefix')).toBeInTheDocument();
});
// Verify flattening is enabled
const enableFlatteningSwitch = screen.getAllByRole('switch')[0];
expect(enableFlatteningSwitch).toBeChecked();
// Verify paths is enabled
const enablePathsSwitch = screen.getAllByRole('switch')[1];
expect(enablePathsSwitch).toBeChecked();
});
}); });
}); });

View File

@ -192,7 +192,7 @@ describe('PipelinePage container test', () => {
'.ant-table-expanded-row [data-icon="delete"]', '.ant-table-expanded-row [data-icon="delete"]',
); );
expect(deleteBtns.length).toBe(2); expect(deleteBtns.length).toBe(3);
// delete pipeline // delete pipeline
await fireEvent.click(deleteBtns[0] as HTMLElement); await fireEvent.click(deleteBtns[0] as HTMLElement);
@ -213,7 +213,7 @@ describe('PipelinePage container test', () => {
expect( expect(
document.querySelectorAll('.ant-table-expanded-row [data-icon="delete"]') document.querySelectorAll('.ant-table-expanded-row [data-icon="delete"]')
.length, .length,
).toBe(1); ).toBe(2);
}); });
it('should be able to toggle and delete pipeline', async () => { it('should be able to toggle and delete pipeline', async () => {

View File

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render AddNewProcessor section 1`] = `<DocumentFragment />`;

View File

@ -124,6 +124,37 @@ exports[`PipelinePage should render PipelineExpandView section 1`] = `
</div> </div>
</td> </td>
</tr> </tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="jsonparser"
draggable="true"
>
<td
class="ant-table-cell"
style="text-align: right;"
>
<span
class="ant-avatar ant-avatar-circle c1 css-dev-only-do-not-override-2i2tap"
>
<span
class="ant-avatar-string"
style="transform: scale(1) translateX(-50%);"
>
3
</span>
</span>
</td>
<td
class="ant-table-cell"
style="text-align: left;"
>
<div
class="c2"
>
json parser
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -118,7 +118,7 @@ exports[`PipelinePage container test should render PipelinePageLayout section 1`
learn_more  learn_more 
<a <a
href="https://signoz.io/docs/logs-pipelines/introduction/?utm_source=product&utm_medium=pipelines-tab" href="https://signoz.io/docs/logs-pipelines/introduction/?utm_source=product&utm_medium=pipelines-tab"
rel="noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
here here

View File

@ -31,6 +31,13 @@ export interface ProcessorData {
// time parser fields // time parser fields
layout_type?: string; layout_type?: string;
layout?: string; layout?: string;
// json flattening fields
enable_flattening?: boolean;
enable_paths?: boolean;
path_prefix?: string;
enable_mapping?: boolean;
mapping?: Record<string, string[]>;
} }
export interface PipelineData { export interface PipelineData {