Compare commits

...

14 Commits

16 changed files with 674 additions and 187 deletions

View File

@@ -42,5 +42,7 @@
"processor_span_id_placeholder": "Parse Span ID from", "processor_span_id_placeholder": "Parse Span ID from",
"processor_trace_flags_placeholder": "Parse Trace flags from", "processor_trace_flags_placeholder": "Parse Trace flags from",
"processor_from_placeholder": "From", "processor_from_placeholder": "From",
"processor_to_placeholder": "To" "processor_to_placeholder": "To",
"share_pipelines": "Share Pipelines",
"import_pipelines": "Import Pipelines"
} }

View File

@@ -9,6 +9,8 @@ function Editor({
readOnly, readOnly,
height, height,
options, options,
beforeMount,
onValidate,
}: MEditorProps): JSX.Element { }: MEditorProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@@ -31,6 +33,8 @@ function Editor({
options={editorOptions} options={editorOptions}
height={height} height={height}
onChange={onChangeHandler} onChange={onChangeHandler}
beforeMount={beforeMount}
onValidate={onValidate}
data-testid="monaco-editor" data-testid="monaco-editor"
/> />
); );
@@ -43,6 +47,8 @@ interface MEditorProps {
readOnly?: boolean; readOnly?: boolean;
height?: string; height?: string;
options?: EditorProps['options']; options?: EditorProps['options'];
beforeMount?: EditorProps['beforeMount'];
onValidate?: EditorProps['onValidate'];
} }
Editor.defaultProps = { Editor.defaultProps = {
@@ -51,6 +57,8 @@ Editor.defaultProps = {
height: '40vh', height: '40vh',
options: {}, options: {},
onChange: (): void => {}, onChange: (): void => {},
beforeMount: (): void => {},
onValidate: (): void => {},
}; };
export default Editor; export default Editor;

View File

@@ -1,77 +0,0 @@
import { EditFilled, PlusOutlined } from '@ant-design/icons';
import TextToolTip from 'components/TextToolTip';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
import { ButtonContainer, CustomButton } from '../../styles';
import { checkDataLength } from '../utils';
function CreatePipelineButton({
setActionType,
isActionMode,
setActionMode,
pipelineData,
}: CreatePipelineButtonProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const { trackEvent } = useAnalytics();
const isAddNewPipelineVisible = useMemo(
() => checkDataLength(pipelineData?.pipelines),
[pipelineData?.pipelines],
);
const isDisabled = isActionMode === ActionMode.Editing;
const onEnterEditMode = (): void => {
setActionMode(ActionMode.Editing);
trackEvent('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
};
const onAddNewPipeline = (): void => {
setActionMode(ActionMode.Editing);
setActionType(ActionType.AddPipeline);
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui',
});
};
return (
<ButtonContainer>
<TextToolTip
text={t('learn_more')}
url="https://signoz.io/docs/logs-pipelines/introduction/"
/>
{isAddNewPipelineVisible && (
<CustomButton
icon={<EditFilled />}
onClick={onEnterEditMode}
disabled={isDisabled}
>
{t('enter_edit_mode')}
</CustomButton>
)}
{!isAddNewPipelineVisible && (
<CustomButton
icon={<PlusOutlined />}
onClick={onAddNewPipeline}
type="primary"
>
{t('new_pipeline')}
</CustomButton>
)}
</ButtonContainer>
);
}
interface CreatePipelineButtonProps {
setActionType: (actionType: string) => void;
isActionMode: string;
setActionMode: (actionMode: string) => void;
pipelineData: Pipeline;
}
export default CreatePipelineButton;

View File

@@ -0,0 +1,125 @@
import {
EditFilled,
ImportOutlined,
PlusOutlined,
ShareAltOutlined,
} from '@ant-design/icons';
import TextToolTip from 'components/TextToolTip';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActionMode,
ActionType,
Pipeline,
PipelineData,
} from 'types/api/pipeline/def';
import { ButtonContainer, CustomButton } from '../../styles';
import PipelinesExportModal from './PipelinesExportModal';
import PipelinesImportModal from './PipelinesImportModal/PipelinesImportModal';
function PipelinesActions({
setActionType,
isActionMode,
setActionMode,
pipelineData,
setCurrentPipelines,
}: PipelinesActionsProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const { trackEvent } = useAnalytics();
const [isExportModalVisible, setIsExportModalVisible] = useState(false);
const [isImportModalVisible, setIsImportModalVisible] = useState(false);
const pipelinesExist = useMemo(() => pipelineData?.pipelines?.length > 0, [
pipelineData?.pipelines,
]);
const inEditMode = isActionMode === ActionMode.Editing;
const onEnterEditMode = (): void => {
setActionMode(ActionMode.Editing);
trackEvent('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui',
});
};
const onAddNewPipeline = (): void => {
setActionMode(ActionMode.Editing);
setActionType(ActionType.AddPipeline);
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui',
});
};
return (
<>
<ButtonContainer>
<TextToolTip
text={t('learn_more')}
url="https://signoz.io/docs/logs-pipelines/introduction/"
/>
{pipelinesExist && !inEditMode && (
<CustomButton
onClick={(): void => setIsExportModalVisible(true)}
icon={<ShareAltOutlined />}
>
{t('share_pipelines')}
</CustomButton>
)}
{(inEditMode || !pipelinesExist) && (
<CustomButton
onClick={(): void => {
onEnterEditMode();
setIsImportModalVisible(true);
}}
icon={<ImportOutlined />}
>
{t('import_pipelines')}
</CustomButton>
)}
{pipelinesExist && (
<CustomButton
icon={<EditFilled />}
onClick={onEnterEditMode}
disabled={inEditMode}
>
{t('enter_edit_mode')}
</CustomButton>
)}
{!pipelinesExist && (
<CustomButton
icon={<PlusOutlined />}
onClick={onAddNewPipeline}
type="primary"
>
{t('new_pipeline')}
</CustomButton>
)}
</ButtonContainer>
<PipelinesExportModal
open={isExportModalVisible}
onClose={(): void => setIsExportModalVisible(false)}
pipelines={pipelineData.pipelines}
/>
<PipelinesImportModal
open={isImportModalVisible}
onClose={(): void => setIsImportModalVisible(false)}
setCurrentPipelines={setCurrentPipelines}
/>
</>
);
}
interface PipelinesActionsProps {
setActionType: (actionType: string) => void;
isActionMode: string;
setActionMode: (actionMode: string) => void;
pipelineData: Pipeline;
setCurrentPipelines: (
value: React.SetStateAction<Array<PipelineData>>,
) => void;
}
export default PipelinesActions;

View File

@@ -0,0 +1,99 @@
import { CopyFilled, DownloadOutlined } from '@ant-design/icons';
import { Button, Modal } from 'antd';
import Editor from 'components/Editor';
import { downloadObjectAsJson } from 'container/NewDashboard/DashboardDescription/utils';
import { useNotifications } from 'hooks/useNotifications';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCopyToClipboard } from 'react-use';
import { PipelineData } from 'types/api/pipeline/def';
export default function PipelinesExportModal({
open,
onClose,
pipelines,
}: PipelinesExportModalProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const postablePipelines = pipelines.map((p) =>
Object.fromEntries(
Object.entries(p).filter((e) => !['createdBy', 'createdAt'].includes(e[0])),
),
);
const pipelinesPropJson = JSON.stringify(postablePipelines, null, 2);
const [pipelinesJson, setPipelinesJson] = useState(pipelinesPropJson);
useEffect(() => {
setPipelinesJson(pipelinesPropJson);
}, [open, pipelinesPropJson]);
const { notifications } = useNotifications();
const [clipboardContent, setClipboardContent] = useCopyToClipboard();
useEffect(() => {
if (clipboardContent.error) {
notifications.error({
message: t('something_went_wrong', {
ns: 'common',
}),
});
}
if (clipboardContent.value) {
notifications.success({
message: t('success', {
ns: 'common',
}),
});
}
}, [clipboardContent.error, clipboardContent.value, t, notifications]);
const footer = useMemo(
() => (
<>
<Button
style={{
marginTop: '16px',
}}
onClick={(): void => setClipboardContent(pipelinesJson)}
type="primary"
size="small"
>
<CopyFilled /> {t('copy_to_clipboard')}
</Button>
<Button
type="primary"
size="small"
onClick={(): void => {
downloadObjectAsJson(JSON.parse(pipelinesJson), 'pipelines');
}}
>
<DownloadOutlined /> {t('download_json')}
</Button>
</>
),
[pipelinesJson, t, setClipboardContent],
);
return (
<Modal
open={open}
onCancel={onClose}
width="80vw"
centered
title={t('share')}
destroyOnClose
footer={footer}
>
<Editor
height="70vh"
onChange={(value): void => setPipelinesJson(value)}
value={pipelinesJson}
/>
</Modal>
);
}
interface PipelinesExportModalProps {
open: boolean;
onClose: VoidFunction;
pipelines: Array<PipelineData>;
}

View File

@@ -0,0 +1,95 @@
import './styles.scss';
import { ImportOutlined } from '@ant-design/icons';
import { Monaco } from '@monaco-editor/react';
import { Button, Modal } from 'antd';
import Editor from 'components/Editor';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PipelineData } from 'types/api/pipeline/def';
import { PipelinesJSONSchema } from '../schema';
export default function PipelinesImportModal({
open,
onClose,
setCurrentPipelines,
}: PipelinesImportModalProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const [pipelinesJson, setPipelinesJson] = useState('');
const [editorErrors, setEditorErrors] = useState<string[]>([]);
const isEmpty = pipelinesJson.trim().length < 1;
const isInvalid = (editorErrors || []).length > 0;
const firstError = editorErrors?.[0];
const onImport = useCallback((): void => {
try {
const pipelines = JSON.parse(pipelinesJson);
setCurrentPipelines(pipelines);
onClose();
} catch (error) {
setEditorErrors([String(error)]);
}
}, [pipelinesJson, setCurrentPipelines, onClose]);
const footer = useMemo(
() => (
<div className="pipelines-import-modal-footer">
<div className="pipelines-import-modal-error">{firstError || ''}</div>
<Button
disabled={isEmpty || isInvalid}
type="primary"
size="small"
onClick={onImport}
>
<ImportOutlined /> {t('import')}
</Button>
</div>
),
[t, isEmpty, isInvalid, firstError, onImport],
);
return (
<Modal
open={open}
onCancel={onClose}
width="80vw"
centered
title={t('import')}
destroyOnClose
footer={footer}
>
<Editor
height="70vh"
onChange={(value): void => setPipelinesJson(value)}
value={pipelinesJson}
language="json"
beforeMount={(monaco: Monaco): void => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [
{
fileMatch: ['*'],
schema: PipelinesJSONSchema,
},
],
});
}}
onValidate={(markers): void =>
setEditorErrors(
markers.map(
(m) => `Ln ${m.startLineNumber}, Col ${m.startColumn}: ${m.message}`,
),
)
}
/>
</Modal>
);
}
interface PipelinesImportModalProps {
open: boolean;
onClose: VoidFunction;
setCurrentPipelines: (
value: React.SetStateAction<Array<PipelineData>>,
) => void;
}

View File

@@ -0,0 +1,3 @@
import PipelinesImportModal from './PipelinesImportModal';
export default PipelinesImportModal;

View File

@@ -0,0 +1,10 @@
.pipelines-import-modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.pipelines-import-modal-error {
flex-grow: 1;
text-align: left;
}

View File

@@ -1,8 +1,9 @@
import cloneDeep from 'lodash-es/cloneDeep';
import { useState } from 'react'; import { useState } from 'react';
import { Pipeline } from 'types/api/pipeline/def'; import { Pipeline, PipelineData } from 'types/api/pipeline/def';
import PipelineListsView from '../../PipelineListsView'; import PipelineListsView from '../../PipelineListsView';
import CreatePipelineButton from './CreatePipelineButton'; import PipelinesActions from './PipelinesActions';
function PipelinePageLayout({ function PipelinePageLayout({
refetchPipelineLists, refetchPipelineLists,
@@ -10,21 +11,32 @@ function PipelinePageLayout({
}: PipelinePageLayoutProps): JSX.Element { }: PipelinePageLayoutProps): JSX.Element {
const [isActionType, setActionType] = useState<string>(); const [isActionType, setActionType] = useState<string>();
const [isActionMode, setActionMode] = useState<string>('viewing-mode'); const [isActionMode, setActionMode] = useState<string>('viewing-mode');
const [savedPipelines, setSavedPipelines] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
const [currentPipelines, setCurrentPipelines] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
return ( return (
<> <>
<CreatePipelineButton <PipelinesActions
setActionType={setActionType} setActionType={setActionType}
setActionMode={setActionMode} setActionMode={setActionMode}
isActionMode={isActionMode} isActionMode={isActionMode}
pipelineData={pipelineData} pipelineData={pipelineData}
setCurrentPipelines={setCurrentPipelines}
/> />
<PipelineListsView <PipelineListsView
isActionType={String(isActionType)} isActionType={String(isActionType)}
setActionType={setActionType} setActionType={setActionType}
setActionMode={setActionMode} setActionMode={setActionMode}
isActionMode={isActionMode} isActionMode={isActionMode}
pipelineData={pipelineData} savedPipelinesVersion={pipelineData?.version}
savedPipelines={savedPipelines}
setSavedPipelines={setSavedPipelines}
currentPipelines={currentPipelines}
setCurrentPipelines={setCurrentPipelines}
refetchPipelineLists={refetchPipelineLists} refetchPipelineLists={refetchPipelineLists}
/> />
</> </>

View File

@@ -0,0 +1,247 @@
// JSON schema for pipelines payload.
export const PipelinesJSONSchema = JSON.parse(`
{
"items": {
"properties": {
"id": {
"type": "string"
},
"orderId": {
"type": "integer"
},
"name": {
"type": "string"
},
"alias": {
"type": "string"
},
"description": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"filter": {
"properties": {
"op": {
"type": "string"
},
"items": {
"items": {
"properties": {
"key": {
"properties": {
"key": {
"type": "string"
},
"dataType": {
"type": "string"
},
"type": {
"type": "string"
},
"isColumn": {
"type": "boolean"
},
"isJSON": {
"type": "boolean"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"key",
"dataType",
"type",
"isColumn",
"isJSON"
]
},
"value": true,
"op": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"key",
"value",
"op"
]
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"op",
"items"
]
},
"config": {
"items": {
"properties": {
"type": {
"type": "string"
},
"id": {
"type": "string"
},
"output": {
"type": "string"
},
"on_error": {
"type": "string"
},
"if": {
"type": "string"
},
"orderId": {
"type": "integer"
},
"enabled": {
"type": "boolean"
},
"name": {
"type": "string"
},
"parse_to": {
"type": "string"
},
"pattern": {
"type": "string"
},
"regex": {
"type": "string"
},
"parse_from": {
"type": "string"
},
"trace_id": {
"properties": {
"parse_from": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"parse_from"
]
},
"span_id": {
"properties": {
"parse_from": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"parse_from"
]
},
"trace_flags": {
"properties": {
"parse_from": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"parse_from"
]
},
"field": {
"type": "string"
},
"value": {
"type": "string"
},
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"expr": {
"type": "string"
},
"routes": {
"items": {
"properties": {
"output": {
"type": "string"
},
"expr": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"output",
"expr"
]
},
"type": "array"
},
"fields": {
"items": {
"type": "string"
},
"type": "array"
},
"default": {
"type": "string"
},
"layout": {
"type": "string"
},
"layout_type": {
"type": "string"
},
"mapping": {
"additionalProperties": {
"items": {
"type": "string"
},
"type": "array"
},
"type": "object"
},
"overwrite_text": {
"type": "boolean"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"type",
"orderId",
"enabled"
]
},
"type": "array"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"id",
"orderId",
"name",
"alias",
"description",
"enabled",
"filter",
"config"
]
},
"type": "array"
}
`);

View File

@@ -1,9 +1,9 @@
import { Button, Divider, Form, Modal } from 'antd'; import { Button, Divider, Form, Modal } from 'antd';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { ActionMode, ActionType, PipelineData } from 'types/api/pipeline/def'; import { ActionType, PipelineData } from 'types/api/pipeline/def';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@@ -15,7 +15,6 @@ function AddNewPipeline({
isActionType, isActionType,
setActionType, setActionType,
selectedPipelineData, selectedPipelineData,
setShowSaveButton,
setCurrPipelineData, setCurrPipelineData,
currPipelineData, currPipelineData,
}: AddNewPipelineProps): JSX.Element { }: AddNewPipelineProps): JSX.Element {
@@ -90,11 +89,6 @@ function AddNewPipeline({
[isEdit, selectedPipelineData?.name, t], [isEdit, selectedPipelineData?.name, t],
); );
const onOkModalHandler = useCallback(
() => setShowSaveButton(ActionMode.Editing),
[setShowSaveButton],
);
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]); const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
return ( return (
@@ -122,7 +116,7 @@ function AddNewPipeline({
key="submit" key="submit"
type="primary" type="primary"
htmlType="submit" htmlType="submit"
onClick={onOkModalHandler} onClick={(): void => {}}
> >
{isEdit ? t('update') : t('create')} {isEdit ? t('update') : t('create')}
</Button> </Button>
@@ -140,7 +134,6 @@ interface AddNewPipelineProps {
isActionType: string; isActionType: string;
setActionType: (actionType?: ActionType) => void; setActionType: (actionType?: ActionType) => void;
selectedPipelineData: PipelineData | undefined; selectedPipelineData: PipelineData | undefined;
setShowSaveButton: (actionMode: ActionMode) => void;
setCurrPipelineData: ( setCurrPipelineData: (
value: React.SetStateAction<Array<PipelineData>>, value: React.SetStateAction<Array<PipelineData>>,
) => void; ) => void;

View File

@@ -2,7 +2,6 @@ import { Button, Divider, Form, Modal } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
ActionMode,
ActionType, ActionType,
PipelineData, PipelineData,
ProcessorData, ProcessorData,
@@ -19,7 +18,6 @@ function AddNewProcessor({
isActionType, isActionType,
setActionType, setActionType,
selectedProcessorData, selectedProcessorData,
setShowSaveButton,
expandedPipelineData, expandedPipelineData,
setExpandedPipelineData, setExpandedPipelineData,
}: AddNewProcessorProps): JSX.Element { }: AddNewProcessorProps): JSX.Element {
@@ -134,11 +132,6 @@ function AddNewProcessor({
[isEdit, selectedProcessorData?.name, t], [isEdit, selectedProcessorData?.name, t],
); );
const onOkModalHandler = useCallback(
() => setShowSaveButton(ActionMode.Editing),
[setShowSaveButton],
);
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]); const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
const onFormValuesChanged = useCallback( const onFormValuesChanged = useCallback(
@@ -179,7 +172,7 @@ function AddNewProcessor({
key="submit" key="submit"
type="primary" type="primary"
htmlType="submit" htmlType="submit"
onClick={onOkModalHandler} onClick={(): void => {}}
> >
{isEdit ? t('update') : t('create')} {isEdit ? t('update') : t('create')}
</Button> </Button>
@@ -202,7 +195,6 @@ interface AddNewProcessorProps {
isActionType: string; isActionType: string;
setActionType: (actionType?: ActionType) => void; setActionType: (actionType?: ActionType) => void;
selectedProcessorData?: ProcessorData; selectedProcessorData?: ProcessorData;
setShowSaveButton: (actionMode: ActionMode) => void;
expandedPipelineData?: PipelineData; expandedPipelineData?: PipelineData;
setExpandedPipelineData: (data: PipelineData) => void; setExpandedPipelineData: (data: PipelineData) => void;
} }

View File

@@ -32,7 +32,6 @@ function PipelineExpandView({
setActionType, setActionType,
processorEditAction, processorEditAction,
isActionMode, isActionMode,
setShowSaveButton,
expandedPipelineData, expandedPipelineData,
setExpandedPipelineData, setExpandedPipelineData,
prevPipelineData, prevPipelineData,
@@ -44,7 +43,6 @@ function PipelineExpandView({
const deleteProcessorHandler = useCallback( const deleteProcessorHandler = useCallback(
(record: ProcessorData) => (): void => { (record: ProcessorData) => (): void => {
setShowSaveButton(ActionMode.Editing);
if (expandedPipelineData && expandedPipelineData?.config) { if (expandedPipelineData && expandedPipelineData?.config) {
const filteredData = expandedPipelineData?.config.filter( const filteredData = expandedPipelineData?.config.filter(
(item: ProcessorData) => item.id !== record.id, (item: ProcessorData) => item.id !== record.id,
@@ -62,7 +60,7 @@ function PipelineExpandView({
setExpandedPipelineData(pipelineData); setExpandedPipelineData(pipelineData);
} }
}, },
[expandedPipelineData, setShowSaveButton, setExpandedPipelineData], [expandedPipelineData, setExpandedPipelineData],
); );
const processorDeleteAction = useCallback( const processorDeleteAction = useCallback(
@@ -80,7 +78,6 @@ function PipelineExpandView({
const onSwitchProcessorChange = useCallback( const onSwitchProcessorChange = useCallback(
(checked: boolean, record: ProcessorData): void => { (checked: boolean, record: ProcessorData): void => {
if (expandedPipelineData && expandedPipelineData?.config) { if (expandedPipelineData && expandedPipelineData?.config) {
setShowSaveButton(ActionMode.Editing);
const findRecordIndex = getRecordIndex( const findRecordIndex = getRecordIndex(
expandedPipelineData?.config, expandedPipelineData?.config,
record, record,
@@ -102,7 +99,7 @@ function PipelineExpandView({
setExpandedPipelineData(modifiedProcessorData); setExpandedPipelineData(modifiedProcessorData);
} }
}, },
[expandedPipelineData, setExpandedPipelineData, setShowSaveButton], [expandedPipelineData, setExpandedPipelineData],
); );
const columns = useMemo(() => { const columns = useMemo(() => {
@@ -145,14 +142,13 @@ function PipelineExpandView({
const reorderProcessorRow = useCallback( const reorderProcessorRow = useCallback(
(updatedRow: ProcessorData[]) => (): void => { (updatedRow: ProcessorData[]) => (): void => {
setShowSaveButton(ActionMode.Editing);
if (expandedPipelineData) { if (expandedPipelineData) {
const modifiedProcessorData = { ...expandedPipelineData }; const modifiedProcessorData = { ...expandedPipelineData };
modifiedProcessorData.config = updatedRow; modifiedProcessorData.config = updatedRow;
setExpandedPipelineData(modifiedProcessorData); setExpandedPipelineData(modifiedProcessorData);
} }
}, },
[expandedPipelineData, setShowSaveButton, setExpandedPipelineData], [expandedPipelineData, setExpandedPipelineData],
); );
const onCancelReorderProcessorRow = useCallback( const onCancelReorderProcessorRow = useCallback(
@@ -267,7 +263,6 @@ interface PipelineExpandViewProps {
setActionType: (actionType?: ActionType) => void; setActionType: (actionType?: ActionType) => void;
processorEditAction: (record: ProcessorData) => () => void; processorEditAction: (record: ProcessorData) => () => void;
isActionMode: string; isActionMode: string;
setShowSaveButton: (actionMode: ActionMode) => void;
expandedPipelineData?: PipelineData; expandedPipelineData?: PipelineData;
setExpandedPipelineData: (data: PipelineData) => void; setExpandedPipelineData: (data: PipelineData) => void;
prevPipelineData: Array<PipelineData>; prevPipelineData: Array<PipelineData>;

View File

@@ -7,6 +7,7 @@ import savePipeline from 'api/pipeline/post';
import useAnalytics from 'hooks/analytics/useAnalytics'; import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import cloneDeep from 'lodash-es/cloneDeep'; import cloneDeep from 'lodash-es/cloneDeep';
import isEqual from 'lodash-es/isEqual';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
@@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next';
import { import {
ActionMode, ActionMode,
ActionType, ActionType,
Pipeline,
PipelineData, PipelineData,
ProcessorData, ProcessorData,
} from 'types/api/pipeline/def'; } from 'types/api/pipeline/def';
@@ -85,7 +85,11 @@ function PipelineListsView({
setActionType, setActionType,
isActionMode, isActionMode,
setActionMode, setActionMode,
pipelineData, savedPipelinesVersion,
savedPipelines,
setSavedPipelines,
currentPipelines,
setCurrentPipelines,
refetchPipelineLists, refetchPipelineLists,
}: PipelineListsViewProps): JSX.Element { }: PipelineListsViewProps): JSX.Element {
const { t } = useTranslation(['pipeline', 'common']); const { t } = useTranslation(['pipeline', 'common']);
@@ -93,34 +97,28 @@ function PipelineListsView({
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>(''); const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
const { trackEvent } = useAnalytics(); const { trackEvent } = useAnalytics();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []),
);
const [expandedPipelineId, setExpandedPipelineId] = useState< const [expandedPipelineId, setExpandedPipelineId] = useState<
string | undefined string | undefined
>(undefined); >(undefined);
const expandedPipelineData = useCallback( const expandedPipelineData = useCallback(
() => currPipelineData?.find((p) => p.id === expandedPipelineId), () => currentPipelines?.find((p) => p.id === expandedPipelineId),
[currPipelineData, expandedPipelineId], [currentPipelines, expandedPipelineId],
); );
const setExpandedPipelineData = useCallback( const setExpandedPipelineData = useCallback(
(newData: PipelineData): void => { (newData: PipelineData): void => {
if (expandedPipelineId) { if (expandedPipelineId) {
const pipelineIdx = currPipelineData?.findIndex( const pipelineIdx = currentPipelines?.findIndex(
(p) => p.id === expandedPipelineId, (p) => p.id === expandedPipelineId,
); );
if (pipelineIdx >= 0) { if (pipelineIdx >= 0) {
const newPipelineData = [...currPipelineData]; const newPipelineData = cloneDeep(currentPipelines);
newPipelineData[pipelineIdx] = newData; newPipelineData[pipelineIdx] = newData;
setCurrPipelineData(newPipelineData); setCurrentPipelines(newPipelineData);
} }
} }
}, },
[expandedPipelineId, currPipelineData], [expandedPipelineId, currentPipelines, setCurrentPipelines],
); );
const [ const [
@@ -134,17 +132,16 @@ function PipelineListsView({
] = useState<PipelineData>(); ] = useState<PipelineData>();
const [expandedRowKeys, setExpandedRowKeys] = useState<Array<string>>(); const [expandedRowKeys, setExpandedRowKeys] = useState<Array<string>>();
const [showSaveButton, setShowSaveButton] = useState<string>();
const isEditingActionMode = isActionMode === ActionMode.Editing; const isEditingActionMode = isActionMode === ActionMode.Editing;
const visibleCurrPipelines = useMemo((): Array<PipelineData> => { const visibleCurrPipelines = useMemo((): Array<PipelineData> => {
if (pipelineSearchValue === '') { if (pipelineSearchValue === '') {
return currPipelineData; return currentPipelines;
} }
return currPipelineData.filter((data) => return currentPipelines.filter((data) =>
getDataOnSearch(data as never, pipelineSearchValue), getDataOnSearch(data as never, pipelineSearchValue),
); );
}, [currPipelineData, pipelineSearchValue]); }, [currentPipelines, pipelineSearchValue]);
const handleAlert = useCallback( const handleAlert = useCallback(
({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => { ({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => {
@@ -171,15 +168,18 @@ function PipelineListsView({
const pipelineDeleteHandler = useCallback( const pipelineDeleteHandler = useCallback(
(record: PipelineData) => (): void => { (record: PipelineData) => (): void => {
setShowSaveButton(ActionMode.Editing); const filteredData = getElementFromArray(
const filteredData = getElementFromArray(currPipelineData, record, 'id'); cloneDeep(currentPipelines),
record,
'id',
);
filteredData.forEach((item, index) => { filteredData.forEach((item, index) => {
const obj = item; const obj = item;
obj.orderId = index + 1; obj.orderId = index + 1;
}); });
setCurrPipelineData(filteredData); setCurrentPipelines(filteredData);
}, },
[currPipelineData], [currentPipelines, setCurrentPipelines],
); );
const pipelineDeleteAction = useCallback( const pipelineDeleteAction = useCallback(
@@ -204,21 +204,20 @@ function PipelineListsView({
const onSwitchPipelineChange = useCallback( const onSwitchPipelineChange = useCallback(
(checked: boolean, record: PipelineData): void => { (checked: boolean, record: PipelineData): void => {
setShowSaveButton(ActionMode.Editing); const findRecordIndex = getRecordIndex(currentPipelines, record, 'id');
const findRecordIndex = getRecordIndex(currPipelineData, record, 'id');
const updateSwitch = { const updateSwitch = {
...currPipelineData[findRecordIndex], ...currentPipelines[findRecordIndex],
enabled: checked, enabled: checked,
}; };
const editedPipelineData = getEditedDataSource( const editedPipelineData = getEditedDataSource(
currPipelineData, cloneDeep(currentPipelines),
record, record,
'id', 'id',
updateSwitch, updateSwitch,
); );
setCurrPipelineData(editedPipelineData); setCurrentPipelines(editedPipelineData);
}, },
[currPipelineData], [currentPipelines, setCurrentPipelines],
); );
const columns = useMemo(() => { const columns = useMemo(() => {
@@ -271,28 +270,13 @@ function PipelineListsView({
onSwitchPipelineChange, onSwitchPipelineChange,
]); ]);
const updatePipelineSequence = useCallback(
(updatedRow: PipelineData[]) => (): void => {
setShowSaveButton(ActionMode.Editing);
setCurrPipelineData(updatedRow);
},
[],
);
const onCancelPipelineSequence = useCallback(
(rawData: PipelineData[]) => (): void => {
setCurrPipelineData(rawData);
},
[],
);
const movePipelineRow = useCallback( const movePipelineRow = useCallback(
(dragIndex: number, hoverIndex: number) => { (dragIndex: number, hoverIndex: number) => {
if (currPipelineData && isEditingActionMode) { if (currentPipelines && isEditingActionMode) {
const rawData = currPipelineData; const rawData = currentPipelines;
const updatedRows = getUpdatedRow( const updatedRows = getUpdatedRow(
currPipelineData, cloneDeep(currentPipelines),
visibleCurrPipelines[dragIndex].orderId - 1, visibleCurrPipelines[dragIndex].orderId - 1,
visibleCurrPipelines[hoverIndex].orderId - 1, visibleCurrPipelines[hoverIndex].orderId - 1,
); );
@@ -305,19 +289,18 @@ function PipelineListsView({
title: t('reorder_pipeline'), title: t('reorder_pipeline'),
descrition: t('reorder_pipeline_description'), descrition: t('reorder_pipeline_description'),
buttontext: t('reorder'), buttontext: t('reorder'),
onOk: updatePipelineSequence(updatedRows), onOk: (): void => setCurrentPipelines(updatedRows),
onCancel: onCancelPipelineSequence(rawData), onCancel: (): void => setCurrentPipelines(rawData),
}); });
} }
}, },
[ [
currPipelineData, currentPipelines,
isEditingActionMode, isEditingActionMode,
visibleCurrPipelines, visibleCurrPipelines,
handleAlert, handleAlert,
t, t,
updatePipelineSequence, setCurrentPipelines,
onCancelPipelineSequence,
], ],
); );
@@ -328,10 +311,9 @@ function PipelineListsView({
isActionMode={isActionMode} isActionMode={isActionMode}
setActionType={setActionType} setActionType={setActionType}
processorEditAction={processorEditAction} processorEditAction={processorEditAction}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData()} expandedPipelineData={expandedPipelineData()}
setExpandedPipelineData={setExpandedPipelineData} setExpandedPipelineData={setExpandedPipelineData}
prevPipelineData={prevPipelineData} prevPipelineData={savedPipelines}
/> />
), ),
[ [
@@ -340,7 +322,7 @@ function PipelineListsView({
isActionMode, isActionMode,
expandedPipelineData, expandedPipelineData,
setActionType, setActionType,
prevPipelineData, savedPipelines,
setExpandedPipelineData, setExpandedPipelineData,
], ],
); );
@@ -390,7 +372,7 @@ function PipelineListsView({
}, [isEditingActionMode, addNewPipelineHandler, t]); }, [isEditingActionMode, addNewPipelineHandler, t]);
const onSaveConfigurationHandler = useCallback(async () => { const onSaveConfigurationHandler = useCallback(async () => {
const modifiedPipelineData = currPipelineData.map((item: PipelineData) => { const modifiedPipelineData = currentPipelines.map((item: PipelineData) => {
const pipelineData = { ...item }; const pipelineData = { ...item };
delete pipelineData?.id; delete pipelineData?.id;
return pipelineData; return pipelineData;
@@ -401,11 +383,10 @@ function PipelineListsView({
if (response.statusCode === 200) { if (response.statusCode === 200) {
refetchPipelineLists(); refetchPipelineLists();
setActionMode(ActionMode.Viewing); setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
const pipelinesInDB = response.payload?.pipelines || []; const pipelinesInDB = response.payload?.pipelines || [];
setCurrPipelineData(pipelinesInDB); setCurrentPipelines(cloneDeep(pipelinesInDB));
setPrevPipelineData(pipelinesInDB); setSavedPipelines(cloneDeep(pipelinesInDB));
trackEvent('Logs: Pipelines: Saved Pipelines', { trackEvent('Logs: Pipelines: Saved Pipelines', {
count: pipelinesInDB.length, count: pipelinesInDB.length,
@@ -419,21 +400,19 @@ function PipelineListsView({
return pipelineData; return pipelineData;
}); });
setActionMode(ActionMode.Editing); setActionMode(ActionMode.Editing);
setShowSaveButton(ActionMode.Editing);
notifications.error({ notifications.error({
message: 'Error', message: 'Error',
description: response.error || t('something_went_wrong'), description: response.error || t('something_went_wrong'),
}); });
setCurrPipelineData(modifiedPipelineData); setCurrentPipelines(cloneDeep(modifiedPipelineData));
setPrevPipelineData(modifiedPipelineData); setSavedPipelines(cloneDeep(modifiedPipelineData));
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currPipelineData, notifications, refetchPipelineLists, setActionMode, t]); }, [currentPipelines, notifications, refetchPipelineLists, setActionMode, t]);
const onCancelConfigurationHandler = useCallback((): void => { const onCancelConfigurationHandler = useCallback((): void => {
setActionMode(ActionMode.Viewing); setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined); savedPipelines.forEach((item, index) => {
prevPipelineData.forEach((item, index) => {
const obj = item; const obj = item;
obj.orderId = index + 1; obj.orderId = index + 1;
if (obj.config) { if (obj.config) {
@@ -446,9 +425,9 @@ function PipelineListsView({
} }
} }
}); });
setCurrPipelineData(prevPipelineData); setCurrentPipelines(cloneDeep(savedPipelines));
setExpandedRowKeys([]); setExpandedRowKeys([]);
}, [prevPipelineData, setActionMode]); }, [savedPipelines, setCurrentPipelines, setActionMode]);
const onRowHandler = ( const onRowHandler = (
_data: PipelineData, _data: PipelineData,
@@ -473,25 +452,23 @@ function PipelineListsView({
isActionType={isActionType} isActionType={isActionType}
setActionType={setActionType} setActionType={setActionType}
selectedPipelineData={selectedPipelineData} selectedPipelineData={selectedPipelineData}
setShowSaveButton={setShowSaveButton} setCurrPipelineData={setCurrentPipelines}
setCurrPipelineData={setCurrPipelineData} currPipelineData={currentPipelines}
currPipelineData={currPipelineData}
/> />
<AddNewProcessor <AddNewProcessor
isActionType={isActionType} isActionType={isActionType}
setActionType={setActionType} setActionType={setActionType}
selectedProcessorData={selectedProcessorData} selectedProcessorData={selectedProcessorData}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData()} expandedPipelineData={expandedPipelineData()}
setExpandedPipelineData={setExpandedPipelineData} setExpandedPipelineData={setExpandedPipelineData}
/> />
{prevPipelineData?.length > 0 || currPipelineData?.length > 0 ? ( {savedPipelines?.length > 0 || currentPipelines?.length > 0 ? (
<> <>
<PipelinesSearchSection setPipelineSearchValue={setPipelineSearchValue} /> <PipelinesSearchSection setPipelineSearchValue={setPipelineSearchValue} />
<Container> <Container>
<ModeAndConfiguration <ModeAndConfiguration
isActionMode={isActionMode} isActionMode={isActionMode}
version={pipelineData?.version} version={savedPipelinesVersion}
/> />
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Table <Table
@@ -508,7 +485,7 @@ function PipelineListsView({
</DndProvider> </DndProvider>
{isEditingActionMode && ( {isEditingActionMode && (
<SaveConfigButton <SaveConfigButton
showSaveButton={Boolean(showSaveButton)} showSaveButton={!isEqual(currentPipelines, savedPipelines)}
onSaveConfigurationHandler={onSaveConfigurationHandler} onSaveConfigurationHandler={onSaveConfigurationHandler}
onCancelConfigurationHandler={onCancelConfigurationHandler} onCancelConfigurationHandler={onCancelConfigurationHandler}
/> />
@@ -529,7 +506,13 @@ interface PipelineListsViewProps {
setActionType: (actionType?: ActionType) => void; setActionType: (actionType?: ActionType) => void;
isActionMode: string; isActionMode: string;
setActionMode: (actionMode: ActionMode) => void; setActionMode: (actionMode: ActionMode) => void;
pipelineData: Pipeline; savedPipelinesVersion: number | string;
savedPipelines: Array<PipelineData>;
setSavedPipelines: (value: React.SetStateAction<Array<PipelineData>>) => void;
currentPipelines: Array<PipelineData>;
setCurrentPipelines: (
value: React.SetStateAction<Array<PipelineData>>,
) => void;
refetchPipelineLists: VoidFunction; refetchPipelineLists: VoidFunction;
} }

View File

@@ -12,7 +12,7 @@ export const ButtonContainer = styled.div`
export const CustomButton = styled(Button)` export const CustomButton = styled(Button)`
&&& { &&& {
margin-left: 1rem; margin-left: 0.5rem;
} }
`; `;

View File

@@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18'; import i18n from 'ReactI18';
import store from 'store'; import store from 'store';
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton'; import PipelinesActions from '../Layouts/Pipeline/PipelinesActions';
import { pipelineApiResponseMockData } from '../mocks/pipeline'; import { pipelineApiResponseMockData } from '../mocks/pipeline';
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
@@ -14,7 +14,7 @@ describe('PipelinePage container test', () => {
<MemoryRouter> <MemoryRouter>
<Provider store={store}> <Provider store={store}>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<CreatePipelineButton <PipelinesActions
setActionType={jest.fn()} setActionType={jest.fn()}
isActionMode="viewing-mode" isActionMode="viewing-mode"
setActionMode={jest.fn()} setActionMode={jest.fn()}