#14136: duplicate column issue in pipeline execution and date picker clear action (#14137)

* fix duplicate column issue in pipeline execution and date picker clear action

* minor code improvement

* changes as per comments

* supported unit test
This commit is contained in:
Ashish Gupta 2023-12-01 11:43:35 +05:30 committed by GitHub
parent 71b8b1d5fe
commit 344ab4d77d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 323 additions and 33 deletions

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g fill="#0968da" clip-path="url(#a)"><path d="M13.938 1.25H12.5V.5a.5.5 0 1 0-1 0v.75h-7V.5a.5.5 0 1 0-1 0v.75H2.062A2.065 2.065 0 0 0 0 3.313v10.624C0 15.075.925 16 2.063 16h11.874A2.065 2.065 0 0 0 16 13.937V3.313a2.065 2.065 0 0 0-2.063-2.063Zm-11.876 1H3.5v.5a.5.5 0 0 0 1 0v-.5h7v.5a.5.5 0 1 0 1 0v-.5h1.438c.585 0 1.062.477 1.062 1.063V4.5H1V3.312c0-.585.477-1.062 1.063-1.062ZM13.938 15H2.062A1.064 1.064 0 0 1 1 13.937V5.5h14v8.438c0 .585-.477 1.062-1.063 1.062Z"/><path d="M6.375 9C6.72 9 7 8.776 7 8.5S6.72 8 6.375 8H3.696c-.345 0-.625.224-.625.5s.28.5.625.5h2.68ZM12.353 9c.345 0 .625-.224.625-.5s-.28-.5-.625-.5H9.625C9.28 8 9 8.224 9 8.5s.28.5.625.5h2.728ZM6.375 12c.345 0 .625-.224.625-.5s-.28-.5-.625-.5H3.696c-.345 0-.625.224-.625.5s.28.5.625.5h2.68ZM12.353 12c.345 0 .625-.224.625-.5s-.28-.5-.625-.5H9.625c-.345 0-.625.224-.625.5s.28.5.625.5h2.728Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" ><g fill="currentColor" clip-path="url(#a)"><path d="M13.938 1.25H12.5V.5a.5.5 0 1 0-1 0v.75h-7V.5a.5.5 0 1 0-1 0v.75H2.062A2.065 2.065 0 0 0 0 3.313v10.624C0 15.075.925 16 2.063 16h11.874A2.065 2.065 0 0 0 16 13.937V3.313a2.065 2.065 0 0 0-2.063-2.063Zm-11.876 1H3.5v.5a.5.5 0 0 0 1 0v-.5h7v.5a.5.5 0 1 0 1 0v-.5h1.438c.585 0 1.062.477 1.062 1.063V4.5H1V3.312c0-.585.477-1.062 1.063-1.062ZM13.938 15H2.062A1.064 1.064 0 0 1 1 13.937V5.5h14v8.438c0 .585-.477 1.062-1.063 1.062Z"/><path d="M6.375 9C6.72 9 7 8.776 7 8.5S6.72 8 6.375 8H3.696c-.345 0-.625.224-.625.5s.28.5.625.5h2.68ZM12.353 9c.345 0 .625-.224.625-.5s-.28-.5-.625-.5H9.625C9.28 8 9 8.224 9 8.5s.28.5.625.5h2.728ZM6.375 12c.345 0 .625-.224.625-.5s-.28-.5-.625-.5H3.696c-.345 0-.625.224-.625.5s.28.5.625.5h2.68ZM12.353 12c.345 0 .625-.224.625-.5s-.28-.5-.625-.5H9.625c-.345 0-.625.224-.625.5s.28.5.625.5h2.728Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="#0968da" d="M.5 2.82h7.613a2.333 2.333 0 0 0 2.274 1.829 2.333 2.333 0 0 0 2.274-1.828H15.5a.5.5 0 0 0 0-1h-2.839a2.335 2.335 0 0 0-2.274-1.829c-1.11 0-2.044.785-2.274 1.829H.5a.5.5 0 0 0 0 1Zm8.559-.498v-.006A1.33 1.33 0 0 1 10.387.992c.73 0 1.325.593 1.328 1.323v.008a1.33 1.33 0 0 1-1.328 1.326 1.33 1.33 0 0 1-1.328-1.325v-.002ZM15.5 13.179h-2.839a2.335 2.335 0 0 0-2.274-1.828c-1.11 0-2.044.784-2.274 1.828H.5a.5.5 0 1 0 0 1h7.613a2.333 2.333 0 0 0 2.274 1.829 2.333 2.333 0 0 0 2.274-1.829H15.5a.5.5 0 1 0 0-1Zm-5.113 1.829a1.33 1.33 0 0 1-1.328-1.326v-.007a1.33 1.33 0 0 1 1.328-1.324c.73 0 1.325.593 1.328 1.323v.007a1.33 1.33 0 0 1-1.328 1.327ZM15.5 7.5H7.887a2.332 2.332 0 0 0-2.274-1.828A2.333 2.333 0 0 0 3.339 7.5H.5a.5.5 0 0 0 0 1h2.839a2.335 2.335 0 0 0 2.274 1.828c1.11 0 2.044-.784 2.274-1.828H15.5a.5.5 0 0 0 0-1Zm-8.559.499v.005a1.33 1.33 0 0 1-1.328 1.324c-.73 0-1.325-.593-1.328-1.323v-.007a1.33 1.33 0 0 1 1.328-1.326 1.33 1.33 0 0 1 1.328 1.325v.002Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M.5 2.82h7.613a2.333 2.333 0 0 0 2.274 1.829 2.333 2.333 0 0 0 2.274-1.828H15.5a.5.5 0 0 0 0-1h-2.839a2.335 2.335 0 0 0-2.274-1.829c-1.11 0-2.044.785-2.274 1.829H.5a.5.5 0 0 0 0 1Zm8.559-.498v-.006A1.33 1.33 0 0 1 10.387.992c.73 0 1.325.593 1.328 1.323v.008a1.33 1.33 0 0 1-1.328 1.326 1.33 1.33 0 0 1-1.328-1.325v-.002ZM15.5 13.179h-2.839a2.335 2.335 0 0 0-2.274-1.828c-1.11 0-2.044.784-2.274 1.828H.5a.5.5 0 1 0 0 1h7.613a2.333 2.333 0 0 0 2.274 1.829 2.333 2.333 0 0 0 2.274-1.829H15.5a.5.5 0 1 0 0-1Zm-5.113 1.829a1.33 1.33 0 0 1-1.328-1.326v-.007a1.33 1.33 0 0 1 1.328-1.324c.73 0 1.325.593 1.328 1.323v.007a1.33 1.33 0 0 1-1.328 1.327ZM15.5 7.5H7.887a2.332 2.332 0 0 0-2.274-1.828A2.333 2.333 0 0 0 3.339 7.5H.5a.5.5 0 0 0 0 1h2.839a2.335 2.335 0 0 0 2.274 1.828c1.11 0 2.044-.784 2.274-1.828H15.5a.5.5 0 0 0 0-1Zm-8.559.499v.005a1.33 1.33 0 0 1-1.328 1.324c-.73 0-1.325-.593-1.328-1.323v-.007a1.33 1.33 0 0 1 1.328-1.326 1.33 1.33 0 0 1 1.328 1.325v.002Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,108 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import ExecutionsTab from './Execution.component';
jest.mock('./ListView/ListViewTab.component', () =>
jest.fn().mockImplementation(() => <div>ListViewTab</div>)
);
jest.mock('./TreeView/TreeViewTab.component', () =>
jest.fn().mockImplementation(() => <div>TreeViewTab</div>)
);
jest.mock('../../assets/svg/calendar.svg', () =>
jest.fn().mockImplementation(() => <div>Calendar</div>)
);
jest.mock('../../assets/svg/filter.svg', () =>
jest.fn().mockImplementation(() => <div>FilterIcon</div>)
);
jest.mock('../../utils/ToastUtils', () => ({
showErrorToast: jest.fn(),
}));
jest.mock('../../utils/date-time/DateTimeUtils', () => ({
getCurrentMillis: jest.fn().mockImplementation((data) => data),
getEpochMillisForPastDays: jest
.fn()
.mockImplementation(() => <div>StatusIndicator</div>),
}));
jest.mock('../../rest/pipelineAPI', () => ({
getPipelineStatus: jest.fn().mockImplementation(() =>
Promise.resolve({
data: [],
})
),
}));
const mockProps = {
pipelineFQN: 'pipelineFQN',
tasks: [],
};
describe('Test Execution Component', () => {
it('Should render component properly', async () => {
render(<ExecutionsTab {...mockProps} />);
expect(screen.getByTestId('execution-tab')).toBeInTheDocument();
expect(screen.getByTestId('radio-switch')).toBeInTheDocument();
expect(screen.getByTestId('status-button')).toBeInTheDocument();
expect(screen.getByTestId('data-range-picker-button')).toBeInTheDocument();
expect(screen.getByTestId('data-range-picker')).toBeInTheDocument();
});
it('Should render ListViewTab component', async () => {
render(<ExecutionsTab {...mockProps} />);
expect(screen.getByText('ListViewTab')).toBeInTheDocument();
});
it('Should render TreeViewTab component on tabView change', async () => {
render(<ExecutionsTab {...mockProps} />);
expect(screen.getByText('ListViewTab')).toBeInTheDocument();
const treeRadioButton = screen.getByText('Tree');
act(() => {
fireEvent.click(treeRadioButton);
});
expect(screen.getByText('TreeViewTab')).toBeInTheDocument();
});
it('Should render data picker button only on Tree View Tab', async () => {
render(<ExecutionsTab {...mockProps} />);
expect(screen.getByText('ListViewTab')).toBeInTheDocument();
expect(screen.getByTestId('data-range-picker-button')).toBeInTheDocument();
const treeRadioButton = screen.getByText('Tree');
act(() => {
fireEvent.click(treeRadioButton);
});
expect(screen.getByText('TreeViewTab')).toBeInTheDocument();
expect(
screen.queryByTestId('data-range-picker-button')
).not.toBeInTheDocument();
});
});

View File

@ -11,13 +11,12 @@
* limitations under the License.
*/
import { CloseCircleOutlined } from '@ant-design/icons';
import Icon, { CloseCircleOutlined } from '@ant-design/icons';
import {
Button,
Col,
DatePicker,
Dropdown,
Menu,
MenuProps,
Radio,
Row,
@ -27,7 +26,7 @@ import { RangePickerProps } from 'antd/lib/date-picker';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isNaN, map } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as Calendar } from '../../assets/svg/calendar.svg';
import { ReactComponent as FilterIcon } from '../../assets/svg/filter.svg';
@ -84,22 +83,19 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
}
};
const handleMenuClick: MenuProps['onClick'] = (event) => {
if (event?.key) {
setStatus(MenuOptions[event.key as keyof typeof MenuOptions]);
}
};
const handleMenuClick: MenuProps['onClick'] = useCallback(
(event) => setStatus(MenuOptions[event.key as keyof typeof MenuOptions]),
[]
);
const menu = useMemo(
() => (
<Menu
items={map(MenuOptions, (value, key) => ({
key: key,
label: value,
}))}
onClick={handleMenuClick}
/>
),
const statusMenuItems = useMemo(
() => ({
items: map(MenuOptions, (value, key) => ({
key: key,
label: value,
})),
onClick: handleMenuClick,
}),
[handleMenuClick]
);
@ -123,6 +119,12 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
}
setDatesSelected(true);
} else {
setDatesSelected(false);
setStartTime(
getEpochMillisForPastDays(EXECUTION_FILTER_RANGE.last365days.days)
);
setEndTime(getCurrentMillis());
}
};
@ -131,7 +133,7 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
}, [pipelineFQN, datesSelected, startTime, endTime]);
return (
<Row className="h-full p-md" gutter={16}>
<Row className="h-full p-md" data-testid="execution-tab" gutter={16}>
<Col flex="auto">
<Row gutter={[16, 16]}>
<Col span={24}>
@ -139,18 +141,20 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
<Radio.Group
buttonStyle="solid"
className="radio-switch"
data-testid="radio-switch"
optionType="button"
options={Object.values(PIPELINE_EXECUTION_TABS)}
value={view}
onChange={(e) => setView(e.target.value)}
/>
<Space>
<Dropdown overlay={menu} placement="bottom">
<Button ghost type="primary">
<Space>
<FilterIcon />
{status === MenuOptions.all ? t('label.status') : status}
</Space>
<Dropdown menu={statusMenuItems} placement="bottom">
<Button
ghost
data-testid="status-button"
icon={<Icon component={FilterIcon} size={12} />}
type="primary">
{status === MenuOptions.all ? t('label.status') : status}
</Button>
</Dropdown>
{view === PIPELINE_EXECUTION_TABS.LIST_VIEW ? (
@ -161,12 +165,13 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
'range-picker-button-width delay-100':
!datesSelected && !isClickedCalendar,
})}
data-testid="data-range-picker-button"
icon={<Icon component={Calendar} size={12} />}
type="primary"
onClick={() => {
setIsClickedCalendar(true);
}}>
<Space>
<Calendar />
<span>
{!datesSelected && (
<label>{t('label.date-filter')}</label>
)}
@ -176,6 +181,7 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
bordered={false}
className="executions-date-picker"
clearIcon={<CloseCircleOutlined />}
data-testid="data-range-picker"
open={isClickedCalendar}
placeholder={['', '']}
suffixIcon={null}
@ -184,7 +190,7 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
setIsClickedCalendar(isOpen);
}}
/>
</Space>
</span>
</Button>
</>
) : null}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from '@testing-library/react';
import React from 'react';
import { StatusType } from '../../../generated/entity/data/pipeline';
import { EXECUTION_LIST_MOCK } from '../../../mocks/PipelineVersion.mock';
import ListView from './ListViewTab.component';
jest.mock('../../common/ErrorWithPlaceholder/FilterTablePlaceHolder', () =>
jest.fn().mockImplementation(() => <div>FilterTablePlaceHolder</div>)
);
jest.mock('../../../utils/executionUtils', () => ({
getTableViewData: jest.fn().mockImplementation((data) => data),
StatusIndicator: jest
.fn()
.mockImplementation(() => <div>StatusIndicator</div>),
}));
const mockProps = {
executions: EXECUTION_LIST_MOCK,
status: StatusType.Successful,
loading: false,
};
describe('Test ListViewTab Component', () => {
it('Should render loader in table component', async () => {
render(<ListView {...mockProps} loading />);
expect(screen.getByTestId('skeleton-table')).toBeInTheDocument();
});
it('Should render component properly if not loading', async () => {
render(<ListView {...mockProps} />);
expect(screen.getByTestId('list-view-table')).toBeInTheDocument();
});
it('Should render NoDataPlaceholder if no data present', async () => {
render(<ListView {...mockProps} executions={[]} />);
expect(screen.getByTestId('list-view-table')).toBeInTheDocument();
expect(screen.getByText('FilterTablePlaceHolder')).toBeInTheDocument();
});
it('Should render columns if data present', async () => {
render(<ListView {...mockProps} />);
expect(screen.getByTestId('list-view-table')).toBeInTheDocument();
expect(
screen.queryByText('FilterTablePlaceHolder')
).not.toBeInTheDocument();
expect(screen.getAllByText('StatusIndicator')).toHaveLength(7);
});
});

View File

@ -11,7 +11,6 @@
* limitations under the License.
*/
import { Table } from 'antd';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
@ -23,6 +22,7 @@ import {
StatusIndicator,
} from '../../../utils/executionUtils';
import FilterTablePlaceHolder from '../../common/ErrorWithPlaceholder/FilterTablePlaceHolder';
import Table from '../../common/Table/Table';
interface ListViewProps {
executions: Array<PipelineStatus> | undefined;
@ -65,13 +65,14 @@ const ListView = ({ executions, status, loading }: ListViewProps) => {
bordered
className="h-full"
columns={columns}
data-testid="list-view-table"
dataSource={tableData}
loading={loading}
locale={{
emptyText: <FilterTablePlaceHolder />,
}}
pagination={false}
rowKey={(record) => record.name.concat(record.timestamp as string)}
rowKey={(record) => `${record.name}-${record.status}-${record.key}`}
/>
);
};

View File

@ -12,7 +12,10 @@
*/
import { PipelineVersionProp } from '../components/PipelineVersion/PipelineVersion.interface';
import { PipelineServiceType } from '../generated/entity/data/pipeline';
import {
PipelineServiceType,
StatusType,
} from '../generated/entity/data/pipeline';
import { ENTITY_PERMISSIONS } from '../mocks/Permissions.mock';
import {
mockBackHandler,
@ -144,3 +147,104 @@ export const mockColumnDiffPipelineVersionMockProps = {
...pipelineVersionMockProps,
currentVersionData: mockColumnDiffPipelineData,
};
export const EXECUTION_LIST_MOCK = [
{
timestamp: 1697265270340,
executionStatus: StatusType.Pending,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Pending,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Pending,
},
],
},
{
timestamp: 1697265270200,
executionStatus: StatusType.Pending,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Failed,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Pending,
},
],
},
{
timestamp: 1697265269958,
executionStatus: StatusType.Pending,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Pending,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Successful,
},
],
},
{
timestamp: 1697265269825,
executionStatus: StatusType.Failed,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Failed,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Successful,
},
],
},
{
timestamp: 1697265269683,
executionStatus: StatusType.Failed,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Successful,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Failed,
},
],
},
{
timestamp: 1697265269509,
executionStatus: StatusType.Successful,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Successful,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Successful,
},
],
},
{
timestamp: 1697265269363,
executionStatus: StatusType.Failed,
taskStatus: [
{
name: 'dim_address_task',
executionStatus: StatusType.Failed,
},
{
name: 'assert_table_exists',
executionStatus: StatusType.Failed,
},
],
},
];

View File

@ -35,6 +35,7 @@ export interface ViewDataInterface {
timestamp?: string;
executionStatus?: StatusType;
type?: string;
key: number;
}
export const StatusIndicator = ({ status }: StatusIndicatorInterface) => (
@ -73,6 +74,7 @@ export const getTableViewData = (
timestamp: formatDateTime(execution.timestamp),
executionStatus: execute.executionStatus,
type: '--',
key: execution.timestamp,
});
});
});