Skip to content

Commit cc7fd47

Browse files
authored
feat: Added UI for Features list (#5192)
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent bd3448b commit cc7fd47

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

ui/src/FeastUISansProviders.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import DatasourceIndex from "./pages/data-sources/Index";
1313
import DatasetIndex from "./pages/saved-data-sets/Index";
1414
import EntityIndex from "./pages/entities/Index";
1515
import EntityInstance from "./pages/entities/EntityInstance";
16+
import FeatureListPage from "./pages/features/FeatureListPage";
1617
import FeatureInstance from "./pages/features/FeatureInstance";
1718
import FeatureServiceIndex from "./pages/feature-services/Index";
1819
import FeatureViewIndex from "./pages/feature-views/Index";
@@ -88,6 +89,7 @@ const FeastUISansProviders = ({
8889
path="data-source/:dataSourceName/*"
8990
element={<DataSourceInstance />}
9091
/>
92+
<Route path="features/" element={<FeatureListPage />} />
9193
<Route
9294
path="feature-view/"
9395
element={<FeatureViewIndex />}

ui/src/pages/Sidebar.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { EntityIcon } from "../graphics/EntityIcon";
1111
import { FeatureViewIcon } from "../graphics/FeatureViewIcon";
1212
import { FeatureServiceIcon } from "../graphics/FeatureServiceIcon";
1313
import { DatasetIcon } from "../graphics/DatasetIcon";
14+
import { FeatureIcon } from "../graphics/FeatureIcon";
1415

1516
const SideNav = () => {
1617
const registryUrl = useContext(RegistryPathContext);
@@ -41,6 +42,12 @@ const SideNav = () => {
4142
: ""
4243
}`;
4344

45+
const featureListLabel = `Features ${
46+
isSuccess && data?.allFeatures && data?.allFeatures.length > 0
47+
? `(${data?.allFeatures.length})`
48+
: ""
49+
}`;
50+
4451
const featureServicesLabel = `Feature Services ${
4552
isSuccess && data?.objects.featureServices
4653
? `(${data?.objects.featureServices?.length})`
@@ -77,6 +84,13 @@ const SideNav = () => {
7784
renderItem: (props) => <Link {...props} to={`${baseUrl}/entity`} />,
7885
isSelected: useMatchSubpath(`${baseUrl}/entity`),
7986
},
87+
{
88+
name: featureListLabel,
89+
id: htmlIdGenerator("featureList")(),
90+
icon: <EuiIcon type={FeatureIcon} />,
91+
renderItem: (props) => <Link {...props} to={`${baseUrl}/features`} />,
92+
isSelected: useMatchSubpath(`${baseUrl}/features`),
93+
},
8094
{
8195
name: featureViewsLabel,
8296
id: htmlIdGenerator("featureView")(),
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React, { useState, useContext } from "react";
2+
import {
3+
EuiBasicTable,
4+
EuiTableFieldDataColumnType,
5+
EuiTableComputedColumnType,
6+
EuiFieldSearch,
7+
EuiPageTemplate,
8+
CriteriaWithPagination,
9+
Pagination,
10+
} from "@elastic/eui";
11+
import EuiCustomLink from "../../components/EuiCustomLink";
12+
import { useParams } from "react-router-dom";
13+
import useLoadRegistry from "../../queries/useLoadRegistry";
14+
import RegistryPathContext from "../../contexts/RegistryPathContext";
15+
16+
interface Feature {
17+
name: string;
18+
featureView: string;
19+
type: string;
20+
}
21+
22+
type FeatureColumn =
23+
| EuiTableFieldDataColumnType<Feature>
24+
| EuiTableComputedColumnType<Feature>;
25+
26+
const FeatureListPage = () => {
27+
const { projectName } = useParams();
28+
const registryUrl = useContext(RegistryPathContext);
29+
const { data, isLoading, isError } = useLoadRegistry(registryUrl);
30+
const [searchText, setSearchText] = useState("");
31+
32+
const [sortField, setSortField] = useState<keyof Feature>("name");
33+
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
34+
35+
const [pageIndex, setPageIndex] = useState(0);
36+
const [pageSize, setPageSize] = useState(100);
37+
38+
if (isLoading) return <p>Loading...</p>;
39+
if (isError) return <p>Error loading features.</p>;
40+
41+
const features: Feature[] = data?.allFeatures || [];
42+
43+
const filteredFeatures = features.filter((feature) =>
44+
feature.name.toLowerCase().includes(searchText.toLowerCase()),
45+
);
46+
47+
const sortedFeatures = [...filteredFeatures].sort((a, b) => {
48+
const valueA = a[sortField].toLowerCase();
49+
const valueB = b[sortField].toLowerCase();
50+
return sortDirection === "asc"
51+
? valueA.localeCompare(valueB)
52+
: valueB.localeCompare(valueA);
53+
});
54+
55+
const paginatedFeatures = sortedFeatures.slice(
56+
pageIndex * pageSize,
57+
(pageIndex + 1) * pageSize,
58+
);
59+
60+
const columns: FeatureColumn[] = [
61+
{
62+
name: "Feature Name",
63+
field: "name",
64+
sortable: true,
65+
render: (name: string, feature: Feature) => (
66+
<EuiCustomLink
67+
to={`/p/${projectName}/feature-view/${feature.featureView}/feature/${name}`}
68+
>
69+
{name}
70+
</EuiCustomLink>
71+
),
72+
},
73+
{
74+
name: "Feature View",
75+
field: "featureView",
76+
sortable: true,
77+
render: (featureView: string) => (
78+
<EuiCustomLink to={`/p/${projectName}/feature-view/${featureView}`}>
79+
{featureView}
80+
</EuiCustomLink>
81+
),
82+
},
83+
{ name: "Type", field: "type", sortable: true },
84+
];
85+
86+
const onTableChange = ({ page, sort }: CriteriaWithPagination<Feature>) => {
87+
if (sort) {
88+
setSortField(sort.field as keyof Feature);
89+
setSortDirection(sort.direction);
90+
}
91+
if (page) {
92+
setPageIndex(page.index);
93+
setPageSize(page.size);
94+
}
95+
};
96+
97+
const getRowProps = (feature: Feature) => ({
98+
"data-test-subj": `row-${feature.name}`,
99+
});
100+
101+
const pagination: Pagination = {
102+
pageIndex,
103+
pageSize,
104+
totalItemCount: sortedFeatures.length,
105+
pageSizeOptions: [20, 50, 100],
106+
};
107+
108+
return (
109+
<EuiPageTemplate panelled>
110+
<EuiPageTemplate.Header pageTitle="Feature List" />
111+
<EuiPageTemplate.Section>
112+
<EuiFieldSearch
113+
placeholder="Search features"
114+
value={searchText}
115+
onChange={(e) => setSearchText(e.target.value)}
116+
fullWidth
117+
/>
118+
<EuiBasicTable
119+
columns={columns}
120+
items={paginatedFeatures}
121+
rowProps={getRowProps}
122+
sorting={{
123+
sort: { field: sortField, direction: sortDirection },
124+
}}
125+
onChange={onTableChange}
126+
pagination={pagination}
127+
/>
128+
</EuiPageTemplate.Section>
129+
</EuiPageTemplate>
130+
);
131+
};
132+
133+
export default FeatureListPage;

ui/src/queries/useLoadRegistry.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ interface FeatureStoreAllData {
1414
mergedFVMap: Record<string, genericFVType>;
1515
mergedFVList: genericFVType[];
1616
indirectRelationships: EntityRelation[];
17+
allFeatures: Feature[];
18+
}
19+
20+
interface Feature {
21+
name: string;
22+
featureView: string;
23+
type: string;
1724
}
1825

1926
const useLoadRegistry = (url: string) => {
@@ -51,6 +58,18 @@ const useLoadRegistry = (url: string) => {
5158
// relationships,
5259
// indirectRelationships,
5360
// });
61+
const allFeatures: Feature[] =
62+
objects.featureViews?.flatMap(
63+
(fv) =>
64+
fv?.spec?.features?.map((feature) => ({
65+
name: feature.name ?? "Unknown",
66+
featureView: fv?.spec?.name || "Unknown FeatureView",
67+
type:
68+
feature.valueType != null
69+
? feast.types.ValueType.Enum[feature.valueType]
70+
: "Unknown Type",
71+
})) || [],
72+
) || [];
5473

5574
return {
5675
project: objects.projects[0].spec?.name!,
@@ -59,6 +78,7 @@ const useLoadRegistry = (url: string) => {
5978
mergedFVList,
6079
relationships,
6180
indirectRelationships,
81+
allFeatures,
6282
};
6383
});
6484
},

0 commit comments

Comments
 (0)