diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/calendar.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/calendar.svg
index b7fa71e12ec..c79063fe208 100644
--- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/calendar.svg
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/calendar.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/filter.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/filter.svg
index 7f7be1ee132..8f4a9852135 100644
--- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/filter.svg
+++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/filter.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.test.tsx
new file mode 100644
index 00000000000..33417efd714
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.test.tsx
@@ -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(() =>
ListViewTab
)
+);
+
+jest.mock('./TreeView/TreeViewTab.component', () =>
+ jest.fn().mockImplementation(() => TreeViewTab
)
+);
+
+jest.mock('../../assets/svg/calendar.svg', () =>
+ jest.fn().mockImplementation(() => Calendar
)
+);
+
+jest.mock('../../assets/svg/filter.svg', () =>
+ jest.fn().mockImplementation(() => FilterIcon
)
+);
+
+jest.mock('../../utils/ToastUtils', () => ({
+ showErrorToast: jest.fn(),
+}));
+
+jest.mock('../../utils/date-time/DateTimeUtils', () => ({
+ getCurrentMillis: jest.fn().mockImplementation((data) => data),
+ getEpochMillisForPastDays: jest
+ .fn()
+ .mockImplementation(() => StatusIndicator
),
+}));
+
+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( );
+
+ 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( );
+
+ expect(screen.getByText('ListViewTab')).toBeInTheDocument();
+ });
+
+ it('Should render TreeViewTab component on tabView change', async () => {
+ render( );
+
+ 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( );
+
+ 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();
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx
index ce6b1d2ba49..ca3db77d4d3 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx
@@ -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(
- () => (
- ({
- 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 (
-
+
@@ -139,18 +141,20 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
setView(e.target.value)}
/>
-
-
-
-
- {status === MenuOptions.all ? t('label.status') : status}
-
+
+ }
+ type="primary">
+ {status === MenuOptions.all ? t('label.status') : status}
{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={ }
type="primary"
onClick={() => {
setIsClickedCalendar(true);
}}>
-
-
+
{!datesSelected && (
{t('label.date-filter')}
)}
@@ -176,6 +181,7 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
bordered={false}
className="executions-date-picker"
clearIcon={ }
+ data-testid="data-range-picker"
open={isClickedCalendar}
placeholder={['', '']}
suffixIcon={null}
@@ -184,7 +190,7 @@ const ExecutionsTab = ({ pipelineFQN, tasks }: ExecutionProps) => {
setIsClickedCalendar(isOpen);
}}
/>
-
+
>
) : null}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.test.tsx
new file mode 100644
index 00000000000..833c2775a31
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.test.tsx
@@ -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(() => FilterTablePlaceHolder
)
+);
+
+jest.mock('../../../utils/executionUtils', () => ({
+ getTableViewData: jest.fn().mockImplementation((data) => data),
+ StatusIndicator: jest
+ .fn()
+ .mockImplementation(() => StatusIndicator
),
+}));
+
+const mockProps = {
+ executions: EXECUTION_LIST_MOCK,
+ status: StatusType.Successful,
+ loading: false,
+};
+
+describe('Test ListViewTab Component', () => {
+ it('Should render loader in table component', async () => {
+ render( );
+
+ expect(screen.getByTestId('skeleton-table')).toBeInTheDocument();
+ });
+
+ it('Should render component properly if not loading', async () => {
+ render( );
+
+ expect(screen.getByTestId('list-view-table')).toBeInTheDocument();
+ });
+
+ it('Should render NoDataPlaceholder if no data present', async () => {
+ render( );
+
+ expect(screen.getByTestId('list-view-table')).toBeInTheDocument();
+
+ expect(screen.getByText('FilterTablePlaceHolder')).toBeInTheDocument();
+ });
+
+ it('Should render columns if data present', async () => {
+ render( );
+
+ expect(screen.getByTestId('list-view-table')).toBeInTheDocument();
+
+ expect(
+ screen.queryByText('FilterTablePlaceHolder')
+ ).not.toBeInTheDocument();
+
+ expect(screen.getAllByText('StatusIndicator')).toHaveLength(7);
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.tsx
index 48ff1e07916..b29f2b1d06c 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.tsx
@@ -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 | 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: ,
}}
pagination={false}
- rowKey={(record) => record.name.concat(record.timestamp as string)}
+ rowKey={(record) => `${record.name}-${record.status}-${record.key}`}
/>
);
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/PipelineVersion.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/PipelineVersion.mock.ts
index 407b1769897..97506bbfaa2 100644
--- a/openmetadata-ui/src/main/resources/ui/src/mocks/PipelineVersion.mock.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/mocks/PipelineVersion.mock.ts
@@ -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,
+ },
+ ],
+ },
+];
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx
index 615976a822d..63a31b75d12 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx
@@ -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,
});
});
});