feat: pipeline page (#2168) (#3185)

* feat: pipeline page (#2168)

* feat: Added POC of drag row table

* fix: resolved eslint issue

* fix: resolved webpack issue

* fix: config changes

* fix: removed unwanted code of antd table

* feat: added icon on expand row

* feat: ui of modal, alertbox & drag table

* feat: added DraggableTableRow component

* fix: issue on row reorder alert message

* fix: styling & dynamic name when delete pipeline

* feat: added edit modal ui

* fix: modal on create or edit open issue

* fix: types issue

* fix: text change & styled component

* fix: added react-i18next to translate constant

* fix: removed webpack change

* fix: webpack change

* feat: added processor expand row poc

* fix: linting issue

* fix: sonar gate issues

* fix: processor expand ui break issue

* fix: added missing types

* feat: added create & delete logic

* fix: types issue

* feat: added edit pipeline & processor logic

* fix: added diff. local file for pipeline

* fix: suggested changes for pipeline

* fix: order of key name on useTranslation

* test: added test cases

* fix: code level changes

* fix: code level changes

* fix: edit tags issue

* fix: changed inline function to handler

* test: resolved test cases issue

* fix: code level changes

* fix: changed file structure

* fix: added required styled component

* feat: added common utils functions

* fix: code level changes

* test: added test cases

* test: added more test cases

* fix: abstracted code of pipeline column

* fix: added utils for DraggableTableRow

* fix: issue on drag at DraggableTableRow

* test: added more test case

* fix: abstracted code of processor column

* fix: removed playwrite test

* fix: abstracted code render method

* fix: text correction pipline -> pipeline

* test: added more test cases

* fix: add pipeline form restructure

* fix: add processor form restructure

* fix: processor type issue

* fix: forms abstraction

* fix: on finish form abstraction

* test: additional test cases of utils

* feat: added new ui as per save config

* fix: test cases issue

* feat: added redux for data set managment

* fix: updated logic of redux

* fix: removed unused code

* fix: modified pipeline data onchangeof processor data

* fix: removed redux from pipeline

* fix: test cases prop issue resolved

* fix: reset field on add data

* fix: sonar gate code smell

* fix: sonar gate code duplicated issue

* fix: code level changes

* fix: add processor issue

* fix: code level changes

* chore: some of the types are updated

* fix: inline css into styled component

* fix: jsx element & type

* fix: username, email object issue

* fix: username, email object issue

* fix: types issues

* fix: inline condition removed

* fix: code level changes

* feat: integrated listing of pipeline & processor api

* feat: integrated post api of pipeline & processor poc

* feat: integrated delete api of pipeline & processor

* fix: create pipeline api payload issue

* fix: updated jest test cases

* fix: processor order id ui issue

* fix: create pipeline issue on payload

* fix: add processor payload issue resolved

* fix: added missing field on add pipeline

* fix: processor type selection issue

* fix: test cases updated

* fix: sonar gate failed issue

* fix: removed inline function

* fix: enable switch logic at pipeline & processor level

* fix: retain removed from type list

* fix: build issue on jest

* fix: test cases updated

* chore: config is updated

* chore: test snapshot is updated

* fix: test cases updated

* chore: test snapshot is updated

* chore: test snapshot is updated

* fix: api & ui integration of change history tab

* chore: webpack is updated

* test: test is updated

* chore: build is fixed

* chore: react-dnd is downgraded

* chore: process is added

* chore: build is fixed

* chore: react-dnd is updated

* fix: suggested changes

* fix: tab pane issue

* fix: build issue

* fix: code level changes

* fix: code level changes

* fix: added types in def file

* fix: code level changes

* fix: test cases updated

* fix: error message notification

* fix: after reorder pipeline expand is not working

* fix: on add of processor added optional field

* feat: added search pipeline feature

* fix: sonar gate failed issue

* fix: processor reorder issue

* fix: processor reorder output property issue

* feat: added json_parser processor

* fix: scalable code of component of column

* fix: processor reorder issue

* fix: search pipeline issue

* fix: creating a pipeline description is an optional field

* fix: nitya's suggested changes

* fix: test cases updated

* fix: edit data pipeline & processor

* fix: pipeline cancel issue

* fix: edit processor wrong payload

* fix: processor reorder issue at payload

* fix: pipeline undefined handle

* fix: pipeline no data case

* fix: updated test case

* fix: resolved pipeline undefined issue

* fix: processor data case

* feat: added submenu system for pipeline

* fix: pipeline suggested changes

* fix: updated test case

* fix: pipeline suggested changes

* fix: test cases updated

* test: updated test cases

* chore: build issue

* fix: pipeline level changes

* fix: pipeline page access issue

* fix: resolved issue on add operator when pipeline is empty

* test: jest test cases updated

* chore: try signoz cloud link is updated (#2928)

* fix: solve history page issue

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Pranay Prateek <pranay@signoz.io>

* chore: merge conflicts is resolved

* test: snaps are updated

* fix: remove unused dependency on process^0.11.10

---------

Co-authored-by: Chintan Sudani <46838508+techchintan@users.noreply.github.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Pranay Prateek <pranay@signoz.io>
Co-authored-by: Raj <rkssisodiya@gmail.com>
This commit is contained in:
Palash Gupta 2023-08-02 11:22:24 +05:30 committed by GitHub
parent fafa6f9960
commit 2cdafa0564
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
103 changed files with 4871 additions and 12 deletions

View File

@ -21,7 +21,9 @@ const config: Config.InitialOptions = {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: ['node_modules/(?!(lodash-es)/)'],
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],

View File

@ -69,6 +69,9 @@
"mini-css-extract-plugin": "2.4.5",
"papaparse": "5.4.1",
"react": "18.2.0",
"react-addons-update": "15.6.3",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-drag-listview": "2.0.0",
"react-force-graph": "^1.41.0",
@ -134,6 +137,7 @@
"@types/node": "^16.10.3",
"@types/papaparse": "5.3.7",
"@types/react": "18.0.26",
"@types/react-addons-update": "0.14.21",
"@types/react-dom": "18.0.10",
"@types/react-grid-layout": "^1.1.2",
"@types/react-redux": "^7.1.11",

View File

@ -5,5 +5,8 @@
"my_settings": "My Settings",
"overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls",
"external_metrics": "External Calls"
"external_metrics": "External Calls",
"pipelines": "Pipelines",
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics"
}

View File

@ -12,6 +12,7 @@
"routes": {
"general": "General",
"alert_channels": "Alert Channels",
"all_errors": "All Exceptions"
"all_errors": "All Exceptions",
"pipelines": "Pipelines"
}
}

View File

@ -0,0 +1,44 @@
{
"delete": "Delete",
"filter": "Filter",
"update": "Update",
"create": "Create",
"reorder": "Reorder",
"cancel": "Cancel",
"reorder_pipeline": "Do you want to reorder pipeline?",
"reorder_pipeline_description": "Logs are processed sequentially in processors and pipelines. Reordering it may change how data is processed by them.",
"delete_pipeline": "Do you want to delete pipeline",
"delete_pipeline_description": "Logs are processed sequentially in processors and pipelines. Deleting a pipeline may change content of data processed by other pipelines & processors",
"add_new_pipeline": "Add a New Pipeline",
"new_pipeline": "New Pipeline",
"enter_edit_mode": "Enter Edit Mode",
"save_configuration": "Save Configuration",
"edit_pipeline": "Edit Pipeline",
"create_pipeline": "Create New Pipeline",
"add_new_processor": "Add Processor",
"edit_processor": "Edit Processor",
"create_processor": "Create New Processor",
"processor_type": "Select Processor Type",
"reorder_processor": "Do you want to reorder processor?",
"reorder_processor_description": "Logs are processed sequentially in processors. Reordering it may change how data is processed by them.",
"delete_processor": "Do you want to delete processor",
"delete_processor_description": "Logs are processed sequentially in processors. Deleting a processor may change content of data processed by other processors",
"search_pipeline_placeholder": "Filter Pipelines",
"pipeline_name_placeholder": "Name",
"pipeline_tags_placeholder": "Tags",
"pipeline_description_placeholder": "Enter description for your pipeline",
"processor_name_placeholder": "Name",
"processor_regex_placeholder": "Regex",
"processor_parsefrom_placeholder": "Parse From",
"processor_parseto_placeholder": "Parse From",
"processor_onerror_placeholder": "on Error",
"processor_pattern_placeholder": "Pattern",
"processor_field_placeholder": "Field",
"processor_value_placeholder": "Value",
"processor_description_placeholder": "example rule: %{word:first}",
"processor_trace_id_placeholder": "Trace Id Parce From",
"processor_span_id_placeholder": "Span id Parse From",
"processor_trace_flags_placeholder": "Trace flags parse from",
"processor_from_placeholder": "From",
"processor_to_placeholder": "To"
}

View File

@ -5,5 +5,8 @@
"my_settings": "My Settings",
"overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls",
"external_metrics": "External Calls"
"external_metrics": "External Calls",
"pipelines": "Pipelines",
"archives": "Archives",
"logs_to_metrics": "Logs To Metrics"
}

View File

@ -12,6 +12,7 @@
"routes": {
"general": "General",
"alert_channels": "Alert Channels",
"all_errors": "All Exceptions"
"all_errors": "All Exceptions",
"pipelines": "Pipelines"
}
}

View File

@ -132,3 +132,7 @@ export const SomethingWentWrong = Loadable(
export const LicensePage = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/License'),
);
export const PipelinePage = Loadable(
() => import(/* webpackChunkName: "Pipelines" */ 'pages/Pipelines'),
);

View File

@ -21,6 +21,7 @@ import {
NewDashboardPage,
OrganizationSettings,
PasswordReset,
PipelinePage,
ServiceMapPage,
ServiceMetricsPage,
ServicesTablePage,
@ -253,6 +254,13 @@ const routes: AppRoutes[] = [
key: 'SOMETHING_WENT_WRONG',
isPrivate: false,
},
{
path: ROUTES.PIPELINES,
exact: true,
component: PipelinePage,
key: 'PIPELINES',
isPrivate: true,
},
];
export interface AppRoutes {

View File

@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import { Props } from 'types/api/pipeline/get';
const get = async (
props: Props,
): Promise<SuccessResponse<Pipeline> | ErrorResponse> => {
try {
const response = await axios.get(`/logs/pipelines/${props.version}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response?.data?.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default get;

View File

@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import { Props } from 'types/api/pipeline/post';
const post = async (
props: Props,
): Promise<SuccessResponse<Pipeline> | ErrorResponse> => {
try {
const response = await axios.post('/logs/pipelines', props.data);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default post;

View File

@ -0,0 +1,54 @@
import React, { useCallback, useRef } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { dragHandler, dropHandler } from './utils';
const type = 'DraggableTableRow';
function DraggableTableRow({
index,
moveRow,
className,
style,
...restProps
}: DraggableTableRowProps): JSX.Element {
const ref = useRef<HTMLTableRowElement>(null);
const handleDrop = useCallback(
(item: { index: number }) => {
if (moveRow) moveRow(item.index, index);
},
[moveRow, index],
);
const [, drop] = useDrop({
accept: type,
collect: dropHandler,
drop: handleDrop,
});
const [, drag] = useDrag({
type,
item: { index },
collect: dragHandler,
});
drop(drag(ref));
return (
<tr
ref={ref}
className={className}
style={{ ...style }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...restProps}
/>
);
}
interface DraggableTableRowProps
extends React.HTMLAttributes<HTMLTableRowElement> {
index: number;
moveRow: (dragIndex: number, hoverIndex: number) => void;
}
export default DraggableTableRow;

View File

@ -0,0 +1,38 @@
import { render } from '@testing-library/react';
import { Table } from 'antd';
import { matchMedia } from 'container/PipelinePage/tests/AddNewPipeline.test';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import i18n from 'ReactI18';
import store from 'store';
import DraggableTableRow from '..';
beforeAll(() => {
matchMedia();
});
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
}));
describe('DraggableTableRow Snapshot test', () => {
it('should render DraggableTableRow', async () => {
const { asFragment } = render(
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<Table
components={{
body: {
row: DraggableTableRow,
},
}}
pagination={false}
/>
</I18nextProvider>
</Provider>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,103 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
<DocumentFragment>
<div
class="ant-table-wrapper css-dev-only-do-not-override-1i536d8"
>
<div
class="ant-spin-nested-loading css-dev-only-do-not-override-1i536d8"
>
<div
class="ant-spin-container"
>
<div
class="ant-table ant-table-empty"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
>
<table
style="table-layout: auto;"
>
<colgroup />
<thead
class="ant-table-thead"
>
<tr>
<th
class="ant-table-cell"
/>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
class="ant-table-placeholder"
>
<td
class="ant-table-cell"
>
<div
class="css-dev-only-do-not-override-1i536d8 ant-empty ant-empty-normal"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;

View File

@ -0,0 +1,44 @@
import { dragHandler, dropHandler } from '../utils';
jest.mock('react-dnd', () => ({
useDrop: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
useDrag: jest.fn().mockImplementation(() => [jest.fn(), jest.fn(), jest.fn()]),
}));
describe('Utils testing of DraggableTableRow component', () => {
test('Should dropHandler return true', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(true),
} as never;
const dropDataTruthy = dropHandler(monitor);
expect(dropDataTruthy).toEqual({ isOver: true });
});
test('Should dropHandler return false', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(false),
} as never;
const dropDataFalsy = dropHandler(monitor);
expect(dropDataFalsy).toEqual({ isOver: false });
});
test('Should dragHandler return true', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(true),
} as never;
const dragDataTruthy = dragHandler(monitor);
expect(dragDataTruthy).toEqual({ isDragging: true });
});
test('Should dragHandler return false', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(false),
} as never;
const dragDataFalsy = dragHandler(monitor);
expect(dragDataFalsy).toEqual({ isDragging: false });
});
});

View File

@ -0,0 +1,15 @@
import { DragSourceMonitor, DropTargetMonitor } from 'react-dnd';
export function dropHandler(monitor: DropTargetMonitor): { isOver: boolean } {
return {
isOver: monitor.isOver(),
};
}
export function dragHandler(
monitor: DragSourceMonitor,
): { isDragging: boolean } {
return {
isDragging: monitor.isDragging(),
};
}

View File

@ -32,6 +32,8 @@ const ROUTES = {
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses',
TRACE_EXPLORER: '/trace-explorer',
PIPELINES: '/pipelines',
};
export default ROUTES;

View File

@ -44,6 +44,12 @@ const themeColors = {
lightWhite: '#ffffffd9',
borderLightGrey: '#d9d9d9',
borderDarkGrey: '#424242',
gainsboro: '#DBDBDB',
navyBlue: '#1668DC',
lightSkyBlue: '#8DCFF8',
neroBlack: '#1d1d1d',
snowWhite: '#fafafa',
gamboge: '#D89614',
bckgGrey: '#1d1d1d',
};

View File

@ -0,0 +1,14 @@
import { IconDataSpan } from 'container/PipelinePage/styles';
import { getDeploymentStage, getDeploymentStageIcon } from './utils';
function DeploymentStage(deployStatus: string): JSX.Element {
return (
<>
{getDeploymentStageIcon(deployStatus)}
<IconDataSpan>{getDeploymentStage(deployStatus)}</IconDataSpan>
</>
);
}
export default DeploymentStage;

View File

@ -0,0 +1,9 @@
import dayjs from 'dayjs';
function DeploymentTime(deployTime: string): JSX.Element {
return (
<span>{dayjs(deployTime).locale('en').format('MMMM DD, YYYY hh:mm A')}</span>
);
}
export default DeploymentTime;

View File

@ -0,0 +1,24 @@
import { Table } from 'antd';
import { Pipeline } from 'types/api/pipeline/def';
import { changeHistoryColumns } from '../../PipelineListsView/config';
import { HistoryTableWrapper } from '../../styles';
import { historyPagination } from '../config';
function ChangeHistory({ piplineData }: ChangeHistoryProps): JSX.Element {
return (
<HistoryTableWrapper>
<Table
columns={changeHistoryColumns}
dataSource={piplineData?.history ?? []}
pagination={historyPagination}
/>
</HistoryTableWrapper>
);
}
interface ChangeHistoryProps {
piplineData: Pipeline;
}
export default ChangeHistory;

View File

@ -0,0 +1,39 @@
import {
CheckCircleFilled,
CloseCircleFilled,
ExclamationCircleFilled,
LoadingOutlined,
} from '@ant-design/icons';
import { Spin } from 'antd';
export function getDeploymentStage(value: string): string {
switch (value) {
case 'IN_PROGRESS':
return 'In Progress';
case 'DEPLOYED':
return 'Deployed';
case 'DIRTY':
return 'Dirty';
case 'FAILED':
return 'Failed';
default:
return '';
}
}
export function getDeploymentStageIcon(value: string): JSX.Element {
switch (value) {
case 'IN_PROGRESS':
return (
<Spin indicator={<LoadingOutlined style={{ fontSize: 15 }} spin />} />
);
case 'DEPLOYED':
return <CheckCircleFilled />;
case 'DIRTY':
return <ExclamationCircleFilled />;
case 'FAILED':
return <CloseCircleFilled />;
default:
return <span />;
}
}

View File

@ -0,0 +1,62 @@
import { EditFilled, PlusOutlined } from '@ant-design/icons';
import TextToolTip from 'components/TextToolTip';
import { useCallback, 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,
piplineData,
}: CreatePipelineButtonProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const isAddNewPipelineVisible = useMemo(
() => checkDataLength(piplineData?.pipelines),
[piplineData?.pipelines],
);
const isDisabled = isActionMode === ActionMode.Editing;
const actionHandler = useCallback(
(action: string, setStateFunc: (action: string) => void) => (): void =>
setStateFunc(action),
[],
);
return (
<ButtonContainer>
<TextToolTip text={t('add_new_pipeline')} />
{isAddNewPipelineVisible && (
<CustomButton
icon={<EditFilled />}
onClick={actionHandler(ActionMode.Editing, setActionMode)}
disabled={isDisabled}
>
{t('enter_edit_mode')}
</CustomButton>
)}
{!isAddNewPipelineVisible && (
<CustomButton
icon={<PlusOutlined />}
onClick={actionHandler(ActionType.AddPipeline, setActionType)}
type="primary"
>
{t('new_pipeline')}
</CustomButton>
)}
</ButtonContainer>
);
}
interface CreatePipelineButtonProps {
setActionType: (actionType: string) => void;
isActionMode: string;
setActionMode: (actionMode: string) => void;
piplineData: Pipeline;
}
export default CreatePipelineButton;

View File

@ -0,0 +1,30 @@
import { Input } from 'antd';
import React, { Dispatch, SetStateAction, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
function PipelinesSearchSection({
setPipelineSearchValue,
}: PipelinesSearchSectionProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const onSeachHandler = useCallback(
(event: React.SetStateAction<string>) => {
setPipelineSearchValue(event);
},
[setPipelineSearchValue],
);
return (
<Input.Search
allowClear
placeholder={t('search_pipeline_placeholder')}
onSearch={onSeachHandler}
/>
);
}
interface PipelinesSearchSectionProps {
setPipelineSearchValue: Dispatch<SetStateAction<string>>;
}
export default PipelinesSearchSection;

View File

@ -0,0 +1,43 @@
import { useState } from 'react';
import { Pipeline } from 'types/api/pipeline/def';
import PipelineListsView from '../../PipelineListsView';
import CreatePipelineButton from './CreatePipelineButton';
import PipelinesSearchSection from './PipelinesSearchSection';
function PipelinePageLayout({
refetchPipelineLists,
piplineData,
}: PipelinePageLayoutProps): JSX.Element {
const [isActionType, setActionType] = useState<string>();
const [isActionMode, setActionMode] = useState<string>('viewing-mode');
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
return (
<>
<CreatePipelineButton
setActionType={setActionType}
setActionMode={setActionMode}
isActionMode={isActionMode}
piplineData={piplineData}
/>
<PipelinesSearchSection setPipelineSearchValue={setPipelineSearchValue} />
<PipelineListsView
isActionType={String(isActionType)}
setActionType={setActionType}
setActionMode={setActionMode}
isActionMode={isActionMode}
piplineData={piplineData}
refetchPipelineLists={refetchPipelineLists}
pipelineSearchValue={pipelineSearchValue}
/>
</>
);
}
interface PipelinePageLayoutProps {
refetchPipelineLists: VoidFunction;
piplineData: Pipeline;
}
export default PipelinePageLayout;

View File

@ -0,0 +1,3 @@
export const historyPagination = {
defaultPageSize: 5,
};

View File

@ -0,0 +1,4 @@
import { PipelineData } from 'types/api/pipeline/def';
export const checkDataLength = (data: Array<PipelineData>): boolean =>
data?.length > 0;

View File

@ -0,0 +1,31 @@
import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { FormLabelStyle } from '../styles';
function DescriptionTextArea({
fieldData,
}: DescriptionTextAreaProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Form.Item
required={false}
name={fieldData.name}
label={<FormLabelStyle>{fieldData.fieldName}</FormLabelStyle>}
key={fieldData.id}
>
<Input.TextArea
rows={3}
name={fieldData.name}
placeholder={t(fieldData.placeholder)}
/>
</Form.Item>
);
}
interface DescriptionTextAreaProps {
fieldData: ProcessorFormField;
}
export default DescriptionTextArea;

View File

@ -0,0 +1,31 @@
import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { formValidationRules } from '../../config';
import { FormLabelStyle } from '../styles';
function FilterSearch({ fieldData }: FilterSearchProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Form.Item
required={false}
label={<FormLabelStyle>{fieldData.fieldName}</FormLabelStyle>}
key={fieldData.id}
rules={formValidationRules}
name={fieldData.name}
>
<Input.Search
id={fieldData.id.toString()}
name={fieldData.name}
placeholder={t(fieldData.placeholder)}
allowClear
/>
</Form.Item>
);
}
interface FilterSearchProps {
fieldData: ProcessorFormField;
}
export default FilterSearch;

View File

@ -0,0 +1,27 @@
import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { formValidationRules } from '../../config';
import { FormLabelStyle } from '../styles';
function NameInput({ fieldData }: NameInputProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Form.Item
required={false}
label={<FormLabelStyle>{fieldData.fieldName}</FormLabelStyle>}
key={fieldData.id}
rules={formValidationRules}
name={fieldData.name}
>
<Input name={fieldData.name} placeholder={t(fieldData.placeholder)} />
</Form.Item>
);
}
interface NameInputProps {
fieldData: ProcessorFormField;
}
export default NameInput;

View File

@ -0,0 +1,36 @@
import { Form } from 'antd';
import TagInput from 'container/PipelinePage/components/TagInput';
import { useTranslation } from 'react-i18next';
import { ProcessorFormField } from '../../AddNewProcessor/config';
import { FormLabelStyle } from '../styles';
function ProcessorTags({
fieldData,
setTagsListData,
tagsListData,
}: ProcessorTagsProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Form.Item
required={false}
label={<FormLabelStyle>{fieldData.fieldName}</FormLabelStyle>}
key={fieldData.id}
name={fieldData.name}
>
<TagInput
setTagsListData={setTagsListData}
tagsListData={tagsListData}
placeHolder={t(fieldData.placeholder)}
/>
</Form.Item>
);
}
interface ProcessorTagsProps {
fieldData: ProcessorFormField;
setTagsListData: (tags: Array<string>) => void;
tagsListData: Array<string>;
}
export default ProcessorTags;

View File

@ -0,0 +1,150 @@
import { Button, Divider, Form, Modal } from 'antd';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ActionMode, ActionType, PipelineData } from 'types/api/pipeline/def';
import AppReducer from 'types/reducer/app';
import { v4 } from 'uuid';
import { ModalButtonWrapper, ModalTitle } from '../styles';
import { getEditedDataSource, getRecordIndex } from '../utils';
import { renderPipelineForm } from './utils';
function AddNewPipeline({
isActionType,
setActionType,
selectedPipelineData,
setShowSaveButton,
setCurrPipelineData,
currPipelineData,
}: AddNewPipelineProps): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation('pipeline');
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const isEdit = isActionType === 'edit-pipeline';
const isAdd = isActionType === 'add-pipeline';
useEffect(() => {
if (isEdit) {
form.setFieldsValue(selectedPipelineData);
}
if (isAdd) {
form.resetFields();
}
}, [form, isEdit, isAdd, selectedPipelineData]);
const onFinish = (values: PipelineData): void => {
const newPipeLineData: PipelineData = {
id: v4(),
orderId: (currPipelineData?.length || 0) + 1,
createdAt: new Date().toISOString(),
createdBy: user?.name || '',
name: values.name,
alias: values.name.replace(/\s/g, ''),
description: values.description,
filter: values.filter,
config: [],
enabled: true,
};
if (isEdit && selectedPipelineData) {
const findRecordIndex = getRecordIndex(
currPipelineData,
selectedPipelineData,
'id',
);
const updatedPipelineData: PipelineData = {
...currPipelineData[findRecordIndex],
...values,
};
const editedPipelineData = getEditedDataSource(
currPipelineData,
selectedPipelineData,
'id',
updatedPipelineData,
);
setCurrPipelineData(editedPipelineData);
}
if (isAdd) {
setCurrPipelineData((prevState) => {
if (prevState) return [...prevState, newPipeLineData];
return [newPipeLineData];
});
}
setActionType(undefined);
};
const onCancelModalHandler = (): void => {
setActionType(undefined);
};
const modalTitle = useMemo(
(): string =>
isEdit
? `${t('edit_pipeline')} : ${selectedPipelineData?.name}`
: t('create_pipeline'),
[isEdit, selectedPipelineData?.name, t],
);
const onOkModalHandler = useCallback(
() => setShowSaveButton(ActionMode.Editing),
[setShowSaveButton],
);
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
return (
<Modal
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
centered
open={isOpen}
width={800}
footer={null}
onCancel={onCancelModalHandler}
>
<Divider plain />
<Form
name="add-new-pipeline"
layout="vertical"
onFinish={onFinish}
autoComplete="off"
form={form}
>
{renderPipelineForm()}
<Divider plain />
<Form.Item>
<ModalButtonWrapper>
<Button
key="submit"
type="primary"
htmlType="submit"
onClick={onOkModalHandler}
>
{isEdit ? t('update') : t('create')}
</Button>
<Button key="cancel" onClick={onCancelModalHandler}>
{t('cancel')}
</Button>
</ModalButtonWrapper>
</Form.Item>
</Form>
</Modal>
);
}
interface AddNewPipelineProps {
isActionType: string;
setActionType: (actionType?: ActionType) => void;
selectedPipelineData: PipelineData | undefined;
setShowSaveButton: (actionMode: ActionMode) => void;
setCurrPipelineData: (
value: React.SetStateAction<Array<PipelineData>>,
) => void;
currPipelineData: Array<PipelineData>;
}
export default AddNewPipeline;

View File

@ -0,0 +1,7 @@
import styled from 'styled-components';
export const FormLabelStyle = styled.span`
font-size: 0.75rem;
font-weight: 400;
line-height: 1.25rem;
`;

View File

@ -0,0 +1,7 @@
import { pipelineFields } from '../config';
export const renderPipelineForm = (): Array<JSX.Element> =>
pipelineFields.map((field) => {
const Component = field.component;
return <Component key={field.id} fieldData={field} />;
});

View File

@ -0,0 +1,36 @@
import { Form, Input } from 'antd';
import { ModalFooterTitle } from 'container/PipelinePage/styles';
import { useTranslation } from 'react-i18next';
import { formValidationRules } from '../../config';
import { ProcessorFormField } from '../config';
import { Container, FormWrapper, PipelineIndexIcon } from '../styles';
function NameInput({ fieldData }: NameInputProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Container>
<PipelineIndexIcon size="small">
{Number(fieldData.id) + 1}
</PipelineIndexIcon>
<FormWrapper>
<Form.Item
required={false}
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
key={fieldData.id}
name={fieldData.name}
initialValue={fieldData.initialValue}
rules={fieldData.rules ? fieldData.rules : formValidationRules}
>
<Input placeholder={t(fieldData.placeholder)} name={fieldData.name} />
</Form.Item>
</FormWrapper>
</Container>
);
}
interface NameInputProps {
fieldData: ProcessorFormField;
}
export default NameInput;

View File

@ -0,0 +1,37 @@
import { Form, Input } from 'antd';
import { ModalFooterTitle } from 'container/PipelinePage/styles';
import { useTranslation } from 'react-i18next';
import { ProcessorFormField } from '../config';
import { Container, FormWrapper, PipelineIndexIcon } from '../styles';
function ParsingRulesTextArea({
fieldData,
}: ParsingRulesTextAreaProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<Container>
<PipelineIndexIcon size="small">
{Number(fieldData.id) + 1}
</PipelineIndexIcon>
<FormWrapper>
<Form.Item
name={fieldData.name}
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
>
<Input.TextArea
rows={4}
name={fieldData.name}
placeholder={t(fieldData.placeholder)}
/>
</Form.Item>
</FormWrapper>
</Container>
);
}
interface ParsingRulesTextAreaProps {
fieldData: ProcessorFormField;
}
export default ParsingRulesTextArea;

View File

@ -0,0 +1,44 @@
import { Select } from 'antd';
import { useTranslation } from 'react-i18next';
import { DEFAULT_PROCESSOR_TYPE, processorTypes } from '../config';
import {
PipelineIndexIcon,
ProcessorType,
ProcessorTypeContainer,
ProcessorTypeWrapper,
StyledSelect,
} from '../styles';
function TypeSelect({ onChange, value }: TypeSelectProps): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<ProcessorTypeWrapper>
<PipelineIndexIcon size="small">1</PipelineIndexIcon>
<ProcessorTypeContainer>
<ProcessorType>{t('processor_type')}</ProcessorType>
<StyledSelect
onChange={(value: string | unknown): void => onChange(value)}
value={value}
>
{processorTypes.map(({ value, label }) => (
<Select.Option key={value + label} value={value}>
{label}
</Select.Option>
))}
</StyledSelect>
</ProcessorTypeContainer>
</ProcessorTypeWrapper>
);
}
TypeSelect.defaultProps = {
value: DEFAULT_PROCESSOR_TYPE,
};
interface TypeSelectProps {
onChange: (value: string | unknown) => void;
value?: string;
}
export default TypeSelect;

View File

@ -0,0 +1,226 @@
type ProcessorType = {
key: string;
value: string;
label: string;
title?: string;
disabled?: boolean;
};
export const processorTypes: Array<ProcessorType> = [
{ key: 'grok_parser', value: 'grok_parser', label: 'Grok' },
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
{ key: 'regex_parser', value: 'regex_parser', label: 'Regex' },
{ key: 'add', value: 'add', label: 'Add' },
{ key: 'remove', value: 'remove', label: 'Remove' },
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
// { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion
{ key: 'move', value: 'move', label: 'Move' },
{ key: 'copy', value: 'copy', label: 'Copy' },
];
export const DEFAULT_PROCESSOR_TYPE = processorTypes[0].value;
export type ProcessorFormField = {
id: number;
fieldName: string;
placeholder: string;
name: string;
rules?: Array<{ [key: string]: boolean }>;
initialValue?: string;
};
const commonFields = [
{
id: 3,
fieldName: 'Parse From',
placeholder: 'processor_parsefrom_placeholder',
name: 'parse_from', // optional
rules: [],
initialValue: 'body',
},
{
id: 4,
fieldName: 'Parse To',
placeholder: 'processor_parseto_placeholder',
name: 'parse_to', // optional
rules: [],
initialValue: 'attributes',
},
{
id: 5,
fieldName: 'On Error',
placeholder: 'processor_onerror_placeholder',
name: 'on_error', // optional
rules: [],
initialValue: 'send',
},
];
export const processorFields: { [key: string]: Array<ProcessorFormField> } = {
grok_parser: [
{
id: 1,
fieldName: 'Name of Grok Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Pattern',
placeholder: 'processor_pattern_placeholder',
name: 'pattern',
},
...commonFields,
],
json_parser: [
{
id: 1,
fieldName: 'Name of Json Parser Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Parse From',
placeholder: 'processor_parsefrom_placeholder',
name: 'parse_from',
initialValue: 'body',
},
{
id: 3,
fieldName: 'Parse To',
placeholder: 'processor_parseto_placeholder',
name: 'parse_to',
initialValue: 'attributes',
},
],
regex_parser: [
{
id: 1,
fieldName: 'Name of Regex Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Define Regex',
placeholder: 'processor_regex_placeholder',
name: 'regex',
},
...commonFields,
],
add: [
{
id: 1,
fieldName: 'Name of Add Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Field',
placeholder: 'processor_field_placeholder',
name: 'field',
},
{
id: 3,
fieldName: 'Value',
placeholder: 'processor_value_placeholder',
name: 'value',
},
],
remove: [
{
id: 1,
fieldName: 'Name of Remove Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Field',
placeholder: 'processor_field_placeholder',
name: 'field',
},
],
trace_parser: [
{
id: 1,
fieldName: 'Name of Trace Parser Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Trace Id Parce From',
placeholder: 'processor_trace_id_placeholder',
name: 'traceId',
},
{
id: 3,
fieldName: 'Span id Parse From',
placeholder: 'processor_span_id_placeholder',
name: 'spanId',
},
{
id: 4,
fieldName: 'Trace flags parse from',
placeholder: 'processor_trace_flags_placeholder',
name: 'traceFlags',
},
],
retain: [
{
id: 1,
fieldName: 'Name of Retain Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'Fields',
placeholder: 'processor_fields_placeholder',
name: 'fields',
},
],
move: [
{
id: 1,
fieldName: 'Name of Move Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'From',
placeholder: 'processor_from_placeholder',
name: 'from',
},
{
id: 3,
fieldName: 'To',
placeholder: 'processor_to_placeholder',
name: 'to',
},
],
copy: [
{
id: 1,
fieldName: 'Name of Copy Processor',
placeholder: 'processor_name_placeholder',
name: 'name',
},
{
id: 2,
fieldName: 'From',
placeholder: 'processor_from_placeholder',
name: 'from',
},
{
id: 3,
fieldName: 'To',
placeholder: 'processor_to_placeholder',
name: 'to',
},
],
};

View File

@ -0,0 +1,195 @@
import { Button, Divider, Form, Modal } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActionMode,
ActionType,
PipelineData,
ProcessorData,
} from 'types/api/pipeline/def';
import { ModalButtonWrapper, ModalTitle } from '../styles';
import { getEditedDataSource, getRecordIndex } from '../utils';
import { DEFAULT_PROCESSOR_TYPE } from './config';
import TypeSelect from './FormFields/TypeSelect';
import { renderProcessorForm } from './utils';
function AddNewProcessor({
isActionType,
setActionType,
selectedProcessorData,
setShowSaveButton,
expandedPipelineData,
setExpandedPipelineData,
}: AddNewProcessorProps): JSX.Element {
const [form] = Form.useForm();
const { t } = useTranslation('pipeline');
const [processorType, setProcessorType] = useState<string>(
DEFAULT_PROCESSOR_TYPE,
);
const isEdit = isActionType === 'edit-processor';
const isAdd = isActionType === 'add-processor';
useEffect(() => {
if (isEdit && selectedProcessorData && expandedPipelineData?.config) {
const findRecordIndex = getRecordIndex(
expandedPipelineData?.config,
selectedProcessorData,
'id',
);
const updatedProcessorData = {
...expandedPipelineData?.config?.[findRecordIndex],
};
setProcessorType(updatedProcessorData.type);
form.setFieldsValue(updatedProcessorData);
}
if (isAdd) {
form.resetFields();
}
}, [form, isEdit, isAdd, selectedProcessorData, expandedPipelineData?.config]);
const handleProcessorType = (value: string | unknown): void => {
const typedValue = String(value) || DEFAULT_PROCESSOR_TYPE;
setProcessorType(typedValue);
};
const onFinish = (values: { name: string }): void => {
const totalDataLength = expandedPipelineData?.config?.length || 0;
const newProcessorData = {
id: values.name.replace(/\s/g, ''),
orderId: Number(totalDataLength || 0) + 1,
type: processorType,
enabled: true,
...values,
};
if (isEdit && selectedProcessorData && expandedPipelineData?.config) {
const findRecordIndex = getRecordIndex(
expandedPipelineData?.config,
selectedProcessorData,
'id',
);
const updatedProcessorData = {
id: values.name.replace(/\s/g, ''),
orderId: expandedPipelineData?.config?.[findRecordIndex].orderId,
type: processorType,
enabled: expandedPipelineData?.config?.[findRecordIndex].enabled,
output: expandedPipelineData?.config?.[findRecordIndex].output,
...values,
};
const editedData = getEditedDataSource(
expandedPipelineData.config,
selectedProcessorData,
'name',
updatedProcessorData,
);
const modifiedProcessorData = { ...expandedPipelineData };
modifiedProcessorData.config = editedData;
setExpandedPipelineData(modifiedProcessorData);
}
if (isAdd && expandedPipelineData) {
const modifiedProcessorData = {
...expandedPipelineData,
};
if (
modifiedProcessorData.config !== undefined &&
modifiedProcessorData.config
) {
modifiedProcessorData.config = [
...modifiedProcessorData.config,
newProcessorData,
];
if (totalDataLength > 0) {
modifiedProcessorData.config[totalDataLength - 1].output =
newProcessorData.id;
}
}
setExpandedPipelineData(modifiedProcessorData);
}
setActionType(undefined);
handleProcessorType(DEFAULT_PROCESSOR_TYPE);
};
const onCancelModal = (): void => {
setActionType(undefined);
handleProcessorType(DEFAULT_PROCESSOR_TYPE);
};
const modalTitle = useMemo(
(): string =>
isEdit
? `${t('edit_processor')} ${selectedProcessorData?.name}`
: t('create_processor'),
[isEdit, selectedProcessorData?.name, t],
);
const onOkModalHandler = useCallback(
() => setShowSaveButton(ActionMode.Editing),
[setShowSaveButton],
);
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
return (
<Modal
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
centered
open={isOpen}
width={800}
footer={null}
onCancel={onCancelModal}
>
<Divider plain />
<Form
name="add-new-processor"
layout="vertical"
onFinish={onFinish}
autoComplete="off"
form={form}
>
<TypeSelect value={processorType} onChange={handleProcessorType} />
{renderProcessorForm(processorType)}
<Divider plain />
<Form.Item>
<ModalButtonWrapper>
<Button
key="submit"
type="primary"
htmlType="submit"
onClick={onOkModalHandler}
>
{isEdit ? t('update') : t('create')}
</Button>
<Button key="cancel" onClick={onCancelModal}>
{t('cancel')}
</Button>
</ModalButtonWrapper>
</Form.Item>
</Form>
</Modal>
);
}
AddNewProcessor.defaultProps = {
selectedProcessorData: undefined,
expandedPipelineData: {},
};
interface AddNewProcessorProps {
isActionType: string;
setActionType: (actionType?: ActionType) => void;
selectedProcessorData?: ProcessorData;
setShowSaveButton: (actionMode: ActionMode) => void;
expandedPipelineData?: PipelineData;
setExpandedPipelineData: (data: PipelineData) => void;
}
export default AddNewProcessor;

View File

@ -0,0 +1,46 @@
import { Avatar, Select } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const PipelineIndexIcon = styled(Avatar)`
background-color: ${themeColors.navyBlue};
height: 1.5rem;
width: 1.5rem;
font-size: 0.875rem;
line-height: 1.375rem;
`;
export const ProcessorTypeWrapper = styled.div`
display: flex;
gap: 1rem;
align-items: flex-start;
margin-bottom: 1.5rem;
`;
export const ProcessorTypeContainer = styled.div`
display: flex;
flex-direction: column;
padding-bottom: 0.5rem;
gap: 0.313rem;
`;
export const Container = styled.div`
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 0rem;
gap: 1rem;
width: 100%;
`;
export const FormWrapper = styled.div`
width: 100%;
`;
export const ProcessorType = styled.span`
padding-bottom: 0.5rem;
`;
export const StyledSelect = styled(Select)`
width: 12.5rem;
`;

View File

@ -0,0 +1,9 @@
import { processorFields, ProcessorFormField } from './config';
import NameInput from './FormFields/NameInput';
export const renderProcessorForm = (
processorType: string,
): Array<JSX.Element> =>
processorFields[processorType]?.map((fieldName: ProcessorFormField) => (
<NameInput key={fieldName.id} fieldData={fieldName} />
));

View File

@ -0,0 +1,24 @@
import { ActionMode } from 'types/api/pipeline/def';
import { ModeAndConfigWrapper } from './styles';
function ModeAndConfiguration({
isActionMode,
verison,
}: ModeAndConfigurationType): JSX.Element {
const actionMode = isActionMode === ActionMode.Editing;
return (
<ModeAndConfigWrapper>
Mode: <span>{actionMode ? 'Editing' : 'Viewing'}</span>
<div>Configuration Version: {verison}</div>
</ModeAndConfigWrapper>
);
}
export interface ModeAndConfigurationType {
isActionMode: string;
verison: string | number;
}
export default ModeAndConfiguration;

View File

@ -0,0 +1,269 @@
import { PlusCircleOutlined } from '@ant-design/icons';
import { TableLocale } from 'antd/es/table/interface';
import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useMemo } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import {
ActionMode,
ActionType,
PipelineData,
ProcessorData,
} from 'types/api/pipeline/def';
import { tableComponents } from '../config';
import { ModalFooterTitle } from '../styles';
import { AlertMessage } from '.';
import { processorColumns } from './config';
import { FooterButton, StyledTable } from './styles';
import DragAction from './TableComponents/DragAction';
import PipelineActions from './TableComponents/PipelineActions';
import {
getEditedDataSource,
getProcessorUpdatedRow,
getRecordIndex,
getTableColumn,
} from './utils';
function PipelineExpandView({
handleAlert,
setActionType,
processorEditAction,
isActionMode,
setShowSaveButton,
expandedPipelineData,
setExpandedPipelineData,
prevPipelineData,
}: PipelineExpandViewProps): JSX.Element {
const { t } = useTranslation(['pipeline']);
const isDarkMode = useIsDarkMode();
const isEditingActionMode = isActionMode === ActionMode.Editing;
const deleteProcessorHandler = useCallback(
(record: ProcessorData) => (): void => {
setShowSaveButton(ActionMode.Editing);
if (expandedPipelineData && expandedPipelineData?.config) {
const filteredData = expandedPipelineData?.config.filter(
(item: ProcessorData) => item.id !== record.id,
);
const pipelineData = { ...expandedPipelineData };
pipelineData.config = filteredData;
pipelineData.config.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
for (let i = 0; i < pipelineData.config.length - 1; i += 1) {
pipelineData.config[i].output = pipelineData.config[i + 1].id;
}
delete pipelineData.config[pipelineData.config.length - 1]?.output;
setExpandedPipelineData(pipelineData);
}
},
[expandedPipelineData, setShowSaveButton, setExpandedPipelineData],
);
const processorDeleteAction = useCallback(
(record: ProcessorData) => (): void => {
handleAlert({
title: `${t('delete_processor')} : ${record.name}?`,
descrition: t('delete_processor_description'),
buttontext: t('delete'),
onOk: deleteProcessorHandler(record),
});
},
[handleAlert, deleteProcessorHandler, t],
);
const onSwitchProcessorChange = useCallback(
(checked: boolean, record: ProcessorData): void => {
if (expandedPipelineData && expandedPipelineData?.config) {
setShowSaveButton(ActionMode.Editing);
const findRecordIndex = getRecordIndex(
expandedPipelineData?.config,
record,
'id',
);
const updateSwitch = {
...expandedPipelineData?.config[findRecordIndex],
enabled: checked,
};
const editedData = getEditedDataSource(
expandedPipelineData?.config,
record,
'id',
updateSwitch,
);
const modifiedProcessorData = { ...expandedPipelineData };
modifiedProcessorData.config = editedData;
setExpandedPipelineData(modifiedProcessorData);
}
},
[expandedPipelineData, setExpandedPipelineData, setShowSaveButton],
);
const columns = useMemo(() => {
const fieldColumns = getTableColumn(processorColumns);
if (isEditingActionMode) {
fieldColumns.push(
{
title: '',
dataIndex: 'action',
key: 'action',
render: (_value, record): JSX.Element => (
<PipelineActions
isPipelineAction={false}
editAction={processorEditAction(record)}
deleteAction={processorDeleteAction(record)}
/>
),
},
{
title: '',
dataIndex: 'enabled',
key: 'enabled',
render: (value, record) => (
<DragAction
isEnabled={value}
onChange={(checked: boolean): void =>
onSwitchProcessorChange(checked, record)
}
/>
),
},
);
}
return fieldColumns;
}, [
isEditingActionMode,
processorEditAction,
processorDeleteAction,
onSwitchProcessorChange,
]);
const reorderProcessorRow = useCallback(
(updatedRow: ProcessorData[]) => (): void => {
setShowSaveButton(ActionMode.Editing);
if (expandedPipelineData) {
const modifiedProcessorData = { ...expandedPipelineData };
modifiedProcessorData.config = updatedRow;
setExpandedPipelineData(modifiedProcessorData);
}
},
[expandedPipelineData, setShowSaveButton, setExpandedPipelineData],
);
const onCancelReorderProcessorRow = useCallback(
() => (): void => {
if (expandedPipelineData) setExpandedPipelineData(expandedPipelineData);
},
[expandedPipelineData, setExpandedPipelineData],
);
const moveProcessorRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (expandedPipelineData?.config && isEditingActionMode) {
const updatedRow = getProcessorUpdatedRow(
expandedPipelineData?.config,
dragIndex,
hoverIndex,
);
handleAlert({
title: t('reorder_processor'),
descrition: t('reorder_processor_description'),
buttontext: t('reorder'),
onOk: reorderProcessorRow(updatedRow),
onCancel: onCancelReorderProcessorRow(),
});
}
},
[
expandedPipelineData?.config,
isEditingActionMode,
handleAlert,
t,
reorderProcessorRow,
onCancelReorderProcessorRow,
],
);
const addNewProcessorHandler = useCallback((): void => {
setActionType(ActionType.AddProcessor);
}, [setActionType]);
const footer = useCallback((): JSX.Element | undefined => {
if (prevPipelineData.length === 0 || isEditingActionMode) {
return (
<FooterButton type="link" onClick={addNewProcessorHandler}>
<PlusCircleOutlined />
<ModalFooterTitle>{t('add_new_processor')}</ModalFooterTitle>
</FooterButton>
);
}
return undefined;
}, [isEditingActionMode, prevPipelineData, addNewProcessorHandler, t]);
const onRowHandler = (
_data: ProcessorData,
index?: number,
): React.HTMLAttributes<unknown> =>
({
index,
moveRow: moveProcessorRow,
} 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 => ({
emptyText: <span />,
});
return (
<DndProvider backend={HTML5Backend}>
<StyledTable
locale={getLocales()}
isDarkMode={isDarkMode}
showHeader={false}
columns={columns}
rowKey="name"
size="small"
components={tableComponents}
dataSource={processorData}
pagination={false}
onRow={onRowHandler}
footer={footer}
/>
</DndProvider>
);
}
PipelineExpandView.defaultProps = {
expandedPipelineData: {},
};
interface PipelineExpandViewProps {
handleAlert: (props: AlertMessage) => void;
setActionType: (actionType?: ActionType) => void;
processorEditAction: (record: ProcessorData) => () => void;
isActionMode: string;
setShowSaveButton: (actionMode: ActionMode) => void;
expandedPipelineData?: PipelineData;
setExpandedPipelineData: (data: PipelineData) => void;
prevPipelineData: Array<PipelineData>;
}
export default PipelineExpandView;

View File

@ -0,0 +1,33 @@
import { Button } from 'antd';
import { useTranslation } from 'react-i18next';
import { SaveConfigWrapper } from './styles';
function SaveConfigButton({
onSaveConfigurationHandler,
onCancelConfigurationHandler,
}: SaveConfigButtonTypes): JSX.Element {
const { t } = useTranslation('pipeline');
return (
<SaveConfigWrapper>
<Button
key="submit"
type="primary"
htmlType="submit"
onClick={onSaveConfigurationHandler}
>
{t('save_configuration')}
</Button>
<Button key="cancel" onClick={onCancelConfigurationHandler}>
{t('cancel')}
</Button>
</SaveConfigWrapper>
);
}
export interface SaveConfigButtonTypes {
onSaveConfigurationHandler: VoidFunction;
onCancelConfigurationHandler: VoidFunction;
}
export default SaveConfigButton;

View File

@ -0,0 +1,21 @@
import { HolderOutlined } from '@ant-design/icons';
import { Switch } from 'antd';
import { holdIconStyle } from '../config';
import { LastActionColumn } from '../styles';
function DragAction({ isEnabled, onChange }: DragActionProps): JSX.Element {
return (
<LastActionColumn>
<Switch defaultChecked={isEnabled} onChange={onChange} />
<HolderOutlined style={holdIconStyle} />
</LastActionColumn>
);
}
interface DragActionProps {
isEnabled: boolean;
onChange: (checked: boolean) => void;
}
export default DragAction;

View File

@ -0,0 +1,28 @@
import { IconListStyle } from '../styles';
import DeleteAction from './TableActions/DeleteAction';
import EditAction from './TableActions/EditAction';
// import ViewAction from './TableActions/ViewAction';
function PipelineActions({
isPipelineAction,
editAction,
deleteAction,
}: PipelineActionsProps): JSX.Element {
return (
<IconListStyle>
<EditAction editAction={editAction} isPipelineAction={isPipelineAction} />
{/* <ViewAction isPipelineAction={isPipelineAction} /> */}
<DeleteAction
deleteAction={deleteAction}
isPipelineAction={isPipelineAction}
/>
</IconListStyle>
);
}
export interface PipelineActionsProps {
isPipelineAction: boolean;
editAction: VoidFunction;
deleteAction: VoidFunction;
}
export default PipelineActions;

View File

@ -0,0 +1,23 @@
import { DeleteFilled } from '@ant-design/icons';
import { iconStyle, smallIconStyle } from '../../config';
function DeleteAction({
isPipelineAction,
deleteAction,
}: DeleteActionProps): JSX.Element {
if (isPipelineAction) {
return <DeleteFilled onClick={deleteAction} style={iconStyle} />;
}
return (
<span key="delete-action">
<DeleteFilled onClick={deleteAction} style={smallIconStyle} />
</span>
);
}
export interface DeleteActionProps {
isPipelineAction: boolean;
deleteAction: VoidFunction;
}
export default DeleteAction;

View File

@ -0,0 +1,23 @@
import { EditOutlined } from '@ant-design/icons';
import { iconStyle, smallIconStyle } from '../../config';
function EditAction({
isPipelineAction,
editAction,
}: EditActionProps): JSX.Element {
if (isPipelineAction) {
return <EditOutlined style={iconStyle} onClick={editAction} />;
}
return (
<span key="edit-action">
<EditOutlined style={smallIconStyle} onClick={editAction} />
</span>
);
}
export interface EditActionProps {
isPipelineAction: boolean;
editAction: VoidFunction;
}
export default EditAction;

View File

@ -0,0 +1,19 @@
import { CopyFilled, EyeFilled } from '@ant-design/icons';
import { iconStyle, smallIconStyle } from '../../config';
function ViewAction({ isPipelineAction }: ViewActionProps): JSX.Element {
if (isPipelineAction) {
return <EyeFilled style={iconStyle} />;
}
return (
<span key="view-action">
<CopyFilled style={smallIconStyle} />
</span>
);
}
export interface ViewActionProps {
isPipelineAction: boolean;
}
export default ViewAction;

View File

@ -0,0 +1,28 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons';
import React from 'react';
import { PipelineData } from 'types/api/pipeline/def';
function TableExpandIcon({
expanded,
onExpand,
record,
}: TableExpandIconProps): JSX.Element {
const handleOnExpand = (
e: React.MouseEvent<HTMLElement, MouseEvent>,
): void => {
onExpand(record, e);
};
if (expanded) {
return <DownOutlined onClick={handleOnExpand} />;
}
return <RightOutlined onClick={handleOnExpand} />;
}
interface TableExpandIconProps {
expanded: boolean;
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void;
record: PipelineData;
}
export default TableExpandIcon;

View File

@ -0,0 +1,19 @@
import { Tag } from 'antd';
function Tags({ tags }: TagsProps): JSX.Element {
return (
<span>
{tags?.map((tag) => (
<Tag color="magenta" key={tag}>
{tag}
</Tag>
))}
</span>
);
}
interface TagsProps {
tags: Array<string>;
}
export default Tags;

View File

@ -0,0 +1,41 @@
import dayjs from 'dayjs';
import React from 'react';
import { PipelineData, ProcessorData } from 'types/api/pipeline/def';
import { PipelineIndexIcon } from '../AddNewProcessor/styles';
import { ColumnDataStyle, ListDataStyle, ProcessorIndexIcon } from '../styles';
const componentMap: ComponentMap = {
orderId: ({ record }) => <PipelineIndexIcon>{record}</PipelineIndexIcon>,
createdAt: ({ record }) => (
<ColumnDataStyle>
{dayjs(record).locale('en').format('MMMM DD, YYYY hh:mm A')}
</ColumnDataStyle>
),
id: ({ record }) => <ProcessorIndexIcon>{record}</ProcessorIndexIcon>,
name: ({ record }) => <ListDataStyle>{record}</ListDataStyle>,
};
function TableComponents({
columnKey,
record,
}: TableComponentsProps): JSX.Element {
const Component =
componentMap[columnKey] ??
(({ record }): JSX.Element => <ColumnDataStyle>{record}</ColumnDataStyle>);
return <Component record={record} />;
}
type ComponentMap = {
[key: string]: React.FC<{ record: Record }>;
};
export type Record = PipelineData['orderId'] & ProcessorData;
interface TableComponentsProps {
columnKey: string;
record: Record;
}
export default TableComponents;

View File

@ -0,0 +1,131 @@
import { ColumnGroupType, ColumnType } from 'antd/lib/table/interface';
import {
HistoryData,
PipelineData,
ProcessorData,
} from 'types/api/pipeline/def';
import DeploymentStage from '../Layouts/ChangeHistory/DeploymentStage';
import DeploymentTime from '../Layouts/ChangeHistory/DeploymentTime';
import DescriptionTextArea from './AddNewPipeline/FormFields/DescriptionTextArea';
import NameInput from './AddNewPipeline/FormFields/NameInput';
export const pipelineFields = [
{
id: 1,
fieldName: 'Filter',
placeholder: 'search_pipeline_placeholder',
name: 'filter',
component: NameInput,
},
{
id: 2,
fieldName: 'Name',
placeholder: 'pipeline_name_placeholder',
name: 'name',
component: NameInput,
},
{
id: 4,
fieldName: 'Description',
placeholder: 'pipeline_description_placeholder',
name: 'description',
component: DescriptionTextArea,
},
];
export const tagInputStyle: React.CSSProperties = {
width: 78,
verticalAlign: 'top',
flex: 1,
};
export const pipelineColumns: Array<
ColumnType<PipelineData> | ColumnGroupType<PipelineData>
> = [
{
key: 'orderId',
title: '',
dataIndex: 'orderId',
},
{
key: 'name',
title: 'Pipeline Name',
dataIndex: 'name',
},
{
key: 'filter',
title: 'Filters',
dataIndex: 'filter',
},
{
key: 'createdAt',
title: 'Last Edited',
dataIndex: 'createdAt',
},
{
key: 'createdBy',
title: 'Edited By',
dataIndex: 'createdBy',
},
];
export const processorColumns: Array<
ColumnType<ProcessorData> | ColumnGroupType<ProcessorData>
> = [
{
key: 'id',
title: '',
dataIndex: 'orderId',
width: 150,
},
{
key: 'name',
title: '',
dataIndex: 'name',
},
];
export const changeHistoryColumns: Array<
ColumnType<HistoryData> | ColumnGroupType<HistoryData>
> = [
{
key: 'version',
title: 'Version',
dataIndex: 'version',
},
{
title: 'Deployment Stage',
key: 'deployStatus',
dataIndex: 'deployStatus',
render: DeploymentStage,
},
{
key: 'deployResult',
title: 'Last Deploy Message',
dataIndex: 'deployResult',
ellipsis: true,
},
{
key: 'createdAt',
title: 'Last Deployed Time',
dataIndex: 'createdAt',
render: DeploymentTime,
},
{
key: 'createdByName',
title: 'Edited by',
dataIndex: 'createdByName',
},
];
export const formValidationRules = [
{
required: true,
},
];
export const iconStyle = { fontSize: '1.5rem' };
export const smallIconStyle = { fontSize: '1rem' };
export const holdIconStyle = { ...iconStyle, cursor: 'move' };

View File

@ -0,0 +1,467 @@
import { ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Modal, Table } from 'antd';
import { ExpandableConfig } from 'antd/es/table/interface';
import savePipeline from 'api/pipeline/post';
import { useNotifications } from 'hooks/useNotifications';
import { cloneDeep } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useTranslation } from 'react-i18next';
import {
ActionMode,
ActionType,
Pipeline,
PipelineData,
ProcessorData,
} from 'types/api/pipeline/def';
import { v4 } from 'uuid';
import { tableComponents } from '../config';
import AddNewPipeline from './AddNewPipeline';
import AddNewProcessor from './AddNewProcessor';
import { pipelineColumns } from './config';
import ModeAndConfiguration from './ModeAndConfiguration';
import PipelineExpanView from './PipelineExpandView';
import SaveConfigButton from './SaveConfigButton';
import {
AlertContentWrapper,
AlertModalTitle,
Container,
FooterButton,
} from './styles';
import DragAction from './TableComponents/DragAction';
import PipelineActions from './TableComponents/PipelineActions';
import TableExpandIcon from './TableComponents/TableExpandIcon';
import {
getDataOnSearch,
getEditedDataSource,
getElementFromArray,
getRecordIndex,
getTableColumn,
getUpdatedRow,
} from './utils';
function PipelineListsView({
isActionType,
setActionType,
isActionMode,
setActionMode,
piplineData,
refetchPipelineLists,
pipelineSearchValue,
}: PipelineListsViewProps): JSX.Element {
const { t } = useTranslation(['pipeline', 'common']);
const [modal, contextHolder] = Modal.useModal();
const { notifications } = useNotifications();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(piplineData?.pipelines),
);
const [currPipelineData, setCurrPipelineData] = useState<Array<PipelineData>>(
cloneDeep(piplineData?.pipelines),
);
const [
expandedPipelineData,
setExpandedPipelineData,
] = useState<PipelineData>();
const [
selectedProcessorData,
setSelectedProcessorData,
] = useState<ProcessorData>();
const [
selectedPipelineData,
setSelectedPipelineData,
] = useState<PipelineData>();
const [expandedRowKeys, setExpandedRowKeys] = useState<Array<string>>();
const [showSaveButton, setShowSaveButton] = useState<string>();
const isEditingActionMode = isActionMode === ActionMode.Editing;
useEffect(() => {
if (pipelineSearchValue === '') setCurrPipelineData(piplineData?.pipelines);
if (pipelineSearchValue !== '') {
const filterData = piplineData?.pipelines.filter((data: PipelineData) =>
getDataOnSearch(data as never, pipelineSearchValue),
);
setCurrPipelineData(filterData);
}
}, [pipelineSearchValue, piplineData?.pipelines]);
const handleAlert = useCallback(
({ title, descrition, buttontext, onCancel, onOk }: AlertMessage) => {
modal.confirm({
title: <AlertModalTitle>{title}</AlertModalTitle>,
icon: <ExclamationCircleOutlined />,
content: <AlertContentWrapper>{descrition}</AlertContentWrapper>,
okText: <span>{buttontext}</span>,
cancelText: <span>{t('cancel')}</span>,
onOk,
onCancel,
});
},
[modal, t],
);
const pipelineEditAction = useCallback(
(record: PipelineData) => (): void => {
setActionType(ActionType.EditPipeline);
setSelectedPipelineData(record);
},
[setActionType],
);
const pipelineDeleteHandler = useCallback(
(record: PipelineData) => (): void => {
setShowSaveButton(ActionMode.Editing);
const filteredData = getElementFromArray(currPipelineData, record, 'id');
filteredData.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
setCurrPipelineData(filteredData);
},
[currPipelineData],
);
const pipelineDeleteAction = useCallback(
(record: PipelineData) => (): void => {
handleAlert({
title: `${t('delete_pipeline')} : ${record.name}?`,
descrition: t('delete_pipeline_description'),
buttontext: t('delete'),
onOk: pipelineDeleteHandler(record),
});
},
[handleAlert, pipelineDeleteHandler, t],
);
const processorEditAction = useCallback(
(record: ProcessorData) => (): void => {
setActionType(ActionType.EditProcessor);
setSelectedProcessorData(record);
},
[setActionType],
);
const onSwitchPipelineChange = useCallback(
(checked: boolean, record: PipelineData): void => {
setShowSaveButton(ActionMode.Editing);
const findRecordIndex = getRecordIndex(currPipelineData, record, 'id');
const updateSwitch = {
...currPipelineData[findRecordIndex],
enabled: checked,
};
const editedPipelineData = getEditedDataSource(
currPipelineData,
record,
'id',
updateSwitch,
);
setCurrPipelineData(editedPipelineData);
},
[currPipelineData],
);
const columns = useMemo(() => {
const fieldColumns = getTableColumn(pipelineColumns);
if (isEditingActionMode) {
fieldColumns.push(
{
title: 'Actions',
dataIndex: 'smartAction',
key: 'smartAction',
align: 'center',
render: (_value, record): JSX.Element => (
<PipelineActions
isPipelineAction
editAction={pipelineEditAction(record)}
deleteAction={pipelineDeleteAction(record)}
/>
),
},
{
title: '',
dataIndex: 'enabled',
key: 'enabled',
render: (value, record) => (
<DragAction
isEnabled={value}
onChange={(checked: boolean): void =>
onSwitchPipelineChange(checked, record)
}
/>
),
},
);
}
return fieldColumns;
}, [
isEditingActionMode,
pipelineEditAction,
pipelineDeleteAction,
onSwitchPipelineChange,
]);
const updatePipelineSequence = useCallback(
(updatedRow: PipelineData[]) => (): void => {
setShowSaveButton(ActionMode.Editing);
setCurrPipelineData(updatedRow);
},
[],
);
const onCancelPipelineSequence = useCallback(
(rawData: PipelineData[]) => (): void => {
setCurrPipelineData(rawData);
},
[],
);
const movePipelineRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
if (currPipelineData && isEditingActionMode) {
const rawData = currPipelineData;
const updatedRow = getUpdatedRow(currPipelineData, dragIndex, hoverIndex);
updatedRow.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
handleAlert({
title: t('reorder_pipeline'),
descrition: t('reorder_pipeline_description'),
buttontext: t('reorder'),
onOk: updatePipelineSequence(updatedRow),
onCancel: onCancelPipelineSequence(rawData),
});
}
},
[
currPipelineData,
isEditingActionMode,
handleAlert,
t,
updatePipelineSequence,
onCancelPipelineSequence,
],
);
const expandedRowView = useCallback(
(): JSX.Element => (
<PipelineExpanView
handleAlert={handleAlert}
isActionMode={isActionMode}
setActionType={setActionType}
processorEditAction={processorEditAction}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData}
setExpandedPipelineData={setExpandedPipelineData}
prevPipelineData={prevPipelineData}
/>
),
[
handleAlert,
processorEditAction,
isActionMode,
expandedPipelineData,
setActionType,
prevPipelineData,
],
);
const onExpand = useCallback(
(expanded: boolean, record: PipelineData): void => {
const keys = [];
if (expanded && record.id) {
keys.push(record?.id);
}
setExpandedRowKeys(keys);
setExpandedPipelineData(record);
},
[],
);
const getExpandIcon = (
expanded: boolean,
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void,
record: PipelineData,
): JSX.Element => (
<TableExpandIcon expanded={expanded} onExpand={onExpand} record={record} />
);
const addNewPipelineHandler = useCallback((): void => {
setActionType(ActionType.AddPipeline);
}, [setActionType]);
const footer = useCallback((): JSX.Element | undefined => {
if (isEditingActionMode) {
return (
<FooterButton
type="link"
onClick={addNewPipelineHandler}
icon={<PlusOutlined />}
>
{t('add_new_pipeline')}
</FooterButton>
);
}
return undefined;
}, [isEditingActionMode, addNewPipelineHandler, t]);
const onSaveConfigurationHandler = useCallback(async () => {
const modifiedPipelineData = currPipelineData.map((item: PipelineData) => {
const pipelineData = item;
if (
expandedPipelineData !== undefined &&
item.id === expandedPipelineData?.id
) {
pipelineData.config = expandedPipelineData?.config;
}
pipelineData.config = item.config;
return pipelineData;
});
modifiedPipelineData.forEach((item: PipelineData) => {
const pipelineData = item;
delete pipelineData?.id;
return pipelineData;
});
const response = await savePipeline({
data: { pipelines: modifiedPipelineData },
});
if (response.statusCode === 200) {
refetchPipelineLists();
setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
setCurrPipelineData(response.payload?.pipelines);
setPrevPipelineData(response.payload?.pipelines);
} else {
modifiedPipelineData.forEach((item: PipelineData) => {
const pipelineData = item;
pipelineData.id = v4();
return pipelineData;
});
setActionMode(ActionMode.Editing);
setShowSaveButton(ActionMode.Editing);
notifications.error({
message: 'Error',
description: response.error || t('something_went_wrong'),
});
setCurrPipelineData(modifiedPipelineData);
setPrevPipelineData(modifiedPipelineData);
}
}, [
currPipelineData,
expandedPipelineData,
notifications,
refetchPipelineLists,
setActionMode,
t,
]);
const onCancelConfigurationHandler = useCallback((): void => {
setActionMode(ActionMode.Viewing);
setShowSaveButton(undefined);
prevPipelineData.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
if (obj.config) {
obj.config?.forEach((configItem, index) => {
const config = configItem;
config.orderId = index + 1;
});
for (let i = 0; i < obj.config.length - 1; i += 1) {
obj.config[i].output = obj.config[i + 1].id;
}
}
});
setCurrPipelineData(prevPipelineData);
setExpandedRowKeys([]);
}, [prevPipelineData, setActionMode]);
const onRowHandler = (
_data: PipelineData,
index?: number,
): React.HTMLAttributes<unknown> =>
({
index,
moveRow: movePipelineRow,
} as React.HTMLAttributes<unknown>);
const expandableConfig: ExpandableConfig<PipelineData> = {
expandedRowKeys,
onExpand,
expandIcon: ({ expanded, onExpand, record }: ExpandRowConfig) =>
getExpandIcon(expanded, onExpand, record),
};
return (
<>
{contextHolder}
<AddNewPipeline
isActionType={isActionType}
setActionType={setActionType}
selectedPipelineData={selectedPipelineData}
setShowSaveButton={setShowSaveButton}
setCurrPipelineData={setCurrPipelineData}
currPipelineData={currPipelineData}
/>
<AddNewProcessor
isActionType={isActionType}
setActionType={setActionType}
selectedProcessorData={selectedProcessorData}
setShowSaveButton={setShowSaveButton}
expandedPipelineData={expandedPipelineData}
setExpandedPipelineData={setExpandedPipelineData}
/>
<Container>
<ModeAndConfiguration
isActionMode={isActionMode}
verison={piplineData?.version}
/>
<DndProvider backend={HTML5Backend}>
<Table
rowKey="id"
columns={columns}
expandedRowRender={expandedRowView}
expandable={expandableConfig}
components={tableComponents}
dataSource={currPipelineData}
onRow={onRowHandler}
footer={footer}
pagination={false}
/>
</DndProvider>
{showSaveButton && (
<SaveConfigButton
onSaveConfigurationHandler={onSaveConfigurationHandler}
onCancelConfigurationHandler={onCancelConfigurationHandler}
/>
)}
</Container>
</>
);
}
interface PipelineListsViewProps {
isActionType: string;
setActionType: (actionType?: ActionType) => void;
isActionMode: string;
setActionMode: (actionMode: ActionMode) => void;
piplineData: Pipeline;
refetchPipelineLists: VoidFunction;
pipelineSearchValue: string;
}
interface ExpandRowConfig {
expanded: boolean;
onExpand: (record: PipelineData, e: React.MouseEvent<HTMLElement>) => void;
record: PipelineData;
}
export interface AlertMessage {
title: string;
descrition: string;
buttontext: string;
onOk: VoidFunction;
onCancel?: VoidFunction;
}
export default PipelineListsView;

View File

@ -0,0 +1,113 @@
import { Avatar, Button, Table, Typography } from 'antd';
import { TableProps } from 'antd/lib/table';
import { themeColors } from 'constants/theme';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import styled from 'styled-components';
export const FooterButton = styled(Button)`
display: flex;
gap: 0.5rem;
margin-left: 6.2rem;
align-items: center;
font-weight: 400;
font-size: 0.875rem;
line-height: 1.25rem;
`;
export const IconListStyle = styled.div`
display: flex;
gap: 1rem;
justify-content: flex-end;
`;
export const ColumnDataStyle = styled.span`
font-size: 0.75rem;
`;
export const ListDataStyle = styled.div`
margin: 0.125rem;
padding: 0.313rem;
border: none;
font-style: normal;
font-weight: 400;
font-size: 0.75rem;
line-height: 1.25rem;
`;
export const ProcessorIndexIcon = styled(Avatar)`
background-color: ${themeColors.navyBlue};
height: 1rem;
width: 1rem;
font-size: 0.75rem;
line-height: 0.813rem;
font-weight: 400;
`;
export const StyledTable: React.FC<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TableProps<any> & { isDarkMode: boolean }
> = styled(Table)`
.ant-table-tbody > tr > td {
border: none;
}
.ant-table-tbody > tr:last-child > td {
border: none;
}
.ant-table-content {
background: ${({ isDarkMode }: { isDarkMode: boolean }): StyledCSS =>
isDarkMode ? themeColors.neroBlack : themeColors.snowWhite};
}
`;
export const AlertContentWrapper = styled.div`
font-weight: 400;
font-style: normal;
font-size: 0.75rem;
margin-bottom: 0.5rem;
`;
export const AlertModalTitle = styled.h1`
font-weight: 600;
font-size: 0.875rem;
line-height: 1rem;
`;
export const Container = styled.div`
margin-top: 3rem;
`;
export const LastActionColumn = styled.div`
display: flex;
justify-content: center;
gap: 1.25rem;
align-items: center;
`;
export const ModalTitle = styled(Typography.Title)`
font-style: normal;
font-weight: 600;
font-size: 1.125rem;
line-height: 1.5rem;
`;
export const ModalButtonWrapper = styled.div`
display: flex;
flex-direction: row-reverse;
gap: 0.625rem;
`;
export const ModeAndConfigWrapper = styled.div`
display: flex;
gap: 0.5rem;
justify-content: flex-end;
color: ${themeColors.gamboge};
margin: 0.125rem;
padding: 0.313rem;
`;
export const SaveConfigWrapper = styled.div`
display: flex;
gap: 0.938rem;
margin-top: 1.25rem;
`;

View File

@ -0,0 +1,95 @@
import { ColumnType } from 'antd/lib/table/interface';
import dayjs from 'dayjs';
import update from 'react-addons-update';
import { ProcessorData } from 'types/api/pipeline/def';
import TableComponents, { Record } from './TableComponents';
export function getElementFromArray<T>(
arr: Array<T>,
target: T,
key: keyof T,
): Array<T> {
return arr.filter((data) => data[key] !== target?.[key]);
}
export function getRecordIndex<T>(
arr: Array<T>,
target: T,
key: keyof T,
): number {
return arr?.findIndex((item) => item[key] === target?.[key]);
}
export function getUpdatedRow<T>(
data: Array<T>,
dragIndex: number,
hoverIndex: number,
): Array<T> {
return update(data, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, data[dragIndex]],
],
});
}
export function getTableColumn<T>(
columnData: Array<ColumnType<T>>,
): Array<ColumnType<T>> {
return columnData.map(({ title, key, dataIndex, ellipsis, width }) => ({
title,
dataIndex,
key,
align: key === 'id' ? 'right' : 'left',
ellipsis,
width,
render: (record: Record): JSX.Element => (
<TableComponents columnKey={String(key)} record={record} />
),
}));
}
export function getEditedDataSource<T>(
arr: Array<T>,
target: T,
key: keyof T,
editedArr: T,
): Array<T> {
return arr?.map((data) => (data[key] === target?.[key] ? editedArr : data));
}
export function getDataOnSearch(
data: {
[key: string]: never;
},
searchValue: string,
): boolean {
return Object.keys(data).some((key) =>
key === 'createdAt'
? dayjs(data[key])
.locale('en')
.format('MMMM DD, YYYY hh:mm A')
.includes(searchValue)
: String(data[key]).toLowerCase().includes(searchValue.toLowerCase()),
);
}
export function getProcessorUpdatedRow<T extends ProcessorData>(
processorData: Array<T>,
dragIndex: number,
hoverIndex: number,
): Array<T> {
const data = processorData;
const item = data.splice(dragIndex, 1)[0];
data.splice(hoverIndex, 0, item);
data.forEach((item, index) => {
const obj = item;
obj.orderId = index + 1;
});
for (let i = 0; i < data.length - 1; i += 1) {
data[i].output = data[i + 1].id;
}
delete data[data.length - 1].output;
return data;
}

View File

@ -0,0 +1,157 @@
import {
CloseCircleFilled,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { Button, Input, InputRef, message, Modal, Tag, Tooltip } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { tagInputStyle } from '../PipelineListsView/config';
import { TagInputWrapper } from './styles';
function TagInput({
setTagsListData,
tagsListData,
placeHolder,
}: TagInputProps): JSX.Element {
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState<string>('');
const [editInputIndex, setEditInputIndex] = useState(-1);
const [editInputValue, setEditInputValue] = useState('');
const inputRef = useRef<InputRef>(null);
const editInputRef = useRef<InputRef>(null);
const { t } = useTranslation(['alerts']);
useEffect(() => {
if (inputVisible) {
inputRef.current?.focus();
}
}, [inputVisible]);
useEffect(() => {
editInputRef.current?.focus();
}, [inputValue]);
const handleClose = (removedTag: string) => (): void => {
const newTags = tagsListData?.filter((tag) => tag !== removedTag);
setTagsListData(newTags);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
};
const handleInputConfirm = (): void => {
if (inputValue && tagsListData?.indexOf(inputValue) === -1) {
setTagsListData([...tagsListData, inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const handleEditInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
): void => {
setEditInputValue(e.target.value);
};
const handleEditInputConfirm = (): void => {
const newTags = [...tagsListData];
newTags[editInputIndex] = editInputValue;
setTagsListData(newTags);
setEditInputIndex(-1);
setInputValue('');
};
const handleClearAll = (): void => {
Modal.confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
content: t('remove_label_confirm'),
onOk() {
setTagsListData([]);
message.success(t('remove_label_success'));
},
okText: t('button_yes'),
cancelText: t('button_no'),
});
};
const showAllData = tagsListData?.map((tag: string, index: number) => {
if (editInputIndex === index) {
return (
<Input
ref={editInputRef}
key={tag}
style={tagInputStyle}
value={editInputValue}
onChange={handleEditInputChange}
onBlur={handleEditInputConfirm}
onPressEnter={handleEditInputConfirm}
/>
);
}
const isLongTag = tag.length > 20;
const tagElem = (
<Tag
key={tag}
closable
style={{ userSelect: 'none' }}
onClose={handleClose(tag)}
>
<span
onDoubleClick={(e): void => {
setEditInputIndex(index);
setEditInputValue(tag);
e.preventDefault();
}}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span>
</Tag>
);
return isLongTag ? (
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : (
tagElem
);
});
const isButtonVisible = useMemo(
() => tagsListData?.length || inputValue.length || inputValue,
[inputValue, tagsListData?.length],
);
return (
<TagInputWrapper>
<Input
ref={inputRef}
type="text"
style={tagInputStyle}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={(e): void => {
e.preventDefault();
handleInputConfirm();
}}
placeholder={placeHolder}
prefix={showAllData}
/>
{isButtonVisible ? (
<Button onClick={handleClearAll} icon={<CloseCircleFilled />} type="text" />
) : null}
</TagInputWrapper>
);
}
interface TagInputProps {
setTagsListData: (tags: Array<string>) => void;
tagsListData: Array<string>;
placeHolder: string;
}
export default TagInput;

View File

@ -0,0 +1,6 @@
import styled from 'styled-components';
export const TagInputWrapper = styled.div`
display: flex;
width: 100%;
`;

View File

@ -0,0 +1,7 @@
import DraggableTableRow from 'components/DraggableTableRow';
export const tableComponents = {
body: {
row: DraggableTableRow,
},
};

View File

@ -0,0 +1,155 @@
import { Pipeline, PipelineData } from 'types/api/pipeline/def';
export const configurationVerison = '1.0';
export const pipelineMockData: Array<PipelineData> = [
{
id: '4453c8b0-c0fd-42bf-bf09-7cc1b04ccdc9',
orderId: 1,
name: 'Apache common parser',
alias: 'apachecommonparser',
description: 'This is a desc',
enabled: false,
filter: 'attributes.source == nginx',
config: [
{
orderId: 1,
enabled: true,
type: 'grok_parser',
id: 'grokusecommon',
name: 'grok use common asd',
output: 'renameauth',
parse_to: 'attributes',
pattern: '%{COMMONAPACHELOG}',
parse_from: 'body',
},
{
orderId: 2,
enabled: true,
type: 'move',
id: 'renameauth',
name: 'rename auth',
from: 'attributes.auth',
to: 'attributes.username',
},
],
createdBy: 'nityananda@signoz.io',
createdAt: '2023-03-07T16:56:53.36071141Z',
},
{
id: 'a3675a0c-ff73-4ddb-be39-4351ace69231',
orderId: 2,
name: 'Moving pipeline new',
alias: 'movingpipelinenew',
description: 'This is a desc of move',
enabled: false,
filter: 'attributes.method == POST',
config: [
{
orderId: 1,
enabled: true,
type: 'copy',
id: 'mv1',
name: 'mymove',
from: 'attributes.method',
to: 'attributes.method11',
},
],
createdBy: 'chintan@signoz.io',
createdAt: '2023-03-07T16:55:27.789595116Z',
},
];
export const pipelineApiResponseMockData: Pipeline = {
id: '67ace08a-6b6c-4221-ab58-a5d3bd5eb6f2',
version: 5,
elementType: 'log_pipelines',
createdBy: 'test@signoz.io',
active: false,
is_valid: false,
disabled: false,
deployStatus: 'IN_PROGRESS',
deployResult: 'Deployment started',
lastHash: 'q<><71>҂<EFBFBD>&覣ʝup<75><70>\u0003<30><33><EFBFBD>q<EFBFBD>6<EFBFBD>\u001e<31><ѥIb<49>',
lastConf:
'[{"id":"a3675a0c-ff73-4ddb-be39-4351ace69231","orderId":"1","name":"Moving pipeline new","alias":"movingpipelinenew","enabled":false,"filter":"attributes.method == POST","config":[{"type":"copy","id":"mv1","name":"mymove","from":"attributes.method","to":"attributes.method11"}],"createdBy":"nityananda@signoz.io","createdAt":"2023-03-07T16:55:27.789595116Z"},{"id":"4453c8b0-c0fd-42bf-bf09-7cc1b04ccdc9","orderId":"2","name":"Apache common parser","alias":"apachecommonparser","enabled":false,"filter":"attributes.source == nginx","config":[{"type":"grok_parser","id":"grokusecommon","name":"grok use common asd","output":"renameauth","parse_to":"attributes","pattern":"%{COMMONAPACHELOG}","parse_from":"body"},{"type":"move","id":"renameauth","name":"rename auth","from":"attributes.auth","to":"attributes.username"}],"createdBy":"nityananda@signoz.io","createdAt":"2023-03-07T16:56:53.36071141Z"}]',
pipelines: pipelineMockData,
history: [
{
id: 'e118dedd-e996-455a-9cb2-5bf50b77fc35',
version: 1,
elementType: 'log_pipelines',
createdBy: 'test2@signoz.io',
active: false,
isValid: false,
disabled: false,
deployStatus: 'DEPLOYED',
deployResult: 'deploy successful',
lastHash: '',
lastConf: '',
// eslint-disable-next-line sonarjs/no-duplicate-string
createdAt: '2021-03-07T16:56:53.36071141Z',
createdByName: 'test2',
},
{
id: '9a98673d-b5db-4281-89d3-d85ed9ffe311',
version: 2,
elementType: 'log_pipelines',
createdBy: 'test3@signoz.io',
active: false,
isValid: false,
disabled: false,
deployStatus: 'DEPLOYED',
deployResult: 'deploy successful',
lastHash: '',
lastConf: '',
createdAt: '2021-03-07T16:56:53.36071141Z',
createdByName: 'test3',
},
{
id: '9fdb0813-f77f-4837-815e-bb6eedd64f68',
version: 3,
elementType: 'log_pipelines',
createdBy: 'nityananda+1@signoz.io',
active: false,
isValid: false,
disabled: false,
deployStatus: 'IN_PROGRESS',
deployResult: '',
lastHash: '',
lastConf: '',
createdAt: '2021-03-07T16:56:53.36071141Z',
createdByName: 'nityananda+1',
},
{
id: '87efb1cf-85b0-4aa4-934e-62a118fa4ec7',
version: 4,
elementType: 'log_pipelines',
createdBy: 'nityananda+2@signoz.io',
active: false,
isValid: false,
disabled: false,
deployStatus: 'IN_PROGRESS',
deployResult: '',
lastHash: '',
lastConf: '',
createdAt: '2021-03-07T16:56:53.36071141Z',
createdByName: 'nityananda+2',
},
{
id: '67ace08a-6b6c-4221-ab58-a5d3bd5eb6f2',
version: 5,
elementType: 'log_pipelines',
createdBy: 'nityananda+4@signoz.io',
active: false,
isValid: false,
disabled: false,
deployStatus: 'IN_PROGRESS',
deployResult: '',
lastHash: '',
lastConf: '',
createdAt: '2021-03-07T16:56:53.36071141Z',
createdByName: 'nityananda+4',
},
],
};

View File

@ -0,0 +1,32 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const ButtonContainer = styled.div`
&&& {
display: flex;
justify-content: flex-end;
margin-bottom: 2rem;
align-items: center;
}
`;
export const CustomButton = styled(Button)`
&&& {
margin-left: 1rem;
}
`;
export const ModalFooterTitle = styled.span`
font-style: normal;
font-weight: 400;
font-size: 0.875rem;
line-height: 1.25rem;
`;
export const HistoryTableWrapper = styled.div`
margin-top: 3rem;
`;
export const IconDataSpan = styled.span`
padding: 0.625rem;
`;

View File

@ -0,0 +1,53 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import AddNewPipeline from '../PipelineListsView/AddNewPipeline';
export function matchMedia(): void {
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(),
})),
});
}
beforeAll(() => {
matchMedia();
});
describe('PipelinePage container test', () => {
it('should render AddNewPipeline section', () => {
const setActionType = jest.fn();
const selectedPipelineData = pipelineMockData[0];
const isActionType = 'add-pipeline';
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<AddNewPipeline
isActionType={isActionType}
setActionType={setActionType}
selectedPipelineData={selectedPipelineData}
setShowSaveButton={jest.fn()}
setCurrPipelineData={jest.fn()}
currPipelineData={pipelineMockData}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,46 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import AddNewProcessor from '../PipelineListsView/AddNewProcessor';
import { matchMedia } from './AddNewPipeline.test';
beforeAll(() => {
matchMedia();
});
const selectedProcessorData = {
id: '1',
orderId: 1,
type: 'grok_parser',
name: 'grok use common',
output: 'grokusecommon',
};
describe('PipelinePage container test', () => {
it('should render AddNewProcessor section', () => {
const setActionType = jest.fn();
const isActionType = 'add-processor';
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<AddNewProcessor
isActionType={isActionType}
setActionType={setActionType}
selectedProcessorData={selectedProcessorData}
setShowSaveButton={jest.fn()}
expandedPipelineData={pipelineMockData[0]}
setExpandedPipelineData={jest.fn()}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,29 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
describe('PipelinePage container test', () => {
it('should render CreatePipelineButton section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<CreatePipelineButton
setActionType={jest.fn()}
isActionMode="viewing-mode"
setActionMode={jest.fn()}
piplineData={pipelineApiResponseMockData}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,22 @@
import { render } from '@testing-library/react';
import DeleteAction from 'container/PipelinePage/PipelineListsView/TableComponents/TableActions/DeleteAction';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
describe('PipelinePage container test', () => {
it('should render DeleteAction section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<DeleteAction isPipelineAction deleteAction={jest.fn()} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,22 @@
import { render } from '@testing-library/react';
import DragAction from 'container/PipelinePage/PipelineListsView/TableComponents/DragAction';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
describe('PipelinePage container test', () => {
it('should render DragAction section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<DragAction isEnabled onChange={jest.fn()} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,22 @@
import { render } from '@testing-library/react';
import EditAction from 'container/PipelinePage/PipelineListsView/TableComponents/TableActions/EditAction';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
describe('PipelinePage container test', () => {
it('should render EditAction section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<EditAction isPipelineAction editAction={jest.fn()} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,26 @@
import { render } from '@testing-library/react';
import PipelineActions from 'container/PipelinePage/PipelineListsView/TableComponents/PipelineActions';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
describe('PipelinePage container test', () => {
it('should render PipelineActions section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<PipelineActions
isPipelineAction
editAction={jest.fn()}
deleteAction={jest.fn()}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,38 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import { pipelineMockData } from '../mocks/pipeline';
import PipelineExpandView from '../PipelineListsView/PipelineExpandView';
import { matchMedia } from './AddNewPipeline.test';
beforeAll(() => {
matchMedia();
});
describe('PipelinePage', () => {
it('should render PipelineExpandView section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<PipelineExpandView
handleAlert={jest.fn()}
setActionType={jest.fn()}
processorEditAction={jest.fn()}
isActionMode="viewing-mode"
setShowSaveButton={jest.fn()}
expandedPipelineData={pipelineMockData[0]}
setExpandedPipelineData={jest.fn()}
prevPipelineData={pipelineMockData}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,62 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import { Pipeline } from 'types/api/pipeline/def';
import { v4 } from 'uuid';
import PipelinePageLayout from '../Layouts/Pipeline';
import { matchMedia } from './AddNewPipeline.test';
beforeAll(() => {
matchMedia();
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
describe('PipelinePage container test', () => {
it('should render PipelinePageLayout section', () => {
const pipelinedata: Pipeline = {
active: true,
createdBy: 'admin',
deployResult: 'random_data',
deployStatus: 'random_data',
disabled: false,
elementType: 'random_data',
history: [],
id: v4(),
is_valid: true,
lastConf: 'random_data',
lastHash: 'random_data',
pipelines: [],
version: 1,
};
const refetchPipelineLists = jest.fn();
const { asFragment } = render(
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<PipelinePageLayout
piplineData={pipelinedata}
refetchPipelineLists={refetchPipelineLists}
/>
</I18nextProvider>
</Provider>
</QueryClientProvider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import PipelinesSearchSection from '../Layouts/Pipeline/PipelinesSearchSection';
describe('PipelinePage container test', () => {
it('should render PipelinesSearchSection section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<PipelinesSearchSection setPipelineSearchValue={jest.fn()} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,27 @@
import { render } from '@testing-library/react';
import { pipelineMockData } from 'container/PipelinePage/mocks/pipeline';
import TableExpandIcon from 'container/PipelinePage/PipelineListsView/TableComponents/TableExpandIcon';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
describe('PipelinePage container test', () => {
it('should render TableExpandIcon section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<TableExpandIcon
expanded
onExpand={jest.fn()}
record={pipelineMockData[0]}
/>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,23 @@
import { render } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
import TagInput from '../components/TagInput';
describe('Pipeline Page', () => {
it('should render TagInput section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<TagInput setTagsListData={jest.fn()} tagsListData={[]} placeHolder="" />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,24 @@
import { render } from '@testing-library/react';
import Tags from 'container/PipelinePage/PipelineListsView/TableComponents/Tags';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
const tags = ['server', 'app'];
describe('PipelinePage container test', () => {
it('should render Tags section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<Tags tags={tags} />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@ -0,0 +1,22 @@
import { render } from '@testing-library/react';
import ViewAction from 'container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import i18n from 'ReactI18';
import store from 'store';
describe('PipelinePage container test', () => {
it('should render ViewAction section', () => {
const { asFragment } = render(
<MemoryRouter>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<ViewAction isPipelineAction />
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

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

View File

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

View File

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render CreatePipelineButton section 1`] = `
<DocumentFragment>
.c0.c0.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
justify-content: flex-end;
margin-bottom: 2rem;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1.c1.c1 {
margin-left: 1rem;
}
<div
class="c0"
>
<span
aria-label="question-circle"
class="anticon anticon-question-circle"
role="img"
style="font-size: 1rem; color: rgba(255, 255, 255, 0.835);"
>
<svg
aria-hidden="true"
data-icon="question-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 708c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm62.9-219.5a48.3 48.3 0 00-30.9 44.8V620c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8v-21.5c0-23.1 6.7-45.9 19.9-64.9 12.9-18.6 30.9-32.8 52.1-40.9 34-13.1 56-41.6 56-72.7 0-44.1-43.1-80-96-80s-96 35.9-96 80v7.6c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V420c0-39.3 17.2-76 48.4-103.3C430.4 290.4 470 276 512 276s81.6 14.5 111.6 40.7C654.8 344 672 380.7 672 420c0 57.8-38.1 109.8-97.1 132.5z"
/>
</svg>
</span>
<button
class="ant-btn css-dev-only-do-not-override-1i536d8 ant-btn-default c1"
type="button"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="img"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32zm-622.3-84c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9z"
/>
</svg>
</span>
<span>
enter_edit_mode
</span>
</button>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render DeleteAction section 1`] = `
<DocumentFragment>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
style="font-size: 1.5rem;"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M864 256H736v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zm-200 0H360v-72h304v72z"
/>
</svg>
</span>
</DocumentFragment>
`;

View File

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render DragAction section 1`] = `
<DocumentFragment>
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 1.25rem;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
<div
class="c0"
>
<button
aria-checked="true"
class="ant-switch css-dev-only-do-not-override-1i536d8 ant-switch-checked"
role="switch"
type="button"
>
<div
class="ant-switch-handle"
/>
<span
class="ant-switch-inner"
>
<span
class="ant-switch-inner-checked"
/>
<span
class="ant-switch-inner-unchecked"
/>
</span>
</button>
<span
aria-label="holder"
class="anticon anticon-holder"
role="img"
style="font-size: 1.5rem; cursor: move;"
>
<svg
aria-hidden="true"
data-icon="holder"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M300 276.5a56 56 0 1056-97 56 56 0 00-56 97zm0 284a56 56 0 1056-97 56 56 0 00-56 97zM640 228a56 56 0 10112 0 56 56 0 00-112 0zm0 284a56 56 0 10112 0 56 56 0 00-112 0zM300 844.5a56 56 0 1056-97 56 56 0 00-56 97zM640 796a56 56 0 10112 0 56 56 0 00-112 0z"
/>
</svg>
</span>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render EditAction section 1`] = `
<DocumentFragment>
<span
aria-label="edit"
class="anticon anticon-edit"
role="img"
style="font-size: 1.5rem;"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
</DocumentFragment>
`;

View File

@ -0,0 +1,64 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render PipelineActions section 1`] = `
<DocumentFragment>
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 1rem;
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
justify-content: flex-end;
}
<div
class="c0"
>
<span
aria-label="edit"
class="anticon anticon-edit"
role="img"
style="font-size: 1.5rem;"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="edit"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z"
/>
</svg>
</span>
<span
aria-label="delete"
class="anticon anticon-delete"
role="img"
style="font-size: 1.5rem;"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="delete"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M864 256H736v-80c0-35.3-28.7-64-64-64H352c-35.3 0-64 28.7-64 64v80H160c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h60.4l24.7 523c1.6 34.1 29.8 61 63.9 61h454c34.2 0 62.3-26.8 63.9-61l24.7-523H888c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zm-200 0H360v-72h304v72z"
/>
</svg>
</span>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,141 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;
exports[`PipelinePage should render PipelineExpandView section 1`] = `
<DocumentFragment>
.c2 {
margin: 0.125rem;
padding: 0.313rem;
border: none;
font-style: normal;
font-weight: 400;
font-size: 0.75rem;
line-height: 1.25rem;
}
.c1 {
background-color: #1668DC;
height: 1rem;
width: 1rem;
font-size: 0.75rem;
line-height: 0.813rem;
font-weight: 400;
}
.c0 .ant-table-tbody > tr > td {
border: none;
}
.c0 .ant-table-tbody > tr:last-child > td {
border: none;
}
.c0 .ant-table-content {
background: #1d1d1d;
}
<div
class="ant-table-wrapper c0 css-dev-only-do-not-override-1i536d8"
>
<div
class="ant-spin-nested-loading css-dev-only-do-not-override-1i536d8"
>
<div
class="ant-spin-container"
>
<div
class="ant-table ant-table-small"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
>
<table
style="table-layout: auto;"
>
<colgroup>
<col
style="width: 150px;"
/>
</colgroup>
<tbody
class="ant-table-tbody"
>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="grok use common asd"
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-1i536d8"
>
<span
class="ant-avatar-string"
style="transform: scale(1) translateX(-50%);"
>
1
</span>
</span>
</td>
<td
class="ant-table-cell"
style="text-align: left;"
>
<div
class="c2"
>
grok use common asd
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
data-row-key="rename auth"
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-1i536d8"
>
<span
class="ant-avatar-string"
style="transform: scale(1) translateX(-50%);"
>
2
</span>
</span>
</td>
<td
class="ant-table-cell"
style="text-align: left;"
>
<div
class="c2"
>
rename auth
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
class="ant-table-footer"
/>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,326 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;
exports[`PipelinePage container test should render PipelinePageLayout section 1`] = `
<DocumentFragment>
.c0.c0.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
justify-content: flex-end;
margin-bottom: 2rem;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1.c1.c1 {
margin-left: 1rem;
}
<div
class="c0"
>
<span
aria-label="question-circle"
class="anticon anticon-question-circle"
role="img"
style="font-size: 1rem; color: rgba(255, 255, 255, 0.835);"
>
<svg
aria-hidden="true"
data-icon="question-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 708c-22.1 0-40-17.9-40-40s17.9-40 40-40 40 17.9 40 40-17.9 40-40 40zm62.9-219.5a48.3 48.3 0 00-30.9 44.8V620c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8v-21.5c0-23.1 6.7-45.9 19.9-64.9 12.9-18.6 30.9-32.8 52.1-40.9 34-13.1 56-41.6 56-72.7 0-44.1-43.1-80-96-80s-96 35.9-96 80v7.6c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V420c0-39.3 17.2-76 48.4-103.3C430.4 290.4 470 276 512 276s81.6 14.5 111.6 40.7C654.8 344 672 380.7 672 420c0 57.8-38.1 109.8-97.1 132.5z"
/>
</svg>
</span>
<button
class="ant-btn css-dev-only-do-not-override-1i536d8 ant-btn-primary c1"
type="button"
>
<span
aria-label="plus"
class="anticon anticon-plus"
role="img"
>
<svg
aria-hidden="true"
data-icon="plus"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<defs>
<style />
</defs>
<path
d="M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"
/>
<path
d="M176 474h672q8 0 8 8v60q0 8-8 8H176q-8 0-8-8v-60q0-8 8-8z"
/>
</svg>
</span>
<span>
new_pipeline
</span>
</button>
</div>
<span
class="ant-input-group-wrapper ant-input-search css-dev-only-do-not-override-1i536d8"
>
<span
class="ant-input-wrapper ant-input-group css-dev-only-do-not-override-1i536d8"
>
<span
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder="search_pipeline_placeholder"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</span>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn css-dev-only-do-not-override-1i536d8 ant-btn-default ant-btn-icon-only ant-input-search-button"
type="button"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
.c0 {
margin-top: 3rem;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 0.5rem;
-webkit-box-pack: end;
-webkit-justify-content: flex-end;
-ms-flex-pack: end;
justify-content: flex-end;
color: #D89614;
margin: 0.125rem;
padding: 0.313rem;
}
<div
class="c0"
>
<div
class="c1"
>
Mode:
<span>
Viewing
</span>
<div>
Configuration Version: 1
</div>
</div>
<div
class="ant-table-wrapper css-dev-only-do-not-override-1i536d8"
>
<div
class="ant-spin-nested-loading css-dev-only-do-not-override-1i536d8"
>
<div
class="ant-spin-container"
>
<div
class="ant-table ant-table-empty"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
>
<table
style="table-layout: auto;"
>
<colgroup>
<col
class="ant-table-expand-icon-col"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
class="ant-table-cell ant-table-row-expand-icon-cell"
/>
<th
class="ant-table-cell"
style="text-align: left;"
/>
<th
class="ant-table-cell"
style="text-align: left;"
>
Pipeline Name
</th>
<th
class="ant-table-cell"
style="text-align: left;"
>
Filters
</th>
<th
class="ant-table-cell"
style="text-align: left;"
>
Last Edited
</th>
<th
class="ant-table-cell"
style="text-align: left;"
>
Edited By
</th>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
class="ant-table-placeholder"
draggable="true"
>
<td
class="ant-table-cell"
colspan="6"
>
<div
class="css-dev-only-do-not-override-1i536d8 ant-empty ant-empty-normal"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div
class="ant-table-footer"
/>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render PipelinesSearchSection section 1`] = `
<DocumentFragment>
<span
class="ant-input-group-wrapper ant-input-search css-dev-only-do-not-override-1i536d8"
>
<span
class="ant-input-wrapper ant-input-group css-dev-only-do-not-override-1i536d8"
>
<span
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder="search_pipeline_placeholder"
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66-.3L512 563.4l-99.3 118.4-66.1.3c-4.4 0-8-3.5-8-8 0-1.9.7-3.7 1.9-5.2l130.1-155L340.5 359a8.32 8.32 0 01-1.9-5.2c0-4.4 3.6-8 8-8l66.1.3L512 464.6l99.3-118.4 66-.3c4.4 0 8 3.5 8 8 0 1.9-.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
/>
</svg>
</span>
</span>
</span>
</span>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn css-dev-only-do-not-override-1i536d8 ant-btn-default ant-btn-icon-only ant-input-search-button"
type="button"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</DocumentFragment>
`;

View File

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render TableExpandIcon section 1`] = `
<DocumentFragment>
<span
aria-label="down"
class="anticon anticon-down"
role="img"
tabindex="-1"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</DocumentFragment>
`;

View File

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Pipeline Page should render TagInput section 1`] = `
<DocumentFragment>
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
width: 100%;
}
<div
class="c0"
>
<span
class="ant-input-affix-wrapper css-dev-only-do-not-override-1i536d8"
style="width: 78px; vertical-align: top; flex: 1;"
>
<span
class="ant-input-prefix"
/>
<input
class="ant-input css-dev-only-do-not-override-1i536d8"
placeholder=""
type="text"
value=""
/>
</span>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render Tags section 1`] = `
<DocumentFragment>
<span>
<span
class="ant-tag ant-tag-magenta css-dev-only-do-not-override-1i536d8"
>
server
</span>
<span
class="ant-tag ant-tag-magenta css-dev-only-do-not-override-1i536d8"
>
app
</span>
</span>
</DocumentFragment>
`;

View File

@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PipelinePage container test should render ViewAction section 1`] = `
<DocumentFragment>
<span
aria-label="eye"
class="anticon anticon-eye"
role="img"
style="font-size: 1.5rem;"
>
<svg
aria-hidden="true"
data-icon="eye"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M396 512a112 112 0 10224 0 112 112 0 10-224 0zm546.2-25.8C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM508 688c-97.2 0-176-78.8-176-176s78.8-176 176-176 176 78.8 176 176-78.8 176-176 176z"
/>
</svg>
</span>
</DocumentFragment>
`;

View File

@ -0,0 +1,88 @@
import { pipelineMockData } from '../mocks/pipeline';
import {
processorFields,
processorTypes,
} from '../PipelineListsView/AddNewProcessor/config';
import { pipelineFields, processorColumns } from '../PipelineListsView/config';
import {
getEditedDataSource,
getElementFromArray,
getRecordIndex,
getTableColumn,
} from '../PipelineListsView/utils';
describe('Utils testing of Pipeline Page', () => {
test('it should be check form field of add pipeline', () => {
expect(pipelineFields.length).toBe(3);
expect(pipelineFields.length).toBeGreaterThan(1);
});
test('it should be check processor types field of add pipeline', () => {
expect(processorTypes.length).toBeGreaterThan(1);
});
test('it should check form field of add processor', () => {
Object.keys(processorFields).forEach((key) => {
expect(processorFields[key].length).toBeGreaterThan(1);
});
});
test('it should be check data length of pipeline', () => {
expect(pipelineMockData.length).toBe(2);
expect(pipelineMockData.length).toBeGreaterThan(0);
});
test('it should be return filtered data and perform deletion', () => {
const filterData = getElementFromArray(
pipelineMockData,
pipelineMockData[0],
'id',
);
expect(pipelineMockData).not.toEqual(filterData);
expect(pipelineMockData[0]).not.toEqual(filterData);
});
test('it should be return index data and perform deletion', () => {
const findRecordIndex = getRecordIndex(
pipelineMockData,
pipelineMockData[0],
'id',
);
expect(pipelineMockData).not.toEqual(findRecordIndex);
expect(pipelineMockData[0]).not.toEqual(findRecordIndex);
});
test('it should be return modified column data', () => {
const columnData = getTableColumn(processorColumns);
expect(processorColumns).not.toEqual(columnData);
expect(processorColumns.length).toEqual(columnData.length);
});
test('it should be return modified column data', () => {
const findRecordIndex = getRecordIndex(
pipelineMockData,
pipelineMockData[0],
'name',
);
const updatedPipelineData = {
...pipelineMockData[findRecordIndex],
name: 'updated name',
description: 'changed description',
filter: 'value == test',
tags: ['test'],
};
const editedData = getEditedDataSource(
pipelineMockData,
pipelineMockData[0],
'name',
updatedPipelineData,
);
expect(pipelineMockData).not.toEqual(editedData);
expect(pipelineMockData.length).toEqual(editedData.length);
expect(pipelineMockData[0].name).not.toEqual(editedData[0].name);
expect(pipelineMockData[0].description).not.toEqual(
editedData[0].description,
);
expect(pipelineMockData[0].tags).not.toEqual(editedData[0].tags);
});
});

View File

@ -1,8 +1,16 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { themeColors } from 'constants/theme';
export const styles = { background: '#1f1f1f' };
export const subMenuStyles = {
background: '#1f1f1f',
margin: '0rem',
width: '100%',
color: themeColors.gainsboro,
};
export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.SERVICE_METRICS]: [QueryParams.resourceAttributes],
[ROUTES.SERVICE_MAP]: [QueryParams.resourceAttributes],
@ -36,4 +44,6 @@ export const routeConfig: Record<string, QueryParams[]> = {
[ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes],
[ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.VERSION]: [QueryParams.resourceAttributes],
[ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes],
[ROUTES.PIPELINES]: [QueryParams.resourceAttributes],
};

View File

@ -53,14 +53,14 @@ function SideNav(): JSX.Element {
}, [collapsed, dispatch]);
const onClickHandler = useCallback(
(to: string) => {
(key: string) => {
const params = new URLSearchParams(search);
const availableParams = routeConfig[to];
const availableParams = routeConfig[key];
const queryString = getQueryString(availableParams || [], params);
if (pathname !== to) {
history.push(`${to}?${queryString.join('&')}`);
if (pathname !== key) {
history.push(`${key}?${queryString.join('&')}`);
}
},
[pathname, search],

View File

@ -68,6 +68,11 @@ const menus: SidebarMenu[] = [
// label: 'Views',
// },
// ],
// {
// key: ROUTES.PIPELINES,
// label: 'Pipelines',
// },
// ],
},
{
key: ROUTES.ALL_DASHBOARD,

View File

@ -21,6 +21,7 @@ const breadcrumbNameMap = {
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
[ROUTES.LOGS]: 'Logs',
[ROUTES.LOGS_EXPLORER]: 'Logs Explorer',
[ROUTES.PIPELINES]: 'Pipelines',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -82,4 +82,5 @@ export const routesToSkip = [
ROUTES.ALERTS_NEW,
ROUTES.EDIT_ALERTS,
ROUTES.LIST_ALL_ALERT,
ROUTES.PIPELINES,
];

View File

@ -0,0 +1,64 @@
import type { TabsProps } from 'antd';
import { Tabs } from 'antd';
import getPipeline from 'api/pipeline/get';
import Spinner from 'components/Spinner';
import ChangeHistory from 'container/PipelinePage/Layouts/ChangeHistory';
import PipelinePage from 'container/PipelinePage/Layouts/Pipeline';
import { useNotifications } from 'hooks/useNotifications';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { Pipeline } from 'types/api/pipeline/def';
function Pipelines(): JSX.Element {
const { t } = useTranslation('common');
const { notifications } = useNotifications();
const {
isLoading,
data: piplineData,
isError,
refetch: refetchPipelineLists,
} = useQuery(['version', 'latest', 'pipeline'], {
queryFn: () =>
getPipeline({
version: 'latest',
}),
});
const tabItems: TabsProps['items'] = useMemo(
() => [
{
key: 'pipelines',
label: `Pipelines`,
children: (
<PipelinePage
refetchPipelineLists={refetchPipelineLists}
piplineData={piplineData?.payload as Pipeline}
/>
),
},
{
key: 'change-history',
label: `Change History`,
children: <ChangeHistory piplineData={piplineData?.payload as Pipeline} />,
},
],
[piplineData?.payload, refetchPipelineLists],
);
useEffect(() => {
if (piplineData?.error && isError) {
notifications.error({
message: piplineData?.error || t('something_went_wrong'),
});
}
}, [isError, notifications, piplineData?.error, t]);
if (isLoading) {
return <Spinner height="75vh" tip="Loading Pipelines..." />;
}
return <Tabs defaultActiveKey="pipelines" items={tabItems} />;
}
export default Pipelines;

View File

@ -0,0 +1,76 @@
export interface ProcessorData {
type: string;
id?: string;
orderId: number;
name: string;
enabled?: boolean;
output?: string;
parse_to?: string;
pattern?: string;
parse_from?: string;
from?: string;
to?: string;
regex?: string;
on_error?: string;
field?: string;
value?: string;
}
export interface PipelineData {
alias: string;
config?: Array<ProcessorData>;
createdAt: string;
description?: string;
createdBy: string;
enabled: boolean;
filter: string;
id?: string;
name: string;
orderId: number;
tags?: Array<string>; // Tags data is missing in API response
}
export interface HistoryData {
active: boolean;
createdAt: string;
createdBy: string;
createdByName: string;
deployStatus: string;
deployResult: string;
disabled: boolean;
elementType: string;
id: string;
isValid: boolean;
lastConf: string;
lastHash: string;
version: number;
}
export interface Pipeline {
active: boolean;
createdBy: string;
deployResult: string;
deployStatus: string;
disabled: boolean;
elementType: string;
history: Array<HistoryData>;
id: string;
is_valid: boolean;
lastConf: string;
lastHash: string;
pipelines: Array<PipelineData>;
version: string | number;
}
export enum ActionType {
AddPipeline = 'add-pipeline',
EditPipeline = 'edit-pipeline',
AddProcessor = 'add-processor',
EditProcessor = 'edit-processor',
}
export enum ActionMode {
Viewing = 'viewing-mode',
Editing = 'editing-mode',
Deploying = 'deploying-mode',
}

View File

@ -0,0 +1,3 @@
export type Props = {
version: string | number;
};

View File

@ -0,0 +1,5 @@
import { PipelineData } from './def';
export interface Props {
data: { pipelines: Array<PipelineData> };
}

View File

@ -17,7 +17,8 @@ export type ComponentTypes =
| 'new_dashboard'
| 'new_alert_action'
| 'edit_widget'
| 'add_panel';
| 'add_panel'
| 'page_pipelines';
export const componentPermission: Record<ComponentTypes, ROLES[]> = {
current_org_settings: ['ADMIN'],
@ -36,6 +37,7 @@ export const componentPermission: Record<ComponentTypes, ROLES[]> = {
new_alert_action: ['ADMIN'],
edit_widget: ['ADMIN', 'EDITOR'],
add_panel: ['ADMIN', 'EDITOR'],
page_pipelines: ['ADMIN', 'EDITOR'],
};
export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
@ -72,4 +74,6 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
LIST_LICENSES: ['ADMIN'],
TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'],
};

Some files were not shown because too many files have changed in this diff Show More