diff --git a/ui/src/api/api.tsx b/ui/src/api/api.tsx index bcb25bd21..67bc3456d 100644 --- a/ui/src/api/api.tsx +++ b/ui/src/api/api.tsx @@ -75,10 +75,10 @@ export const fetchProjectLineages = async (project: string) => { }); }; -export const fetchFeatureLineages = async (project: string) => { +export const fetchFeatureLineages = async (featureId: string) => { const axios = await authAxios(msalInstance); return axios - .get(`${getApiBaseUrl()}/features/lineage/${project}`, {}) + .get(`${getApiBaseUrl()}/features/${featureId}/lineage`, {}) .then((response) => { return response.data; }); diff --git a/ui/src/components/graph/graph.tsx b/ui/src/components/graph/graph.tsx index ab625761c..55e5f9746 100644 --- a/ui/src/components/graph/graph.tsx +++ b/ui/src/components/graph/graph.tsx @@ -146,7 +146,7 @@ const Graph: React.FC = ({ data, nodeId }) => {
, }; +const getElements = (lineageData: FeatureLineage, featureType: string|null) => { + if (lineageData.guidEntityMap === null && lineageData.relations === null) { + return; + } + + const elements: Elements = []; + const elementObj: Record = {}; + + for (let index = 0; index < Object.values(lineageData.guidEntityMap).length; index++) { + const currentNode: any = Object.values(lineageData.guidEntityMap)[index]; + + if (currentNode.typeName === "feathr_workspace_v1") { + continue; // Open issue: should project node get displayed as well? + } + + const nodeId = currentNode.guid; + + // If toggled feature type exists, skip other types + if (featureType && featureType !== "all_nodes" && currentNode.typeName !== featureType) { + continue; + } + + const node = generateNode({ + index, + nodeId, + currentNode + }); + + elementObj[nodeId] = index?.toString(); + + elements.push(node); + } + + for (let index = 0; index < lineageData.relations.length; index++) { + var { fromEntityId: from, toEntityId: to, relationshipType } = lineageData.relations[index]; + if (relationshipType === "Consumes") [from, to] = [to, from]; + const edge = generateEdge({ obj: elementObj, from, to }); + if (edge?.source && edge?.target) { + if (relationshipType === "Consumes" || relationshipType === "Produces") { + elements.push(edge); + } + } + } + + return elements; +}; + const getLayoutedElements = (elements: Elements, direction = 'LR'): getLayoutElementsRet => { const dagreGraph = new dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); @@ -112,6 +160,7 @@ export { generateEdge, generateNode, getLayoutedElements, + getElements }; export const findNodeInElement = (nodeId: string | null, elements: Elements): Node | null => { diff --git a/ui/src/pages/feature/featureDetails.tsx b/ui/src/pages/feature/featureDetails.tsx index 896a59c76..f3d8cf9ec 100644 --- a/ui/src/pages/feature/featureDetails.tsx +++ b/ui/src/pages/feature/featureDetails.tsx @@ -1,11 +1,17 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, Button, Card, Col, Row, Space, Spin, Typography } from 'antd'; import { LoadingOutlined } from '@ant-design/icons'; -import { useNavigate, useParams } from "react-router-dom"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { QueryStatus, useQuery } from "react-query"; import { AxiosError } from 'axios'; import { fetchFeature } from '../../api'; import { Feature } from "../../models/model"; +import { FeatureLineage } from "../../models/model"; +import { fetchFeatureLineages } from "../../api"; +import { Elements, isNode } from 'react-flow-renderer'; +import Graph from "../../components/graph/graph"; +import { getElements } from "../../components/graph/utils" +import { getLayoutedElements } from "../../components/graph/utils" const { Title } = Typography; @@ -118,6 +124,51 @@ function InputDerivedFeatures(props: { project: string, feature: Feature }) { ; } + +function FeatureLineageGraph() { + const { featureId } = useParams() as Params; + const [lineageData, setLineageData] = useState({ guidEntityMap: null, relations: null }); + const [elements, SetElements] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + const fetchLineageData = async () => { + setLoading(true); + const data = await fetchFeatureLineages(featureId); + setLineageData(data); + setLoading(false); + }; + + fetchLineageData(); + }, [featureId]); + + // Generate graph data on client side, invoked after graphData or featureType is changed + useEffect(() => { + const generateGraphData = async () => { + SetElements(getElements(lineageData, "all_nodes")!); + }; + + generateGraphData(); + }, [lineageData]); + + return <> + { + loading + ? ( + } /> + ) + : ( + + + Lineage + + + + ) + } + ; +} + type Params = { project: string; featureId: string; @@ -193,6 +244,7 @@ const FeatureDetails: React.FC = () => { +
diff --git a/ui/src/pages/feature/lineageGraph.tsx b/ui/src/pages/feature/lineageGraph.tsx index 0ba733550..051030d28 100644 --- a/ui/src/pages/feature/lineageGraph.tsx +++ b/ui/src/pages/feature/lineageGraph.tsx @@ -3,11 +3,11 @@ import { Card, Col, Radio, Row, Spin, Tabs, Typography } from 'antd'; import { useParams, useSearchParams } from "react-router-dom"; import { Elements } from 'react-flow-renderer'; import Graph from "../../components/graph/graph"; -import { generateEdge, generateNode } from "../../components/graph/utils"; import { fetchProjectLineages } from "../../api"; import { FeatureLineage } from "../../models/model"; import { LoadingOutlined } from "@ant-design/icons"; import GraphNodeDetails from "../../components/graph/graphNodeDetails"; +import { getElements } from "../../components/graph/utils" const { Title } = Typography; const { TabPane } = Tabs; @@ -40,50 +40,7 @@ const LineageGraph: React.FC = () => { // Generate graph data on client side, invoked after graphData or featureType is changed useEffect(() => { const generateGraphData = async () => { - if (lineageData.guidEntityMap === null && lineageData.relations === null) { - return; - } - - const elements: Elements = []; - const elementObj: Record = {}; - - for (let index = 0; index < Object.values(lineageData.guidEntityMap).length; index++) { - const currentNode: any = Object.values(lineageData.guidEntityMap)[index]; - - if (currentNode.typeName === "feathr_workspace_v1") { - continue; // Open issue: should project node get displayed as well? - } - - const nodeId = currentNode.guid; - - // If toggled feature type exists, skip other types - if (featureType && featureType !== "all_nodes" && currentNode.typeName !== featureType) { - continue; - } - - const node = generateNode({ - index, - nodeId, - currentNode - }); - - elementObj[nodeId] = index?.toString(); - - elements.push(node); - } - - for (let index = 0; index < lineageData.relations.length; index++) { - const { fromEntityId: from, toEntityId: to, relationshipType } = lineageData.relations[index]; - const edge = generateEdge({ obj: elementObj, from, to }); - if (edge?.source && edge?.target) { - // Currently, API returns all relationships, filter out Contains, Consumes, etc - if (relationshipType === "Produces") { - elements.push(edge); - } - } - } - - SetElements(elements); + SetElements(getElements(lineageData, featureType)!); }; generateGraphData(); @@ -118,7 +75,7 @@ const LineageGraph: React.FC = () => { : ( - +