Skip to content

Commit a65f889

Browse files
committed
feat: Added UI for Features list
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent bd3448b commit a65f889

File tree

4 files changed

+167
-0
lines changed

4 files changed

+167
-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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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" ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
51+
});
52+
53+
const paginatedFeatures = sortedFeatures.slice(
54+
pageIndex * pageSize,
55+
(pageIndex + 1) * pageSize
56+
);
57+
58+
const columns: FeatureColumn[] = [
59+
{
60+
name: "Feature Name",
61+
field: "name",
62+
sortable: true,
63+
render: (name: string, feature: Feature) => (
64+
<EuiCustomLink
65+
to={`/p/${projectName}/feature-view/${feature.featureView}/feature/${name}`}
66+
>
67+
{name}
68+
</EuiCustomLink>
69+
),
70+
},
71+
{
72+
name: "Feature View",
73+
field: "featureView",
74+
sortable: true,
75+
render: (featureView: string) => (
76+
<EuiCustomLink to={`/p/${projectName}/feature-view/${featureView}`}>
77+
{featureView}
78+
</EuiCustomLink>
79+
),
80+
},
81+
{ name: "Type", field: "type", sortable: true },
82+
];
83+
84+
const onTableChange = ({ page, sort }: CriteriaWithPagination<Feature>) => {
85+
if (sort) {
86+
setSortField(sort.field as keyof Feature);
87+
setSortDirection(sort.direction);
88+
}
89+
if (page) {
90+
setPageIndex(page.index);
91+
setPageSize(page.size);
92+
}
93+
};
94+
95+
const getRowProps = (feature: Feature) => ({
96+
"data-test-subj": `row-${feature.name}`,
97+
});
98+
99+
const pagination: Pagination = {
100+
pageIndex,
101+
pageSize,
102+
totalItemCount: sortedFeatures.length,
103+
pageSizeOptions: [20, 50, 100],
104+
};
105+
106+
return (
107+
<EuiPageTemplate panelled>
108+
<EuiPageTemplate.Header pageTitle="Feature List" />
109+
<EuiPageTemplate.Section>
110+
<EuiFieldSearch
111+
placeholder="Search features"
112+
value={searchText}
113+
onChange={(e) => setSearchText(e.target.value)}
114+
fullWidth
115+
/>
116+
<EuiBasicTable
117+
columns={columns}
118+
items={paginatedFeatures}
119+
rowProps={getRowProps}
120+
sorting={{
121+
sort: { field: sortField, direction: sortDirection },
122+
}}
123+
onChange={onTableChange}
124+
pagination={pagination}
125+
/>
126+
</EuiPageTemplate.Section>
127+
</EuiPageTemplate>
128+
);
129+
};
130+
131+
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)