From c83690d07e398ac42227748bc54ce70ecb9c2a5c Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Wed, 9 Nov 2022 16:04:47 +0530 Subject: [PATCH] feat(ui): update pipeline status UI (#8253) * feat(ui): update pipeline status UI * fix typos * minor update * Merge branch 'main' into support-7127 * design updates * minor fixes * address review comments * fix tests * fix cypress failing due to wrong localization keys --- .../e2e/AddNewService/redshiftWithDBT.spec.js | 19 +- .../cypress/e2e/Pages/EntityDetails.spec.js | 13 +- .../ui/cypress/e2e/Pages/Tags.spec.js | 15 +- .../ui/cypress/e2e/Pages/myData.spec.js | 23 +- .../resources/ui/src/assets/svg/calendar.svg | 14 ++ .../resources/ui/src/assets/svg/filter.svg | 3 + .../Execution/Execution.component.tsx | 229 ++++++++++++++++++ .../components/Execution/Execution.style.less | 31 +++ .../ListView/ListViewTab.component.tsx | 75 ++++++ .../TreeView/TreeViewTab.component.tsx | 110 +++++++++ .../Execution/TreeView/tree-view-tab.less | 24 ++ .../PipelineDetails.component.tsx | 45 ++-- .../PipelineDetails/PipelineDetails.test.tsx | 27 ++- .../ui/src/constants/execution.constants.ts | 31 +++ .../resources/ui/src/enums/entity.enum.ts | 1 + .../ui/src/locale/languages/en-us.json | 16 +- .../ui/src/pages/teams/TeamsPage.tsx | 23 +- .../src/main/resources/ui/src/styles/app.less | 7 + .../main/resources/ui/src/styles/spacing.less | 7 + .../ui/src/utils/PipelineDetailsUtils.ts | 16 +- .../main/resources/ui/src/utils/SvgUtils.tsx | 6 + .../main/resources/ui/src/utils/TimeUtils.ts | 76 +++++- .../resources/ui/src/utils/executionUtils.tsx | 108 +++++++++ 23 files changed, 800 insertions(+), 119 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/calendar.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/assets/svg/filter.svg create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.style.less create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less create mode 100644 openmetadata-ui/src/main/resources/ui/src/constants/execution.constants.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/AddNewService/redshiftWithDBT.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/AddNewService/redshiftWithDBT.spec.js index d2cae49a78f..c37eac35945 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/AddNewService/redshiftWithDBT.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/AddNewService/redshiftWithDBT.spec.js @@ -11,23 +11,8 @@ * limitations under the License. */ -import { - deleteCreatedService, - editOwnerforCreatedService, - goToAddNewServicePage, - interceptURL, - login, - testServiceCreationAndIngestion, - updateDescriptionForIngestedTables, - verifyResponseStatusCode, - visitEntityDetailsPage -} from '../../common/common'; -import { - DBT, - HTTP_CONFIG_SOURCE, - LOGIN, - SERVICE_TYPE -} from '../../constants/constants'; +import { deleteCreatedService, editOwnerforCreatedService, goToAddNewServicePage, interceptURL, login, testServiceCreationAndIngestion, updateDescriptionForIngestedTables, verifyResponseStatusCode, visitEntityDetailsPage } from '../../common/common'; +import { DBT, HTTP_CONFIG_SOURCE, LOGIN, SERVICE_TYPE } from '../../constants/constants'; import { REDSHIFT } from '../../constants/service.constants'; describe('RedShift Ingestion', () => { diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityDetails.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityDetails.spec.js index 7d4177851b8..1d73e5f335c 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityDetails.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityDetails.spec.js @@ -11,17 +11,8 @@ * limitations under the License. */ -import { - getCurrentLocaleDate, - getFutureLocaleDateFromCurrentDate -} from '../../../src/utils/TimeUtils'; -import { - descriptionBox, - interceptURL, - login, - verifyResponseStatusCode, - visitEntityDetailsPage -} from '../../common/common'; +import { getCurrentLocaleDate, getFutureLocaleDateFromCurrentDate } from '../../../src/utils/TimeUtils'; +import { descriptionBox, interceptURL, login, verifyResponseStatusCode, visitEntityDetailsPage } from '../../common/common'; import { DELETE_ENTITY, DELETE_TERM, LOGIN } from '../../constants/constants'; describe('Entity Details Page', () => { diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js index b56e6b11e5b..1b6552264ee 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Tags.spec.js @@ -11,19 +11,8 @@ * limitations under the License. */ -import { - addNewTagToEntity, - descriptionBox, - interceptURL, - login, - verifyResponseStatusCode -} from '../../common/common'; -import { - LOGIN, - NEW_TAG, - NEW_TAG_CATEGORY, - SEARCH_ENTITY_TABLE -} from '../../constants/constants'; +import { addNewTagToEntity, descriptionBox, interceptURL, login, verifyResponseStatusCode } from '../../common/common'; +import { LOGIN, NEW_TAG, NEW_TAG_CATEGORY, SEARCH_ENTITY_TABLE } from '../../constants/constants'; describe('Tags page should work', () => { beforeEach(() => { diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/myData.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/myData.spec.js index 681c7b7283c..451d7e1b6b7 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/myData.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/myData.spec.js @@ -13,27 +13,8 @@ /// -import { - interceptURL, - login, - searchEntity, - verifyResponseStatusCode, - visitEntityDetailsPage, - visitEntityTab -} from '../../common/common'; -import { - FOLLOWING_TITLE, - LOGIN, - MYDATA_SUMMARY_OPTIONS, - MY_DATA_TITLE, - NO_SEARCHED_TERMS, - RECENT_SEARCH_TITLE, - RECENT_VIEW_TITLE, - SEARCH_ENTITY_DASHBOARD, - SEARCH_ENTITY_PIPELINE, - SEARCH_ENTITY_TABLE, - SEARCH_ENTITY_TOPIC -} from '../../constants/constants'; +import { interceptURL, login, searchEntity, verifyResponseStatusCode, visitEntityDetailsPage, visitEntityTab } from '../../common/common'; +import { FOLLOWING_TITLE, LOGIN, MYDATA_SUMMARY_OPTIONS, MY_DATA_TITLE, NO_SEARCHED_TERMS, RECENT_SEARCH_TITLE, RECENT_VIEW_TITLE, SEARCH_ENTITY_DASHBOARD, SEARCH_ENTITY_PIPELINE, SEARCH_ENTITY_TABLE, SEARCH_ENTITY_TOPIC } from '../../constants/constants'; const tables = Object.values(SEARCH_ENTITY_TABLE); const topics = Object.values(SEARCH_ENTITY_TOPIC); 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 new file mode 100644 index 00000000000..887bf200998 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/calendar.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000000..e8614d06a3e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/filter.svg @@ -0,0 +1,3 @@ + + + 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 new file mode 100644 index 00000000000..ab1a88563bc --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.component.tsx @@ -0,0 +1,229 @@ +/* + * Copyright 2022 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 { CloseCircleOutlined } from '@ant-design/icons'; +import { + Button, + Col, + DatePicker, + Dropdown, + Menu, + MenuProps, + Radio, + RadioChangeEvent, + Row, + Space, +} from 'antd'; +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 { useTranslation } from 'react-i18next'; +import { ReactComponent as Calendar } from '../../assets/svg/calendar.svg'; +import { ReactComponent as FilterIcon } from '../../assets/svg/filter.svg'; +import { getPipelineStatus } from '../../axiosAPIs/pipelineAPI'; +import { + EXECUTION_FILTER_RANGE, + MenuOptions, +} from '../../constants/execution.constants'; +import { PipelineStatus } from '../../generated/entity/data/pipeline'; +import { + getCurrentDateTimeStamp, + getPastDatesTimeStampFromCurrentDate, + getTimeStampByDate, +} from '../../utils/TimeUtils'; +import { showErrorToast } from '../../utils/ToastUtils'; +import './Execution.style.less'; +import ListView from './ListView/ListViewTab.component'; +import TreeViewTab from './TreeView/TreeViewTab.component'; + +interface ExecutionProps { + pipelineFQN: string; +} + +const ExecutionsTab = ({ pipelineFQN }: ExecutionProps) => { + const { t } = useTranslation(); + + const listViewLabel = t('label.list'); + const treeViewLabel = t('label.tree'); + + const [view, setView] = useState(listViewLabel); + const [executions, setExecutions] = useState>(); + const [datesSelected, setDatesSelected] = useState(false); + const [startTime, setStartTime] = useState( + getPastDatesTimeStampFromCurrentDate( + EXECUTION_FILTER_RANGE.last365days.days + ) + ); + const [endTime, setEndTime] = useState(getCurrentDateTimeStamp()); + const [isClickedCalendar, setIsClickedCalendar] = useState(false); + const [status, setStatus] = useState(MenuOptions.all); + const [isLoading, setIsLoading] = useState(false); + + const fetchPipelineStatus = async (startRange: number, endRange: number) => { + try { + setIsLoading(true); + + const response = await getPipelineStatus(pipelineFQN, { + startTs: startRange, + endTs: endRange, + }); + setExecutions(response.data); + } catch (error) { + showErrorToast( + error as AxiosError, + t('message.fetch-pipeline-status-error') + ); + } finally { + setIsLoading(false); + } + }; + + const handleModeChange = (e: RadioChangeEvent) => { + setView(e.target.value); + }; + + const handleMenuClick: MenuProps['onClick'] = (event) => { + if (event?.key) { + setStatus(MenuOptions[event.key as keyof typeof MenuOptions]); + } + }; + + const menu = useMemo( + () => ( + ({ + key: key, + label: value, + }))} + onClick={handleMenuClick} + /> + ), + [handleMenuClick] + ); + + const onDateChange: RangePickerProps['onChange'] = (_, dateStrings) => { + if (dateStrings) { + const startTime = getTimeStampByDate(dateStrings[0]); + + const endTime = getTimeStampByDate(dateStrings[1]); + + if (!isNaN(startTime) && !isNaN(endTime)) { + setStartTime(startTime); + setEndTime(endTime); + } + if (isNaN(startTime)) { + setIsClickedCalendar(false); + setStartTime( + getPastDatesTimeStampFromCurrentDate( + EXECUTION_FILTER_RANGE.last365days.days + ) + ); + setEndTime(getCurrentDateTimeStamp()); + + setDatesSelected(false); + } + + setDatesSelected(true); + } + }; + + useEffect(() => { + fetchPipelineStatus(startTime, endTime); + }, [pipelineFQN, datesSelected, startTime, endTime]); + + return ( + + + + + + + + {listViewLabel} + + + {treeViewLabel} + + + + + + + {view === listViewLabel ? ( + <> + + + ) : null} + + + + + {view === listViewLabel ? ( + + ) : ( + + )} + + + + + ); +}; + +export default ExecutionsTab; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.style.less b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.style.less new file mode 100644 index 00000000000..4eace6224ea --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/Execution.style.less @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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. + */ + +.executions-date-picker { + .ant-picker-range-separator { + display: none; + } + .ant-picker-clear { + right: 4px; + } +} + +.range-picker-button-width { + @apply delay-100; + max-width: 130px; + position: relative; +} + +.range-picker-button { + padding: 0px 8px; +} 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 new file mode 100644 index 00000000000..4ab4e2595ef --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/ListView/ListViewTab.component.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2022 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 { Table } from 'antd'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PipelineStatus, + StatusType, +} from '../../../generated/entity/data/pipeline'; +import { + getTableViewData, + StatusIndicator, +} from '../../../utils/executionUtils'; +import Loader from '../../Loader/Loader'; + +interface ListViewProps { + executions: Array | undefined; + status: string; + loading: boolean; +} + +const ListView = ({ executions, status, loading }: ListViewProps) => { + const { t } = useTranslation(); + + const tableData = useMemo( + () => getTableViewData(executions, status), + [executions, status] + ); + + const columns = useMemo( + () => [ + { + title: t('label.name'), + dataIndex: 'name', + key: 'name', + }, + { + title: t('label.status'), + dataIndex: 'status', + key: 'status', + render: (status: StatusType) => , + }, + { + title: t('label.date-and-time'), + dataIndex: 'timestamp', + key: 'timestamp', + }, + ], + [] + ); + + return ( + }} + pagination={false} + rowKey="name" + /> + ); +}; + +export default ListView; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx new file mode 100644 index 00000000000..2e2f97f6ee6 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/TreeViewTab.component.tsx @@ -0,0 +1,110 @@ +/* + * Copyright 2022 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 { Card, Col, Empty, Row, Space, Typography } from 'antd'; +import { isEmpty, uniqueId } from 'lodash'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Tooltip } from 'react-tippy'; +import { PipelineStatus } from '../../../generated/entity/data/pipeline'; +import { getTreeViewData } from '../../../utils/executionUtils'; +import { getStatusBadgeIcon } from '../../../utils/PipelineDetailsUtils'; +import SVGIcons, { Icons } from '../../../utils/SvgUtils'; +import { formatDateTimeFromSeconds } from '../../../utils/TimeUtils'; +import './tree-view-tab.less'; + +interface TreeViewProps { + executions: Array | undefined; + status: string; + startTime: number; + endTime: number; +} + +const TreeViewTab = ({ + executions, + status, + startTime, + endTime, +}: TreeViewProps) => { + const viewData = useMemo( + () => getTreeViewData(executions as PipelineStatus[], status), + [executions, status] + ); + + const { t } = useTranslation(); + + return ( + + + + + {formatDateTimeFromSeconds(startTime)} to{' '} + {formatDateTimeFromSeconds(endTime)} + + + + + + {isEmpty(viewData) && ( + + )} + + {Object.entries(viewData).map(([key, value]) => { + return ( + + + +
+ {key} + + + +
+ + {value.map((status) => ( + +
{status.timestamp}
+
{status.executionStatus}
+
+ } + key={uniqueId()} + position="bottom"> + + + ))} + + + + ); + })} + + ); +}; + +export default TreeViewTab; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less new file mode 100644 index 00000000000..89d03e87db1 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Execution/TreeView/tree-view-tab.less @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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. + */ +.status-border-top { + border-top: 1px solid rgba(220, 227, 236, 0.3); +} + +.status-border-right { + border-right: 1px solid rgba(220, 227, 236, 0.3); +} + +.tree-view-dot { + border-radius: 6px; + border: 6px solid #cccccc; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx index a28a1318f40..2b4c8b7a0af 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx @@ -54,12 +54,12 @@ import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo'; import TabsPane from '../common/TabsPane/TabsPane'; import PageContainer from '../containers/PageContainer'; import EntityLineageComponent from '../EntityLineage/EntityLineage.component'; +import ExecutionsTab from '../Execution/Execution.component'; import Loader from '../Loader/Loader'; import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal'; import { usePermissionProvider } from '../PermissionProvider/PermissionProvider'; import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface'; -import PipelineStatusList from '../PipelineStatusList/PipelineStatusList.component'; import TasksDAGView from '../TasksDAGView/TasksDAGView'; import { PipeLineDetailsProp } from './PipelineDetails.interface'; @@ -123,14 +123,8 @@ const PipelineDetails = ({ const [selectedField, setSelectedField] = useState(''); const [elementRef, isInView] = useInfiniteScroll(observerOptions); - const [selectedExecution, setSelectedExecution] = useState( - () => { - if (pipelineStatus) { - return pipelineStatus; - } else { - return {} as PipelineStatus; - } - } + const [selectedExecution] = useState( + pipelineStatus ); const [threadType, setThreadType] = useState( ThreadType.Conversation @@ -199,6 +193,17 @@ const PipelineDetails = ({ position: 2, count: feedCount, }, + { + name: 'Executions', + icon: { + alt: 'executions', + name: 'executions', + title: 'Executions', + selectedName: 'activity-feed-color', + }, + isProtected: false, + position: 3, + }, { name: 'Lineage', icon: { @@ -208,12 +213,12 @@ const PipelineDetails = ({ selectedName: 'icon-lineagecolor', }, isProtected: false, - position: 3, + position: 4, }, { name: 'Custom Properties', isProtected: false, - position: 4, + position: 5, }, ]; @@ -464,7 +469,7 @@ const PipelineDetails = ({ />
-
+
{activeTab === 1 && ( <>
@@ -514,17 +519,6 @@ const PipelineDetails = ({
)}
-
-
- { - setSelectedExecution(exec); - }} - /> -
)} {activeTab === 2 && ( @@ -546,7 +540,8 @@ const PipelineDetails = ({
)} - {activeTab === 3 && ( + {activeTab === 3 && } + {activeTab === 4 && (
)} - {activeTab === 4 && ( + {activeTab === 5 && ( ({ getOwnerValue: jest.fn().mockReturnValue('Owner'), })); +jest.mock('', () => ({ + ExecutionsTab: jest.fn().mockImplementation(() =>

Executions

), +})); + describe('Test PipelineDetails component', () => { it('Checks if the PipelineDetails component has all the proper components rendered', async () => { const { container } = render( @@ -230,13 +234,8 @@ describe('Test PipelineDetails component', () => { } ); const taskDetail = await findByTestId(container, 'tasks-dag'); - const pipelineStatus = await findByTestId( - container, - 'pipeline-status-list' - ); expect(taskDetail).toBeInTheDocument(); - expect(pipelineStatus).toBeInTheDocument(); }); it('Should render no tasks data placeholder is tasks list is empty', async () => { @@ -262,13 +261,25 @@ describe('Test PipelineDetails component', () => { expect(activityFeedList).toBeInTheDocument(); }); - it('Check if active tab is lineage', async () => { + it('should render execution tab if active tab is 3', async () => { const { container } = render( , { wrapper: MemoryRouter, } ); + const executions = await findByText(container, 'Executions'); + + expect(executions).toBeInTheDocument(); + }); + + it('Check if active tab is lineage', async () => { + const { container } = render( + , + { + wrapper: MemoryRouter, + } + ); const lineage = await findByTestId(container, 'lineage'); expect(lineage).toBeInTheDocument(); @@ -276,7 +287,7 @@ describe('Test PipelineDetails component', () => { it('Check if active tab is custom properties', async () => { const { container } = render( - , + , { wrapper: MemoryRouter, } @@ -291,7 +302,7 @@ describe('Test PipelineDetails component', () => { it('Should create an observer if IntersectionObserver is available', async () => { const { container } = render( - , + , { wrapper: MemoryRouter, } diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/execution.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/execution.constants.ts new file mode 100644 index 00000000000..b1a29be3f3b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/constants/execution.constants.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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 { StatusType } from '../generated/entity/data/pipeline'; + +export const MenuOptions = { + all: 'All', + [StatusType.Successful]: 'Success', + [StatusType.Failed]: 'Failure', + [StatusType.Pending]: 'Pending', + Aborted: 'Aborted', +}; + +export const EXECUTION_FILTER_RANGE = { + last3days: { days: 3, title: 'Last 3 days' }, + last7days: { days: 7, title: 'Last 7 days' }, + last14days: { days: 14, title: 'Last 14 days' }, + last30days: { days: 30, title: 'Last 30 days' }, + last60days: { days: 60, title: 'Last 60 days' }, + last365days: { days: 365, title: 'Last 365 days' }, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts index 4ddc3e35b26..7177357e8ab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/entity.enum.ts @@ -83,6 +83,7 @@ export enum TabSpecificField { DASHBOARD = 'dashboard', TABLE_CONSTRAINTS = 'tableConstraints', EXTENSION = 'extension', + EXECUTIONS = 'executions', } export enum FqnPart { diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 4680f40556d..4a090acd645 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -4,13 +4,16 @@ "type": "Type", "description": "Description", "docs": "Docs", + "date-filter": "Date Filter", "api-uppercase": "API", + "date-and-time": "Date & Time", "slack": "Slack", "logout": "Logout", "version": "Version", "explore": "Explore", "glossary": "Glossary", "tags": "Tags", + "status": "Status", "settings": "Settings", "search-global": "Search for Tables, Topics, Dashboards, Pipelines and ML Models", "summary": "Summary", @@ -193,7 +196,6 @@ "pause": "Pause", "metadata-ingestion": "Metadata Ingestion", "unpause": "UnPause", - "end-date": "End Date", "execution-date": "Execution Date", "start-date": "Start Date", "view-dag": "View Dag", @@ -201,6 +203,8 @@ "show-deleted": "Show deleted", "add-bot": "Add Bot", "search-for-bots": "Search for bots...", + "list": "List", + "tree": "Tree", "condition-is-invalid": "Condition is invalid", "rule-name": "Rule Name", "write-your-description": "Write your description", @@ -208,6 +212,7 @@ "select-rule-effect": "Select Rule Effect", "field-required": "{{field}} is required", "field-required-plural": "{{field}} are required", + "no-execution-runs-found": "No execution runs found for the pipeline.", "last-no-of-days": "Last {{day}} Days", "tier-number": "Tier{{tier}}", "profiler-amp-data-quality": "Profiler & Data Quality", @@ -239,22 +244,23 @@ "entity-restored-error": "Error while restoring {{entity}}", "no-ingestion-available": "No ingestion data available", "no-ingestion-description": "To view Ingestion Data, run the MetaData Ingestion. Please refer to this doc to schedule the", + "fetch-pipeline-status-error": "Error while fetching pipeline status." + }, + "server": { "no-followed-entities": "You have not followed anything yet.", "no-owned-entities": "You have not owned anything yet.", "entity-fetch-error": "Error while fetching {{entity}}", "feed-post-error": "Error while posting the message!", "unexpected-error": "An unexpected error occurred.", "entity-creation-error": "Error while creating {{entity}}", - "entity-updation-error": "Error while updating {{entity}}", + "entity-updating-error": "Error while updating {{entity}}", "join-team-success": "Team joined successfully!", "leave-team-success": "Left the team successfully!", "join-team-error": "Error while joining the team!", "leave-team-error": "Error while leaving the team!", "field-insight": "Display the percentage of datasets with {{field}} by type.", "total-entity-insight": "Display the total of datasets by type.", - "no-query-available": "No query available" - }, - "server": { + "no-query-available": "No query available", "unexpected-response": "Unexpected response from server!" }, "url": {} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/teams/TeamsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/teams/TeamsPage.tsx index 1f7871de59a..6c9d72437a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/teams/TeamsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/teams/TeamsPage.tsx @@ -297,7 +297,7 @@ const TeamsPage = () => { .catch((error: AxiosError) => { showErrorToast( error, - t('message.entity-updation-error', { + t('server.entity-updating-error', { entity: 'Team', }) ); @@ -322,41 +322,36 @@ const TeamsPage = () => { }; const handleJoinTeamClick = (id: string, data: Operation[]) => { - // setIsPageLoading(true); updateUserDetail(id, data) .then((res) => { if (res) { AppState.updateUserDetails(res); fetchTeamByFqn(selectedTeam.name); - showSuccessToast(t('message.join-team-success'), 2000); + showSuccessToast(t('server.join-team-success'), 2000); } else { - throw t('message.join-team-error'); + throw t('server.join-team-error'); } }) .catch((err: AxiosError) => { - showErrorToast(err, t('message.join-team-error')); - // setIsRightPanelLoading(false); + showErrorToast(err, t('server.join-team-error')); }); }; const handleLeaveTeamClick = (id: string, data: Operation[]) => { - // setIsRightPanelLoading(true); - return new Promise((resolve) => { updateUserDetail(id, data) .then((res) => { if (res) { AppState.updateUserDetails(res); fetchTeamByFqn(selectedTeam.name); - showSuccessToast(t('message.leave-team-success'), 2000); + showSuccessToast(t('server.leave-team-success'), 2000); resolve(); } else { - throw t('message.leave-team-error'); + throw t('server.leave-team-error'); } }) .catch((err: AxiosError) => { - showErrorToast(err, t('message.leave-team-error')); - // setIsRightPanelLoading(false); + showErrorToast(err, t('server.leave-team-error')); }); }); }; @@ -383,7 +378,7 @@ const TeamsPage = () => { .catch((error: AxiosError) => { showErrorToast( error, - t('message.entity-updation-error', { + t('server.entity-updating-error', { entity: 'Team', }) ); @@ -421,7 +416,7 @@ const TeamsPage = () => { .catch((error: AxiosError) => { showErrorToast( error, - t('message.entity-updation-error', { + t('server.entity-updating-error', { entity: 'Team', }) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/app.less b/openmetadata-ui/src/main/resources/ui/src/styles/app.less index 7deb8155943..e9037b40a3a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less @@ -74,6 +74,9 @@ text-decoration: underline; } // Width +.w-4 { + width: 16px; +} .w-8 { width: 32px; } @@ -269,6 +272,10 @@ } } +.transform-180 { + transform: rotate(180deg); +} + .no-underline { text-decoration: none; } diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less b/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less index 39bc5b19aff..206c6dabc89 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/spacing.less @@ -166,6 +166,13 @@ margin-bottom: auto; } +.m-l-7 { + margin-left: 7.5rem; +} + +.m-r-7 { + margin-right: 7.5rem; +} .mt-0 { margin-top: 0; } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts index ee2de597c9a..6c7e0a26522 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/PipelineDetailsUtils.ts @@ -32,6 +32,11 @@ export const pipelineDetailsTabs = [ path: 'activity_feed', field: TabSpecificField.ACTIVITY_FEED, }, + { + name: 'Executions', + path: 'executions', + field: TabSpecificField.EXECUTIONS, + }, { name: 'Lineage', path: 'lineage', @@ -51,13 +56,18 @@ export const getCurrentPipelineTab = (tab: string) => { break; - case 'lineage': + case 'executions': currentTab = 3; break; - case 'custom_properties': + + case 'lineage': currentTab = 4; + break; + case 'custom_properties': + currentTab = 5; + break; case 'details': @@ -109,7 +119,7 @@ export const STATUS_OPTIONS = [ { value: StatusType.Pending, label: StatusType.Pending }, ]; -export const getStatusBadgeIcon = (status: StatusType) => { +export const getStatusBadgeIcon = (status?: StatusType) => { switch (status) { case StatusType.Successful: return Icons.SUCCESS_BADGE; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index c7828b7614b..bc88cdc1140 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -31,6 +31,7 @@ import IconAnnouncements from '../assets/svg/announcements.svg'; import IconAPI from '../assets/svg/api.svg'; import IconArrowDownPrimary from '../assets/svg/arrow-down-primary.svg'; import IconArrowRightPrimary from '../assets/svg/arrow-right-primary.svg'; +import IconArrowRight from '../assets/svg/arrow-right.svg'; import IconBotProfile from '../assets/svg/bot-profile.svg'; import IconSuccess from '../assets/svg/check.svg'; import IconCheckboxPrimary from '../assets/svg/checkbox-primary.svg'; @@ -317,6 +318,7 @@ export const Icons = { CIRCLE_CHECKBOX: 'icon-circle-checkbox', ARROW_RIGHT_PRIMARY: 'icon-arrow-right-primary', ARROW_DOWN_PRIMARY: 'icon-arrow-down-primary', + ARROW_RIGHT: 'icon-arrow-right', ANNOUNCEMENT: 'icon-announcement', ANNOUNCEMENT_BLACK: 'icon-announcement-black', ANNOUNCEMENT_PURPLE: 'icon-announcement-purple', @@ -858,6 +860,10 @@ const SVGIcons: FunctionComponent = ({ case Icons.ARROW_DOWN_PRIMARY: IconComponent = IconArrowDownPrimary; + break; + case Icons.ARROW_RIGHT: + IconComponent = IconArrowRight; + break; case Icons.ARROW_RIGHT_PRIMARY: IconComponent = IconArrowRightPrimary; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts index 72a11dae279..5d1a0226196 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TimeUtils.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { toNumber } from 'lodash'; +import { isNil, toNumber } from 'lodash'; import { DateTime } from 'luxon'; const msPerSecond = 1000; @@ -181,6 +181,19 @@ export const getDateTimeByTimeStamp = ( ); }; +/** + * It takes a timestamp and returns a formatted date string + * @param {number} timeStamp - The timestamp you want to convert to a date. + * @param {string} [format] - The format of the date you want to return. + * @returns A string + */ +export const getDateByTimeStamp = ( + timeStamp: number, + format?: string +): string => { + return DateTime.fromMillis(timeStamp).toFormat(format || 'dd MMM yyyy'); +}; + /** * It takes a timestamp and returns a relative date time string * @param {number} timestamp - number - The timestamp to convert to a relative date time. @@ -216,7 +229,7 @@ export const getTimeZone = (): string => { }) .slice(4); - // Line below finds out the abbrevation for time zone + // Line below finds out the abbreviation for time zone // e.g. India Standard Time --> IST const abbreviation = timeZoneToString.match(/\b[A-Z]+/g)?.join('') || ''; @@ -322,3 +335,62 @@ export const getFormattedDateFromMilliSeconds = ( */ export const getDateTimeFromMilliSeconds = (timeStamp: number) => DateTime.fromMillis(timeStamp).toLocaleString(DateTime.DATETIME_MED); + +/** + * It takes a timestamp and returns a string in the format of "dd MMM yyyy, hh:mm" + * @param {number} timeStamp - number - The timestamp you want to convert to a date. + * @returns A string ex: 23 May 2022, 23:59 + */ +export const getDateTimeByTimeStampWithCommaSeparated = ( + timeStamp: number +): string => { + return `${DateTime.fromMillis(timeStamp).toFormat('dd MMM yyyy, hh:mm')}`; +}; + +/** + * Given a date string, return the time stamp of that date. + * @param {string} date - The date you want to convert to a timestamp. + */ +export const getTimeStampByDate = (date: string) => Date.parse(date); + +/** + * @param date EPOCH Millis + * @returns Formatted date for valid input. Format: MMM DD, YYYY, HH:MM AM/PM TimeZone + */ +export const formatDateTimeWithTimeZone = (date: number) => { + if (isNil(date)) { + return ''; + } + + const dateTime = DateTime.fromMillis(date); + + return dateTime.toLocaleString(DateTime.DATETIME_FULL); +}; + +/** + * @param date EPOCH Millis + * @returns Formatted date for valid input. Format: MMM DD, YYYY, HH:MM AM/PM + */ +export const formatDateTime = (date: number) => { + if (isNil(date)) { + return ''; + } + + const dateTime = DateTime.fromMillis(date); + + return dateTime.toLocaleString(DateTime.DATETIME_MED); +}; + +/** + * @param date EPOCH seconds + * @returns Formatted date for valid input. Format: MMM DD, YYYY, HH:MM AM/PM + */ +export const formatDateTimeFromSeconds = (date: number) => { + if (isNil(date)) { + return ''; + } + + const dateTime = DateTime.fromSeconds(date); + + return dateTime.toLocaleString(DateTime.DATETIME_MED); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx new file mode 100644 index 00000000000..eb97d6531de --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/executionUtils.tsx @@ -0,0 +1,108 @@ +/* + * Copyright 2022 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 { Space } from 'antd'; +import { groupBy, isUndefined, toLower } from 'lodash'; +import React from 'react'; +import { MenuOptions } from '../constants/execution.constants'; +import { PipelineStatus, StatusType } from '../generated/entity/data/pipeline'; +import { getStatusBadgeIcon } from './PipelineDetailsUtils'; +import SVGIcons from './SvgUtils'; +import { formatDateTimeFromSeconds } from './TimeUtils'; + +interface StatusIndicatorInterface { + status: StatusType; +} + +export interface ViewDataInterface { + name: string; + status?: StatusType; + timestamp?: string; + executionStatus?: StatusType; + type?: string; +} + +export const StatusIndicator = ({ status }: StatusIndicatorInterface) => ( + + +

+ {status === StatusType.Successful + ? MenuOptions[StatusType.Successful] + : ''} + {status === StatusType.Failed ? MenuOptions[StatusType.Failed] : ''} + {status === StatusType.Pending ? MenuOptions[StatusType.Pending] : ''} +

+
+); + +/** + * It takes in an array of PipelineStatus objects and a string, and returns an array of + * ViewDataInterface objects + * @param {PipelineStatus[] | undefined} executions - PipelineStatus[] | undefined + * @param {string | undefined} status - The status of the pipeline. + */ +export const getTableViewData = ( + executions: PipelineStatus[] | undefined, + status: string | undefined +): Array | undefined => { + if (isUndefined(executions)) return; + + const viewData: Array = []; + executions?.map((execution) => { + execution.taskStatus?.map((execute) => { + viewData.push({ + name: execute.name, + status: execute.executionStatus, + timestamp: formatDateTimeFromSeconds(execution.timestamp as number), + executionStatus: execute.executionStatus, + type: '--', + }); + }); + }); + + return viewData.filter((data) => + status !== MenuOptions.all + ? toLower(data.status)?.includes(toLower(status)) + : data + ); +}; + +/** + * It takes an array of objects and groups them by a property + * @param {PipelineStatus[]} executions - PipelineStatus[] - This is the array of pipeline status + * objects that we get from the API. + * @param {string | undefined} status - The status of the pipeline. + */ +export const getTreeViewData = ( + executions: PipelineStatus[], + status: string | undefined +) => { + const taskStatusArr = getTableViewData(executions, status); + + return groupBy(taskStatusArr, 'name'); +}; + +export const getStatusLabel = (status: string) => { + switch (status) { + case StatusType.Successful: + case StatusType.Pending: + case StatusType.Failed: + return MenuOptions[status]; + + default: + return; + } +};