mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-24 08:56:26 +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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
Loader2
|
Loader2
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { DashboardLayout } from "@/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/layouts/dashboard-layout";
|
||||||
|
import { DashboardExport } from "@/components/dashboard/dashboard-export";
|
||||||
|
|
||||||
interface DashboardSummary {
|
interface DashboardSummary {
|
||||||
userCount: number;
|
userCount: number;
|
||||||
@@ -41,6 +42,9 @@ interface DashboardSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MainDashboardPage() {
|
export default function MainDashboardPage() {
|
||||||
|
// Create a ref for the dashboard content
|
||||||
|
const dashboardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Fetch summary data
|
// Fetch summary data
|
||||||
const { data: summary, isLoading: summaryLoading } = useQuery<DashboardSummary>({
|
const { data: summary, isLoading: summaryLoading } = useQuery<DashboardSummary>({
|
||||||
queryKey: ['/api/dashboard-summary'],
|
queryKey: ['/api/dashboard-summary'],
|
||||||
@@ -101,13 +105,17 @@ export default function MainDashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="Active Directory Dashboard" description="Overview of your Active Directory environment">
|
<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 ? (
|
{isLoading ? (
|
||||||
<div className="flex justify-center items-center min-h-[60vh]">
|
<div className="flex justify-center items-center min-h-[60vh]">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
<span className="ml-2">Loading dashboard data...</span>
|
<span className="ml-2">Loading dashboard data...</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div ref={dashboardRef}>
|
||||||
{/* Summary Cards */}
|
{/* Summary Cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 mb-6">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 mb-6">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -270,7 +278,7 @@ export default function MainDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
Generated
+1
-5
@@ -67,6 +67,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
@@ -4093,7 +4094,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
@@ -4661,7 +4661,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
@@ -6228,7 +6227,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-line-break": "^2.1.0",
|
"css-line-break": "^2.1.0",
|
||||||
"text-segmentation": "^1.0.3"
|
"text-segmentation": "^1.0.3"
|
||||||
@@ -8795,7 +8793,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"utrie": "^1.0.2"
|
"utrie": "^1.0.2"
|
||||||
}
|
}
|
||||||
@@ -9485,7 +9482,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-arraybuffer": "^1.0.2"
|
"base64-arraybuffer": "^1.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.1",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user