diff --git a/ui/src/components/RegistryVisualization.tsx b/ui/src/components/RegistryVisualization.tsx index eac6f470c19..72777218b45 100644 --- a/ui/src/components/RegistryVisualization.tsx +++ b/ui/src/components/RegistryVisualization.tsx @@ -572,12 +572,14 @@ interface RegistryVisualizationProps { registryData: feast.core.Registry; relationships: EntityRelation[]; indirectRelationships: EntityRelation[]; + filterNode?: { type: FEAST_FCO_TYPES; name: string }; } const RegistryVisualization: React.FC = ({ registryData, relationships, indirectRelationships, + filterNode, }) => { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -592,10 +594,22 @@ 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) => { // Add additional validation as needed for your use case @@ -625,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 4d6de96baa6..b7577241a13 100644 --- a/ui/src/components/RegistryVisualizationTab.tsx +++ b/ui/src/components/RegistryVisualizationTab.tsx @@ -1,12 +1,56 @@ -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 +75,58 @@ 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..65c4b472f31 --- /dev/null +++ b/ui/src/pages/feature-views/FeatureViewLineageTab.tsx @@ -0,0 +1,61 @@ +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}