Skip to content

Commit 47d4c93

Browse files
hao-affirmadchia
authored andcommitted
fix: Add stream feature view in the Web UI (feast-dev#3257)
* add stream feature view to ui Signed-off-by: hao-affirm <[email protected]> * update source Signed-off-by: hao-affirm <[email protected]> * update example Signed-off-by: hao-affirm <[email protected]> * add registry Signed-off-by: hao-affirm <[email protected]> * fix lint Signed-off-by: hao-affirm <[email protected]> * fix bug Signed-off-by: hao-affirm <[email protected]> * add batch source Signed-off-by: hao-affirm <[email protected]> * fix warning Signed-off-by: hao-affirm <[email protected]> Signed-off-by: hao-affirm <[email protected]>
1 parent e7ed3d5 commit 47d4c93

16 files changed

+625
-5
lines changed

ui/public/registry.json

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@
2929
"name": "zipcode",
3030
"timestampField": "event_timestamp",
3131
"type": "BATCH_FILE"
32+
},
33+
{
34+
"batchSource": {
35+
"fileOptions": {
36+
"uri": "data/zipcode_table.parquet"
37+
},
38+
"name": "user_stats",
39+
"timestampField": "timestamp",
40+
"type": "BATCH_FILE"
41+
},
42+
"dataSourceClassType": "feast.data_source.KafkaSource",
43+
"description": "The Kafka stream example",
44+
"kafkaOptions": {"messageFormat": {"jsonFormat": {"schemaJson": "id string, timestamp timestamp"}},
45+
"watermarkDelayThreshold": "300s"},
46+
"name": "driver_stats_stream",
47+
"owner": "[email protected]",
48+
"timestampField": "timestamp",
49+
"type": "STREAM_KAFKA"
3250
}
3351
],
3452
"entities": [
@@ -630,5 +648,59 @@
630648
}
631649
}
632650
],
651+
"streamFeatureViews": [
652+
{
653+
"meta": {
654+
"createdTimestamp": "2022-05-11T19:27:03.171556Z",
655+
"lastUpdatedTimestamp": "2022-05-11T19:27:03.171556Z"
656+
},
657+
"spec": {
658+
"batchSource": {
659+
"createdTimestampColumn": "created_timestamp",
660+
"dataSourceClassType": "feast.infra.offline_stores.file_source.FileSource",
661+
"fileOptions": {
662+
"uri": "data/zipcode_table.parquet"
663+
},
664+
"name": "zipcode",
665+
"timestampField": "event_timestamp",
666+
"type": "BATCH_FILE"
667+
},
668+
"features": [
669+
{
670+
"name": "conv_percentage",
671+
"valueType": "FLOAT"
672+
},
673+
{
674+
"name": "acc_percentage",
675+
"valueType": "FLOAT"
676+
}
677+
],
678+
"name": "transaction_stream_example",
679+
"streamSource": {
680+
"batchSource": {
681+
"fileOptions": {
682+
"uri": "data/zipcode_table.parquet"
683+
},
684+
"name": "user_stats",
685+
"timestampField": "timestamp",
686+
"type": "BATCH_FILE"
687+
},
688+
"dataSourceClassType": "feast.data_source.KafkaSource",
689+
"description": "The Kafka stream example",
690+
"kafkaOptions": {"messageFormat": {"jsonFormat": {"schemaJson": "id string, timestamp timestamp"}},
691+
"watermarkDelayThreshold": "300s"},
692+
"name": "driver_stats_stream",
693+
"owner": "[email protected]",
694+
"timestampField": "timestamp",
695+
"type": "STREAM_KAFKA"
696+
},
697+
"ttl": "86400s",
698+
"userDefinedFunction": {
699+
"body": "@stream_feature_view(\n sources=[driver_stats_stream_source],\n mode=\"spark\",\n schema=[\n Field(name=\"conv_percentage\", dtype=Float32),\n Field(name=\"acc_percentage\", dtype=Float32),\n ],\n timestamp_field=\"event_timestamp\",\n online=True,\n source=driver_stats_stream_source,\n tags={},\n)\ndef driver_hourly_stats_stream(df: DataFrame) -> DataFrame:\n from pyspark.sql.functions import col\n return (\n df.withColumn(\"conv_percentage\", col(\"conv_rate\") * 100.0)\n .withColumn(\"acc_percentage\", col(\"acc_rate\") * 100.0)\n .drop(\"conv_rate\", \"acc_rate\")\n )\n",
700+
"name": "driver_hourly_stats_stream"
701+
}
702+
}
703+
}
704+
],
633705
"project": "credit_scoring_aws"
634706
}

ui/src/custom-tabs/TabsRegistryContext.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010

1111
import RegularFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/RegularFeatureViewCustomTabLoadingWrapper";
1212
import OnDemandFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/OnDemandFeatureViewCustomTabLoadingWrapper";
13+
import StreamFeatureViewCustomTabLoadingWrapper from "../utils/custom-tabs/StreamFeatureViewCustomTabLoadingWrapper";
1314
import FeatureServiceCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureServiceCustomTabLoadingWrapper";
1415
import FeatureCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureCustomTabLoadingWrapper";
1516
import DataSourceCustomTabLoadingWrapper from "../utils/custom-tabs/DataSourceCustomTabLoadingWrapper";
@@ -19,6 +20,7 @@ import DatasetCustomTabLoadingWrapper from "../utils/custom-tabs/DatasetCustomTa
1920
import {
2021
RegularFeatureViewCustomTabRegistrationInterface,
2122
OnDemandFeatureViewCustomTabRegistrationInterface,
23+
StreamFeatureViewCustomTabRegistrationInterface,
2224
FeatureServiceCustomTabRegistrationInterface,
2325
FeatureCustomTabRegistrationInterface,
2426
DataSourceCustomTabRegistrationInterface,
@@ -30,6 +32,7 @@ import {
3032
interface FeastTabsRegistryInterface {
3133
RegularFeatureViewCustomTabs?: RegularFeatureViewCustomTabRegistrationInterface[];
3234
OnDemandFeatureViewCustomTabs?: OnDemandFeatureViewCustomTabRegistrationInterface[];
35+
StreamFeatureViewCustomTabs?: StreamFeatureViewCustomTabRegistrationInterface[];
3336
FeatureServiceCustomTabs?: FeatureServiceCustomTabRegistrationInterface[];
3437
FeatureCustomTabs?: FeatureCustomTabRegistrationInterface[];
3538
DataSourceCustomTabs?: DataSourceCustomTabRegistrationInterface[];
@@ -148,6 +151,16 @@ const useOnDemandFeatureViewCustomTabs = (navigate: NavigateFunction) => {
148151
);
149152
};
150153

154+
const useStreamFeatureViewCustomTabs = (navigate: NavigateFunction) => {
155+
const { StreamFeatureViewCustomTabs } =
156+
React.useContext(TabsRegistryContext);
157+
158+
return useGenericCustomTabsNavigation<StreamFeatureViewCustomTabRegistrationInterface>(
159+
StreamFeatureViewCustomTabs || [],
160+
navigate
161+
);
162+
};
163+
151164
const useFeatureServiceCustomTabs = (navigate: NavigateFunction) => {
152165
const { FeatureServiceCustomTabs } = React.useContext(TabsRegistryContext);
153166

@@ -214,6 +227,16 @@ const useOnDemandFeatureViewCustomTabRoutes = () => {
214227
);
215228
};
216229

230+
const useStreamFeatureViewCustomTabRoutes = () => {
231+
const { StreamFeatureViewCustomTabs } =
232+
React.useContext(TabsRegistryContext);
233+
234+
return genericCustomTabRoutes(
235+
StreamFeatureViewCustomTabs || [],
236+
StreamFeatureViewCustomTabLoadingWrapper
237+
);
238+
};
239+
217240
const useFeatureServiceCustomTabRoutes = () => {
218241
const { FeatureServiceCustomTabs } = React.useContext(TabsRegistryContext);
219242

@@ -264,6 +287,7 @@ export {
264287
// Navigation
265288
useRegularFeatureViewCustomTabs,
266289
useOnDemandFeatureViewCustomTabs,
290+
useStreamFeatureViewCustomTabs,
267291
useFeatureServiceCustomTabs,
268292
useFeatureCustomTabs,
269293
useDataSourceCustomTabs,
@@ -272,6 +296,7 @@ export {
272296
// Routes
273297
useRegularFeatureViewCustomTabRoutes,
274298
useOnDemandFeatureViewCustomTabRoutes,
299+
useStreamFeatureViewCustomTabRoutes,
275300
useFeatureServiceCustomTabRoutes,
276301
useFeatureCustomTabRoutes,
277302
useDataSourceCustomTabRoutes,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React from "react";
2+
3+
import {
4+
// Feature View Custom Tabs will get these props
5+
StreamFeatureViewCustomTabProps,
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 = ({
22+
id,
23+
feastObjectQuery,
24+
}: StreamFeatureViewCustomTabProps) => {
25+
// Use React Query to fetch data
26+
// that is custom to this tab.
27+
// See: https://react-query.tanstack.com/guides/queries
28+
const { isLoading, isError, isSuccess, data } = useDemoQuery({
29+
featureView: id,
30+
});
31+
32+
if (isLoading) {
33+
// Handle Loading State
34+
// https://elastic.github.io/eui/#/display/loading
35+
return <EuiLoadingContent lines={3} />;
36+
}
37+
38+
if (isError) {
39+
// Handle Data Fetching Error
40+
// https://elastic.github.io/eui/#/display/empty-prompt
41+
return (
42+
<EuiEmptyPrompt
43+
iconType="alert"
44+
color="danger"
45+
title={<h2>Unable to load your demo page</h2>}
46+
body={
47+
<p>
48+
There was an error loading the Dashboard application. Contact your
49+
administrator for help.
50+
</p>
51+
}
52+
/>
53+
);
54+
}
55+
56+
// Feast UI uses the Elastic UI component system.
57+
// <EuiFlexGroup> and <EuiFlexItem> are particularly
58+
// useful for layouts.
59+
return (
60+
<React.Fragment>
61+
<EuiFlexGroup>
62+
<EuiFlexItem grow={1}>
63+
<p>Hello World. The following is fetched data.</p>
64+
<EuiSpacer />
65+
{isSuccess && data && (
66+
<EuiCode>
67+
<pre>{JSON.stringify(data, null, 2)}</pre>
68+
</EuiCode>
69+
)}
70+
</EuiFlexItem>
71+
<EuiFlexItem grow={2}>
72+
<p>... and this is data from Feast UI&rsquo;s own query.</p>
73+
<EuiSpacer />
74+
{feastObjectQuery.isSuccess && feastObjectQuery.data && (
75+
<EuiCode>
76+
<pre>{JSON.stringify(feastObjectQuery.data, null, 2)}</pre>
77+
</EuiCode>
78+
)}
79+
</EuiFlexItem>
80+
</EuiFlexGroup>
81+
</React.Fragment>
82+
);
83+
};
84+
85+
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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
useLoadOnDemandFeatureView,
3+
useLoadStreamFeatureView,
34
useLoadRegularFeatureView,
45
} from "../pages/feature-views/useLoadFeatureView";
56
import useLoadFeature from "../pages/features/useLoadFeature";
@@ -48,6 +49,23 @@ interface OnDemandFeatureViewCustomTabRegistrationInterface
4849
}: OnDemandFeatureViewCustomTabProps) => JSX.Element;
4950
}
5051

52+
// Type for Stream Feature View Custom Tabs
53+
type StreamFeatureViewQueryReturnType = ReturnType<
54+
typeof useLoadStreamFeatureView
55+
>;
56+
interface StreamFeatureViewCustomTabProps {
57+
id: string | undefined;
58+
feastObjectQuery: StreamFeatureViewQueryReturnType;
59+
}
60+
interface StreamFeatureViewCustomTabRegistrationInterface
61+
extends CustomTabRegistrationInterface {
62+
Component: ({
63+
id,
64+
feastObjectQuery,
65+
...args
66+
}: StreamFeatureViewCustomTabProps) => JSX.Element;
67+
}
68+
5169
// Type for Entity Custom Tabs
5270
interface EntityCustomTabProps {
5371
id: string | undefined;
@@ -127,6 +145,9 @@ export type {
127145
OnDemandFeatureViewQueryReturnType,
128146
OnDemandFeatureViewCustomTabProps,
129147
OnDemandFeatureViewCustomTabRegistrationInterface,
148+
StreamFeatureViewQueryReturnType,
149+
StreamFeatureViewCustomTabProps,
150+
StreamFeatureViewCustomTabRegistrationInterface,
130151
FeatureServiceCustomTabRegistrationInterface,
131152
FeatureServiceCustomTabProps,
132153
DataSourceCustomTabRegistrationInterface,

ui/src/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import FeastUI from "./FeastUI";
1818
import DataTab from "./custom-tabs/data-tab/DataTab";
1919
import RFVDemoCustomTab from "./custom-tabs/reguar-fv-demo-tab/DemoCustomTab";
2020
import ODFVDemoCustomTab from "./custom-tabs/ondemand-fv-demo-tab/DemoCustomTab";
21+
import SFVDemoCustomTab from "./custom-tabs/stream-fv-demo-tab/DemoCustomTab";
2122
import FSDemoCustomTab from "./custom-tabs/feature-service-demo-tab/DemoCustomTab";
2223
import DSDemoCustomTab from "./custom-tabs/data-source-demo-tab/DemoCustomTab";
2324
import EntDemoCustomTab from "./custom-tabs/entity-demo-tab/DemoCustomTab";
@@ -46,6 +47,13 @@ const tabsRegistry = {
4647
Component: ODFVDemoCustomTab,
4748
},
4849
],
50+
StreamFeatureViewCustomTabs: [
51+
{
52+
label: "Custom Tab Demo",
53+
path: "demo-tab",
54+
Component: SFVDemoCustomTab,
55+
},
56+
],
4957
FeatureServiceCustomTabs: [
5058
{
5159
label: "Custom Tab Demo",
@@ -93,4 +101,4 @@ ReactDOM.render(
93101
/>
94102
</React.StrictMode>,
95103
document.getElementById("root")
96-
);
104+
);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ import { FeastFeatureViewType } from "../../parsers/feastFeatureViews";
77
import RegularFeatureInstance from "./RegularFeatureViewInstance";
88
import { FEAST_FV_TYPES } from "../../parsers/mergedFVTypes";
99
import { FeastODFVType } from "../../parsers/feastODFVS";
10+
import { FeastSFVType } from "../../parsers/feastSFVS";
1011
import useLoadFeatureView from "./useLoadFeatureView";
1112
import OnDemandFeatureInstance from "./OnDemandFeatureViewInstance";
13+
import StreamFeatureInstance from "./StreamFeatureViewInstance";
14+
1215

1316
const FeatureViewInstance = () => {
1417
const { featureViewName } = useParams();
@@ -45,6 +48,11 @@ const FeatureViewInstance = () => {
4548

4649
return <OnDemandFeatureInstance data={odfv} />;
4750
}
51+
if (data.type === FEAST_FV_TYPES.stream) {
52+
const sfv: FeastSFVType = data.object;
53+
54+
return <StreamFeatureInstance data={sfv} />;
55+
}
4856
}
4957

5058
return <p>No Data So Sad</p>;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const FeatureViewListingTable = ({
3535
href={`/p/${projectName}/feature-view/${name}`}
3636
to={`/p/${projectName}/feature-view/${name}`}
3737
>
38-
{name} {item.type === "ondemand" && <EuiBadge>ondemand</EuiBadge>}
38+
{name} {(item.type === "ondemand" && <EuiBadge>ondemand</EuiBadge>) || (item.type === "stream" && <EuiBadge>stream</EuiBadge>)}
3939
</EuiCustomLink>
4040
);
4141
},

0 commit comments

Comments
 (0)