From e4f0424995a1bb97139bd63e1f6417908f0dfee8 Mon Sep 17 00:00:00 2001
From: snowrugar-beep
Date: Sat, 30 May 2026 19:57:53 +0000
Subject: [PATCH] feat: implement CSV export for asset list
Closes #781
---
frontend/app/(dashboard)/assets/page.tsx | 12 ++--
.../features/assets/ExportAssetsButton.tsx | 65 +++++++++++++++++++
frontend/package-lock.json | 2 +-
frontend/package.json | 2 +-
4 files changed, 75 insertions(+), 6 deletions(-)
create mode 100644 frontend/opsce/features/assets/ExportAssetsButton.tsx
diff --git a/frontend/app/(dashboard)/assets/page.tsx b/frontend/app/(dashboard)/assets/page.tsx
index 3209bdda..9f396496 100644
--- a/frontend/app/(dashboard)/assets/page.tsx
+++ b/frontend/app/(dashboard)/assets/page.tsx
@@ -9,6 +9,7 @@ import { ConditionBadge } from "@/components/assets/condition-badge";
import { CreateAssetModal } from "@/components/assets/create-asset-modal";
import { useAssets } from "@/lib/query/hooks/useAssets";
import { AssetStatus, AssetCondition } from "@/lib/query/types/asset";
+import { ExportAssetsButton } from "@/opsce/features/assets/ExportAssetsButton";
const STATUS_OPTIONS = ["All", ...Object.values(AssetStatus)];
@@ -42,10 +43,13 @@ export default function AssetsPage() {
: "No assets yet"}
-
+
+
+
+
{/* Filters */}
diff --git a/frontend/opsce/features/assets/ExportAssetsButton.tsx b/frontend/opsce/features/assets/ExportAssetsButton.tsx
new file mode 100644
index 00000000..6ab49de6
--- /dev/null
+++ b/frontend/opsce/features/assets/ExportAssetsButton.tsx
@@ -0,0 +1,65 @@
+'use client';
+
+import { useState, useCallback } from 'react';
+import { Download } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { toast } from '@/components/ui/toast';
+import { api } from '@/lib/api';
+
+interface ExportAssetsButtonProps {
+ filters?: Record;
+}
+
+export function ExportAssetsButton({ filters = {} }: ExportAssetsButtonProps) {
+ const [loading, setLoading] = useState(false);
+
+ const handleExport = useCallback(async () => {
+ setLoading(true);
+ try {
+ // Build query params from active filters
+ const params = new URLSearchParams();
+ params.set('format', 'csv');
+ Object.entries(filters).forEach(([key, value]) => {
+ if (value) params.set(key, value);
+ });
+
+ const response = await api.get(`/assets/export?${params.toString()}`, {
+ responseType: 'blob',
+ timeout: 30000, // 30 second timeout for large exports
+ });
+
+ const blob = new Blob([response.data], { type: 'text/csv' });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `assets_export_${new Date().toISOString().split('T')[0]}.csv`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ toast.success('Assets exported as CSV successfully');
+ } catch (err: unknown) {
+ console.error('Export failed:', err);
+ const errorMessage =
+ (err as { response?: { status?: number } })?.response?.status === 403
+ ? 'You do not have permission to export assets.'
+ : 'Failed to export assets. Please try again.';
+ toast.error(errorMessage);
+ } finally {
+ setLoading(false);
+ }
+ }, [filters]);
+
+ return (
+
+ );
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 2a376a59..b54b81d7 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -51,7 +51,7 @@
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"ts-jest": "^29.4.4",
- "typescript": "^5"
+ "typescript": "^5.9.3"
}
},
"node_modules/@alloc/quick-lru": {
diff --git a/frontend/package.json b/frontend/package.json
index 71a9245b..22427d1a 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -53,6 +53,6 @@
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"ts-jest": "^29.4.4",
- "typescript": "^5"
+ "typescript": "^5.9.3"
}
}