Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions frontend/app/(dashboard)/assets/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)];

Expand Down Expand Up @@ -42,10 +43,13 @@ export default function AssetsPage() {
: "No assets yet"}
</p>
</div>
<Button onClick={() => setShowModal(true)}>
<Plus size={16} className="mr-1.5" />
Register Asset
</Button>
<div className="flex items-center gap-2">
<ExportAssetsButton />
<Button onClick={() => setShowModal(true)}>
<Plus size={16} className="mr-1.5" />
Register Asset
</Button>
</div>
</div>

{/* Filters */}
Expand Down
65 changes: 65 additions & 0 deletions frontend/opsce/features/assets/ExportAssetsButton.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string | undefined>;
}

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 (
<Button
size="sm"
variant="outline"
onClick={handleExport}
loading={loading}
>
<Download size={14} className="mr-1.5" />
Export CSV
</Button>
);
}
2 changes: 1 addition & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading