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 [];