diff --git a/ui/src/FeastUISansProviders.tsx b/ui/src/FeastUISansProviders.tsx index 26c62442914..0ab9d3479eb 100644 --- a/ui/src/FeastUISansProviders.tsx +++ b/ui/src/FeastUISansProviders.tsx @@ -13,6 +13,7 @@ import DatasourceIndex from "./pages/data-sources/Index"; import DatasetIndex from "./pages/saved-data-sets/Index"; import EntityIndex from "./pages/entities/Index"; import EntityInstance from "./pages/entities/EntityInstance"; +import FeatureListPage from "./pages/features/FeatureListPage"; import FeatureInstance from "./pages/features/FeatureInstance"; import FeatureServiceIndex from "./pages/feature-services/Index"; import FeatureViewIndex from "./pages/feature-views/Index"; @@ -88,6 +89,7 @@ const FeastUISansProviders = ({ path="data-source/:dataSourceName/*" element={} /> + } /> } diff --git a/ui/src/pages/Sidebar.tsx b/ui/src/pages/Sidebar.tsx index e01d17144a6..ec114ec7e36 100644 --- a/ui/src/pages/Sidebar.tsx +++ b/ui/src/pages/Sidebar.tsx @@ -11,6 +11,7 @@ import { EntityIcon } from "../graphics/EntityIcon"; import { FeatureViewIcon } from "../graphics/FeatureViewIcon"; import { FeatureServiceIcon } from "../graphics/FeatureServiceIcon"; import { DatasetIcon } from "../graphics/DatasetIcon"; +import { FeatureIcon } from "../graphics/FeatureIcon"; const SideNav = () => { const registryUrl = useContext(RegistryPathContext); @@ -41,6 +42,12 @@ const SideNav = () => { : "" }`; + const featureListLabel = `Features ${ + isSuccess && data?.allFeatures && data?.allFeatures.length > 0 + ? `(${data?.allFeatures.length})` + : "" + }`; + const featureServicesLabel = `Feature Services ${ isSuccess && data?.objects.featureServices ? `(${data?.objects.featureServices?.length})` @@ -77,6 +84,13 @@ const SideNav = () => { renderItem: (props) => , isSelected: useMatchSubpath(`${baseUrl}/entity`), }, + { + name: featureListLabel, + id: htmlIdGenerator("featureList")(), + icon: , + renderItem: (props) => , + isSelected: useMatchSubpath(`${baseUrl}/features`), + }, { name: featureViewsLabel, id: htmlIdGenerator("featureView")(), diff --git a/ui/src/pages/features/FeatureListPage.tsx b/ui/src/pages/features/FeatureListPage.tsx new file mode 100644 index 00000000000..6eb800c2c23 --- /dev/null +++ b/ui/src/pages/features/FeatureListPage.tsx @@ -0,0 +1,133 @@ +import React, { useState, useContext } from "react"; +import { + EuiBasicTable, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiFieldSearch, + EuiPageTemplate, + CriteriaWithPagination, + Pagination, +} from "@elastic/eui"; +import EuiCustomLink from "../../components/EuiCustomLink"; +import { useParams } from "react-router-dom"; +import useLoadRegistry from "../../queries/useLoadRegistry"; +import RegistryPathContext from "../../contexts/RegistryPathContext"; + +interface Feature { + name: string; + featureView: string; + type: string; +} + +type FeatureColumn = + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType; + +const FeatureListPage = () => { + const { projectName } = useParams(); + const registryUrl = useContext(RegistryPathContext); + const { data, isLoading, isError } = useLoadRegistry(registryUrl); + const [searchText, setSearchText] = useState(""); + + const [sortField, setSortField] = useState("name"); + const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); + + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(100); + + if (isLoading) return

Loading...

; + if (isError) return

Error loading features.

; + + const features: Feature[] = data?.allFeatures || []; + + const filteredFeatures = features.filter((feature) => + feature.name.toLowerCase().includes(searchText.toLowerCase()), + ); + + const sortedFeatures = [...filteredFeatures].sort((a, b) => { + const valueA = a[sortField].toLowerCase(); + const valueB = b[sortField].toLowerCase(); + return sortDirection === "asc" + ? valueA.localeCompare(valueB) + : valueB.localeCompare(valueA); + }); + + const paginatedFeatures = sortedFeatures.slice( + pageIndex * pageSize, + (pageIndex + 1) * pageSize, + ); + + const columns: FeatureColumn[] = [ + { + name: "Feature Name", + field: "name", + sortable: true, + render: (name: string, feature: Feature) => ( + + {name} + + ), + }, + { + name: "Feature View", + field: "featureView", + sortable: true, + render: (featureView: string) => ( + + {featureView} + + ), + }, + { name: "Type", field: "type", sortable: true }, + ]; + + const onTableChange = ({ page, sort }: CriteriaWithPagination) => { + if (sort) { + setSortField(sort.field as keyof Feature); + setSortDirection(sort.direction); + } + if (page) { + setPageIndex(page.index); + setPageSize(page.size); + } + }; + + const getRowProps = (feature: Feature) => ({ + "data-test-subj": `row-${feature.name}`, + }); + + const pagination: Pagination = { + pageIndex, + pageSize, + totalItemCount: sortedFeatures.length, + pageSizeOptions: [20, 50, 100], + }; + + return ( + + + + setSearchText(e.target.value)} + fullWidth + /> + + + + ); +}; + +export default FeatureListPage; diff --git a/ui/src/queries/useLoadRegistry.ts b/ui/src/queries/useLoadRegistry.ts index 381976b6a12..4ec5af1fd96 100644 --- a/ui/src/queries/useLoadRegistry.ts +++ b/ui/src/queries/useLoadRegistry.ts @@ -14,6 +14,13 @@ interface FeatureStoreAllData { mergedFVMap: Record; mergedFVList: genericFVType[]; indirectRelationships: EntityRelation[]; + allFeatures: Feature[]; +} + +interface Feature { + name: string; + featureView: string; + type: string; } const useLoadRegistry = (url: string) => { @@ -51,6 +58,18 @@ const useLoadRegistry = (url: string) => { // relationships, // indirectRelationships, // }); + const allFeatures: Feature[] = + objects.featureViews?.flatMap( + (fv) => + fv?.spec?.features?.map((feature) => ({ + name: feature.name ?? "Unknown", + featureView: fv?.spec?.name || "Unknown FeatureView", + type: + feature.valueType != null + ? feast.types.ValueType.Enum[feature.valueType] + : "Unknown Type", + })) || [], + ) || []; return { project: objects.projects[0].spec?.name!, @@ -59,6 +78,7 @@ const useLoadRegistry = (url: string) => { mergedFVList, relationships, indirectRelationships, + allFeatures, }; }); },