Skip to content

Commit 73805d3

Browse files
feat: Add version indicators to lineage graph nodes (#6187)
* feat: Add version indicators to lineage graph nodes Display version badges on Feature View nodes in the lineage graph when they have been versioned (currentVersionNumber > 0). Badges show a tooltip with version details and total version count from the registry's version history. Also adds a "Version Changed" entry to the legend and fixes a missing `permissions` dependency in useEffect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Only show version badge when currentVersionNumber > 1 Version 1 is the initial version, so a badge is only meaningful when the feature view has actually changed (version 2+). Also fixes test name to match the updated threshold and adds a test for the v1 case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Code formatting for version indicators Apply prettier formatting to fix CI formatting checks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent effa9ca commit 73805d3

File tree

2 files changed

+479
-9
lines changed

2 files changed

+479
-9
lines changed
Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
import React from "react";
2+
import { render, screen, within } from "../test-utils";
3+
import { waitFor } from "@testing-library/react";
4+
import RegistryVisualization from "./RegistryVisualization";
5+
import { feast } from "../protos";
6+
import { FEAST_FCO_TYPES } from "../parsers/types";
7+
import { EntityRelation } from "../parsers/parseEntityRelationships";
8+
import { ThemeProvider } from "../contexts/ThemeContext";
9+
10+
// ReactFlow requires ResizeObserver
11+
class ResizeObserverMock {
12+
observe() {}
13+
unobserve() {}
14+
disconnect() {}
15+
}
16+
(global as any).ResizeObserver = ResizeObserverMock;
17+
18+
// Helper to build minimal registry data
19+
const makeRegistry = (
20+
overrides: Partial<feast.core.IRegistry> = {},
21+
): feast.core.Registry => {
22+
return feast.core.Registry.create({
23+
featureViews: [],
24+
onDemandFeatureViews: [],
25+
streamFeatureViews: [],
26+
featureServices: [],
27+
entities: [],
28+
dataSources: [],
29+
...overrides,
30+
});
31+
};
32+
33+
const makeRelationship = (
34+
sourceName: string,
35+
sourceType: FEAST_FCO_TYPES,
36+
targetName: string,
37+
targetType: FEAST_FCO_TYPES,
38+
): EntityRelation => ({
39+
source: { name: sourceName, type: sourceType },
40+
target: { name: targetName, type: targetType },
41+
});
42+
43+
const renderVisualization = (
44+
registryData: feast.core.Registry,
45+
relationships: EntityRelation[] = [],
46+
indirectRelationships: EntityRelation[] = [],
47+
) => {
48+
return render(
49+
<ThemeProvider>
50+
<RegistryVisualization
51+
registryData={registryData}
52+
relationships={relationships}
53+
indirectRelationships={indirectRelationships}
54+
/>
55+
</ThemeProvider>,
56+
);
57+
};
58+
59+
describe("RegistryVisualization version indicators", () => {
60+
test("renders version badge on feature view with currentVersionNumber > 1", async () => {
61+
const registry = makeRegistry({
62+
featureViews: [
63+
feast.core.FeatureView.create({
64+
spec: { name: "my_feature_view" },
65+
meta: { currentVersionNumber: 3 },
66+
}),
67+
],
68+
entities: [
69+
feast.core.Entity.create({
70+
spec: { name: "my_entity" },
71+
}),
72+
],
73+
});
74+
75+
const relationships = [
76+
makeRelationship(
77+
"my_feature_view",
78+
FEAST_FCO_TYPES.featureView,
79+
"my_entity",
80+
FEAST_FCO_TYPES.entity,
81+
),
82+
];
83+
84+
renderVisualization(registry, relationships);
85+
86+
await waitFor(() => {
87+
expect(screen.getByText("v3")).toBeInTheDocument();
88+
});
89+
});
90+
91+
test("does not render version badge when currentVersionNumber is 0", async () => {
92+
const registry = makeRegistry({
93+
featureViews: [
94+
feast.core.FeatureView.create({
95+
spec: { name: "unversioned_fv" },
96+
meta: { currentVersionNumber: 0 },
97+
}),
98+
],
99+
entities: [
100+
feast.core.Entity.create({
101+
spec: { name: "my_entity" },
102+
}),
103+
],
104+
});
105+
106+
const relationships = [
107+
makeRelationship(
108+
"unversioned_fv",
109+
FEAST_FCO_TYPES.featureView,
110+
"my_entity",
111+
FEAST_FCO_TYPES.entity,
112+
),
113+
];
114+
115+
renderVisualization(registry, relationships);
116+
117+
await waitFor(() => {
118+
expect(screen.getByText("unversioned_fv")).toBeInTheDocument();
119+
});
120+
121+
expect(screen.queryByText("v0")).not.toBeInTheDocument();
122+
});
123+
124+
test("does not render version badge when currentVersionNumber is 1 (initial version)", async () => {
125+
const registry = makeRegistry({
126+
featureViews: [
127+
feast.core.FeatureView.create({
128+
spec: { name: "initial_fv" },
129+
meta: { currentVersionNumber: 1 },
130+
}),
131+
],
132+
entities: [
133+
feast.core.Entity.create({
134+
spec: { name: "my_entity" },
135+
}),
136+
],
137+
});
138+
139+
const relationships = [
140+
makeRelationship(
141+
"initial_fv",
142+
FEAST_FCO_TYPES.featureView,
143+
"my_entity",
144+
FEAST_FCO_TYPES.entity,
145+
),
146+
];
147+
148+
renderVisualization(registry, relationships);
149+
150+
await waitFor(() => {
151+
expect(screen.getByText("initial_fv")).toBeInTheDocument();
152+
});
153+
154+
expect(screen.queryByText("v1")).not.toBeInTheDocument();
155+
});
156+
157+
test("does not render version badge when meta has no version", async () => {
158+
const registry = makeRegistry({
159+
featureViews: [
160+
feast.core.FeatureView.create({
161+
spec: { name: "no_version_fv" },
162+
meta: {},
163+
}),
164+
],
165+
entities: [
166+
feast.core.Entity.create({
167+
spec: { name: "my_entity" },
168+
}),
169+
],
170+
});
171+
172+
const relationships = [
173+
makeRelationship(
174+
"no_version_fv",
175+
FEAST_FCO_TYPES.featureView,
176+
"my_entity",
177+
FEAST_FCO_TYPES.entity,
178+
),
179+
];
180+
181+
renderVisualization(registry, relationships);
182+
183+
await waitFor(() => {
184+
expect(screen.getByText("no_version_fv")).toBeInTheDocument();
185+
});
186+
187+
// No version badges should exist at all
188+
const badges = screen.queryAllByText(/^v\d+$/);
189+
expect(badges).toHaveLength(0);
190+
});
191+
192+
test("renders version badge on on-demand feature view", async () => {
193+
const registry = makeRegistry({
194+
featureViews: [
195+
feast.core.FeatureView.create({
196+
spec: { name: "regular_fv" },
197+
}),
198+
],
199+
onDemandFeatureViews: [
200+
feast.core.OnDemandFeatureView.create({
201+
spec: { name: "odfv_versioned" },
202+
meta: { currentVersionNumber: 2 },
203+
}),
204+
],
205+
entities: [
206+
feast.core.Entity.create({
207+
spec: { name: "my_entity" },
208+
}),
209+
],
210+
});
211+
212+
// Connect regular FV to entity so nodes render, and ODFV to regular FV
213+
const relationships = [
214+
makeRelationship(
215+
"regular_fv",
216+
FEAST_FCO_TYPES.featureView,
217+
"my_entity",
218+
FEAST_FCO_TYPES.entity,
219+
),
220+
];
221+
222+
// Use the full component and toggle isolated nodes to show ODFV
223+
render(
224+
<ThemeProvider>
225+
<RegistryVisualization
226+
registryData={registry}
227+
relationships={relationships}
228+
indirectRelationships={[]}
229+
/>
230+
</ThemeProvider>,
231+
);
232+
233+
// Enable isolated nodes to show the ODFV
234+
const isolatedCheckbox = screen.getByLabelText(
235+
/Show Objects Without Relationships/i,
236+
);
237+
isolatedCheckbox.click();
238+
239+
await waitFor(() => {
240+
expect(screen.getByText("v2")).toBeInTheDocument();
241+
});
242+
});
243+
244+
test("renders version badge on stream feature view", async () => {
245+
const registry = makeRegistry({
246+
featureViews: [
247+
feast.core.FeatureView.create({
248+
spec: { name: "regular_fv" },
249+
}),
250+
],
251+
streamFeatureViews: [
252+
feast.core.StreamFeatureView.create({
253+
spec: { name: "sfv_versioned" },
254+
meta: { currentVersionNumber: 5 },
255+
}),
256+
],
257+
entities: [
258+
feast.core.Entity.create({
259+
spec: { name: "my_entity" },
260+
}),
261+
],
262+
});
263+
264+
const relationships = [
265+
makeRelationship(
266+
"regular_fv",
267+
FEAST_FCO_TYPES.featureView,
268+
"my_entity",
269+
FEAST_FCO_TYPES.entity,
270+
),
271+
];
272+
273+
render(
274+
<ThemeProvider>
275+
<RegistryVisualization
276+
registryData={registry}
277+
relationships={relationships}
278+
indirectRelationships={[]}
279+
/>
280+
</ThemeProvider>,
281+
);
282+
283+
// Enable isolated nodes to show the SFV
284+
const isolatedCheckbox = screen.getByLabelText(
285+
/Show Objects Without Relationships/i,
286+
);
287+
isolatedCheckbox.click();
288+
289+
await waitFor(() => {
290+
expect(screen.getByText("v5")).toBeInTheDocument();
291+
});
292+
});
293+
294+
test("renders version history info from featureViewVersionHistory", async () => {
295+
const registry = makeRegistry({
296+
featureViews: [
297+
feast.core.FeatureView.create({
298+
spec: { name: "versioned_fv" },
299+
meta: { currentVersionNumber: 2 },
300+
}),
301+
],
302+
entities: [
303+
feast.core.Entity.create({
304+
spec: { name: "my_entity" },
305+
}),
306+
],
307+
featureViewVersionHistory: feast.core.FeatureViewVersionHistory.create({
308+
records: [
309+
feast.core.FeatureViewVersionRecord.create({
310+
featureViewName: "versioned_fv",
311+
versionNumber: 1,
312+
description: "Initial version",
313+
}),
314+
feast.core.FeatureViewVersionRecord.create({
315+
featureViewName: "versioned_fv",
316+
versionNumber: 2,
317+
description: "Added new features",
318+
}),
319+
],
320+
}),
321+
});
322+
323+
const relationships = [
324+
makeRelationship(
325+
"versioned_fv",
326+
FEAST_FCO_TYPES.featureView,
327+
"my_entity",
328+
FEAST_FCO_TYPES.entity,
329+
),
330+
];
331+
332+
renderVisualization(registry, relationships);
333+
334+
await waitFor(() => {
335+
expect(screen.getByText("v2")).toBeInTheDocument();
336+
});
337+
});
338+
});
339+
340+
describe("RegistryVisualization legend", () => {
341+
test("renders Version Changed entry in legend", async () => {
342+
const registry = makeRegistry({
343+
featureViews: [
344+
feast.core.FeatureView.create({
345+
spec: { name: "some_fv" },
346+
}),
347+
],
348+
entities: [
349+
feast.core.Entity.create({
350+
spec: { name: "some_entity" },
351+
}),
352+
],
353+
});
354+
355+
const relationships = [
356+
makeRelationship(
357+
"some_fv",
358+
FEAST_FCO_TYPES.featureView,
359+
"some_entity",
360+
FEAST_FCO_TYPES.entity,
361+
),
362+
];
363+
364+
renderVisualization(registry, relationships);
365+
366+
await waitFor(() => {
367+
expect(screen.getByText("Version Changed")).toBeInTheDocument();
368+
});
369+
370+
expect(screen.getByText("vN")).toBeInTheDocument();
371+
});
372+
});

0 commit comments

Comments
 (0)