Add dashboard export functionality as PDF or image

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/7dd200c2-1934-480c-9df5-4d6187d23b0c.jpg
This commit is contained in:
alphaeusmote
2025-04-10 01:41:15 +00:00
parent 755c91f51d
commit 8349915d1e
4 changed files with 163 additions and 8 deletions
@@ -0,0 +1,150 @@
import React, { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Download, Camera, FileText, Loader2 } from 'lucide-react';
import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
interface DashboardExportProps {
dashboardRef: React.RefObject<HTMLDivElement>;
title?: string;
}
export function DashboardExport({ dashboardRef, title = 'Dashboard' }: DashboardExportProps) {
const [isExporting, setIsExporting] = useState(false);
const handleExportPDF = async () => {
if (!dashboardRef.current) return;
setIsExporting(true);
try {
const dashboard = dashboardRef.current;
// Calculate height for proper scaling
const originalWidth = dashboard.offsetWidth;
const originalHeight = dashboard.offsetHeight;
// A4 dimensions (in pixels at 96 DPI)
const pdfWidth = 595;
const pdfHeight = 842;
// Calculate scale to fit width
const scale = pdfWidth / originalWidth;
const scaledHeight = originalHeight * scale;
// Create canvas from DOM node
const canvas = await html2canvas(dashboard, {
scale: 2, // Higher quality
useCORS: true,
logging: false,
allowTaint: true,
backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff"
});
// Create PDF
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({
orientation: scaledHeight > pdfHeight ? 'landscape' : 'portrait',
unit: 'px',
format: 'a4'
});
// Add title
pdf.setFontSize(16);
pdf.text(title, 20, 20);
// Calculate position to center image
const imgWidth = pdf.internal.pageSize.getWidth() - 40; // margins
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// Add image
pdf.addImage(imgData, 'PNG', 20, 30, imgWidth, imgHeight);
// Save PDF
pdf.save(`${title.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().split('T')[0]}.pdf`);
} catch (error) {
console.error('Error exporting PDF:', error);
} finally {
setIsExporting(false);
}
};
const handleExportImage = async () => {
if (!dashboardRef.current) return;
setIsExporting(true);
try {
const dashboard = dashboardRef.current;
// Create canvas from DOM node
const canvas = await html2canvas(dashboard, {
scale: 2, // Higher quality
useCORS: true,
logging: false,
allowTaint: true,
backgroundColor: getComputedStyle(document.body).backgroundColor || "#ffffff"
});
// Convert to PNG
const imgData = canvas.toDataURL('image/png');
// Create a download link
const link = document.createElement('a');
link.href = imgData;
link.download = `${title.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().split('T')[0]}.png`;
link.click();
} catch (error) {
console.error('Error exporting image:', error);
} finally {
setIsExporting(false);
}
};
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-1"
disabled={isExporting}
>
{isExporting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
Export
</Button>
</PopoverTrigger>
<PopoverContent className="w-56" align="end">
<div className="grid gap-2">
<h4 className="font-medium">Export Dashboard</h4>
<p className="text-sm text-muted-foreground">Choose export format</p>
<div className="grid grid-cols-2 gap-2 pt-2">
<Button
variant="outline"
size="sm"
className="justify-start"
onClick={handleExportPDF}
disabled={isExporting}
>
<FileText className="h-4 w-4 mr-2" />
PDF
</Button>
<Button
variant="outline"
size="sm"
className="justify-start"
onClick={handleExportImage}
disabled={isExporting}
>
<Camera className="h-4 w-4 mr-2" />
Image
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
}
+11 -3
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -29,6 +29,7 @@ import {
Loader2
} from "lucide-react";
import { DashboardLayout } from "@/layouts/dashboard-layout";
import { DashboardExport } from "@/components/dashboard/dashboard-export";
interface DashboardSummary {
userCount: number;
@@ -41,6 +42,9 @@ interface DashboardSummary {
}
export default function MainDashboardPage() {
// Create a ref for the dashboard content
const dashboardRef = useRef<HTMLDivElement>(null);
// Fetch summary data
const { data: summary, isLoading: summaryLoading } = useQuery<DashboardSummary>({
queryKey: ['/api/dashboard-summary'],
@@ -101,13 +105,17 @@ export default function MainDashboardPage() {
return (
<DashboardLayout title="Active Directory Dashboard" description="Overview of your Active Directory environment">
<div className="flex justify-end mb-4">
<DashboardExport dashboardRef={dashboardRef} title="Active Directory Dashboard" />
</div>
{isLoading ? (
<div className="flex justify-center items-center min-h-[60vh]">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="ml-2">Loading dashboard data...</span>
</div>
) : (
<>
<div ref={dashboardRef}>
{/* Summary Cards */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 mb-6">
<Card>
@@ -270,7 +278,7 @@ export default function MainDashboardPage() {
</div>
</CardContent>
</Card>
</>
</div>
)}
</DashboardLayout>
);
+1 -5
View File
@@ -67,6 +67,7 @@
"express-session": "^1.18.1",
"file-saver": "^2.0.5",
"framer-motion": "^11.13.1",
"html2canvas": "^1.4.1",
"input-otp": "^1.2.4",
"ioredis": "^5.6.0",
"jsonwebtoken": "^9.0.2",
@@ -4093,7 +4094,6 @@
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
@@ -4661,7 +4661,6 @@
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -6228,7 +6227,6 @@
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
@@ -8795,7 +8793,6 @@
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
@@ -9485,7 +9482,6 @@
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
+1
View File
@@ -69,6 +69,7 @@
"express-session": "^1.18.1",
"file-saver": "^2.0.5",
"framer-motion": "^11.13.1",
"html2canvas": "^1.4.1",
"input-otp": "^1.2.4",
"ioredis": "^5.6.0",
"jsonwebtoken": "^9.0.2",