From 0d6afafd9eee8da660ee208603d8a29fe8a7f9cf Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 01:47:38 +0000 Subject: [PATCH 1/3] Add feature view lineage tab and filtering to home page lineage Co-Authored-By: Francisco Javier Arceo --- ui/src/components/RegistryVisualization.tsx | 13 +++- .../components/RegistryVisualizationTab.tsx | 78 ++++++++++++++++++- .../feature-views/FeatureViewLineageTab.tsx | 56 +++++++++++++ .../RegularFeatureViewInstance.tsx | 13 ++++ 4 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 ui/src/pages/feature-views/FeatureViewLineageTab.tsx diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index eac6f470c19..54b2c206641 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -572,6 +572,7 @@ interface RegistryVisualizationProps { registryData: feast.core.Registry; relationships: EntityRelation[]; indirectRelationships: EntityRelation[]; + filterNode?: { type: FEAST_FCO_TYPES; name: string }; } const RegistryVisualization: React.FC = ({ @@ -592,9 +593,19 @@ const RegistryVisualization: React.FC = ({ setLoading(true); // Only include indirect relationships if the toggle is on - const relationshipsToShow = showIndirectRelationships + let relationshipsToShow = showIndirectRelationships ? [...relationships, ...indirectRelationships] : relationships; + + // Filter relationships based on filterNode if provided + if (filterNode) { + relationshipsToShow = relationshipsToShow.filter((rel) => { + return ( + (rel.source.type === filterNode.type && rel.source.name === filterNode.name) || + (rel.target.type === filterNode.type && rel.target.name === filterNode.name) + ); + }); + } // Filter out invalid relationships const validRelationships = relationshipsToShow.filter((rel) => { diff --git a/ui/src/components/RegistryVisualizationTab.tsx b/ui/src/components/RegistryVisualizationTab.tsx index 4d6de96baa6..460be0c6f38 100644 --- a/ui/src/components/RegistryVisualizationTab.tsx +++ b/ui/src/components/RegistryVisualizationTab.tsx @@ -1,12 +1,40 @@ -import React, { useContext } from "react"; -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer } from "@elastic/eui"; +import React, { useContext, useState } from "react"; +import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiSelect, EuiFormRow, EuiFlexGroup, EuiFlexItem } from "@elastic/eui"; import useLoadRegistry from "../queries/useLoadRegistry"; import RegistryPathContext from "../contexts/RegistryPathContext"; import RegistryVisualization from "./RegistryVisualization"; +import { FEAST_FCO_TYPES } from "../parsers/types"; const RegistryVisualizationTab = () => { const registryUrl = useContext(RegistryPathContext); const { isLoading, isSuccess, isError, data } = useLoadRegistry(registryUrl); + const [selectedObjectType, setSelectedObjectType] = useState(""); + const [selectedObjectName, setSelectedObjectName] = useState(""); + + const getObjectOptions = (objects: any, type: string) => { + switch (type) { + case "dataSource": + const dataSources = new Set(); + objects.featureViews?.forEach((fv: any) => { + if (fv.spec?.batchSource?.name) dataSources.add(fv.spec.batchSource.name); + }); + objects.streamFeatureViews?.forEach((sfv: any) => { + if (sfv.spec?.batchSource?.name) dataSources.add(sfv.spec.batchSource.name); + if (sfv.spec?.streamSource?.name) dataSources.add(sfv.spec.streamSource.name); + }); + return Array.from(dataSources); + case "entity": + return objects.entities?.map((entity: any) => entity.spec?.name) || []; + case "featureView": + return [...(objects.featureViews?.map((fv: any) => fv.spec?.name) || []), + ...(objects.onDemandFeatureViews?.map((odfv: any) => odfv.spec?.name) || []), + ...(objects.streamFeatureViews?.map((sfv: any) => sfv.spec?.name) || [])]; + case "featureService": + return objects.featureServices?.map((fs: any) => fs.spec?.name) || []; + default: + return []; + } + }; return ( <> @@ -31,10 +59,56 @@ const RegistryVisualizationTab = () => { {isSuccess && data && ( <> + + + + { + setSelectedObjectType(e.target.value); + setSelectedObjectName(""); // Reset name when type changes + }} + aria-label="Select object type" + /> + + + + + ({ + value: name, + text: name, + })), + ]} + value={selectedObjectName} + onChange={(e) => setSelectedObjectName(e.target.value)} + aria-label="Select object" + disabled={selectedObjectType === ""} + /> + + + )} diff --git a/ui/src/pages/feature-views/FeatureViewLineageTab.tsx b/ui/src/pages/feature-views/FeatureViewLineageTab.tsx new file mode 100644 index 00000000000..5667d4e96eb --- /dev/null +++ b/ui/src/pages/feature-views/FeatureViewLineageTab.tsx @@ -0,0 +1,56 @@ +import React, { useContext } from "react"; +import { useParams } from "react-router-dom"; +import { EuiEmptyPrompt, EuiLoadingSpinner } from "@elastic/eui"; +import { feast } from "../../protos"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; +import RegistryVisualization from "../../components/RegistryVisualization"; +import { FEAST_FCO_TYPES } from "../../parsers/types"; + +interface FeatureViewLineageTabProps { + data: feast.core.IFeatureView; +} + +const FeatureViewLineageTab = ({ data }: FeatureViewLineageTabProps) => { + const registryUrl = useContext(RegistryPathContext); + const { isLoading, isSuccess, isError, data: registryData } = useLoadRegistry(registryUrl); + const { featureViewName } = useParams(); + + const filterNode = { + type: FEAST_FCO_TYPES.featureView, + name: featureViewName || data.spec?.name || "", + }; + + return ( + <> + {isLoading && ( +
+ +
+ )} + {isError && ( + Error Loading Registry Data} + body={ +

+ There was an error loading the Registry Data. Please check that{" "} + feature_store.yaml file is available and well-formed. +

+ } + /> + )} + {isSuccess && registryData && ( + + )} + + ); +}; + +export default FeatureViewLineageTab; diff --git a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx index 40adfca0e2b..35b2f0e262f 100644 --- a/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx +++ b/ui/src/pages/feature-views/RegularFeatureViewInstance.tsx @@ -6,6 +6,7 @@ import { FeatureViewIcon } from "../../graphics/FeatureViewIcon"; import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath"; import RegularFeatureViewOverviewTab from "./RegularFeatureViewOverviewTab"; +import FeatureViewLineageTab from "./FeatureViewLineageTab"; import { useRegularFeatureViewCustomTabs, @@ -33,6 +34,14 @@ const RegularFeatureInstance = ({ data }: RegularFeatureInstanceProps) => { }, ]; + tabs.push({ + label: "Lineage", + isSelected: useMatchSubpath("lineage"), + onClick: () => { + navigate("lineage"); + }, + }); + let statisticsIsSelected = useMatchSubpath("statistics"); if (enabledFeatureStatistics) { tabs.push({ @@ -62,6 +71,10 @@ const RegularFeatureInstance = ({ data }: RegularFeatureInstanceProps) => { path="/" element={} /> + } + /> {TabRoutes} From 3a53836367622ad6a00dd127f3b6b0231211f8d6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:02:23 +0000 Subject: [PATCH 2/3] Fix TypeScript errors in lineage visualization components Co-Authored-By: Francisco Javier Arceo --- ui/src/components/RegistryVisualization.tsx | 10 +++-- .../components/RegistryVisualizationTab.tsx | 42 +++++++++++++------ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index 54b2c206641..72777218b45 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -579,6 +579,7 @@ const RegistryVisualization: React.FC = ({ registryData, relationships, indirectRelationships, + filterNode, }) => { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -596,13 +597,15 @@ const RegistryVisualization: React.FC = ({ let relationshipsToShow = showIndirectRelationships ? [...relationships, ...indirectRelationships] : relationships; - + // Filter relationships based on filterNode if provided if (filterNode) { relationshipsToShow = relationshipsToShow.filter((rel) => { return ( - (rel.source.type === filterNode.type && rel.source.name === filterNode.name) || - (rel.target.type === filterNode.type && rel.target.name === filterNode.name) + (rel.source.type === filterNode.type && + rel.source.name === filterNode.name) || + (rel.target.type === filterNode.type && + rel.target.name === filterNode.name) ); }); } @@ -636,6 +639,7 @@ const RegistryVisualization: React.FC = ({ indirectRelationships, showIndirectRelationships, showIsolatedNodes, + filterNode, setNodes, setEdges, ]); diff --git a/ui/src/components/RegistryVisualizationTab.tsx b/ui/src/components/RegistryVisualizationTab.tsx index 460be0c6f38..b7577241a13 100644 --- a/ui/src/components/RegistryVisualizationTab.tsx +++ b/ui/src/components/RegistryVisualizationTab.tsx @@ -1,5 +1,13 @@ import React, { useContext, useState } from "react"; -import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiSelect, EuiFormRow, EuiFlexGroup, EuiFlexItem } from "@elastic/eui"; +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiSpacer, + EuiSelect, + EuiFormRow, + EuiFlexGroup, + EuiFlexItem, +} from "@elastic/eui"; import useLoadRegistry from "../queries/useLoadRegistry"; import RegistryPathContext from "../contexts/RegistryPathContext"; import RegistryVisualization from "./RegistryVisualization"; @@ -10,25 +18,33 @@ const RegistryVisualizationTab = () => { const { isLoading, isSuccess, isError, data } = useLoadRegistry(registryUrl); const [selectedObjectType, setSelectedObjectType] = useState(""); const [selectedObjectName, setSelectedObjectName] = useState(""); - + const getObjectOptions = (objects: any, type: string) => { switch (type) { case "dataSource": const dataSources = new Set(); objects.featureViews?.forEach((fv: any) => { - if (fv.spec?.batchSource?.name) dataSources.add(fv.spec.batchSource.name); + if (fv.spec?.batchSource?.name) + dataSources.add(fv.spec.batchSource.name); }); objects.streamFeatureViews?.forEach((sfv: any) => { - if (sfv.spec?.batchSource?.name) dataSources.add(sfv.spec.batchSource.name); - if (sfv.spec?.streamSource?.name) dataSources.add(sfv.spec.streamSource.name); + if (sfv.spec?.batchSource?.name) + dataSources.add(sfv.spec.batchSource.name); + if (sfv.spec?.streamSource?.name) + dataSources.add(sfv.spec.streamSource.name); }); return Array.from(dataSources); case "entity": return objects.entities?.map((entity: any) => entity.spec?.name) || []; case "featureView": - return [...(objects.featureViews?.map((fv: any) => fv.spec?.name) || []), - ...(objects.onDemandFeatureViews?.map((odfv: any) => odfv.spec?.name) || []), - ...(objects.streamFeatureViews?.map((sfv: any) => sfv.spec?.name) || [])]; + return [ + ...(objects.featureViews?.map((fv: any) => fv.spec?.name) || []), + ...(objects.onDemandFeatureViews?.map( + (odfv: any) => odfv.spec?.name, + ) || []), + ...(objects.streamFeatureViews?.map((sfv: any) => sfv.spec?.name) || + []), + ]; case "featureService": return objects.featureServices?.map((fs: any) => fs.spec?.name) || []; default: @@ -84,10 +100,12 @@ const RegistryVisualizationTab = () => { ({ - value: name, - text: name, - })), + ...getObjectOptions(data.objects, selectedObjectType).map( + (name: string) => ({ + value: name, + text: name, + }), + ), ]} value={selectedObjectName} onChange={(e) => setSelectedObjectName(e.target.value)} From 57a9d288efbf5a7118e2c067866239cb690aaa1a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:02:53 +0000 Subject: [PATCH 3/3] Format FeatureViewLineageTab.tsx Co-Authored-By: Francisco Javier Arceo --- ui/src/pages/feature-views/FeatureViewLineageTab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/src/pages/feature-views/FeatureViewLineageTab.tsx b/ui/src/pages/feature-views/FeatureViewLineageTab.tsx index 5667d4e96eb..65c4b472f31 100644 --- a/ui/src/pages/feature-views/FeatureViewLineageTab.tsx +++ b/ui/src/pages/feature-views/FeatureViewLineageTab.tsx @@ -13,7 +13,12 @@ interface FeatureViewLineageTabProps { const FeatureViewLineageTab = ({ data }: FeatureViewLineageTabProps) => { const registryUrl = useContext(RegistryPathContext); - const { isLoading, isSuccess, isError, data: registryData } = useLoadRegistry(registryUrl); + const { + isLoading, + isSuccess, + isError, + data: registryData, + } = useLoadRegistry(registryUrl); const { featureViewName } = useParams(); const filterNode = {