Skip to content

Commit 308255d

Browse files
feat: Add feature view lineage tab and filtering to home page lineage (feast-dev#5308)
* Add feature view lineage tab and filtering to home page lineage Co-Authored-By: Francisco Javier Arceo <[email protected]> * Fix TypeScript errors in lineage visualization components Co-Authored-By: Francisco Javier Arceo <[email protected]> * Format FeatureViewLineageTab.tsx Co-Authored-By: Francisco Javier Arceo <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent f2d6a67 commit 308255d

File tree

4 files changed

+184
-3
lines changed

4 files changed

+184
-3
lines changed

ui/src/components/RegistryVisualization.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,12 +572,14 @@ interface RegistryVisualizationProps {
572572
registryData: feast.core.Registry;
573573
relationships: EntityRelation[];
574574
indirectRelationships: EntityRelation[];
575+
filterNode?: { type: FEAST_FCO_TYPES; name: string };
575576
}
576577

577578
const RegistryVisualization: React.FC<RegistryVisualizationProps> = ({
578579
registryData,
579580
relationships,
580581
indirectRelationships,
582+
filterNode,
581583
}) => {
582584
const [nodes, setNodes, onNodesChange] = useNodesState([]);
583585
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
@@ -592,10 +594,22 @@ const RegistryVisualization: React.FC<RegistryVisualizationProps> = ({
592594
setLoading(true);
593595

594596
// Only include indirect relationships if the toggle is on
595-
const relationshipsToShow = showIndirectRelationships
597+
let relationshipsToShow = showIndirectRelationships
596598
? [...relationships, ...indirectRelationships]
597599
: relationships;
598600

601+
// Filter relationships based on filterNode if provided
602+
if (filterNode) {
603+
relationshipsToShow = relationshipsToShow.filter((rel) => {
604+
return (
605+
(rel.source.type === filterNode.type &&
606+
rel.source.name === filterNode.name) ||
607+
(rel.target.type === filterNode.type &&
608+
rel.target.name === filterNode.name)
609+
);
610+
});
611+
}
612+
599613
// Filter out invalid relationships
600614
const validRelationships = relationshipsToShow.filter((rel) => {
601615
// Add additional validation as needed for your use case
@@ -625,6 +639,7 @@ const RegistryVisualization: React.FC<RegistryVisualizationProps> = ({
625639
indirectRelationships,
626640
showIndirectRelationships,
627641
showIsolatedNodes,
642+
filterNode,
628643
setNodes,
629644
setEdges,
630645
]);

ui/src/components/RegistryVisualizationTab.tsx

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,56 @@
1-
import React, { useContext } from "react";
2-
import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer } from "@elastic/eui";
1+
import React, { useContext, useState } from "react";
2+
import {
3+
EuiEmptyPrompt,
4+
EuiLoadingSpinner,
5+
EuiSpacer,
6+
EuiSelect,
7+
EuiFormRow,
8+
EuiFlexGroup,
9+
EuiFlexItem,
10+
} from "@elastic/eui";
311
import useLoadRegistry from "../queries/useLoadRegistry";
412
import RegistryPathContext from "../contexts/RegistryPathContext";
513
import RegistryVisualization from "./RegistryVisualization";
14+
import { FEAST_FCO_TYPES } from "../parsers/types";
615

716
const RegistryVisualizationTab = () => {
817
const registryUrl = useContext(RegistryPathContext);
918
const { isLoading, isSuccess, isError, data } = useLoadRegistry(registryUrl);
19+
const [selectedObjectType, setSelectedObjectType] = useState("");
20+
const [selectedObjectName, setSelectedObjectName] = useState("");
21+
22+
const getObjectOptions = (objects: any, type: string) => {
23+
switch (type) {
24+
case "dataSource":
25+
const dataSources = new Set<string>();
26+
objects.featureViews?.forEach((fv: any) => {
27+
if (fv.spec?.batchSource?.name)
28+
dataSources.add(fv.spec.batchSource.name);
29+
});
30+
objects.streamFeatureViews?.forEach((sfv: any) => {
31+
if (sfv.spec?.batchSource?.name)
32+
dataSources.add(sfv.spec.batchSource.name);
33+
if (sfv.spec?.streamSource?.name)
34+
dataSources.add(sfv.spec.streamSource.name);
35+
});
36+
return Array.from(dataSources);
37+
case "entity":
38+
return objects.entities?.map((entity: any) => entity.spec?.name) || [];
39+
case "featureView":
40+
return [
41+
...(objects.featureViews?.map((fv: any) => fv.spec?.name) || []),
42+
...(objects.onDemandFeatureViews?.map(
43+
(odfv: any) => odfv.spec?.name,
44+
) || []),
45+
...(objects.streamFeatureViews?.map((sfv: any) => sfv.spec?.name) ||
46+
[]),
47+
];
48+
case "featureService":
49+
return objects.featureServices?.map((fs: any) => fs.spec?.name) || [];
50+
default:
51+
return [];
52+
}
53+
};
1054

1155
return (
1256
<>
@@ -31,10 +75,58 @@ const RegistryVisualizationTab = () => {
3175
{isSuccess && data && (
3276
<>
3377
<EuiSpacer size="l" />
78+
<EuiFlexGroup style={{ marginBottom: 16 }}>
79+
<EuiFlexItem grow={false} style={{ width: 200 }}>
80+
<EuiFormRow label="Filter by type">
81+
<EuiSelect
82+
options={[
83+
{ value: "", text: "All" },
84+
{ value: "dataSource", text: "Data Source" },
85+
{ value: "entity", text: "Entity" },
86+
{ value: "featureView", text: "Feature View" },
87+
{ value: "featureService", text: "Feature Service" },
88+
]}
89+
value={selectedObjectType}
90+
onChange={(e) => {
91+
setSelectedObjectType(e.target.value);
92+
setSelectedObjectName(""); // Reset name when type changes
93+
}}
94+
aria-label="Select object type"
95+
/>
96+
</EuiFormRow>
97+
</EuiFlexItem>
98+
<EuiFlexItem grow={false} style={{ width: 300 }}>
99+
<EuiFormRow label="Select object">
100+
<EuiSelect
101+
options={[
102+
{ value: "", text: "All" },
103+
...getObjectOptions(data.objects, selectedObjectType).map(
104+
(name: string) => ({
105+
value: name,
106+
text: name,
107+
}),
108+
),
109+
]}
110+
value={selectedObjectName}
111+
onChange={(e) => setSelectedObjectName(e.target.value)}
112+
aria-label="Select object"
113+
disabled={selectedObjectType === ""}
114+
/>
115+
</EuiFormRow>
116+
</EuiFlexItem>
117+
</EuiFlexGroup>
34118
<RegistryVisualization
35119
registryData={data.objects}
36120
relationships={data.relationships}
37121
indirectRelationships={data.indirectRelationships}
122+
filterNode={
123+
selectedObjectType && selectedObjectName
124+
? {
125+
type: selectedObjectType as FEAST_FCO_TYPES,
126+
name: selectedObjectName,
127+
}
128+
: undefined
129+
}
38130
/>
39131
</>
40132
)}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { useContext } from "react";
2+
import { useParams } from "react-router-dom";
3+
import { EuiEmptyPrompt, EuiLoadingSpinner } from "@elastic/eui";
4+
import { feast } from "../../protos";
5+
import useLoadRegistry from "../../queries/useLoadRegistry";
6+
import RegistryPathContext from "../../contexts/RegistryPathContext";
7+
import RegistryVisualization from "../../components/RegistryVisualization";
8+
import { FEAST_FCO_TYPES } from "../../parsers/types";
9+
10+
interface FeatureViewLineageTabProps {
11+
data: feast.core.IFeatureView;
12+
}
13+
14+
const FeatureViewLineageTab = ({ data }: FeatureViewLineageTabProps) => {
15+
const registryUrl = useContext(RegistryPathContext);
16+
const {
17+
isLoading,
18+
isSuccess,
19+
isError,
20+
data: registryData,
21+
} = useLoadRegistry(registryUrl);
22+
const { featureViewName } = useParams();
23+
24+
const filterNode = {
25+
type: FEAST_FCO_TYPES.featureView,
26+
name: featureViewName || data.spec?.name || "",
27+
};
28+
29+
return (
30+
<>
31+
{isLoading && (
32+
<div style={{ display: "flex", justifyContent: "center", padding: 25 }}>
33+
<EuiLoadingSpinner size="xl" />
34+
</div>
35+
)}
36+
{isError && (
37+
<EuiEmptyPrompt
38+
iconType="alert"
39+
color="danger"
40+
title={<h2>Error Loading Registry Data</h2>}
41+
body={
42+
<p>
43+
There was an error loading the Registry Data. Please check that{" "}
44+
<code>feature_store.yaml</code> file is available and well-formed.
45+
</p>
46+
}
47+
/>
48+
)}
49+
{isSuccess && registryData && (
50+
<RegistryVisualization
51+
registryData={registryData.objects}
52+
relationships={registryData.relationships}
53+
indirectRelationships={registryData.indirectRelationships}
54+
filterNode={filterNode}
55+
/>
56+
)}
57+
</>
58+
);
59+
};
60+
61+
export default FeatureViewLineageTab;

ui/src/pages/feature-views/RegularFeatureViewInstance.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { FeatureViewIcon } from "../../graphics/FeatureViewIcon";
66

77
import { useMatchExact, useMatchSubpath } from "../../hooks/useMatchSubpath";
88
import RegularFeatureViewOverviewTab from "./RegularFeatureViewOverviewTab";
9+
import FeatureViewLineageTab from "./FeatureViewLineageTab";
910

1011
import {
1112
useRegularFeatureViewCustomTabs,
@@ -33,6 +34,14 @@ const RegularFeatureInstance = ({ data }: RegularFeatureInstanceProps) => {
3334
},
3435
];
3536

37+
tabs.push({
38+
label: "Lineage",
39+
isSelected: useMatchSubpath("lineage"),
40+
onClick: () => {
41+
navigate("lineage");
42+
},
43+
});
44+
3645
let statisticsIsSelected = useMatchSubpath("statistics");
3746
if (enabledFeatureStatistics) {
3847
tabs.push({
@@ -62,6 +71,10 @@ const RegularFeatureInstance = ({ data }: RegularFeatureInstanceProps) => {
6271
path="/"
6372
element={<RegularFeatureViewOverviewTab data={data} />}
6473
/>
74+
<Route
75+
path="/lineage"
76+
element={<FeatureViewLineageTab data={data} />}
77+
/>
6578
{TabRoutes}
6679
</Routes>
6780
</EuiPageTemplate.Section>

0 commit comments

Comments
 (0)