Enhance dashboard to display data for all Active Directory objects and dynamically generate available fields.

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/3caa578d-3a2c-45ec-a6c0-75313ad18bfb.jpg
This commit is contained in:
alphaeusmote
2025-04-10 00:32:56 +00:00
parent d0748723c3
commit 9d39faf2cf
5 changed files with 874 additions and 58 deletions
@@ -0,0 +1,421 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import { ColorPicker } from "@/components/ui/color-picker";
import { CopyIcon, Share2, CheckIcon, Download } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { DashboardConfig } from "./dashboard-layout";
import { generateSnapshot } from "@/lib/snapshot-utils";
interface ShareDashboardDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
dashboard: DashboardConfig;
dataSources: Array<{
id: string;
name: string;
data: any[];
fields: Array<{ name: string; type: string }>;
}>;
}
export function ShareDashboardDialog({
open,
onOpenChange,
dashboard,
dataSources
}: ShareDashboardDialogProps) {
const { toast } = useToast();
const [shareUrl, setShareUrl] = useState("");
const [copied, setCopied] = useState(false);
const [logoUrl, setLogoUrl] = useState("");
const [companyName, setCompanyName] = useState("");
const [customTitle, setCustomTitle] = useState(dashboard.name);
const [description, setDescription] = useState("");
const [primaryColor, setPrimaryColor] = useState("#0f172a");
const [secondaryColor, setSecondaryColor] = useState("#6366f1");
const [showDate, setShowDate] = useState(true);
const [showFooter, setShowFooter] = useState(true);
const [footerText, setFooterText] = useState("Generated with Active Directory Management Platform");
const [expiryDays, setExpiryDays] = useState(7);
const [includeData, setIncludeData] = useState(true);
const [isSharing, setIsSharing] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
// When user uploads a logo
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setLogoUrl(reader.result as string);
};
reader.readAsDataURL(file);
}
};
// Generate a sharable URL with snapshot data
const handleGenerateShareLink = async () => {
setIsSharing(true);
try {
const snapshotData = generateSnapshot({
dashboard,
dataSources: includeData ? dataSources : [],
branding: {
logoUrl,
companyName,
title: customTitle,
description,
primaryColor,
secondaryColor,
showDate,
showFooter,
footerText,
expiresInDays: expiryDays
}
});
// In a real app, this would make an API call to store the snapshot
// For now, we're simulating it with localStorage
const snapshotId = `dash_${Date.now().toString(36)}`;
localStorage.setItem(`snapshot_${snapshotId}`, JSON.stringify(snapshotData));
// Generate share URL
const shareUrl = `${window.location.origin}/dashboards/shared/${snapshotId}`;
setShareUrl(shareUrl);
toast({
title: "Share link generated",
description: "The share link for this dashboard has been created.",
});
} catch (error) {
console.error("Error generating share link:", error);
toast({
title: "Error",
description: "Failed to generate share link. Please try again.",
variant: "destructive",
});
} finally {
setIsSharing(false);
}
};
// Copy the URL to clipboard
const handleCopyLink = () => {
navigator.clipboard.writeText(shareUrl);
setCopied(true);
toast({
title: "Copied!",
description: "Share link copied to clipboard.",
});
setTimeout(() => {
setCopied(false);
}, 2000);
};
// Generate a PDF or image snapshot for download
const handleDownloadSnapshot = async () => {
setIsGenerating(true);
try {
const snapshotData = generateSnapshot({
dashboard,
dataSources: includeData ? dataSources : [],
branding: {
logoUrl,
companyName,
title: customTitle,
description,
primaryColor,
secondaryColor,
showDate,
showFooter,
footerText,
expiresInDays: 0 // Download doesn't expire
}
});
// In a real implementation, we would generate a PDF or image here
// For now, we'll just download the JSON data
const dataStr = JSON.stringify(snapshotData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const a = document.createElement('a');
a.href = url;
a.download = `${dashboard.name.replace(/\s+/g, '_')}_snapshot.json`;
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
toast({
title: "Snapshot downloaded",
description: "Your dashboard snapshot has been downloaded.",
});
} catch (error) {
console.error("Error generating snapshot:", error);
toast({
title: "Error",
description: "Failed to generate snapshot. Please try again.",
variant: "destructive",
});
} finally {
setIsGenerating(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Share Dashboard: {dashboard.name}</DialogTitle>
<DialogDescription>
Create a shareable snapshot of this dashboard with custom branding.
</DialogDescription>
</DialogHeader>
<Tabs defaultValue="link" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="link">Share Link</TabsTrigger>
<TabsTrigger value="branding">Branding Options</TabsTrigger>
</TabsList>
<TabsContent value="link" className="space-y-4 mt-4">
<div className="space-y-4">
<div className="space-y-2">
<Label>Include data in snapshot</Label>
<div className="flex items-center space-x-2">
<Switch
checked={includeData}
onCheckedChange={setIncludeData}
id="include-data"
/>
<Label htmlFor="include-data">
{includeData
? "Data included (snapshot works offline)"
: "Live data only (requires access to API)"}
</Label>
</div>
</div>
<div className="space-y-2">
<Label>Snapshot expiry</Label>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<span>Expires after</span>
<span className="font-medium">
{expiryDays === 0 ? "Never expires" : `${expiryDays} days`}
</span>
</div>
<Slider
value={[expiryDays]}
min={0}
max={30}
step={1}
onValueChange={(value) => setExpiryDays(value[0])}
/>
</div>
</div>
{!shareUrl ? (
<Button
className="w-full"
onClick={handleGenerateShareLink}
disabled={isSharing}
>
{isSharing ? "Generating..." : "Generate Share Link"}
</Button>
) : (
<>
<div className="flex mt-4 space-x-2">
<Input
value={shareUrl}
readOnly
className="flex-1"
/>
<Button
variant="outline"
size="icon"
onClick={handleCopyLink}
className="flex-shrink-0"
>
{copied ? <CheckIcon className="h-4 w-4" /> : <CopyIcon className="h-4 w-4" />}
</Button>
</div>
<div className="flex flex-col sm:flex-row gap-2 mt-4">
<Button
variant="outline"
className="flex-1"
onClick={handleDownloadSnapshot}
disabled={isGenerating}
>
<Download className="h-4 w-4 mr-2" />
{isGenerating ? "Generating..." : "Download Snapshot"}
</Button>
<Button
variant="default"
className="flex-1"
onClick={handleGenerateShareLink}
disabled={isSharing}
>
<Share2 className="h-4 w-4 mr-2" />
{isSharing ? "Regenerating..." : "Regenerate Link"}
</Button>
</div>
</>
)}
</div>
</TabsContent>
<TabsContent value="branding" className="space-y-4 mt-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="company-logo">Company Logo</Label>
<Input
id="company-logo"
type="file"
accept="image/*"
onChange={handleLogoUpload}
/>
{logoUrl && (
<div className="mt-2 border rounded-md p-2 flex justify-center">
<img
src={logoUrl}
alt="Company Logo"
className="max-h-20 object-contain"
/>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="company-name">Company Name</Label>
<Input
id="company-name"
value={companyName}
onChange={(e) => setCompanyName(e.target.value)}
placeholder="Your Company Name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="custom-title">Dashboard Title</Label>
<Input
id="custom-title"
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
placeholder="Dashboard Title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Dashboard Description</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add a description for this dashboard"
rows={3}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2 mt-4">
<div className="space-y-2">
<Label>Primary Color</Label>
<div className="flex items-center space-x-2">
<div
className="w-8 h-8 rounded-md border"
style={{ backgroundColor: primaryColor }}
/>
<Input
value={primaryColor}
onChange={(e) => setPrimaryColor(e.target.value)}
placeholder="#000000"
className="flex-1"
/>
</div>
</div>
<div className="space-y-2">
<Label>Secondary Color</Label>
<div className="flex items-center space-x-2">
<div
className="w-8 h-8 rounded-md border"
style={{ backgroundColor: secondaryColor }}
/>
<Input
value={secondaryColor}
onChange={(e) => setSecondaryColor(e.target.value)}
placeholder="#000000"
className="flex-1"
/>
</div>
</div>
</div>
<div className="space-y-4 mt-4">
<div className="flex items-center space-x-2">
<Switch
checked={showDate}
onCheckedChange={setShowDate}
id="show-date"
/>
<Label htmlFor="show-date">Show date in snapshot</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={showFooter}
onCheckedChange={setShowFooter}
id="show-footer"
/>
<Label htmlFor="show-footer">Show footer in snapshot</Label>
</div>
{showFooter && (
<div className="space-y-2">
<Label htmlFor="footer-text">Footer Text</Label>
<Input
id="footer-text"
value={footerText}
onChange={(e) => setFooterText(e.target.value)}
placeholder="Footer text"
/>
</div>
)}
</div>
</TabsContent>
</Tabs>
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-between sm:space-x-2">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
>
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { useState, useEffect, useRef } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Check, Droplet } from "lucide-react";
interface ColorPickerProps {
value: string;
onChange: (value: string) => void;
className?: string;
}
const PRESET_COLORS = [
"#000000", "#ffffff", "#f44336", "#e91e63", "#9c27b0", "#673ab7",
"#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50",
"#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
"#795548", "#607d8b", "#9e9e9e", "#f5f5f5", "#1a237e", "#004d40",
];
export function ColorPicker({ value, onChange, className = "" }: ColorPickerProps) {
const [color, setColor] = useState(value || "#000000");
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
setColor(value);
}, [value]);
const handleChangeComplete = (newColor: string) => {
setColor(newColor);
onChange(newColor);
};
const isValidColor = (color: string) => {
const s = new Option().style;
s.color = color;
return s.color !== '';
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newColor = e.target.value;
setColor(newColor);
// Validate color format before updating
if (isValidColor(newColor)) {
onChange(newColor);
}
};
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={`w-10 h-10 p-0 border-2 ${className}`}
style={{ backgroundColor: color }}
aria-label="Pick a color"
>
<span className="sr-only">Pick a color</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-64">
<div className="space-y-2">
<div
className="w-full h-8 rounded cursor-pointer border"
style={{ backgroundColor: color }}
onClick={() => inputRef.current?.click()}
/>
<div className="flex gap-2">
<Input
ref={inputRef}
value={color}
onChange={handleInputChange}
className="flex-1"
/>
<Button
size="icon"
variant="outline"
onClick={() => {
if (typeof window !== 'undefined' && 'EyeDropper' in window) {
// @ts-ignore
const eyeDropper = new window.EyeDropper();
eyeDropper.open()
.then((result: { sRGBHex: string }) => {
handleChangeComplete(result.sRGBHex);
})
.catch(() => {
// User canceled the eyedropper
});
}
}}
>
<Droplet className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-8 gap-1">
{PRESET_COLORS.map((presetColor) => (
<Button
key={presetColor}
style={{ backgroundColor: presetColor }}
className="w-6 h-6 p-0 rounded-sm"
onClick={() => {
handleChangeComplete(presetColor);
setIsOpen(false);
}}
>
{color.toLowerCase() === presetColor.toLowerCase() && (
<Check className="h-3 w-3 text-white" />
)}
<span className="sr-only">Select color {presetColor}</span>
</Button>
))}
</div>
</div>
</PopoverContent>
</Popover>
);
}
+95
View File
@@ -0,0 +1,95 @@
import { DashboardConfig } from "@/components/dashboard/dashboard-layout";
// Interface for snapshot branding options
export interface SnapshotBranding {
logoUrl: string;
companyName: string;
title: string;
description: string;
primaryColor: string;
secondaryColor: string;
showDate: boolean;
showFooter: boolean;
footerText: string;
expiresInDays: number;
}
// Interface for snapshot data
export interface SnapshotData {
dashboard: DashboardConfig;
dataSources: Array<{
id: string;
name: string;
data: any[];
fields: Array<{ name: string; type: string }>;
}>;
branding: SnapshotBranding;
createdAt: string;
expiresAt: string | null;
}
/**
* Generate a snapshot of the dashboard data for sharing
*/
export function generateSnapshot({
dashboard,
dataSources,
branding,
}: {
dashboard: DashboardConfig;
dataSources: Array<{
id: string;
name: string;
data: any[];
fields: Array<{ name: string; type: string }>;
}>;
branding: SnapshotBranding;
}): SnapshotData {
const createdAt = new Date().toISOString();
// Calculate expiry date if applicable
let expiresAt: string | null = null;
if (branding.expiresInDays > 0) {
const expiry = new Date();
expiry.setDate(expiry.getDate() + branding.expiresInDays);
expiresAt = expiry.toISOString();
}
const snapshotData: SnapshotData = {
dashboard,
dataSources,
branding,
createdAt,
expiresAt,
};
return snapshotData;
}
/**
* Checks if a snapshot has expired
*/
export function isSnapshotExpired(snapshot: SnapshotData): boolean {
if (!snapshot.expiresAt) {
return false; // No expiry date means it never expires
}
const expiryDate = new Date(snapshot.expiresAt);
const now = new Date();
return now > expiryDate;
}
/**
* Format a date for display in the snapshot
*/
export function formatSnapshotDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
+82 -58
View File
@@ -2,9 +2,10 @@ import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { DashboardLayout, DashboardConfig } from "@/components/dashboard/dashboard-layout";
import { ShareDashboardDialog } from "@/components/dashboard/share-dashboard-dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Plus, RefreshCw } from "lucide-react";
import { Plus, RefreshCw, Share2 } from "lucide-react";
import {
Dialog,
DialogContent,
@@ -60,75 +61,72 @@ export default function DashboardPage() {
queryKey: ['/api/ldap-connections'],
});
// Generic function to fetch and prepare dashboard data
const fetchDashboardData = async (endpoint: string, id: string, displayName: string) => {
const response = await fetch(endpoint);
if (!response.ok) throw new Error(`Failed to fetch ${displayName}`);
const responseData = await response.json();
// Get the structure of the data to determine field types
const data = responseData.data || [];
const fields = getFieldTypes(data);
console.log(`${displayName} data fields:`, fields);
return {
id,
name: displayName,
data,
fields,
};
};
// Get users as a data source
const { data: usersData, isLoading: isLoadingUsers } = useQuery<any>({
queryKey: ['/api/user-dashboard-data'],
queryFn: async () => {
const response = await fetch('/api/user-dashboard-data');
if (!response.ok) throw new Error('Failed to fetch users');
const users = await response.json();
// Get the structure of the data to determine field types
const userData = users.data || [];
const fields = getFieldTypes(userData);
console.log('User data fields:', fields);
return {
id: 'users',
name: 'Active Directory Users',
data: userData,
fields,
};
},
queryFn: () => fetchDashboardData('/api/user-dashboard-data', 'users', 'Active Directory Users'),
enabled: true,
});
// Get computers as a data source
const { data: computersData, isLoading: isLoadingComputers } = useQuery<any>({
queryKey: ['/api/computer-dashboard-data'],
queryFn: async () => {
const response = await fetch('/api/computer-dashboard-data');
if (!response.ok) throw new Error('Failed to fetch computers');
const computers = await response.json();
// Get the structure of the data to determine field types
const computerData = computers.data || [];
const fields = getFieldTypes(computerData);
console.log('Computer data fields:', fields);
return {
id: 'computers',
name: 'Active Directory Computers',
data: computerData,
fields,
};
},
queryFn: () => fetchDashboardData('/api/computer-dashboard-data', 'computers', 'Active Directory Computers'),
enabled: true,
});
// Get groups as a data source
const { data: groupsData, isLoading: isLoadingGroups } = useQuery<any>({
queryKey: ['/api/group-dashboard-data'],
queryFn: async () => {
const response = await fetch('/api/group-dashboard-data');
if (!response.ok) throw new Error('Failed to fetch groups');
const groups = await response.json();
// Get the structure of the data to determine field types
const groupData = groups.data || [];
const fields = getFieldTypes(groupData);
console.log('Group data fields:', fields);
return {
id: 'groups',
name: 'Active Directory Groups',
data: groupData,
fields,
};
},
queryFn: () => fetchDashboardData('/api/group-dashboard-data', 'groups', 'Active Directory Groups'),
enabled: true,
});
// Get OUs as a data source
const { data: ousData, isLoading: isLoadingOUs } = useQuery<any>({
queryKey: ['/api/ou-dashboard-data'],
queryFn: () => fetchDashboardData('/api/ou-dashboard-data', 'ous', 'Organizational Units'),
enabled: true,
});
// Get domains as a data source
const { data: domainsData, isLoading: isLoadingDomains } = useQuery<any>({
queryKey: ['/api/domain-dashboard-data'],
queryFn: () => fetchDashboardData('/api/domain-dashboard-data', 'domains', 'Active Directory Domains'),
enabled: true,
});
// Get sites as a data source
const { data: sitesData, isLoading: isLoadingSites } = useQuery<any>({
queryKey: ['/api/site-dashboard-data'],
queryFn: () => fetchDashboardData('/api/site-dashboard-data', 'sites', 'Active Directory Sites'),
enabled: true,
});
// Get subnets as a data source
const { data: subnetsData, isLoading: isLoadingSubnets } = useQuery<any>({
queryKey: ['/api/subnet-dashboard-data'],
queryFn: () => fetchDashboardData('/api/subnet-dashboard-data', 'subnets', 'Active Directory Subnets'),
enabled: true,
});
@@ -152,14 +150,25 @@ export default function DashboardPage() {
// Update data sources when API data is loaded
useEffect(() => {
if (!isLoadingUsers && !isLoadingComputers && !isLoadingGroups) {
if (!isLoadingUsers &&
!isLoadingComputers &&
!isLoadingGroups &&
!isLoadingOUs &&
!isLoadingDomains &&
!isLoadingSites &&
!isLoadingSubnets) {
const sources = [];
// Add all AD object types as data sources
if (usersData) sources.push(usersData);
if (computersData) sources.push(computersData);
if (groupsData) sources.push(groupsData);
if (ousData) sources.push(ousData);
if (domainsData) sources.push(domainsData);
if (sitesData) sources.push(sitesData);
if (subnetsData) sources.push(subnetsData);
// Add a mock audit logs data source
// Add audit logs data source
sources.push({
id: 'audit-logs',
name: 'Audit Logs',
@@ -178,7 +187,22 @@ export default function DashboardPage() {
setDataSources(sources);
setDataSourcesLoading(false);
}
}, [isLoadingUsers, isLoadingComputers, isLoadingGroups, usersData, computersData, groupsData]);
}, [
isLoadingUsers,
isLoadingComputers,
isLoadingGroups,
isLoadingOUs,
isLoadingDomains,
isLoadingSites,
isLoadingSubnets,
usersData,
computersData,
groupsData,
ousData,
domainsData,
sitesData,
subnetsData
]);
// Helper function to determine field types from data
function getFieldTypes(data: any[]): Array<{ name: string; type: string }> {
+156
View File
@@ -5056,6 +5056,162 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
/**
* @swagger
* /api/ou-dashboard-data:
* get:
* summary: Get Organizational Unit data for dashboard visualizations
* tags: [Dashboard]
* security:
* - cookieAuth: []
* - bearerAuth: []
* responses:
* 200:
* description: Organizational Unit data for dashboard
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
app.get("/api/ou-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => {
try {
// Get data from the first available LDAP connection
const connections = await storage.listLdapConnections();
if (connections.length === 0) {
return res.status(200).json({ data: [], message: "No LDAP connections available" });
}
const connectionId = connections[0].id;
const ous = await storage.listAdOrgUnits(connectionId);
// Add objectType to make filtering easier in the dashboard
const result = ous.map(ou => ({
...ou,
objectType: 'organizationalUnit'
}));
res.json({ data: result });
} catch (error) {
next(error);
}
});
/**
* @swagger
* /api/domain-dashboard-data:
* get:
* summary: Get domain data for dashboard visualizations
* tags: [Dashboard]
* security:
* - cookieAuth: []
* - bearerAuth: []
* responses:
* 200:
* description: Domain data for dashboard
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
app.get("/api/domain-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => {
try {
// Get data from the first available LDAP connection
const connections = await storage.listLdapConnections();
if (connections.length === 0) {
return res.status(200).json({ data: [], message: "No LDAP connections available" });
}
const connectionId = connections[0].id;
const domains = await storage.listAdDomains(connectionId);
// Add objectType to make filtering easier in the dashboard
const result = domains.map(domain => ({
...domain,
objectType: 'domain'
}));
res.json({ data: result });
} catch (error) {
next(error);
}
});
/**
* @swagger
* /api/site-dashboard-data:
* get:
* summary: Get site data for dashboard visualizations
* tags: [Dashboard]
* security:
* - cookieAuth: []
* - bearerAuth: []
* responses:
* 200:
* description: Site data for dashboard
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
app.get("/api/site-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => {
try {
// Get data from the first available LDAP connection
const connections = await storage.listLdapConnections();
if (connections.length === 0) {
return res.status(200).json({ data: [], message: "No LDAP connections available" });
}
const connectionId = connections[0].id;
const sites = await storage.listAdSites(connectionId);
// Add objectType to make filtering easier in the dashboard
const result = sites.map(site => ({
...site,
objectType: 'site'
}));
res.json({ data: result });
} catch (error) {
next(error);
}
});
/**
* @swagger
* /api/subnet-dashboard-data:
* get:
* summary: Get subnet data for dashboard visualizations
* tags: [Dashboard]
* security:
* - cookieAuth: []
* - bearerAuth: []
* responses:
* 200:
* description: Subnet data for dashboard
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
app.get("/api/subnet-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => {
try {
// Get data from the first available LDAP connection
const connections = await storage.listLdapConnections();
if (connections.length === 0) {
return res.status(200).json({ data: [], message: "No LDAP connections available" });
}
const connectionId = connections[0].id;
const subnets = await storage.listAdSubnets(connectionId);
// Add objectType to make filtering easier in the dashboard
const result = subnets.map(subnet => ({
...subnet,
objectType: 'subnet'
}));
res.json({ data: result });
} catch (error) {
next(error);
}
});
/**
* @swagger
* /api/dashboard-summary: