diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx
index eb35d31730d..080d9e0c991 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx
@@ -35,6 +35,7 @@ import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import TabsPane from '../common/TabsPane/TabsPane';
import PageContainer from '../containers/PageContainer';
+import Entitylineage from '../EntityLineage/EntityLineage.component';
import ManageTabComponent from '../ManageTab/ManageTab.component';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import TagsContainer from '../tags-container/tags-container';
@@ -64,6 +65,10 @@ const DashboardDetails = ({
charts,
chartDescriptionUpdateHandler,
chartTagUpdateHandler,
+ entityLineage,
+ isNodeLoading,
+ lineageLeafNodes,
+ loadNodeHandler,
}: DashboardDetailsProps) => {
const { isAuthDisabled } = useAuth();
const [isEdit, setIsEdit] = useState(false);
@@ -102,6 +107,17 @@ const DashboardDetails = ({
isProtected: false,
position: 1,
},
+ {
+ name: 'Lineage',
+ icon: {
+ alt: 'lineage',
+ name: 'icon-lineage',
+ title: 'Lineage',
+ selectedName: 'icon-lineagecolor',
+ },
+ isProtected: false,
+ position: 2,
+ },
{
name: 'Manage',
icon: {
@@ -112,7 +128,7 @@ const DashboardDetails = ({
},
isProtected: true,
protectedState: !owner || hasEditAccess(),
- position: 2,
+ position: 3,
},
];
@@ -464,6 +480,16 @@ const DashboardDetails = ({
>
)}
{activeTab === 2 && (
+
+
+
+ )}
+ {activeTab === 3 && (
;
serviceType: string;
dashboardUrl: string;
@@ -54,4 +65,5 @@ export interface DashboardDetailsProps {
patch: Array
) => void;
tagUpdateHandler: (updatedDashboard: Dashboard) => void;
+ loadNodeHandler: (node: EntityReference, pos: LineagePos) => void;
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx
index ab6a1079783..81cc7ce72bb 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx
@@ -12,10 +12,11 @@
*/
import { getAllByTestId, getByTestId, render } from '@testing-library/react';
-import { TableDetail } from 'Models';
+import { LeafNodes, LoadingNodeState, TableDetail } from 'Models';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Dashboard } from '../../generated/entity/data/dashboard';
+import { EntityLineage } from '../../generated/type/entityLineage';
import { TagLabel } from '../../generated/type/tagLabel';
import DashboardDetails from './DashboardDetails.component';
@@ -45,6 +46,7 @@ const DashboardDetailsProps = {
tagList: [],
users: [],
dashboardDetails: {} as Dashboard,
+ entityLineage: {} as EntityLineage,
entityName: '',
activeTab: 1,
owner: {} as TableDetail['owner'],
@@ -61,6 +63,9 @@ const DashboardDetailsProps = {
chartDescriptionUpdateHandler: jest.fn(),
chartTagUpdateHandler: jest.fn(),
tagUpdateHandler: jest.fn(),
+ loadNodeHandler: jest.fn(),
+ lineageLeafNodes: {} as LeafNodes,
+ isNodeLoading: {} as LoadingNodeState,
};
jest.mock('../ManageTab/ManageTab.component', () => {
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx
index 9cd2784ec31..c75792ce7f8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityInfoDrawer/EntityInfoDrawer.component.tsx
@@ -15,6 +15,7 @@ import { AxiosError, AxiosResponse } from 'axios';
import classNames from 'classnames';
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
+import { getDashboardByFqn } from '../../axiosAPIs/dashboardAPI';
import { getPipelineByFqn } from '../../axiosAPIs/pipelineAPI';
import { getServiceById } from '../../axiosAPIs/serviceAPI';
import { getTableDetailsByFQN } from '../../axiosAPIs/tableAPI';
@@ -135,6 +136,35 @@ const EntityInfoDrawer = ({
break;
}
+ case EntityType.DASHBOARD: {
+ setIsLoading(true);
+ getDashboardByFqn(selectedNode.name, ['tags', 'owner'])
+ .then((res: AxiosResponse) => {
+ getServiceById('dashboardServices', res.data.service?.id)
+ .then((serviceRes: AxiosResponse) => {
+ setServiceType(serviceRes.data.serviceType);
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body:
+ msg ?? `Error while getting ${selectedNode.name} service`,
+ });
+ });
+ setEntityDetail(res.data);
+ setIsLoading(false);
+ })
+ .catch((err: AxiosError) => {
+ const msg = err.message;
+ showToast({
+ variant: 'error',
+ body: msg ?? `Error while getting ${selectedNode.name} details`,
+ });
+ });
+
+ break;
+ }
default:
break;
@@ -224,7 +254,9 @@ const EntityInfoDrawer = ({
Description
- {entityDetail.description ?? (
+ {entityDetail.description?.trim() ? (
+ entityDetail.description
+ ) : (
No description added
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx
index 0c1cc741107..ac857762be8 100644
--- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx
@@ -13,7 +13,13 @@
import { AxiosPromise, AxiosResponse } from 'axios';
import { compare, Operation } from 'fast-json-patch';
-import { EntityTags, TableDetail } from 'Models';
+import {
+ EntityTags,
+ LeafNodes,
+ LineagePos,
+ LoadingNodeState,
+ TableDetail,
+} from 'Models';
import React, { useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState';
@@ -24,6 +30,7 @@ import {
patchDashboardDetails,
removeFollower,
} from '../../axiosAPIs/dashboardAPI';
+import { getLineageByFQN } from '../../axiosAPIs/lineageAPI';
import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/title-breadcrumb.interface';
import DashboardDetails from '../../components/DashboardDetails/DashboardDetails.component';
import Loader from '../../components/Loader/Loader';
@@ -36,12 +43,15 @@ import { ServiceCategory } from '../../enums/service.enum';
import { Chart } from '../../generated/entity/data/chart';
import { Dashboard } from '../../generated/entity/data/dashboard';
import { User } from '../../generated/entity/teams/user';
+import { EntityLineage } from '../../generated/type/entityLineage';
+import { EntityReference } from '../../generated/type/entityReference';
import { TagLabel } from '../../generated/type/tagLabel';
import { addToRecentViewed, getCurrentUserId } from '../../utils/CommonUtils';
import {
dashboardDetailsTabs,
getCurrentDashboardTab,
} from '../../utils/DashboardDetailsUtils';
+import { getEntityLineage } from '../../utils/EntityUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import {
getOwnerFromId,
@@ -79,7 +89,15 @@ const DashboardDetailsPage = () => {
const [slashedDashboardName, setSlashedDashboardName] = useState<
TitleBreadcrumbProps['titleLinks']
>([]);
-
+ const [entityLineage, setEntityLineage] = useState
(
+ {} as EntityLineage
+ );
+ const [isLineageLoading, setIsLineageLoading] = useState(true);
+ const [leafNodes, setLeafNodes] = useState({} as LeafNodes);
+ const [isNodeLoading, setNodeLoading] = useState({
+ id: undefined,
+ state: false,
+ });
const activeTabHandler = (tabValue: number) => {
const currentTabIndex = tabValue - 1;
if (dashboardDetailsTabs[currentTabIndex].path !== tab) {
@@ -142,6 +160,32 @@ const DashboardDetailsPage = () => {
return chartsData;
};
+ const setLeafNode = (val: EntityLineage, pos: LineagePos) => {
+ if (pos === 'to' && val.downstreamEdges?.length === 0) {
+ setLeafNodes((prev) => ({
+ ...prev,
+ downStreamNode: [...(prev.downStreamNode ?? []), val.entity.id],
+ }));
+ }
+ if (pos === 'from' && val.upstreamEdges?.length === 0) {
+ setLeafNodes((prev) => ({
+ ...prev,
+ upStreamNode: [...(prev.upStreamNode ?? []), val.entity.id],
+ }));
+ }
+ };
+
+ const loadNodeHandler = (node: EntityReference, pos: LineagePos) => {
+ setNodeLoading({ id: node.id, state: true });
+ getLineageByFQN(node.name, node.type).then((res: AxiosResponse) => {
+ setLeafNode(res.data, pos);
+ setEntityLineage(getEntityLineage(entityLineage, res.data, pos));
+ setTimeout(() => {
+ setNodeLoading((prev) => ({ ...prev, state: false }));
+ }, 500);
+ });
+ };
+
const fetchDashboardDetail = (dashboardFQN: string) => {
setLoading(true);
getDashboardByFqn(dashboardFQN, [
@@ -199,6 +243,14 @@ const DashboardDetailsPage = () => {
timestamp: 0,
});
+ getLineageByFQN(dashboardFQN, EntityType.DASHBOARD)
+ .then((res: AxiosResponse) => {
+ setEntityLineage(res.data);
+ })
+ .finally(() => {
+ setIsLineageLoading(false);
+ });
+
setDashboardUrl(dashboardUrl);
fetchCharts(charts).then((charts) => setCharts(charts));
setLoading(false);
@@ -296,7 +348,7 @@ const DashboardDetailsPage = () => {
return (
<>
- {isLoading ? (
+ {isLoading || isLineageLoading ? (
) : (
{
dashboardUrl={dashboardUrl}
description={description}
descriptionUpdateHandler={descriptionUpdateHandler}
+ entityLineage={entityLineage}
entityName={displayName}
followDashboardHandler={followDashboard}
followers={followers}
+ isNodeLoading={isNodeLoading}
+ lineageLeafNodes={leafNodes}
+ loadNodeHandler={loadNodeHandler}
owner={owner}
serviceType={serviceType}
setActiveTabHandler={activeTabHandler}
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts
index 323ea28a3c9..bb258bc1b81 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/DashboardDetailsUtils.ts
@@ -16,6 +16,10 @@ export const dashboardDetailsTabs = [
name: 'Details',
path: 'details',
},
+ {
+ name: 'Lineage',
+ path: 'lineage',
+ },
{
name: 'Manage',
path: 'manage',
@@ -26,6 +30,11 @@ export const getCurrentDashboardTab = (tab: string) => {
let currentTab = 1;
switch (tab) {
case 'manage':
+ currentTab = 3;
+
+ break;
+
+ case 'lineage':
currentTab = 2;
break;
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx
index 2f103eb0d0b..015bf490d3d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx
@@ -58,6 +58,9 @@ export const getEntityTags = (
case EntityType.PIPELINE: {
return entityDetail.tags?.map((t) => t.tagFQN) || [];
}
+ case EntityType.DASHBOARD: {
+ return entityDetail.tags?.map((t) => t.tagFQN) || [];
+ }
default:
return [];
@@ -222,6 +225,49 @@ export const getEntityOverview = (
return overview;
}
+ case EntityType.DASHBOARD: {
+ const { owner, tags, dashboardUrl, service, fullyQualifiedName } =
+ entityDetail;
+ const ownerValue = getOwnerFromId(owner?.id);
+ const tier = getTierFromTableTags(tags || []);
+
+ const overview = [
+ {
+ name: 'Service',
+ value: service?.name as string,
+ url: getServiceDetailsPath(
+ service?.name as string,
+ serviceType,
+ ServiceCategory.DASHBOARD_SERVICES
+ ),
+ isLink: true,
+ },
+ {
+ name: 'Owner',
+ value: ownerValue?.displayName || ownerValue?.name || '--',
+ url: getTeamDetailsPath(owner?.name || ''),
+ isLink: ownerValue
+ ? ownerValue.type === 'team'
+ ? true
+ : false
+ : false,
+ },
+ {
+ name: 'Tier',
+ value: tier ? tier.split('.')[1] : '--',
+ isLink: false,
+ },
+ {
+ name: `${serviceType} url`,
+ value: fullyQualifiedName?.split('.')[1] as string,
+ url: dashboardUrl as string,
+ isLink: true,
+ isExternal: true,
+ },
+ ];
+
+ return overview;
+ }
default:
return [];