From 7a31608ef23f50adc6701feaaf0eccc370c17baf Mon Sep 17 00:00:00 2001 From: ntkathole Date: Sun, 30 Mar 2025 20:51:42 +0530 Subject: [PATCH] feat: Added export support in feast UI Signed-off-by: ntkathole --- ui/src/components/ExportButton.tsx | 74 +++++++++++++++++++++++ ui/src/pages/data-sources/Index.tsx | 8 +++ ui/src/pages/entities/Index.tsx | 8 +++ ui/src/pages/feature-services/Index.tsx | 8 +++ ui/src/pages/feature-views/Index.tsx | 8 +++ ui/src/pages/features/FeatureListPage.tsx | 4 ++ 6 files changed, 110 insertions(+) create mode 100644 ui/src/components/ExportButton.tsx diff --git a/ui/src/components/ExportButton.tsx b/ui/src/components/ExportButton.tsx new file mode 100644 index 00000000000..fc5c3fa5ae1 --- /dev/null +++ b/ui/src/components/ExportButton.tsx @@ -0,0 +1,74 @@ +import React, { useState } from "react"; +import { + EuiButton, + EuiPopover, + EuiContextMenuPanel, + EuiContextMenuItem, +} from "@elastic/eui"; + +interface ExportButtonProps { + data: any[]; + fileName: string; + formats?: ("json" | "csv")[]; +} + +const ExportButton: React.FC = ({ + data, + fileName, + formats = ["json", "csv"], +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const exportData = (format: "json" | "csv") => { + let content = ""; + let mimeType = ""; + + if (format === "json") { + content = JSON.stringify(data, null, 2); + mimeType = "application/json"; + } else { + const headers = Object.keys(data[0] || {}).join(",") + "\n"; + const rows = data.map((item) => Object.values(item).join(",")).join("\n"); + content = headers + rows; + mimeType = "text/csv"; + } + + const blob = new Blob([content], { type: mimeType }); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = `${fileName}.${format}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const exportMenu = ( + ( + exportData(format)}> + Export {format.toUpperCase()} + + ))} + /> + ); + + return ( + setIsPopoverOpen(!isPopoverOpen)} + > + Export + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="s" + > + {exportMenu} + + ); +}; +export default ExportButton; diff --git a/ui/src/pages/data-sources/Index.tsx b/ui/src/pages/data-sources/Index.tsx index 09745102385..59bdcecd1df 100644 --- a/ui/src/pages/data-sources/Index.tsx +++ b/ui/src/pages/data-sources/Index.tsx @@ -18,6 +18,7 @@ import DataSourceIndexEmptyState from "./DataSourceIndexEmptyState"; import { DataSourceIcon } from "../../graphics/DataSourceIcon"; import { useSearchQuery } from "../../hooks/useSearchInputWithTags"; import { feast } from "../../protos"; +import ExportButton from "../../components/ExportButton"; const useLoadDatasources = () => { const registryUrl = useContext(RegistryPathContext); @@ -65,6 +66,13 @@ const Index = () => { restrictWidth iconType={DataSourceIcon} pageTitle="Data Sources" + rightSideItems={[ + , + ]} /> {isLoading && ( diff --git a/ui/src/pages/entities/Index.tsx b/ui/src/pages/entities/Index.tsx index 1ce80b583f4..bed1bfb762c 100644 --- a/ui/src/pages/entities/Index.tsx +++ b/ui/src/pages/entities/Index.tsx @@ -9,6 +9,7 @@ import EntitiesListingTable from "./EntitiesListingTable"; import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import EntityIndexEmptyState from "./EntityIndexEmptyState"; +import ExportButton from "../../components/ExportButton"; const useLoadEntities = () => { const registryUrl = useContext(RegistryPathContext); @@ -36,6 +37,13 @@ const Index = () => { restrictWidth iconType={EntityIcon} pageTitle="Entities" + rightSideItems={[ + , + ]} /> {isLoading && ( diff --git a/ui/src/pages/feature-services/Index.tsx b/ui/src/pages/feature-services/Index.tsx index 09b796436dd..0da8986e610 100644 --- a/ui/src/pages/feature-services/Index.tsx +++ b/ui/src/pages/feature-services/Index.tsx @@ -24,6 +24,7 @@ import { useDocumentTitle } from "../../hooks/useDocumentTitle"; import RegistryPathContext from "../../contexts/RegistryPathContext"; import FeatureServiceIndexEmptyState from "./FeatureServiceIndexEmptyState"; import TagSearch from "../../components/TagSearch"; +import ExportButton from "../../components/ExportButton"; import { useFeatureServiceTagsAggregation } from "../../hooks/useTagsAggregation"; import { feast } from "../../protos"; @@ -115,6 +116,13 @@ const Index = () => { restrictWidth iconType={FeatureServiceIcon} pageTitle="Feature Services" + rightSideItems={[ + , + ]} /> {isLoading && ( diff --git a/ui/src/pages/feature-views/Index.tsx b/ui/src/pages/feature-views/Index.tsx index 14dce55134a..57ac597168b 100644 --- a/ui/src/pages/feature-views/Index.tsx +++ b/ui/src/pages/feature-views/Index.tsx @@ -25,6 +25,7 @@ import RegistryPathContext from "../../contexts/RegistryPathContext"; import FeatureViewIndexEmptyState from "./FeatureViewIndexEmptyState"; import { useFeatureViewTagsAggregation } from "../../hooks/useTagsAggregation"; import TagSearch from "../../components/TagSearch"; +import ExportButton from "../../components/ExportButton"; const useLoadFeatureViews = () => { const registryUrl = useContext(RegistryPathContext); @@ -117,6 +118,13 @@ const Index = () => { restrictWidth iconType={FeatureViewIcon} pageTitle="Feature Views" + rightSideItems={[ + , + ]} /> {isLoading && ( diff --git a/ui/src/pages/features/FeatureListPage.tsx b/ui/src/pages/features/FeatureListPage.tsx index 572ef07f450..0fa59d528e1 100644 --- a/ui/src/pages/features/FeatureListPage.tsx +++ b/ui/src/pages/features/FeatureListPage.tsx @@ -9,6 +9,7 @@ import { Pagination, } from "@elastic/eui"; import EuiCustomLink from "../../components/EuiCustomLink"; +import ExportButton from "../../components/ExportButton"; import { useParams } from "react-router-dom"; import useLoadRegistry from "../../queries/useLoadRegistry"; import RegistryPathContext from "../../contexts/RegistryPathContext"; @@ -109,6 +110,9 @@ const FeatureListPage = () => { restrictWidth iconType={FeatureIcon} pageTitle="Feature List" + rightSideItems={[ + , + ]} /> {isLoading ? (