Skip to content

Commit 9b97fca

Browse files
authored
feat: Add pages for individual Features to the Feast UI (#2850)
* Add feature page functionality Signed-off-by: Daniel Kim <[email protected]> * Add links in feature view and feature pages Signed-off-by: Daniel Kim <[email protected]> * Modify Feast provider test to include new Feature pages Signed-off-by: Daniel Kim <[email protected]> * Add initial version of test Signed-off-by: Daniel Kim <[email protected]> * Make some changes to test and remove feature tab functionality from ondemand FVs Signed-off-by: Daniel Kim <[email protected]> * Change feature link EuiLinks to EuiCustomLinks Signed-off-by: Daniel Kim <[email protected]> * Change other links to EuiCustomLinks Signed-off-by: Daniel Kim <[email protected]>
1 parent 8abc2ef commit 9b97fca

19 files changed

+556
-13
lines changed

ui/src/FeastUISansProviders.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,46 @@ test("routes are reachable", async () => {
9494
});
9595
}
9696
});
97+
98+
99+
const featureViewName = registry.featureViews[0].spec.name;
100+
const featureName = registry.featureViews[0].spec.features[0].name;
101+
102+
test("features are reachable", async () => {
103+
render(<FeastUISansProviders />);
104+
105+
// Wait for content to load
106+
await screen.findByText(/Explore this Project/i);
107+
const routeRegExp = new RegExp("Feature Views", "i");
108+
109+
userEvent.click(
110+
screen.getByRole("button", { name: routeRegExp }),
111+
leftClick
112+
);
113+
114+
screen.getByRole("heading", {
115+
name: "Feature Views",
116+
});
117+
118+
await screen.findAllByText(/Feature Views/i);
119+
const fvRegExp = new RegExp(featureViewName, "i");
120+
121+
userEvent.click(
122+
screen.getByRole("link", { name: fvRegExp }),
123+
leftClick
124+
)
125+
126+
await screen.findByText(featureName);
127+
const fRegExp = new RegExp(featureName, "i");
128+
129+
userEvent.click(
130+
screen.getByRole("link", { name: fRegExp }),
131+
leftClick
132+
)
133+
// Should land on a page with the heading
134+
// await screen.findByText("Feature: " + featureName);
135+
screen.getByRole("heading", {
136+
name: "Feature: " + featureName,
137+
level: 1,
138+
});
139+
});

ui/src/FeastUISansProviders.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import DatasourceIndex from "./pages/data-sources/Index";
1313
import DatasetIndex from "./pages/saved-data-sets/Index";
1414
import EntityIndex from "./pages/entities/Index";
1515
import EntityInstance from "./pages/entities/EntityInstance";
16+
import FeatureInstance from "./pages/features/FeatureInstance";
1617
import FeatureServiceIndex from "./pages/feature-services/Index";
1718
import FeatureViewIndex from "./pages/feature-views/Index";
1819
import FeatureViewInstance from "./pages/feature-views/FeatureViewInstance";
@@ -86,10 +87,12 @@ const FeastUISansProviders = ({
8687
path="feature-view/"
8788
element={<FeatureViewIndex />}
8889
/>
90+
<Route path="feature-view/:featureViewName/*" element={<FeatureViewInstance />}>
91+
</Route>
8992
<Route
90-
path="feature-view/:featureViewName/*"
91-
element={<FeatureViewInstance />}
92-
/>
93+
path="feature-view/:FeatureViewName/feature/:FeatureName/*"
94+
element={<FeatureInstance />}
95+
/>
9396
<Route
9497
path="feature-service/"
9598
element={<FeatureServiceIndex />}

ui/src/components/FeaturesListDisplay.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,42 @@ import { FeastFeatureColumnType } from "../parsers/feastFeatureViews";
44
import useLoadFeatureViewSummaryStatistics from "../queries/useLoadFeatureViewSummaryStatistics";
55
import SparklineHistogram from "./SparklineHistogram";
66
import FeatureFlagsContext from "../contexts/FeatureFlagsContext";
7+
import EuiCustomLink from "./EuiCustomLink";
78

89
interface FeaturesListProps {
10+
projectName: string;
911
featureViewName: string;
1012
features: FeastFeatureColumnType[];
13+
link: boolean;
1114
}
1215

13-
const FeaturesList = ({ featureViewName, features }: FeaturesListProps) => {
16+
const FeaturesList = ({ projectName, featureViewName, features, link }: FeaturesListProps) => {
1417
const { enabledFeatureStatistics } = useContext(FeatureFlagsContext);
1518
const { isLoading, isError, isSuccess, data } =
1619
useLoadFeatureViewSummaryStatistics(featureViewName);
1720

1821
let columns: { name: string; render?: any; field: any }[] = [
19-
{ name: "Name", field: "name" },
22+
{
23+
name: "Name",
24+
field: "name",
25+
render: (item: string) => (
26+
<EuiCustomLink
27+
href={`/p/${projectName}/feature-view/${featureViewName}/feature/${item}`}
28+
to={`/p/${projectName}/feature-view/${featureViewName}/feature/${item}`}>
29+
{item}
30+
</EuiCustomLink>
31+
)
32+
},
2033
{
2134
name: "Value Type",
2235
field: "valueType",
2336
},
2437
];
2538

39+
if (!link) {
40+
columns[0].render = undefined;
41+
}
42+
2643
if (enabledFeatureStatistics) {
2744
columns.push(
2845
...[

ui/src/components/TagSearch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ const TagSearch = ({
163163
// HTMLInputElement is hooked into useInputHack
164164
inputNode.current = node;
165165
},
166-
onfocus: () => {
166+
onFocus: () => {
167167
setHasFocus(true);
168168
},
169169
fullWidth: true,

ui/src/custom-tabs/TabsRegistryContext.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import RegularFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/RegularFeatureViewCustomTabLoadingWrapper";
1212
import OnDemandFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/OnDemandFeatureViewCustomTabLoadingWrapper";
1313
import FeatureServiceCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureServiceCustomTabLoadingWrapper";
14+
import FeatureCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureCustomTabLoadingWrapper";
1415
import DataSourceCustomTabLoadingWrapper from "../utils/custom-tabs/DataSourceCustomTabLoadingWrapper";
1516
import EntityCustomTabLoadingWrapper from "../utils/custom-tabs/EntityCustomTabLoadingWrapper";
1617
import DatasetCustomTabLoadingWrapper from "../utils/custom-tabs/DatasetCustomTabLoadingWrapper";
@@ -19,6 +20,7 @@ import {
1920
RegularFeatureViewCustomTabRegistrationInterface,
2021
OnDemandFeatureViewCustomTabRegistrationInterface,
2122
FeatureServiceCustomTabRegistrationInterface,
23+
FeatureCustomTabRegistrationInterface,
2224
DataSourceCustomTabRegistrationInterface,
2325
EntityCustomTabRegistrationInterface,
2426
DatasetCustomTabRegistrationInterface,
@@ -29,6 +31,7 @@ interface FeastTabsRegistryInterface {
2931
RegularFeatureViewCustomTabs?: RegularFeatureViewCustomTabRegistrationInterface[];
3032
OnDemandFeatureViewCustomTabs?: OnDemandFeatureViewCustomTabRegistrationInterface[];
3133
FeatureServiceCustomTabs?: FeatureServiceCustomTabRegistrationInterface[];
34+
FeatureCustomTabs?: FeatureCustomTabRegistrationInterface[];
3235
DataSourceCustomTabs?: DataSourceCustomTabRegistrationInterface[];
3336
EntityCustomTabs?: EntityCustomTabRegistrationInterface[];
3437
DatasetCustomTabs?: DatasetCustomTabRegistrationInterface[];
@@ -154,6 +157,15 @@ const useFeatureServiceCustomTabs = (navigate: NavigateFunction) => {
154157
);
155158
};
156159

160+
const useFeatureCustomTabs = (navigate: NavigateFunction) => {
161+
const { FeatureCustomTabs } = React.useContext(TabsRegistryContext);
162+
163+
return useGenericCustomTabsNavigation<FeatureCustomTabRegistrationInterface>(
164+
FeatureCustomTabs || [],
165+
navigate
166+
);
167+
};
168+
157169
const useDataSourceCustomTabs = (navigate: NavigateFunction) => {
158170
const { DataSourceCustomTabs } = React.useContext(TabsRegistryContext);
159171

@@ -211,6 +223,15 @@ const useFeatureServiceCustomTabRoutes = () => {
211223
);
212224
};
213225

226+
const useEntityCustomTabRoutes = () => {
227+
const { EntityCustomTabs } = React.useContext(TabsRegistryContext);
228+
229+
return genericCustomTabRoutes(
230+
EntityCustomTabs || [],
231+
EntityCustomTabLoadingWrapper
232+
);
233+
};
234+
214235
const useDataSourceCustomTabRoutes = () => {
215236
const { DataSourceCustomTabs } = React.useContext(TabsRegistryContext);
216237

@@ -220,12 +241,12 @@ const useDataSourceCustomTabRoutes = () => {
220241
);
221242
};
222243

223-
const useEntityCustomTabRoutes = () => {
224-
const { EntityCustomTabs } = React.useContext(TabsRegistryContext);
244+
const useFeatureCustomTabRoutes = () => {
245+
const { FeatureCustomTabs } = React.useContext(TabsRegistryContext);
225246

226247
return genericCustomTabRoutes(
227-
EntityCustomTabs || [],
228-
EntityCustomTabLoadingWrapper
248+
FeatureCustomTabs || [],
249+
FeatureCustomTabLoadingWrapper
229250
);
230251
};
231252

@@ -244,13 +265,15 @@ export {
244265
useRegularFeatureViewCustomTabs,
245266
useOnDemandFeatureViewCustomTabs,
246267
useFeatureServiceCustomTabs,
268+
useFeatureCustomTabs,
247269
useDataSourceCustomTabs,
248270
useEntityCustomTabs,
249271
useDatasetCustomTabs,
250272
// Routes
251273
useRegularFeatureViewCustomTabRoutes,
252274
useOnDemandFeatureViewCustomTabRoutes,
253275
useFeatureServiceCustomTabRoutes,
276+
useFeatureCustomTabRoutes,
254277
useDataSourceCustomTabRoutes,
255278
useEntityCustomTabRoutes,
256279
useDatasetCustomTabRoutes,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from "react";
2+
3+
import {
4+
// Feature View Custom Tabs will get these props
5+
FeatureCustomTabProps,
6+
} from "../types";
7+
8+
import {
9+
EuiLoadingContent,
10+
EuiEmptyPrompt,
11+
EuiFlexGroup,
12+
EuiFlexItem,
13+
EuiCode,
14+
EuiSpacer,
15+
} from "@elastic/eui";
16+
17+
// Separating out the query is not required,
18+
// but encouraged for code readability
19+
import useDemoQuery from "./useDemoQuery";
20+
21+
const DemoCustomTab = ({ id, feastObjectQuery }: FeatureCustomTabProps) => {
22+
// Use React Query to fetch data
23+
// that is custom to this tab.
24+
// See: https://react-query.tanstack.com/guides/queries
25+
26+
const { isLoading, isError, isSuccess, data } = useDemoQuery({
27+
featureView: id,
28+
});
29+
30+
if (isLoading) {
31+
// Handle Loading State
32+
// https://elastic.github.io/eui/#/display/loading
33+
return <EuiLoadingContent lines={3} />;
34+
}
35+
36+
if (isError) {
37+
// Handle Data Fetching Error
38+
// https://elastic.github.io/eui/#/display/empty-prompt
39+
return (
40+
<EuiEmptyPrompt
41+
iconType="alert"
42+
color="danger"
43+
title={<h2>Unable to load your demo page</h2>}
44+
body={
45+
<p>
46+
There was an error loading the Dashboard application. Contact your
47+
administrator for help.
48+
</p>
49+
}
50+
/>
51+
);
52+
}
53+
54+
// Feast UI uses the Elastic UI component system.
55+
// <EuiFlexGroup> and <EuiFlexItem> are particularly
56+
// useful for layouts.
57+
return (
58+
<React.Fragment>
59+
<EuiFlexGroup>
60+
<EuiFlexItem grow={1}>
61+
<p>Hello World. The following is fetched data.</p>
62+
<EuiSpacer />
63+
{isSuccess && data && (
64+
<EuiCode>
65+
<pre>{JSON.stringify(data, null, 2)}</pre>
66+
</EuiCode>
67+
)}
68+
</EuiFlexItem>
69+
<EuiFlexItem grow={2}>
70+
<p>... and this is data from Feast UI&rsquo;s own query.</p>
71+
<EuiSpacer />
72+
{feastObjectQuery.isSuccess && feastObjectQuery.featureData && (
73+
<EuiCode>
74+
<pre>{JSON.stringify(feastObjectQuery.featureData, null, 2)}</pre>
75+
</EuiCode>
76+
)}
77+
</EuiFlexItem>
78+
</EuiFlexGroup>
79+
</React.Fragment>
80+
);
81+
};
82+
83+
export default DemoCustomTab;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useQuery } from "react-query";
2+
import { z } from "zod";
3+
4+
// Use Zod to check the shape of the
5+
// json object being loaded
6+
const demoSchema = z.object({
7+
hello: z.string(),
8+
name: z.string().optional(),
9+
});
10+
11+
// Make the type of the object available
12+
type DemoDataType = z.infer<typeof demoSchema>;
13+
14+
interface DemoQueryInterface {
15+
featureView: string | undefined;
16+
}
17+
18+
const useDemoQuery = ({ featureView }: DemoQueryInterface) => {
19+
// React Query manages caching for you based on query keys
20+
// See: https://react-query.tanstack.com/guides/query-keys
21+
const queryKey = `demo-tab-namespace:${featureView}`;
22+
23+
// Pass the type to useQuery
24+
// so that components consuming the
25+
// result gets nice type hints
26+
// on the other side.
27+
return useQuery<DemoDataType>(
28+
queryKey,
29+
() => {
30+
// Customizing the URL based on your needs
31+
const url = `/demo-custom-tabs/demo.json`;
32+
33+
return fetch(url)
34+
.then((res) => res.json())
35+
.then((data) => demoSchema.parse(data)); // Use zod to parse results
36+
},
37+
{
38+
enabled: !!featureView, // Only start the query when the variable is not undefined
39+
}
40+
);
41+
};
42+
43+
export default useDemoQuery;
44+
export type { DemoDataType };

ui/src/custom-tabs/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
useLoadOnDemandFeatureView,
33
useLoadRegularFeatureView,
44
} from "../pages/feature-views/useLoadFeatureView";
5+
import useLoadFeature from "../pages/features/useLoadFeature";
56
import useLoadFeatureService from "../pages/feature-services/useLoadFeatureService";
67
import useLoadDataSource from "../pages/data-sources/useLoadDataSource";
78
import useLoadEntity from "../pages/entities/useLoadEntity";
@@ -47,7 +48,7 @@ interface OnDemandFeatureViewCustomTabRegistrationInterface
4748
}: OnDemandFeatureViewCustomTabProps) => JSX.Element;
4849
}
4950

50-
// Type for Feature Service Custom Tabs
51+
// Type for Entity Custom Tabs
5152
interface EntityCustomTabProps {
5253
id: string | undefined;
5354
feastObjectQuery: ReturnType<typeof useLoadEntity>;
@@ -61,6 +62,21 @@ interface EntityCustomTabRegistrationInterface
6162
}: EntityCustomTabProps) => JSX.Element;
6263
}
6364

65+
// Type for Feature Custom Tabs
66+
interface FeatureCustomTabProps {
67+
id: string | undefined;
68+
feastObjectQuery: ReturnType<typeof useLoadFeature>;
69+
}
70+
interface FeatureCustomTabRegistrationInterface
71+
extends CustomTabRegistrationInterface {
72+
Component: ({
73+
id,
74+
feastObjectQuery,
75+
...args
76+
}: FeatureCustomTabProps) => JSX.Element;
77+
}
78+
79+
6480
// Type for Feature Service Custom Tabs
6581
interface FeatureServiceCustomTabProps {
6682
id: string | undefined;
@@ -117,6 +133,8 @@ export type {
117133
DataSourceCustomTabProps,
118134
EntityCustomTabRegistrationInterface,
119135
EntityCustomTabProps,
136+
FeatureCustomTabRegistrationInterface,
137+
FeatureCustomTabProps,
120138
DatasetCustomTabRegistrationInterface,
121139
DatasetCustomTabProps,
122140
};

0 commit comments

Comments
 (0)