-
+
{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;
+ }
+};