- DetailModal: thread tz through resolveLabel(...) module helper + default export's 3 inline date/time calls. - IntegrationStatusTabs: thread tz through fmtDate helper + VeeamTab sub-component prop. - SyncScheduler: thread tz into closure-scoped formatDate helper. - audit-log-table, user-table, user-sessions, active-sessions: inline toLocale calls in component body. - analysis-view: useUserTimezone in AnalysisView; thread tz into 4 toLocaleString calls. - resolution-trend, volume-trend (recharts): module-scope fmtDate(iso) → fmtDate(iso, tz); useUserTimezone in named export; thread tz into axis tickFormatter + tooltip labelFormatter. - ticket-detail-modal: thread tz into formatDate arrow inside TicketDetailModal. - TimelineView: useUserTimezone; thread tz into 4 toLocale*String calls (hour/day/month/event-time formatters). - ScoreCard: useUserTimezone in AggregateScoreCard; thread tz into the date-range latest call. - addigy-tab: useUserTimezone in AddigyTab; thread tz into 2 inline calls. - activity-sparkline: module-scope fmtHour(iso) → fmtHour(iso, tz); useUserTimezone in ActivitySparkline; update 3 callsites in title/aria. - compliance-detail-table: thread tz from ComplianceDetailTable into ContractCoverageModal sub-component (2 inline date calls). - company-backup-detail: module-scope formatDate(d) → formatDate(d, tz); useUserTimezone in CompanyBackupDetail; update 3 callsites. Migrates 31 of 81 audit leak callsites.
292 lines
13 KiB
TypeScript
292 lines
13 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';
|
|
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
|
|
|
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, tz: string) {
|
|
if (!d) return 'Never';
|
|
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz });
|
|
}
|
|
|
|
function VeeamTab({ data, onSync, syncing, tz }: { data: any; onSync: () => void; syncing: boolean; tz: string }) {
|
|
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, tz)}</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 tz = useUserTimezone();
|
|
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} tz={tz} />
|
|
</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>
|
|
);
|
|
}
|