mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-10 10:48:21 +00:00
Enhance dashboard with customizable widgets and data visualization. Adds drag-and-drop layout, data export options, and improved data fetching.
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/8f36e120-5301-4443-89d0-23e9edfc5120.jpg
This commit is contained in:
@@ -1,100 +1,394 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DashboardLayout } from "@/layouts/dashboard-layout";
|
||||
import StatsCard from "@/components/dashboard/stats-card";
|
||||
import ApiActivityCard from "@/components/dashboard/api-activity-card";
|
||||
import ApiTokensCard from "@/components/dashboard/api-tokens-card";
|
||||
import LdapConnectionsCard from "@/components/dashboard/ldap-connections-card";
|
||||
import { ApiDocumentationCard } from "@/components/dashboard/api-documentation-card";
|
||||
import {
|
||||
Users,
|
||||
UserPlus,
|
||||
FolderClosed,
|
||||
Monitor
|
||||
} from "lucide-react";
|
||||
import { LdapConnection, ApiToken } from "@shared/schema";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { DashboardLayout, DashboardConfig } from "@/components/dashboard/dashboard-layout";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
|
||||
// Dashboard creation form schema
|
||||
const dashboardSchema = z.object({
|
||||
name: z.string().min(1, "Dashboard name is required"),
|
||||
});
|
||||
|
||||
type DashboardFormValues = z.infer<typeof dashboardSchema>;
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: connections = [] } = useQuery<LdapConnection[]>({
|
||||
queryKey: ["/api/ldap-connections"],
|
||||
});
|
||||
|
||||
const { data: tokens = [] } = useQuery<ApiToken[]>({
|
||||
queryKey: ["/api/tokens"],
|
||||
const { toast } = useToast();
|
||||
const [dashboards, setDashboards] = useState<DashboardConfig[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<string>("dashboard-overview");
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [dataSourcesLoading, setDataSourcesLoading] = useState(true);
|
||||
const [dataSources, setDataSources] = useState<Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
data: any[];
|
||||
fields: Array<{ name: string; type: string }>;
|
||||
}>>([]);
|
||||
|
||||
// Form for creating a new dashboard
|
||||
const form = useForm<DashboardFormValues>({
|
||||
resolver: zodResolver(dashboardSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Fetch LDAP connections to use as data sources
|
||||
const { data: connections = [], isLoading: isLoadingConnections } = useQuery<any[]>({
|
||||
queryKey: ['/api/ldap-connections'],
|
||||
});
|
||||
|
||||
// Get users as a data source
|
||||
const { data: usersData, isLoading: isLoadingUsers } = useQuery<any>({
|
||||
queryKey: ['/api/user-dashboard-data'],
|
||||
queryFn: async () => {
|
||||
// This would normally come from an API endpoint
|
||||
// Here we're implementing a mock data fetcher for the dashboard
|
||||
const response = await fetch('/api/ad/users?properties=name,mail,title,department,employeeId,manager,memberOf,whenCreated');
|
||||
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 fields = getFieldTypes(users.data || []);
|
||||
|
||||
return {
|
||||
id: 'users',
|
||||
name: 'Active Directory Users',
|
||||
data: users.data || [],
|
||||
fields,
|
||||
};
|
||||
},
|
||||
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/ad/computers?properties=name,operatingSystem,operatingSystemVersion,whenCreated,lastLogonTimestamp,enabled,memberOf');
|
||||
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 fields = getFieldTypes(computers.data || []);
|
||||
|
||||
return {
|
||||
id: 'computers',
|
||||
name: 'Active Directory Computers',
|
||||
data: computers.data || [],
|
||||
fields,
|
||||
};
|
||||
},
|
||||
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/ad/groups?properties=name,description,whenCreated,member,memberOf');
|
||||
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 fields = getFieldTypes(groups.data || []);
|
||||
|
||||
return {
|
||||
id: 'groups',
|
||||
name: 'Active Directory Groups',
|
||||
data: groups.data || [],
|
||||
fields,
|
||||
};
|
||||
},
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
// Load saved dashboards from localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedDashboards = localStorage.getItem('dashboards');
|
||||
if (savedDashboards) {
|
||||
setDashboards(JSON.parse(savedDashboards));
|
||||
|
||||
// Set the active tab to the first dashboard if it exists
|
||||
const parsedDashboards = JSON.parse(savedDashboards) as DashboardConfig[];
|
||||
if (parsedDashboards.length > 0) {
|
||||
setActiveTab(parsedDashboards[0].id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load dashboards from localStorage', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Update data sources when API data is loaded
|
||||
useEffect(() => {
|
||||
if (!isLoadingUsers && !isLoadingComputers && !isLoadingGroups) {
|
||||
const sources = [];
|
||||
|
||||
if (usersData) sources.push(usersData);
|
||||
if (computersData) sources.push(computersData);
|
||||
if (groupsData) sources.push(groupsData);
|
||||
|
||||
// Add a mock audit logs data source
|
||||
sources.push({
|
||||
id: 'audit-logs',
|
||||
name: 'Audit Logs',
|
||||
data: generateMockAuditLogs(),
|
||||
fields: [
|
||||
{ name: 'timestamp', type: 'date' },
|
||||
{ name: 'action', type: 'string' },
|
||||
{ name: 'user', type: 'string' },
|
||||
{ name: 'objectType', type: 'string' },
|
||||
{ name: 'objectName', type: 'string' },
|
||||
{ name: 'status', type: 'string' },
|
||||
{ name: 'details', type: 'string' },
|
||||
],
|
||||
});
|
||||
|
||||
setDataSources(sources);
|
||||
setDataSourcesLoading(false);
|
||||
}
|
||||
}, [isLoadingUsers, isLoadingComputers, isLoadingGroups, usersData, computersData, groupsData]);
|
||||
|
||||
// Helper function to determine field types from data
|
||||
function getFieldTypes(data: any[]): Array<{ name: string; type: string }> {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
const sample = data[0];
|
||||
const fields: Array<{ name: string; type: string }> = [];
|
||||
|
||||
Object.keys(sample).forEach(key => {
|
||||
let type = 'string';
|
||||
const value = sample[key];
|
||||
|
||||
if (typeof value === 'number') {
|
||||
type = Number.isInteger(value) ? 'integer' : 'float';
|
||||
} else if (typeof value === 'boolean') {
|
||||
type = 'boolean';
|
||||
} else if (value instanceof Date) {
|
||||
type = 'date';
|
||||
} else if (Array.isArray(value)) {
|
||||
type = 'array';
|
||||
} else if (value === null) {
|
||||
// Try to infer type from other records
|
||||
for (let i = 1; i < Math.min(data.length, 10); i++) {
|
||||
const otherValue = data[i][key];
|
||||
if (otherValue !== null) {
|
||||
if (typeof otherValue === 'number') {
|
||||
type = Number.isInteger(otherValue) ? 'integer' : 'float';
|
||||
} else if (typeof otherValue === 'boolean') {
|
||||
type = 'boolean';
|
||||
} else if (otherValue instanceof Date) {
|
||||
type = 'date';
|
||||
} else if (Array.isArray(otherValue)) {
|
||||
type = 'array';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fields.push({ name: key, type });
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
// Mock function to generate audit logs for the dashboard demo
|
||||
function generateMockAuditLogs() {
|
||||
const actions = ['create', 'update', 'delete', 'move', 'enable', 'disable'];
|
||||
const objectTypes = ['user', 'computer', 'group', 'organizationalUnit'];
|
||||
const statuses = ['success', 'failure'];
|
||||
const users = ['admin', 'system', 'operator1', 'operator2'];
|
||||
|
||||
const logs = [];
|
||||
const now = new Date();
|
||||
|
||||
// Generate 100 mock logs
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const action = actions[Math.floor(Math.random() * actions.length)];
|
||||
const objectType = objectTypes[Math.floor(Math.random() * objectTypes.length)];
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
const user = users[Math.floor(Math.random() * users.length)];
|
||||
|
||||
// Create a random date within the last 30 days
|
||||
const date = new Date(now);
|
||||
date.setDate(date.getDate() - Math.floor(Math.random() * 30));
|
||||
|
||||
logs.push({
|
||||
timestamp: date.toISOString(),
|
||||
action,
|
||||
user,
|
||||
objectType,
|
||||
objectName: `${objectType}-${Math.floor(Math.random() * 1000)}`,
|
||||
status,
|
||||
details: `${action} operation on ${objectType} by ${user} ${status === 'success' ? 'completed successfully' : 'failed'}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
logs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
// Save dashboard to localStorage
|
||||
const saveDashboards = (updatedDashboards: DashboardConfig[]) => {
|
||||
localStorage.setItem('dashboards', JSON.stringify(updatedDashboards));
|
||||
setDashboards(updatedDashboards);
|
||||
};
|
||||
|
||||
// Create a new dashboard
|
||||
const handleCreateDashboard = (values: DashboardFormValues) => {
|
||||
const newDashboard: DashboardConfig = {
|
||||
id: `dashboard-${Date.now().toString(36)}`,
|
||||
name: values.name,
|
||||
layouts: {
|
||||
lg: [],
|
||||
md: [],
|
||||
sm: [],
|
||||
xs: [],
|
||||
xxs: [],
|
||||
},
|
||||
widgets: [],
|
||||
};
|
||||
|
||||
const updatedDashboards = [...dashboards, newDashboard];
|
||||
saveDashboards(updatedDashboards);
|
||||
setActiveTab(newDashboard.id);
|
||||
setIsCreateDialogOpen(false);
|
||||
form.reset();
|
||||
|
||||
toast({
|
||||
title: "Dashboard Created",
|
||||
description: `A new dashboard named "${values.name}" has been created.`,
|
||||
});
|
||||
};
|
||||
|
||||
// Save dashboard changes
|
||||
const handleSaveDashboard = (updatedConfig: DashboardConfig) => {
|
||||
const updatedDashboards = dashboards.map(dash =>
|
||||
dash.id === updatedConfig.id ? updatedConfig : dash
|
||||
);
|
||||
saveDashboards(updatedDashboards);
|
||||
};
|
||||
|
||||
// Delete a dashboard
|
||||
const handleDeleteDashboard = (dashboardId: string) => {
|
||||
const updatedDashboards = dashboards.filter(dash => dash.id !== dashboardId);
|
||||
saveDashboards(updatedDashboards);
|
||||
|
||||
// Set active tab to overview or first dashboard
|
||||
if (updatedDashboards.length > 0) {
|
||||
setActiveTab(updatedDashboards[0].id);
|
||||
} else {
|
||||
setActiveTab("dashboard-overview");
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Dashboard Deleted",
|
||||
description: "The dashboard has been deleted.",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title="Dashboard"
|
||||
description="Active Directory Management API Overview"
|
||||
>
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatsCard
|
||||
title="Total Users"
|
||||
value={1284}
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
iconBgColor="bg-blue-100"
|
||||
iconColor="text-primary"
|
||||
changeValue={4.75}
|
||||
changeType="increase"
|
||||
changePeriod="from last month"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Total Groups"
|
||||
value={87}
|
||||
icon={<UserPlus className="h-5 w-5" />}
|
||||
iconBgColor="bg-purple-100"
|
||||
iconColor="text-purple-600"
|
||||
changeValue={2.3}
|
||||
changeType="increase"
|
||||
changePeriod="from last month"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Organizational Units"
|
||||
value={32}
|
||||
icon={<FolderClosed className="h-5 w-5" />}
|
||||
iconBgColor="bg-amber-100"
|
||||
iconColor="text-amber-600"
|
||||
changeValue={0}
|
||||
changeType="nochange"
|
||||
changePeriod="from last month"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Total Computers"
|
||||
value={563}
|
||||
icon={<Monitor className="h-5 w-5" />}
|
||||
iconBgColor="bg-teal-100"
|
||||
iconColor="text-teal-600"
|
||||
changeValue={1.2}
|
||||
changeType="decrease"
|
||||
changePeriod="from last month"
|
||||
/>
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Dashboards</h1>
|
||||
<Button onClick={() => setIsCreateDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
New Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* API Activity Card */}
|
||||
<div className="lg:col-span-2">
|
||||
<ApiActivityCard />
|
||||
</div>
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="mb-6">
|
||||
{dashboards.map((dashboard) => (
|
||||
<TabsTrigger key={dashboard.id} value={dashboard.id}>
|
||||
{dashboard.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{/* Active API Tokens */}
|
||||
<div>
|
||||
<ApiTokensCard tokens={tokens} />
|
||||
</div>
|
||||
</div>
|
||||
{dashboards.map((dashboard) => (
|
||||
<TabsContent key={dashboard.id} value={dashboard.id}>
|
||||
{dataSourcesLoading ? (
|
||||
<div className="flex justify-center items-center min-h-[60vh]">
|
||||
<RefreshCw className="h-8 w-8 animate-spin text-primary" />
|
||||
<span className="ml-2">Loading data sources...</span>
|
||||
</div>
|
||||
) : (
|
||||
<DashboardLayout
|
||||
config={dashboard}
|
||||
onSave={handleSaveDashboard}
|
||||
dataSources={dataSources}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
{/* LDAP Connections */}
|
||||
<div className="mt-6">
|
||||
<LdapConnectionsCard connections={connections} />
|
||||
</div>
|
||||
|
||||
{/* API Documentation Preview */}
|
||||
<div className="mt-6">
|
||||
<ApiDocumentationCard />
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
{/* Create Dashboard Dialog */}
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Dashboard</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleCreateDashboard)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Dashboard Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Enter dashboard name" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Give your dashboard a descriptive name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsCreateDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Create Dashboard</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user