diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index 2c00a985c09..9a2207e22dd 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -30,6 +30,7 @@ import NoProjectGuard from "./components/NoProjectGuard"; import TabsRegistryContext, { FeastTabsRegistryInterface, } from "./custom-tabs/TabsRegistryContext"; +import CurlGeneratorTab from "./pages/feature-views/CurlGeneratorTab"; import FeatureFlagsContext, { FeatureFlags, } from "./contexts/FeatureFlagsContext"; @@ -98,7 +99,31 @@ const FeastUISansProvidersInner = ({ { + const data = feastObjectQuery.data as any; + const [serverUrl, setServerUrl] = useState(() => { + const savedUrl = localStorage.getItem("feast-feature-server-url"); + return savedUrl || "http://localhost:6566"; + }); + const [entityValues, setEntityValues] = useState>({}); + const [selectedFeatures, setSelectedFeatures] = useState< + Record + >({}); + + useEffect(() => { + localStorage.setItem("feast-feature-server-url", serverUrl); + }, [serverUrl]); + + if (feastObjectQuery.isLoading) { + return Loading...; + } + + if (feastObjectQuery.isError || !data) { + return Error loading feature view data.; + } + + const generateFeatureNames = () => { + if (!data?.name || !data?.features) return []; + + return data.features + .filter((feature: any) => selectedFeatures[feature.name] !== false) + .map((feature: any) => `${data.name}:${feature.name}`); + }; + + const generateEntityObject = () => { + if (!data?.object?.spec?.entities) return {}; + + const entities: Record = {}; + data.object.spec.entities.forEach((entityName: string) => { + const userValue = entityValues[entityName]; + if (userValue) { + const values = userValue.split(",").map((v) => { + const num = parseInt(v.trim()); + return isNaN(num) ? 1001 : num; + }); + entities[entityName] = values; + } else { + entities[entityName] = [1001, 1002, 1003]; + } + }); + return entities; + }; + + const generateCurlCommand = () => { + const features = generateFeatureNames(); + const entities = generateEntityObject(); + + const payload = { + features, + entities, + }; + + const curlCommand = `curl -X POST \\ + "${serverUrl}/get-online-features" \\ + -H "Content-Type: application/json" \\ + -d '${JSON.stringify(payload, null, 2)}'`; + + return curlCommand; + }; + + const curlCommand = generateCurlCommand(); + + return ( + + + +

Feature Server CURL Generator

+
+ + +

+ Generate a CURL command to fetch online features from the feature + server. The command is pre-populated with all features and entities + from this feature view. +

+
+ + + + setServerUrl(e.target.value)} + placeholder="http://localhost:6566" + /> + + + + + {data?.features && data.features.length > 0 && ( + <> + + + +

+ Features to Include ( + { + Object.values(selectedFeatures).filter((v) => v !== false) + .length + } + /{data.features.length}) +

+
+
+ + + + { + const allSelected: Record = {}; + data.features.forEach((feature: any) => { + allSelected[feature.name] = true; + }); + setSelectedFeatures(allSelected); + }} + > + Select All + + + + { + const noneSelected: Record = {}; + data.features.forEach((feature: any) => { + noneSelected[feature.name] = false; + }); + setSelectedFeatures(noneSelected); + }} + > + Select None + + + + +
+ + + {Array.from( + { length: Math.ceil(data.features.length / 5) }, + (_, rowIndex) => ( + + {data.features + .slice(rowIndex * 5, (rowIndex + 1) * 5) + .map((feature: any) => ( + + + setSelectedFeatures((prev) => ({ + ...prev, + [feature.name]: e.target.checked, + })) + } + /> + + ))} + + ), + )} + + + + )} + + {data?.object?.spec?.entities && + data.object.spec.entities.length > 0 && ( + <> + +

Entity Values (comma-separated)

+
+ + {data.object.spec.entities.map((entityName: string) => ( + + + setEntityValues((prev) => ({ + ...prev, + [entityName]: e.target.value, + })) + } + placeholder="1001, 1002, 1003" + /> + + ))} + + + )} + + + + +

Generated CURL Command

+
+
+ + + {(copy) => ( + + Copy to Clipboard + + )} + + +
+ + + + + + + + +

+ Features included:{" "} + {generateFeatureNames().join(", ")} +

+ {data?.object?.spec?.entities && ( +

+ Entities: {data.object.spec.entities.join(", ")} +

+ )} +
+
+
+ ); +}; + +export default CurlGeneratorTab;