Skip to content

Commit b079553

Browse files
authored
feat: Added export support in feast UI (#5198)
1 parent 4ab9f74 commit b079553

File tree

6 files changed

+110
-0
lines changed

6 files changed

+110
-0
lines changed

ui/src/components/ExportButton.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { useState } from "react";
2+
import {
3+
EuiButton,
4+
EuiPopover,
5+
EuiContextMenuPanel,
6+
EuiContextMenuItem,
7+
} from "@elastic/eui";
8+
9+
interface ExportButtonProps {
10+
data: any[];
11+
fileName: string;
12+
formats?: ("json" | "csv")[];
13+
}
14+
15+
const ExportButton: React.FC<ExportButtonProps> = ({
16+
data,
17+
fileName,
18+
formats = ["json", "csv"],
19+
}) => {
20+
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
21+
22+
const exportData = (format: "json" | "csv") => {
23+
let content = "";
24+
let mimeType = "";
25+
26+
if (format === "json") {
27+
content = JSON.stringify(data, null, 2);
28+
mimeType = "application/json";
29+
} else {
30+
const headers = Object.keys(data[0] || {}).join(",") + "\n";
31+
const rows = data.map((item) => Object.values(item).join(",")).join("\n");
32+
content = headers + rows;
33+
mimeType = "text/csv";
34+
}
35+
36+
const blob = new Blob([content], { type: mimeType });
37+
const link = document.createElement("a");
38+
link.href = URL.createObjectURL(blob);
39+
link.download = `${fileName}.${format}`;
40+
document.body.appendChild(link);
41+
link.click();
42+
document.body.removeChild(link);
43+
};
44+
45+
const exportMenu = (
46+
<EuiContextMenuPanel
47+
items={formats.map((format) => (
48+
<EuiContextMenuItem key={format} onClick={() => exportData(format)}>
49+
Export {format.toUpperCase()}
50+
</EuiContextMenuItem>
51+
))}
52+
/>
53+
);
54+
55+
return (
56+
<EuiPopover
57+
button={
58+
<EuiButton
59+
color="primary"
60+
fill
61+
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
62+
>
63+
Export
64+
</EuiButton>
65+
}
66+
isOpen={isPopoverOpen}
67+
closePopover={() => setIsPopoverOpen(false)}
68+
panelPaddingSize="s"
69+
>
70+
{exportMenu}
71+
</EuiPopover>
72+
);
73+
};
74+
export default ExportButton;

ui/src/pages/data-sources/Index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import DataSourceIndexEmptyState from "./DataSourceIndexEmptyState";
1818
import { DataSourceIcon } from "../../graphics/DataSourceIcon";
1919
import { useSearchQuery } from "../../hooks/useSearchInputWithTags";
2020
import { feast } from "../../protos";
21+
import ExportButton from "../../components/ExportButton";
2122

2223
const useLoadDatasources = () => {
2324
const registryUrl = useContext(RegistryPathContext);
@@ -65,6 +66,13 @@ const Index = () => {
6566
restrictWidth
6667
iconType={DataSourceIcon}
6768
pageTitle="Data Sources"
69+
rightSideItems={[
70+
<ExportButton
71+
data={filterResult ?? []}
72+
fileName="data_sources"
73+
formats={["json"]}
74+
/>,
75+
]}
6876
/>
6977
<EuiPageTemplate.Section>
7078
{isLoading && (

ui/src/pages/entities/Index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import EntitiesListingTable from "./EntitiesListingTable";
99
import { useDocumentTitle } from "../../hooks/useDocumentTitle";
1010
import RegistryPathContext from "../../contexts/RegistryPathContext";
1111
import EntityIndexEmptyState from "./EntityIndexEmptyState";
12+
import ExportButton from "../../components/ExportButton";
1213

1314
const useLoadEntities = () => {
1415
const registryUrl = useContext(RegistryPathContext);
@@ -36,6 +37,13 @@ const Index = () => {
3637
restrictWidth
3738
iconType={EntityIcon}
3839
pageTitle="Entities"
40+
rightSideItems={[
41+
<ExportButton
42+
data={data ?? []}
43+
fileName="entities"
44+
formats={["json"]}
45+
/>,
46+
]}
3947
/>
4048
<EuiPageTemplate.Section>
4149
{isLoading && (

ui/src/pages/feature-services/Index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { useDocumentTitle } from "../../hooks/useDocumentTitle";
2424
import RegistryPathContext from "../../contexts/RegistryPathContext";
2525
import FeatureServiceIndexEmptyState from "./FeatureServiceIndexEmptyState";
2626
import TagSearch from "../../components/TagSearch";
27+
import ExportButton from "../../components/ExportButton";
2728
import { useFeatureServiceTagsAggregation } from "../../hooks/useTagsAggregation";
2829
import { feast } from "../../protos";
2930

@@ -115,6 +116,13 @@ const Index = () => {
115116
restrictWidth
116117
iconType={FeatureServiceIcon}
117118
pageTitle="Feature Services"
119+
rightSideItems={[
120+
<ExportButton
121+
data={filterResult ?? []}
122+
fileName="feature_services"
123+
formats={["json"]}
124+
/>,
125+
]}
118126
/>
119127
<EuiPageTemplate.Section>
120128
{isLoading && (

ui/src/pages/feature-views/Index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import RegistryPathContext from "../../contexts/RegistryPathContext";
2525
import FeatureViewIndexEmptyState from "./FeatureViewIndexEmptyState";
2626
import { useFeatureViewTagsAggregation } from "../../hooks/useTagsAggregation";
2727
import TagSearch from "../../components/TagSearch";
28+
import ExportButton from "../../components/ExportButton";
2829

2930
const useLoadFeatureViews = () => {
3031
const registryUrl = useContext(RegistryPathContext);
@@ -117,6 +118,13 @@ const Index = () => {
117118
restrictWidth
118119
iconType={FeatureViewIcon}
119120
pageTitle="Feature Views"
121+
rightSideItems={[
122+
<ExportButton
123+
data={filterResult ?? []}
124+
fileName="feature_views"
125+
formats={["json"]}
126+
/>,
127+
]}
120128
/>
121129
<EuiPageTemplate.Section>
122130
{isLoading && (

ui/src/pages/features/FeatureListPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Pagination,
1010
} from "@elastic/eui";
1111
import EuiCustomLink from "../../components/EuiCustomLink";
12+
import ExportButton from "../../components/ExportButton";
1213
import { useParams } from "react-router-dom";
1314
import useLoadRegistry from "../../queries/useLoadRegistry";
1415
import RegistryPathContext from "../../contexts/RegistryPathContext";
@@ -109,6 +110,9 @@ const FeatureListPage = () => {
109110
restrictWidth
110111
iconType={FeatureIcon}
111112
pageTitle="Feature List"
113+
rightSideItems={[
114+
<ExportButton data={filteredFeatures} fileName="features" />,
115+
]}
112116
/>
113117
<EuiPageTemplate.Section>
114118
{isLoading ? (

0 commit comments

Comments
 (0)