Skip to content

Commit 7a5b48f

Browse files
feat: Add feature view curl generator (feast-dev#5415)
* Add GenAI documentation page to Introduction section Co-Authored-By: Francisco Javier Arceo <[email protected]> * Move GenAI page to getting-started directory and update SUMMARY.md Co-Authored-By: Francisco Javier Arceo <[email protected]> * Update SUMMARY.md * Add unstructured data transformation and Spark integration details to GenAI documentation Co-Authored-By: Francisco Javier Arceo <[email protected]> * Update genai.md * feat: Add CURL Generator tab to Feature View pages - Add new CURL Generator tab that generates pre-populated CURL commands for /get-online-features endpoint - Configurable feature selection with checkboxes (all selected by default) - Features organized in rows of exactly 5 for better readability - Entity input fields with customizable comma-separated values - Configurable server URL with localStorage persistence across feature views - Copy to clipboard functionality for generated CURL commands - Real-time updates when feature selection, entity values, or server URL change - Follows existing UI patterns using Elastic UI components Co-Authored-By: Francisco Javier Arceo <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 28c3379 commit 7a5b48f

File tree

3 files changed

+305
-1
lines changed

3 files changed

+305
-1
lines changed

ui/src/FeastUISansProviders.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import NoProjectGuard from "./components/NoProjectGuard";
3030
import TabsRegistryContext, {
3131
FeastTabsRegistryInterface,
3232
} from "./custom-tabs/TabsRegistryContext";
33+
import CurlGeneratorTab from "./pages/feature-views/CurlGeneratorTab";
3334
import FeatureFlagsContext, {
3435
FeatureFlags,
3536
} from "./contexts/FeatureFlagsContext";
@@ -98,7 +99,31 @@ const FeastUISansProvidersInner = ({
9899
<EuiProvider colorMode={colorMode}>
99100
<EuiErrorBoundary>
100101
<TabsRegistryContext.Provider
101-
value={feastUIConfigs?.tabsRegistry || {}}
102+
value={{
103+
RegularFeatureViewCustomTabs: [
104+
{
105+
label: "CURL Generator",
106+
path: "curl-generator",
107+
Component: CurlGeneratorTab,
108+
},
109+
...(feastUIConfigs?.tabsRegistry?.RegularFeatureViewCustomTabs ||
110+
[]),
111+
],
112+
OnDemandFeatureViewCustomTabs:
113+
feastUIConfigs?.tabsRegistry?.OnDemandFeatureViewCustomTabs || [],
114+
StreamFeatureViewCustomTabs:
115+
feastUIConfigs?.tabsRegistry?.StreamFeatureViewCustomTabs || [],
116+
FeatureServiceCustomTabs:
117+
feastUIConfigs?.tabsRegistry?.FeatureServiceCustomTabs || [],
118+
FeatureCustomTabs:
119+
feastUIConfigs?.tabsRegistry?.FeatureCustomTabs || [],
120+
DataSourceCustomTabs:
121+
feastUIConfigs?.tabsRegistry?.DataSourceCustomTabs || [],
122+
EntityCustomTabs:
123+
feastUIConfigs?.tabsRegistry?.EntityCustomTabs || [],
124+
DatasetCustomTabs:
125+
feastUIConfigs?.tabsRegistry?.DatasetCustomTabs || [],
126+
}}
102127
>
103128
<FeatureFlagsContext.Provider
104129
value={feastUIConfigs?.featureFlags || {}}

ui/src/custom-tabs/TabsRegistryContext.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import FeatureCustomTabLoadingWrapper from "../utils/custom-tabs/FeatureCustomTa
1616
import DataSourceCustomTabLoadingWrapper from "../utils/custom-tabs/DataSourceCustomTabLoadingWrapper";
1717
import EntityCustomTabLoadingWrapper from "../utils/custom-tabs/EntityCustomTabLoadingWrapper";
1818
import DatasetCustomTabLoadingWrapper from "../utils/custom-tabs/DatasetCustomTabLoadingWrapper";
19+
import CurlGeneratorTab from "../pages/feature-views/CurlGeneratorTab";
1920

2021
import {
2122
RegularFeatureViewCustomTabRegistrationInterface,
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import React, { useState, useEffect } from "react";
2+
import {
3+
EuiPanel,
4+
EuiTitle,
5+
EuiHorizontalRule,
6+
EuiSpacer,
7+
EuiText,
8+
EuiButton,
9+
EuiFlexGroup,
10+
EuiFlexItem,
11+
EuiFormRow,
12+
EuiFieldText,
13+
EuiCopy,
14+
EuiCheckbox,
15+
} from "@elastic/eui";
16+
import { CodeBlock } from "react-code-blocks";
17+
import { RegularFeatureViewCustomTabProps } from "../../custom-tabs/types";
18+
19+
const CurlGeneratorTab = ({
20+
feastObjectQuery,
21+
}: RegularFeatureViewCustomTabProps) => {
22+
const data = feastObjectQuery.data as any;
23+
const [serverUrl, setServerUrl] = useState(() => {
24+
const savedUrl = localStorage.getItem("feast-feature-server-url");
25+
return savedUrl || "http://localhost:6566";
26+
});
27+
const [entityValues, setEntityValues] = useState<Record<string, string>>({});
28+
const [selectedFeatures, setSelectedFeatures] = useState<
29+
Record<string, boolean>
30+
>({});
31+
32+
useEffect(() => {
33+
localStorage.setItem("feast-feature-server-url", serverUrl);
34+
}, [serverUrl]);
35+
36+
if (feastObjectQuery.isLoading) {
37+
return <EuiText>Loading...</EuiText>;
38+
}
39+
40+
if (feastObjectQuery.isError || !data) {
41+
return <EuiText>Error loading feature view data.</EuiText>;
42+
}
43+
44+
const generateFeatureNames = () => {
45+
if (!data?.name || !data?.features) return [];
46+
47+
return data.features
48+
.filter((feature: any) => selectedFeatures[feature.name] !== false)
49+
.map((feature: any) => `${data.name}:${feature.name}`);
50+
};
51+
52+
const generateEntityObject = () => {
53+
if (!data?.object?.spec?.entities) return {};
54+
55+
const entities: Record<string, number[]> = {};
56+
data.object.spec.entities.forEach((entityName: string) => {
57+
const userValue = entityValues[entityName];
58+
if (userValue) {
59+
const values = userValue.split(",").map((v) => {
60+
const num = parseInt(v.trim());
61+
return isNaN(num) ? 1001 : num;
62+
});
63+
entities[entityName] = values;
64+
} else {
65+
entities[entityName] = [1001, 1002, 1003];
66+
}
67+
});
68+
return entities;
69+
};
70+
71+
const generateCurlCommand = () => {
72+
const features = generateFeatureNames();
73+
const entities = generateEntityObject();
74+
75+
const payload = {
76+
features,
77+
entities,
78+
};
79+
80+
const curlCommand = `curl -X POST \\
81+
"${serverUrl}/get-online-features" \\
82+
-H "Content-Type: application/json" \\
83+
-d '${JSON.stringify(payload, null, 2)}'`;
84+
85+
return curlCommand;
86+
};
87+
88+
const curlCommand = generateCurlCommand();
89+
90+
return (
91+
<React.Fragment>
92+
<EuiPanel hasBorder={true}>
93+
<EuiTitle size="s">
94+
<h2>Feature Server CURL Generator</h2>
95+
</EuiTitle>
96+
<EuiHorizontalRule margin="s" />
97+
<EuiText size="s" color="subdued">
98+
<p>
99+
Generate a CURL command to fetch online features from the feature
100+
server. The command is pre-populated with all features and entities
101+
from this feature view.
102+
</p>
103+
</EuiText>
104+
<EuiSpacer size="m" />
105+
106+
<EuiFormRow label="Feature Server URL">
107+
<EuiFieldText
108+
value={serverUrl}
109+
onChange={(e) => setServerUrl(e.target.value)}
110+
placeholder="http://localhost:6566"
111+
/>
112+
</EuiFormRow>
113+
114+
<EuiSpacer size="m" />
115+
116+
{data?.features && data.features.length > 0 && (
117+
<>
118+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
119+
<EuiFlexItem grow={false}>
120+
<EuiTitle size="xs">
121+
<h3>
122+
Features to Include (
123+
{
124+
Object.values(selectedFeatures).filter((v) => v !== false)
125+
.length
126+
}
127+
/{data.features.length})
128+
</h3>
129+
</EuiTitle>
130+
</EuiFlexItem>
131+
<EuiFlexItem grow={false}>
132+
<EuiFlexGroup gutterSize="s">
133+
<EuiFlexItem grow={false}>
134+
<EuiButton
135+
size="s"
136+
onClick={() => {
137+
const allSelected: Record<string, boolean> = {};
138+
data.features.forEach((feature: any) => {
139+
allSelected[feature.name] = true;
140+
});
141+
setSelectedFeatures(allSelected);
142+
}}
143+
>
144+
Select All
145+
</EuiButton>
146+
</EuiFlexItem>
147+
<EuiFlexItem grow={false}>
148+
<EuiButton
149+
size="s"
150+
onClick={() => {
151+
const noneSelected: Record<string, boolean> = {};
152+
data.features.forEach((feature: any) => {
153+
noneSelected[feature.name] = false;
154+
});
155+
setSelectedFeatures(noneSelected);
156+
}}
157+
>
158+
Select None
159+
</EuiButton>
160+
</EuiFlexItem>
161+
</EuiFlexGroup>
162+
</EuiFlexItem>
163+
</EuiFlexGroup>
164+
<EuiSpacer size="s" />
165+
<EuiPanel color="subdued" paddingSize="s">
166+
{Array.from(
167+
{ length: Math.ceil(data.features.length / 5) },
168+
(_, rowIndex) => (
169+
<EuiFlexGroup
170+
key={rowIndex}
171+
direction="row"
172+
gutterSize="s"
173+
style={{
174+
marginBottom:
175+
rowIndex < Math.ceil(data.features.length / 5) - 1
176+
? "8px"
177+
: "0",
178+
}}
179+
>
180+
{data.features
181+
.slice(rowIndex * 5, (rowIndex + 1) * 5)
182+
.map((feature: any) => (
183+
<EuiFlexItem
184+
key={feature.name}
185+
grow={false}
186+
style={{ minWidth: "180px" }}
187+
>
188+
<EuiCheckbox
189+
id={`feature-${feature.name}`}
190+
label={feature.name}
191+
checked={selectedFeatures[feature.name] !== false}
192+
onChange={(e) =>
193+
setSelectedFeatures((prev) => ({
194+
...prev,
195+
[feature.name]: e.target.checked,
196+
}))
197+
}
198+
/>
199+
</EuiFlexItem>
200+
))}
201+
</EuiFlexGroup>
202+
),
203+
)}
204+
</EuiPanel>
205+
<EuiSpacer size="m" />
206+
</>
207+
)}
208+
209+
{data?.object?.spec?.entities &&
210+
data.object.spec.entities.length > 0 && (
211+
<>
212+
<EuiTitle size="xs">
213+
<h3>Entity Values (comma-separated)</h3>
214+
</EuiTitle>
215+
<EuiSpacer size="s" />
216+
{data.object.spec.entities.map((entityName: string) => (
217+
<EuiFormRow key={entityName} label={entityName}>
218+
<EuiFieldText
219+
value={entityValues[entityName] || ""}
220+
onChange={(e) =>
221+
setEntityValues((prev) => ({
222+
...prev,
223+
[entityName]: e.target.value,
224+
}))
225+
}
226+
placeholder="1001, 1002, 1003"
227+
/>
228+
</EuiFormRow>
229+
))}
230+
<EuiSpacer size="m" />
231+
</>
232+
)}
233+
234+
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
235+
<EuiFlexItem grow={false}>
236+
<EuiTitle size="xs">
237+
<h3>Generated CURL Command</h3>
238+
</EuiTitle>
239+
</EuiFlexItem>
240+
<EuiFlexItem grow={false}>
241+
<EuiCopy textToCopy={curlCommand}>
242+
{(copy) => (
243+
<EuiButton onClick={copy} size="s" iconType="copy">
244+
Copy to Clipboard
245+
</EuiButton>
246+
)}
247+
</EuiCopy>
248+
</EuiFlexItem>
249+
</EuiFlexGroup>
250+
251+
<EuiSpacer size="s" />
252+
253+
<CodeBlock
254+
text={curlCommand}
255+
language="bash"
256+
showLineNumbers={false}
257+
theme="github"
258+
/>
259+
260+
<EuiSpacer size="m" />
261+
262+
<EuiText size="s" color="subdued">
263+
<p>
264+
<strong>Features included:</strong>{" "}
265+
{generateFeatureNames().join(", ")}
266+
</p>
267+
{data?.object?.spec?.entities && (
268+
<p>
269+
<strong>Entities:</strong> {data.object.spec.entities.join(", ")}
270+
</p>
271+
)}
272+
</EuiText>
273+
</EuiPanel>
274+
</React.Fragment>
275+
);
276+
};
277+
278+
export default CurlGeneratorTab;

0 commit comments

Comments
 (0)