wulf-pulse/components/admin/IntegrationStatusTabs.tsx

290 lines
12 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
Shield, Monitor, Network, Apple,
CheckCircle2, XCircle, AlertTriangle, RefreshCw, Loader2,
Server, HardDrive, Cpu, Clock,
} from 'lucide-react';
function StatusDot({ ok, warn }: { ok: boolean; warn?: boolean }) {
if (!ok) return <span className="inline-block w-2 h-2 rounded-full bg-red-500" />;
if (warn) return <span className="inline-block w-2 h-2 rounded-full bg-yellow-500" />;
return <span className="inline-block w-2 h-2 rounded-full bg-green-500" />;
}
function StatCard({ label, value, sub, icon: Icon, cls }: {
label: string; value: string | number; sub?: string;
icon?: React.ElementType; cls?: string;
}) {
return (
<div className={`rounded-lg border p-4 flex flex-col gap-1 ${cls ?? ''}`}>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{Icon && <Icon className="w-3.5 h-3.5" />}
{label}
</div>
<div className="text-2xl font-bold tabular-nums">{value}</div>
{sub && <div className="text-xs text-muted-foreground">{sub}</div>}
</div>
);
}
function fmtDate(d: string | null) {
if (!d) return 'Never';
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
function VeeamTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
const aj = data.agentJobs ?? {};
const bj = data.backupJobs ?? {};
const totalFailed = (aj.failed ?? 0) + (bj.failed ?? 0);
const totalWarning = (aj.warning ?? 0) + (bj.warning ?? 0);
const totalRunning = aj.running ?? 0;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<StatusDot ok={data.configured} warn={totalFailed > 0 || totalWarning > 0} />
<div>
<p className="text-sm font-medium">{data.configured ? 'Connected to VSPC' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync)}</p>
</div>
</div>
<Button size="sm" onClick={onSync} disabled={syncing || !data.configured}>
{syncing ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Sync Now
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard label="Organizations" value={data.organizations ?? 0} icon={Server} />
<StatCard label="Protected Workloads" value={data.protectedWorkloads ?? 0} icon={HardDrive} />
<StatCard label="Agent Jobs" value={aj.total ?? 0} sub={`${aj.success ?? 0} success`} icon={Shield} />
<StatCard label="Backup Jobs" value={bj.total ?? 0} sub={`${bj.success ?? 0} success`} icon={Shield} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<StatCard label="Running" value={totalRunning} icon={Clock}
cls={totalRunning > 0 ? 'border-blue-500/30 bg-blue-500/5' : ''} />
<StatCard label="Failed" value={totalFailed} icon={XCircle}
cls={totalFailed > 0 ? 'border-red-500/30 bg-red-500/5' : ''} />
<StatCard label="Warning" value={totalWarning} icon={AlertTriangle}
cls={totalWarning > 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} />
<StatCard label="Success" value={(aj.success ?? 0) + (bj.success ?? 0)} icon={CheckCircle2}
cls="border-green-500/30 bg-green-500/5" />
</div>
{(totalFailed > 0 || totalWarning > 0 || totalRunning > 0) && (
<div className="rounded-lg border p-4 space-y-2">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Attention Required</p>
{totalFailed > 0 && (
<div className="flex items-center gap-2 text-sm text-red-600">
<XCircle className="w-4 h-4" />
{totalFailed} job{totalFailed !== 1 ? 's' : ''} failed
</div>
)}
{totalWarning > 0 && (
<div className="flex items-center gap-2 text-sm text-yellow-700">
<AlertTriangle className="w-4 h-4" />
{totalWarning} job{totalWarning !== 1 ? 's' : ''} completed with warnings
</div>
)}
{totalRunning > 0 && (
<div className="flex items-center gap-2 text-sm text-blue-600">
<Loader2 className="w-4 h-4 animate-spin" />
{totalRunning} job{totalRunning !== 1 ? 's' : ''} running (stalled jobs appear here)
</div>
)}
</div>
)}
</div>
);
}
function DattoRmmTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
const unlinked = Math.max(0, (data.totalConfigItems ?? 0) - (data.rmmLinkedDevices ?? 0));
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<StatusDot ok={data.configured} />
<div>
<p className="text-sm font-medium">{data.configured ? 'Connected' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">{data.apiUrl}</p>
</div>
</div>
<div className="flex gap-2">
<a href="https://concord.rmm.datto.com" target="_blank" rel="noopener noreferrer">
<Button variant="outline" size="sm">Open Portal</Button>
</a>
<Button size="sm" onClick={onSync} disabled={syncing || !data.configured}>
{syncing ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <RefreshCw className="w-4 h-4 mr-2" />}
Sync CIs
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<StatCard label="Active Config Items" value={data.totalConfigItems ?? 0} icon={Cpu} />
<StatCard label="RMM-Linked Devices" value={data.rmmLinkedDevices ?? 0} icon={Monitor}
sub="with rmm_device_uid" />
<StatCard label="Unlinked Devices" value={unlinked} icon={Monitor}
cls={unlinked > 0 ? 'border-yellow-500/30 bg-yellow-500/5' : ''} />
</div>
<div className="rounded-lg border p-4 bg-muted/30 text-sm text-muted-foreground">
Datto RMM device data is queried live via the RMM API when investigating alerts.
Device records link to Autotask Configuration Items via <code className="text-xs bg-muted px-1 rounded">rmm_device_uid</code>.
Run an Autotask Configuration Items sync to refresh CI data.
</div>
</div>
);
}
function AuvikTab({ data }: { data: any }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<StatusDot ok={data.configured} />
<div>
<p className="text-sm font-medium">{data.configured ? 'Connected' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">{data.apiUrl}</p>
</div>
</div>
<a href="https://auvikapi.us5.my.auvik.com" target="_blank" rel="noopener noreferrer">
<Button variant="outline" size="sm">Open Portal</Button>
</a>
</div>
<div className="rounded-lg border p-4 bg-muted/30 text-sm text-muted-foreground">
Auvik provides network topology and device data. The API is configured and accessible.
Full sync and dashboard integration is planned data is currently available via the Auvik API endpoints
at <code className="text-xs bg-muted px-1 rounded">/api/auvik/devices</code> and <code className="text-xs bg-muted px-1 rounded">/api/auvik/tenant-mappings</code>.
</div>
</div>
);
}
function AddigyTab({ data }: { data: any }) {
if (!data) return <div className="flex items-center justify-center py-12"><Loader2 className="w-5 h-5 animate-spin text-muted-foreground" /></div>;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<StatusDot ok={data.configured} />
<div>
<p className="text-sm font-medium">{data.configured ? 'Connected' : 'Not configured'}</p>
<p className="text-xs text-muted-foreground">{data.apiUrl}</p>
</div>
</div>
<a href="https://app.addigy.com" target="_blank" rel="noopener noreferrer">
<Button variant="outline" size="sm">Open Portal</Button>
</a>
</div>
<div className="rounded-lg border p-4 bg-muted/30 text-sm text-muted-foreground">
Addigy manages Apple (macOS/iOS) devices. The API is configured and accessible via token auth.
Device and policy data is available via <code className="text-xs bg-muted px-1 rounded">/api/addigy-devices</code> and <code className="text-xs bg-muted px-1 rounded">/api/addigy-policies</code>.
Full sync integration is planned.
</div>
</div>
);
}
export default function IntegrationStatusTabs() {
const [status, setStatus] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [veeamSyncing, setVeeamSyncing] = useState(false);
const [rmmSyncing, setRmmSyncing] = useState(false);
const fetchStatus = async () => {
try {
const res = await fetch('/api/integrations/status');
if (res.ok) setStatus(await res.json());
} catch (e) {
console.error('Failed to fetch integration status:', e);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchStatus(); }, []);
const handleVeeamSync = async () => {
setVeeamSyncing(true);
try {
await fetch('/api/veeam/sync', { method: 'POST', body: JSON.stringify({ syncType: 'full' }), headers: { 'Content-Type': 'application/json' } });
// Poll until done
const poll = setInterval(async () => {
const r = await fetch('/api/veeam/sync');
if (r.ok) {
const d = await r.json();
if (!d.isSyncing) {
clearInterval(poll);
setVeeamSyncing(false);
fetchStatus();
}
}
}, 3000);
} catch {
setVeeamSyncing(false);
}
};
const handleRmmSync = async () => {
setRmmSyncing(true);
try {
await fetch('/api/sync/entity', {
method: 'POST',
body: JSON.stringify({ entities: ['configuration_items'] }),
headers: { 'Content-Type': 'application/json' },
});
setTimeout(() => { setRmmSyncing(false); fetchStatus(); }, 5000);
} catch {
setRmmSyncing(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
);
}
return (
<Tabs defaultValue="veeam" className="w-full">
<TabsList className="grid w-full max-w-lg grid-cols-4">
<TabsTrigger value="veeam" className="gap-1.5">
<Shield className="w-3.5 h-3.5" />
Veeam
</TabsTrigger>
<TabsTrigger value="datto" className="gap-1.5">
<Monitor className="w-3.5 h-3.5" />
Datto RMM
</TabsTrigger>
<TabsTrigger value="auvik" className="gap-1.5">
<Network className="w-3.5 h-3.5" />
Auvik
</TabsTrigger>
<TabsTrigger value="addigy" className="gap-1.5">
<Apple className="w-3.5 h-3.5" />
Addigy
</TabsTrigger>
</TabsList>
<TabsContent value="veeam" className="mt-6">
<VeeamTab data={status?.veeam} onSync={handleVeeamSync} syncing={veeamSyncing} />
</TabsContent>
<TabsContent value="datto" className="mt-6">
<DattoRmmTab data={status?.dattoRmm} onSync={handleRmmSync} syncing={rmmSyncing} />
</TabsContent>
<TabsContent value="auvik" className="mt-6">
<AuvikTab data={status?.auvik} />
</TabsContent>
<TabsContent value="addigy" className="mt-6">
<AddigyTab data={status?.addigy} />
</TabsContent>
</Tabs>
);
}