diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts index 51d6776579..00bd8505c8 100644 --- a/frontend/jest.config.ts +++ b/frontend/jest.config.ts @@ -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: ['jest.setup.ts'], testPathIgnorePatterns: ['/node_modules/', '/public/'], moduleDirectories: ['node_modules', 'src'], diff --git a/frontend/package.json b/frontend/package.json index dd64d0bc14..daad1b8269 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/public/locales/en-GB/routes.json b/frontend/public/locales/en-GB/routes.json index 625638d85f..ac17990f3b 100644 --- a/frontend/public/locales/en-GB/routes.json +++ b/frontend/public/locales/en-GB/routes.json @@ -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" } diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index c37480259b..33442c8369 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -12,6 +12,7 @@ "routes": { "general": "General", "alert_channels": "Alert Channels", - "all_errors": "All Exceptions" + "all_errors": "All Exceptions", + "pipelines": "Pipelines" } } diff --git a/frontend/public/locales/en/pipeline.json b/frontend/public/locales/en/pipeline.json new file mode 100644 index 0000000000..515ce19003 --- /dev/null +++ b/frontend/public/locales/en/pipeline.json @@ -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" +} diff --git a/frontend/public/locales/en/routes.json b/frontend/public/locales/en/routes.json index 625638d85f..ac17990f3b 100644 --- a/frontend/public/locales/en/routes.json +++ b/frontend/public/locales/en/routes.json @@ -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" } diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index a563ce92cb..39e7a79fda 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -12,6 +12,7 @@ "routes": { "general": "General", "alert_channels": "Alert Channels", - "all_errors": "All Exceptions" + "all_errors": "All Exceptions", + "pipelines": "Pipelines" } } diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index f3d40ccbcd..b993a4790a 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -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'), +); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 49d48de066..ef7bd5d302 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -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 { diff --git a/frontend/src/api/pipeline/get.ts b/frontend/src/api/pipeline/get.ts new file mode 100644 index 0000000000..ff4dd7fc3d --- /dev/null +++ b/frontend/src/api/pipeline/get.ts @@ -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 | 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; diff --git a/frontend/src/api/pipeline/post.ts b/frontend/src/api/pipeline/post.ts new file mode 100644 index 0000000000..c2e7ca2757 --- /dev/null +++ b/frontend/src/api/pipeline/post.ts @@ -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 | 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; diff --git a/frontend/src/components/DraggableTableRow/index.tsx b/frontend/src/components/DraggableTableRow/index.tsx new file mode 100644 index 0000000000..7c4a48336b --- /dev/null +++ b/frontend/src/components/DraggableTableRow/index.tsx @@ -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(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 ( + + ); +} + +interface DraggableTableRowProps + extends React.HTMLAttributes { + index: number; + moveRow: (dragIndex: number, hoverIndex: number) => void; +} + +export default DraggableTableRow; diff --git a/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx b/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx new file mode 100644 index 0000000000..f938a19203 --- /dev/null +++ b/frontend/src/components/DraggableTableRow/tests/DraggableTableRow.test.tsx @@ -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( + + + + + , + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/DraggableTableRow/tests/__snapshots__/DraggableTableRow.test.tsx.snap b/frontend/src/components/DraggableTableRow/tests/__snapshots__/DraggableTableRow.test.tsx.snap new file mode 100644 index 0000000000..4c70482cb6 --- /dev/null +++ b/frontend/src/components/DraggableTableRow/tests/__snapshots__/DraggableTableRow.test.tsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = ` + +
+
+
+
+
+
+
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + +
+
+ No data +
+
+
+ + + + + + + +`; + +exports[`PipelinePage container test should render AddNewPipeline section 1`] = ``; diff --git a/frontend/src/components/DraggableTableRow/tests/utils.test.ts b/frontend/src/components/DraggableTableRow/tests/utils.test.ts new file mode 100644 index 0000000000..80854944c7 --- /dev/null +++ b/frontend/src/components/DraggableTableRow/tests/utils.test.ts @@ -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 }); + }); +}); diff --git a/frontend/src/components/DraggableTableRow/utils.ts b/frontend/src/components/DraggableTableRow/utils.ts new file mode 100644 index 0000000000..475145fdee --- /dev/null +++ b/frontend/src/components/DraggableTableRow/utils.ts @@ -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(), + }; +} diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index f911b6be57..468c6042bb 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -32,6 +32,8 @@ const ROUTES = { HOME_PAGE: '/', PASSWORD_RESET: '/password-reset', LIST_LICENSES: '/licenses', + TRACE_EXPLORER: '/trace-explorer', + PIPELINES: '/pipelines', }; export default ROUTES; diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index 354ea190a7..fcb8dd171a 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -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', }; diff --git a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/DeploymentStage.tsx b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/DeploymentStage.tsx new file mode 100644 index 0000000000..0c4c432f9e --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/DeploymentStage.tsx @@ -0,0 +1,14 @@ +import { IconDataSpan } from 'container/PipelinePage/styles'; + +import { getDeploymentStage, getDeploymentStageIcon } from './utils'; + +function DeploymentStage(deployStatus: string): JSX.Element { + return ( + <> + {getDeploymentStageIcon(deployStatus)} + {getDeploymentStage(deployStatus)} + + ); +} + +export default DeploymentStage; diff --git a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/DeploymentTime.tsx b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/DeploymentTime.tsx new file mode 100644 index 0000000000..fad712d421 --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/DeploymentTime.tsx @@ -0,0 +1,9 @@ +import dayjs from 'dayjs'; + +function DeploymentTime(deployTime: string): JSX.Element { + return ( + {dayjs(deployTime).locale('en').format('MMMM DD, YYYY hh:mm A')} + ); +} + +export default DeploymentTime; diff --git a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/index.tsx b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/index.tsx new file mode 100644 index 0000000000..ab70101ea3 --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/index.tsx @@ -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 ( + + + + ); +} + +interface ChangeHistoryProps { + piplineData: Pipeline; +} + +export default ChangeHistory; diff --git a/frontend/src/container/PipelinePage/Layouts/ChangeHistory/utils.tsx b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/utils.tsx new file mode 100644 index 0000000000..ff3161f7d4 --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/ChangeHistory/utils.tsx @@ -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 ( + } /> + ); + case 'DEPLOYED': + return ; + case 'DIRTY': + return ; + case 'FAILED': + return ; + default: + return ; + } +} diff --git a/frontend/src/container/PipelinePage/Layouts/Pipeline/CreatePipelineButton.tsx b/frontend/src/container/PipelinePage/Layouts/Pipeline/CreatePipelineButton.tsx new file mode 100644 index 0000000000..05151506df --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/Pipeline/CreatePipelineButton.tsx @@ -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 ( + + + {isAddNewPipelineVisible && ( + } + onClick={actionHandler(ActionMode.Editing, setActionMode)} + disabled={isDisabled} + > + {t('enter_edit_mode')} + + )} + {!isAddNewPipelineVisible && ( + } + onClick={actionHandler(ActionType.AddPipeline, setActionType)} + type="primary" + > + {t('new_pipeline')} + + )} + + ); +} + +interface CreatePipelineButtonProps { + setActionType: (actionType: string) => void; + isActionMode: string; + setActionMode: (actionMode: string) => void; + piplineData: Pipeline; +} + +export default CreatePipelineButton; diff --git a/frontend/src/container/PipelinePage/Layouts/Pipeline/PipelinesSearchSection.tsx b/frontend/src/container/PipelinePage/Layouts/Pipeline/PipelinesSearchSection.tsx new file mode 100644 index 0000000000..ae830cae7f --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/Pipeline/PipelinesSearchSection.tsx @@ -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) => { + setPipelineSearchValue(event); + }, + [setPipelineSearchValue], + ); + + return ( + + ); +} + +interface PipelinesSearchSectionProps { + setPipelineSearchValue: Dispatch>; +} + +export default PipelinesSearchSection; diff --git a/frontend/src/container/PipelinePage/Layouts/Pipeline/index.tsx b/frontend/src/container/PipelinePage/Layouts/Pipeline/index.tsx new file mode 100644 index 0000000000..da17d90e95 --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/Pipeline/index.tsx @@ -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(); + const [isActionMode, setActionMode] = useState('viewing-mode'); + const [pipelineSearchValue, setPipelineSearchValue] = useState(''); + + return ( + <> + + + + + ); +} + +interface PipelinePageLayoutProps { + refetchPipelineLists: VoidFunction; + piplineData: Pipeline; +} + +export default PipelinePageLayout; diff --git a/frontend/src/container/PipelinePage/Layouts/config.ts b/frontend/src/container/PipelinePage/Layouts/config.ts new file mode 100644 index 0000000000..46d2d2738e --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/config.ts @@ -0,0 +1,3 @@ +export const historyPagination = { + defaultPageSize: 5, +}; diff --git a/frontend/src/container/PipelinePage/Layouts/utils.ts b/frontend/src/container/PipelinePage/Layouts/utils.ts new file mode 100644 index 0000000000..870c2e4653 --- /dev/null +++ b/frontend/src/container/PipelinePage/Layouts/utils.ts @@ -0,0 +1,4 @@ +import { PipelineData } from 'types/api/pipeline/def'; + +export const checkDataLength = (data: Array): boolean => + data?.length > 0; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/DescriptionTextArea.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/DescriptionTextArea.tsx new file mode 100644 index 0000000000..6bd456e7f1 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/DescriptionTextArea.tsx @@ -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 ( + {fieldData.fieldName}} + key={fieldData.id} + > + + + ); +} + +interface DescriptionTextAreaProps { + fieldData: ProcessorFormField; +} +export default DescriptionTextArea; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterSearch.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterSearch.tsx new file mode 100644 index 0000000000..5b9863b8dd --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/FilterSearch.tsx @@ -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 ( + {fieldData.fieldName}} + key={fieldData.id} + rules={formValidationRules} + name={fieldData.name} + > + + + ); +} +interface FilterSearchProps { + fieldData: ProcessorFormField; +} +export default FilterSearch; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/NameInput.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/NameInput.tsx new file mode 100644 index 0000000000..21e25118af --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/NameInput.tsx @@ -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 ( + {fieldData.fieldName}} + key={fieldData.id} + rules={formValidationRules} + name={fieldData.name} + > + + + ); +} + +interface NameInputProps { + fieldData: ProcessorFormField; +} +export default NameInput; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/ProcessorTags.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/ProcessorTags.tsx new file mode 100644 index 0000000000..b1d8b2dfcc --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/FormFields/ProcessorTags.tsx @@ -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 ( + {fieldData.fieldName}} + key={fieldData.id} + name={fieldData.name} + > + + + ); +} + +interface ProcessorTagsProps { + fieldData: ProcessorFormField; + setTagsListData: (tags: Array) => void; + tagsListData: Array; +} +export default ProcessorTags; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/index.tsx new file mode 100644 index 0000000000..220ff625b2 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/index.tsx @@ -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((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 ( + {modalTitle}} + centered + open={isOpen} + width={800} + footer={null} + onCancel={onCancelModalHandler} + > + +
+ {renderPipelineForm()} + + + + + + + + +
+ ); +} + +interface AddNewPipelineProps { + isActionType: string; + setActionType: (actionType?: ActionType) => void; + selectedPipelineData: PipelineData | undefined; + setShowSaveButton: (actionMode: ActionMode) => void; + setCurrPipelineData: ( + value: React.SetStateAction>, + ) => void; + currPipelineData: Array; +} + +export default AddNewPipeline; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/styles.ts b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/styles.ts new file mode 100644 index 0000000000..61dc2650c8 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/styles.ts @@ -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; +`; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/utils.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/utils.tsx new file mode 100644 index 0000000000..631bb34377 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewPipeline/utils.tsx @@ -0,0 +1,7 @@ +import { pipelineFields } from '../config'; + +export const renderPipelineForm = (): Array => + pipelineFields.map((field) => { + const Component = field.component; + return ; + }); diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/NameInput.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/NameInput.tsx new file mode 100644 index 0000000000..3991715f63 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/NameInput.tsx @@ -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 ( + + + {Number(fieldData.id) + 1} + + + {fieldData.fieldName}} + key={fieldData.id} + name={fieldData.name} + initialValue={fieldData.initialValue} + rules={fieldData.rules ? fieldData.rules : formValidationRules} + > + + + + + ); +} + +interface NameInputProps { + fieldData: ProcessorFormField; +} +export default NameInput; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/ParsingRulesTextArea.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/ParsingRulesTextArea.tsx new file mode 100644 index 0000000000..4d7f8b2ec0 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/ParsingRulesTextArea.tsx @@ -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 ( + + + {Number(fieldData.id) + 1} + + + {fieldData.fieldName}} + > + + + + + ); +} + +interface ParsingRulesTextAreaProps { + fieldData: ProcessorFormField; +} +export default ParsingRulesTextArea; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/TypeSelect.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/TypeSelect.tsx new file mode 100644 index 0000000000..e293f288f7 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/FormFields/TypeSelect.tsx @@ -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 ( + + 1 + + {t('processor_type')} + onChange(value)} + value={value} + > + {processorTypes.map(({ value, label }) => ( + + {label} + + ))} + + + + ); +} + +TypeSelect.defaultProps = { + value: DEFAULT_PROCESSOR_TYPE, +}; + +interface TypeSelectProps { + onChange: (value: string | unknown) => void; + value?: string; +} +export default TypeSelect; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts new file mode 100644 index 0000000000..e7e5677cd8 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/config.ts @@ -0,0 +1,226 @@ +type ProcessorType = { + key: string; + value: string; + label: string; + title?: string; + disabled?: boolean; +}; + +export const processorTypes: Array = [ + { 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 } = { + 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', + }, + ], +}; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx new file mode 100644 index 0000000000..c9ab5d8aeb --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/index.tsx @@ -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( + 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 ( + {modalTitle}} + centered + open={isOpen} + width={800} + footer={null} + onCancel={onCancelModal} + > + +
+ + {renderProcessorForm(processorType)} + + + + + + + + +
+ ); +} + +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; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/styles.ts b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/styles.ts new file mode 100644 index 0000000000..585ad6284b --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/styles.ts @@ -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; +`; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/utils.tsx b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/utils.tsx new file mode 100644 index 0000000000..b0de303157 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/AddNewProcessor/utils.tsx @@ -0,0 +1,9 @@ +import { processorFields, ProcessorFormField } from './config'; +import NameInput from './FormFields/NameInput'; + +export const renderProcessorForm = ( + processorType: string, +): Array => + processorFields[processorType]?.map((fieldName: ProcessorFormField) => ( + + )); diff --git a/frontend/src/container/PipelinePage/PipelineListsView/ModeAndConfiguration.tsx b/frontend/src/container/PipelinePage/PipelineListsView/ModeAndConfiguration.tsx new file mode 100644 index 0000000000..9c6a0d6a17 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/ModeAndConfiguration.tsx @@ -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 ( + + Mode: {actionMode ? 'Editing' : 'Viewing'} +
Configuration Version: {verison}
+
+ ); +} + +export interface ModeAndConfigurationType { + isActionMode: string; + verison: string | number; +} + +export default ModeAndConfiguration; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/PipelineExpandView.tsx b/frontend/src/container/PipelinePage/PipelineListsView/PipelineExpandView.tsx new file mode 100644 index 0000000000..89dbc4d87d --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/PipelineExpandView.tsx @@ -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 => ( + + ), + }, + { + title: '', + dataIndex: 'enabled', + key: 'enabled', + render: (value, record) => ( + + 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 ( + + + {t('add_new_processor')} + + ); + } + return undefined; + }, [isEditingActionMode, prevPipelineData, addNewProcessorHandler, t]); + + const onRowHandler = ( + _data: ProcessorData, + index?: number, + ): React.HTMLAttributes => + ({ + index, + moveRow: moveProcessorRow, + } as React.HTMLAttributes); + + 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: , + }); + + return ( + + + + ); +} + +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; +} + +export default PipelineExpandView; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/SaveConfigButton.tsx b/frontend/src/container/PipelinePage/PipelineListsView/SaveConfigButton.tsx new file mode 100644 index 0000000000..e7aa0ecedc --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/SaveConfigButton.tsx @@ -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 ( + + + + + ); +} +export interface SaveConfigButtonTypes { + onSaveConfigurationHandler: VoidFunction; + onCancelConfigurationHandler: VoidFunction; +} + +export default SaveConfigButton; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/DragAction.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/DragAction.tsx new file mode 100644 index 0000000000..c35c8b53dc --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/DragAction.tsx @@ -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 ( + + + + + ); +} + +interface DragActionProps { + isEnabled: boolean; + onChange: (checked: boolean) => void; +} + +export default DragAction; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions.tsx new file mode 100644 index 0000000000..1f86d675e8 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/PipelineActions.tsx @@ -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 ( + + + {/* */} + + + ); +} + +export interface PipelineActionsProps { + isPipelineAction: boolean; + editAction: VoidFunction; + deleteAction: VoidFunction; +} +export default PipelineActions; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/DeleteAction.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/DeleteAction.tsx new file mode 100644 index 0000000000..27c5189938 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/DeleteAction.tsx @@ -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 ; + } + return ( + + + + ); +} + +export interface DeleteActionProps { + isPipelineAction: boolean; + deleteAction: VoidFunction; +} +export default DeleteAction; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/EditAction.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/EditAction.tsx new file mode 100644 index 0000000000..14b53b1fd8 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/EditAction.tsx @@ -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 ; + } + return ( + + + + ); +} + +export interface EditActionProps { + isPipelineAction: boolean; + editAction: VoidFunction; +} +export default EditAction; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction.tsx new file mode 100644 index 0000000000..0260114a81 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableActions/ViewAction.tsx @@ -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 ; + } + return ( + + + + ); +} + +export interface ViewActionProps { + isPipelineAction: boolean; +} +export default ViewAction; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableExpandIcon.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableExpandIcon.tsx new file mode 100644 index 0000000000..04d5bee5d5 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/TableExpandIcon.tsx @@ -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, + ): void => { + onExpand(record, e); + }; + + if (expanded) { + return ; + } + return ; +} + +interface TableExpandIconProps { + expanded: boolean; + onExpand: (record: PipelineData, e: React.MouseEvent) => void; + record: PipelineData; +} + +export default TableExpandIcon; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/Tags.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/Tags.tsx new file mode 100644 index 0000000000..2f373b2051 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/Tags.tsx @@ -0,0 +1,19 @@ +import { Tag } from 'antd'; + +function Tags({ tags }: TagsProps): JSX.Element { + return ( + + {tags?.map((tag) => ( + + {tag} + + ))} + + ); +} + +interface TagsProps { + tags: Array; +} + +export default Tags; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx new file mode 100644 index 0000000000..4d351c7a41 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/TableComponents/index.tsx @@ -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 }) => {record}, + createdAt: ({ record }) => ( + + {dayjs(record).locale('en').format('MMMM DD, YYYY hh:mm A')} + + ), + id: ({ record }) => {record}, + name: ({ record }) => {record}, +}; + +function TableComponents({ + columnKey, + record, +}: TableComponentsProps): JSX.Element { + const Component = + componentMap[columnKey] ?? + (({ record }): JSX.Element => {record}); + + return ; +} + +type ComponentMap = { + [key: string]: React.FC<{ record: Record }>; +}; + +export type Record = PipelineData['orderId'] & ProcessorData; + +interface TableComponentsProps { + columnKey: string; + record: Record; +} + +export default TableComponents; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/config.ts b/frontend/src/container/PipelinePage/PipelineListsView/config.ts new file mode 100644 index 0000000000..44f6bcc0d7 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/config.ts @@ -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 | ColumnGroupType +> = [ + { + 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 | ColumnGroupType +> = [ + { + key: 'id', + title: '', + dataIndex: 'orderId', + width: 150, + }, + { + key: 'name', + title: '', + dataIndex: 'name', + }, +]; + +export const changeHistoryColumns: Array< + ColumnType | ColumnGroupType +> = [ + { + 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' }; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/index.tsx b/frontend/src/container/PipelinePage/PipelineListsView/index.tsx new file mode 100644 index 0000000000..9ae3cf6ab9 --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/index.tsx @@ -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>( + cloneDeep(piplineData?.pipelines), + ); + const [currPipelineData, setCurrPipelineData] = useState>( + cloneDeep(piplineData?.pipelines), + ); + const [ + expandedPipelineData, + setExpandedPipelineData, + ] = useState(); + const [ + selectedProcessorData, + setSelectedProcessorData, + ] = useState(); + const [ + selectedPipelineData, + setSelectedPipelineData, + ] = useState(); + const [expandedRowKeys, setExpandedRowKeys] = useState>(); + const [showSaveButton, setShowSaveButton] = useState(); + 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: {title}, + icon: , + content: {descrition}, + okText: {buttontext}, + cancelText: {t('cancel')}, + 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 => ( + + ), + }, + { + title: '', + dataIndex: 'enabled', + key: 'enabled', + render: (value, record) => ( + + 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 => ( + + ), + [ + 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) => void, + record: PipelineData, + ): JSX.Element => ( + + ); + + const addNewPipelineHandler = useCallback((): void => { + setActionType(ActionType.AddPipeline); + }, [setActionType]); + + const footer = useCallback((): JSX.Element | undefined => { + if (isEditingActionMode) { + return ( + } + > + {t('add_new_pipeline')} + + ); + } + 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 => + ({ + index, + moveRow: movePipelineRow, + } as React.HTMLAttributes); + + const expandableConfig: ExpandableConfig = { + expandedRowKeys, + onExpand, + expandIcon: ({ expanded, onExpand, record }: ExpandRowConfig) => + getExpandIcon(expanded, onExpand, record), + }; + + return ( + <> + {contextHolder} + + + + + +
+ + {showSaveButton && ( + + )} + + + ); +} + +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) => void; + record: PipelineData; +} + +export interface AlertMessage { + title: string; + descrition: string; + buttontext: string; + onOk: VoidFunction; + onCancel?: VoidFunction; +} + +export default PipelineListsView; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/styles.ts b/frontend/src/container/PipelinePage/PipelineListsView/styles.ts new file mode 100644 index 0000000000..0b3ddbff3d --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/styles.ts @@ -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 & { 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; +`; diff --git a/frontend/src/container/PipelinePage/PipelineListsView/utils.tsx b/frontend/src/container/PipelinePage/PipelineListsView/utils.tsx new file mode 100644 index 0000000000..306f6d1b4e --- /dev/null +++ b/frontend/src/container/PipelinePage/PipelineListsView/utils.tsx @@ -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( + arr: Array, + target: T, + key: keyof T, +): Array { + return arr.filter((data) => data[key] !== target?.[key]); +} + +export function getRecordIndex( + arr: Array, + target: T, + key: keyof T, +): number { + return arr?.findIndex((item) => item[key] === target?.[key]); +} + +export function getUpdatedRow( + data: Array, + dragIndex: number, + hoverIndex: number, +): Array { + return update(data, { + $splice: [ + [dragIndex, 1], + [hoverIndex, 0, data[dragIndex]], + ], + }); +} + +export function getTableColumn( + columnData: Array>, +): Array> { + return columnData.map(({ title, key, dataIndex, ellipsis, width }) => ({ + title, + dataIndex, + key, + align: key === 'id' ? 'right' : 'left', + ellipsis, + width, + render: (record: Record): JSX.Element => ( + + ), + })); +} + +export function getEditedDataSource( + arr: Array, + target: T, + key: keyof T, + editedArr: T, +): Array { + 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( + processorData: Array, + dragIndex: number, + hoverIndex: number, +): Array { + 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; +} diff --git a/frontend/src/container/PipelinePage/components/TagInput.tsx b/frontend/src/container/PipelinePage/components/TagInput.tsx new file mode 100644 index 0000000000..e0583b90bf --- /dev/null +++ b/frontend/src/container/PipelinePage/components/TagInput.tsx @@ -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(''); + const [editInputIndex, setEditInputIndex] = useState(-1); + const [editInputValue, setEditInputValue] = useState(''); + const inputRef = useRef(null); + const editInputRef = useRef(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): 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, + ): 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: , + 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 ( + + ); + } + const isLongTag = tag.length > 20; + const tagElem = ( + + { + setEditInputIndex(index); + setEditInputValue(tag); + e.preventDefault(); + }} + > + {isLongTag ? `${tag.slice(0, 20)}...` : tag} + + + ); + return isLongTag ? ( + + {tagElem} + + ) : ( + tagElem + ); + }); + + const isButtonVisible = useMemo( + () => tagsListData?.length || inputValue.length || inputValue, + [inputValue, tagsListData?.length], + ); + + return ( + + { + e.preventDefault(); + handleInputConfirm(); + }} + placeholder={placeHolder} + prefix={showAllData} + /> + + {isButtonVisible ? ( + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/DeleteAction.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/DeleteAction.test.tsx.snap new file mode 100644 index 0000000000..7f01c2f6cd --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/DeleteAction.test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render DeleteAction section 1`] = ` + + + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/DragAction.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/DragAction.test.tsx.snap new file mode 100644 index 0000000000..a9574993a0 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/DragAction.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render DragAction section 1`] = ` + + .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; +} + +
+ + + + +
+
+`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/EditAction.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/EditAction.test.tsx.snap new file mode 100644 index 0000000000..1bf7485d94 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/EditAction.test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render EditAction section 1`] = ` + + + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap new file mode 100644 index 0000000000..e96d3ac9a1 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineActions.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render PipelineActions section 1`] = ` + + .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; +} + +
+ + + + + + +
+
+`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineExpandView.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineExpandView.test.tsx.snap new file mode 100644 index 0000000000..ab46e3dd8e --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelineExpandView.test.tsx.snap @@ -0,0 +1,141 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render AddNewPipeline section 1`] = ``; + +exports[`PipelinePage should render PipelineExpandView section 1`] = ` + + .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; +} + +
+
+
+
+
+
+
+ + + + + + + + + + + + + +
+ + + 1 + + + +
+ grok use common asd +
+
+ + + 2 + + + +
+ rename auth +
+
+ + + + + + +
+`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/PipelinePageLayout.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelinePageLayout.test.tsx.snap new file mode 100644 index 0000000000..fc3092c4f6 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelinePageLayout.test.tsx.snap @@ -0,0 +1,326 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render AddNewPipeline section 1`] = ``; + +exports[`PipelinePage container test should render PipelinePageLayout section 1`] = ` + + .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; +} + +
+ + + + +
+ + + + + + + + + + + + + + + + + + .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; +} + +
+
+ Mode: + + Viewing + +
+ Configuration Version: 1 +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + +
+ + + Pipeline Name + + Filters + + Last Edited + + Edited By +
+
+
+ + + + + + + + + +
+
+ No data +
+
+
+
+
+ +
+
+
+
+ +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/PipelinesSearchSection.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelinesSearchSection.test.tsx.snap new file mode 100644 index 0000000000..17d488d290 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/PipelinesSearchSection.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render PipelinesSearchSection section 1`] = ` + + + + + + + + + + + + + + + + + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/TableExpandIcon.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/TableExpandIcon.test.tsx.snap new file mode 100644 index 0000000000..a3ce5e3110 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/TableExpandIcon.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render TableExpandIcon section 1`] = ` + + + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/TagInput.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/TagInput.test.tsx.snap new file mode 100644 index 0000000000..6a97ec9ad6 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/TagInput.test.tsx.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pipeline Page should render TagInput section 1`] = ` + + .c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + width: 100%; +} + +
+ + + + +
+
+`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/Tags.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/Tags.test.tsx.snap new file mode 100644 index 0000000000..1fa7dd1d55 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/Tags.test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render Tags section 1`] = ` + + + + server + + + app + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/__snapshots__/ViewAction.test.tsx.snap b/frontend/src/container/PipelinePage/tests/__snapshots__/ViewAction.test.tsx.snap new file mode 100644 index 0000000000..c318633144 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/__snapshots__/ViewAction.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinePage container test should render ViewAction section 1`] = ` + + + + + +`; diff --git a/frontend/src/container/PipelinePage/tests/utils.test.ts b/frontend/src/container/PipelinePage/tests/utils.test.ts new file mode 100644 index 0000000000..f433c422b9 --- /dev/null +++ b/frontend/src/container/PipelinePage/tests/utils.test.ts @@ -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); + }); +}); diff --git a/frontend/src/container/SideNav/config.ts b/frontend/src/container/SideNav/config.ts index 0246bc0e8e..dd1a2a1201 100644 --- a/frontend/src/container/SideNav/config.ts +++ b/frontend/src/container/SideNav/config.ts @@ -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 = { [ROUTES.SERVICE_METRICS]: [QueryParams.resourceAttributes], [ROUTES.SERVICE_MAP]: [QueryParams.resourceAttributes], @@ -36,4 +44,6 @@ export const routeConfig: Record = { [ROUTES.UN_AUTHORIZED]: [QueryParams.resourceAttributes], [ROUTES.USAGE_EXPLORER]: [QueryParams.resourceAttributes], [ROUTES.VERSION]: [QueryParams.resourceAttributes], + [ROUTES.TRACE_EXPLORER]: [QueryParams.resourceAttributes], + [ROUTES.PIPELINES]: [QueryParams.resourceAttributes], }; diff --git a/frontend/src/container/SideNav/index.tsx b/frontend/src/container/SideNav/index.tsx index 2fe53d4d44..0a4942f115 100644 --- a/frontend/src/container/SideNav/index.tsx +++ b/frontend/src/container/SideNav/index.tsx @@ -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], diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 3da53f194c..b7348e3d50 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -68,6 +68,11 @@ const menus: SidebarMenu[] = [ // label: 'Views', // }, // ], + // { + // key: ROUTES.PIPELINES, + // label: 'Pipelines', + // }, + // ], }, { key: ROUTES.ALL_DASHBOARD, diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index f3bcfe560f..ca127b388f 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -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 { diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index d327476e08..ef31201da7 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -82,4 +82,5 @@ export const routesToSkip = [ ROUTES.ALERTS_NEW, ROUTES.EDIT_ALERTS, ROUTES.LIST_ALL_ALERT, + ROUTES.PIPELINES, ]; diff --git a/frontend/src/pages/Pipelines/index.tsx b/frontend/src/pages/Pipelines/index.tsx new file mode 100644 index 0000000000..8828ad4ab7 --- /dev/null +++ b/frontend/src/pages/Pipelines/index.tsx @@ -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: ( + + ), + }, + { + key: 'change-history', + label: `Change History`, + children: , + }, + ], + [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 ; + } + + return ; +} + +export default Pipelines; diff --git a/frontend/src/types/api/pipeline/def.ts b/frontend/src/types/api/pipeline/def.ts new file mode 100644 index 0000000000..5008cace6c --- /dev/null +++ b/frontend/src/types/api/pipeline/def.ts @@ -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; + createdAt: string; + description?: string; + createdBy: string; + enabled: boolean; + filter: string; + id?: string; + name: string; + orderId: number; + tags?: Array; // 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; + id: string; + is_valid: boolean; + lastConf: string; + lastHash: string; + pipelines: Array; + 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', +} diff --git a/frontend/src/types/api/pipeline/get.ts b/frontend/src/types/api/pipeline/get.ts new file mode 100644 index 0000000000..969770cd06 --- /dev/null +++ b/frontend/src/types/api/pipeline/get.ts @@ -0,0 +1,3 @@ +export type Props = { + version: string | number; +}; diff --git a/frontend/src/types/api/pipeline/post.ts b/frontend/src/types/api/pipeline/post.ts new file mode 100644 index 0000000000..b4015635b5 --- /dev/null +++ b/frontend/src/types/api/pipeline/post.ts @@ -0,0 +1,5 @@ +import { PipelineData } from './def'; + +export interface Props { + data: { pipelines: Array }; +} diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 452d495fec..db2a7eb049 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -17,7 +17,8 @@ export type ComponentTypes = | 'new_dashboard' | 'new_alert_action' | 'edit_widget' - | 'add_panel'; + | 'add_panel' + | 'page_pipelines'; export const componentPermission: Record = { current_org_settings: ['ADMIN'], @@ -36,6 +37,7 @@ export const componentPermission: Record = { new_alert_action: ['ADMIN'], edit_widget: ['ADMIN', 'EDITOR'], add_panel: ['ADMIN', 'EDITOR'], + page_pipelines: ['ADMIN', 'EDITOR'], }; export const routePermission: Record = { @@ -72,4 +74,6 @@ export const routePermission: Record = { LOGS: ['ADMIN', 'EDITOR', 'VIEWER'], LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], LIST_LICENSES: ['ADMIN'], + TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], + PIPELINES: ['ADMIN', 'EDITOR', 'VIEWER'], }; diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index e201a202c8..f03b264435 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -26,6 +26,9 @@ if (process.env.BUNDLE_ANALYSER === 'true') { plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'server' })); } +/** + * @type {import('webpack').Configuration} + */ const config = { mode: 'development', devtool: 'source-map', @@ -51,6 +54,7 @@ const config = { resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], plugins: [new TsconfigPathsPlugin({})], + fallback: { 'process/browser': require.resolve('process/browser') }, }, module: { rules: [ diff --git a/frontend/webpack.config.prod.js b/frontend/webpack.config.prod.js index 78dbb67875..13fad223d5 100644 --- a/frontend/webpack.config.prod.js +++ b/frontend/webpack.config.prod.js @@ -53,6 +53,7 @@ const config = { resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], plugins: [new TsconfigPathsPlugin({})], + fallback: { 'process/browser': require.resolve('process/browser') }, }, cache: { type: 'filesystem', diff --git a/frontend/yarn.lock b/frontend/yarn.lock index f1f6cfbe71..769b65d920 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1850,6 +1850,21 @@ rc-trigger "^5.3.4" rc-util "^5.24.4" +"@react-dnd/asap@^5.0.1": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" + integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A== + +"@react-dnd/invariant@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df" + integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw== + +"@react-dnd/shallowequal@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4" + integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA== + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz" @@ -2314,6 +2329,13 @@ resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/react-addons-update@0.14.21": + version "0.14.21" + resolved "https://registry.yarnpkg.com/@types/react-addons-update/-/react-addons-update-0.14.21.tgz#00feaa412c376cba2dd37a5adc347352ba15f4ed" + integrity sha512-HOxr0Hd8C1L4uw8DHyv2etqMVIj78oLEpe567/HgjoE+1Lc+PUsTGXTrkr1BDvFqsu5r49mSlgI5evwrk9eutA== + dependencies: + "@types/react" "*" + "@types/react-dom@18.0.10", "@types/react-dom@^18.0.0": version "18.0.10" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz" @@ -5192,6 +5214,15 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +dnd-core@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19" + integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng== + dependencies: + "@react-dnd/asap" "^5.0.1" + "@react-dnd/invariant" "^4.0.1" + redux "^4.2.0" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" @@ -10523,6 +10554,31 @@ rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.8: rc-resize-observer "^1.0.0" rc-util "^5.15.0" +react-addons-update@15.6.3: + version "15.6.3" + resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.6.3.tgz#c449c309154024d04087b206d0400e020547b313" + integrity sha512-wBkjgx5cR0XTjZEz5jl2kScChrjI9T7rWVdaM0dLiIdHSgeHycLRdHPPiTgKk7vK18Od4rXmLJv91qofBXlE0A== + dependencies: + object-assign "^4.1.0" + +react-dnd-html5-backend@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6" + integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw== + dependencies: + dnd-core "^16.0.1" + +react-dnd@16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37" + integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q== + dependencies: + "@react-dnd/invariant" "^4.0.1" + "@react-dnd/shallowequal" "^4.0.1" + dnd-core "^16.0.1" + fast-deep-equal "^3.1.3" + hoist-non-react-statics "^3.3.2" + react-dom@17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" @@ -10819,9 +10875,9 @@ redux-thunk@^2.3.0: resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== -redux@^4.0.0, redux@^4.0.5: +redux@^4.0.0, redux@^4.0.5, redux@^4.2.0: version "4.2.1" - resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== dependencies: "@babel/runtime" "^7.9.2"