mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 16:07:10 +00:00
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:
parent
893b11c4a0
commit
ddb08b3883
@ -0,0 +1,6 @@
|
|||||||
|
.json-flattening-form {
|
||||||
|
margin-top: 16px;
|
||||||
|
&__item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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,40 +80,82 @@ function ProcessorFieldInput({
|
|||||||
</PipelineIndexIcon>
|
</PipelineIndexIcon>
|
||||||
)}
|
)}
|
||||||
<FormWrapper>
|
<FormWrapper>
|
||||||
<Form.Item
|
{fieldData.name === 'enable_flattening' ? (
|
||||||
required={false}
|
<Form.Item
|
||||||
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
|
required={false}
|
||||||
name={fieldData.name}
|
name="enable_flattening"
|
||||||
initialValue={fieldData.initialValue}
|
initialValue={
|
||||||
rules={fieldData.rules ? fieldData.rules : formValidationRules}
|
selectedProcessorData?.enable_flattening ?? fieldData.initialValue
|
||||||
dependencies={fieldData.dependencies || []}
|
}
|
||||||
>
|
valuePropName="checked"
|
||||||
{inputField}
|
className="enable-flattening-switch"
|
||||||
</Form.Item>
|
>
|
||||||
|
<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
|
||||||
|
required={false}
|
||||||
|
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
|
||||||
|
name={fieldData.name}
|
||||||
|
initialValue={fieldData.initialValue}
|
||||||
|
rules={fieldData.rules ? fieldData.rules : formValidationRules}
|
||||||
|
dependencies={fieldData.dependencies || []}
|
||||||
|
>
|
||||||
|
{inputField}
|
||||||
|
</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;
|
||||||
|
|||||||
@ -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'],
|
||||||
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`PipelinePage container test should render AddNewProcessor section 1`] = `<DocumentFragment />`;
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user