mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-07-26 11:59:14 +00:00
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Generated
+1
-5
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user