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,
};
});
},