feat(07.1-05): user-tz on admin sync pages
- duo, sentinelone, datto-rmm, veeam, itglue, mimecast: each gets useUserTimezone() in default export and threads tz through fmtDate/sub-component props. - duo (1 callsite, closure inline), sentinelone (1, module-scope helper), datto-rmm (1 helper + StatusTab/HistoryTab props), veeam (1 helper + 5 sub-components), itglue (1 helper + StatusTab/HistoryTab props), mimecast (1 helper + 6 sub-components incl. 2 dialogs with inline toLocaleString calls). - Module-scope fmtDate(d) signatures converted to fmtDate(d, tz). Migrates 13 of 81 audit leak callsites.
This commit is contained in:
parent
a709144685
commit
8c56cafe0b
6 changed files with 80 additions and 64 deletions
|
|
@ -17,10 +17,11 @@ import {
|
|||
ArrowLeft, Activity, History, Monitor, Loader2, RefreshCw,
|
||||
ExternalLink, Server, Wifi, WifiOff, AlertTriangle, Bell, XCircle, CheckCircle2, Clock,
|
||||
} from 'lucide-react';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
function fmtDate(d: string | null) {
|
||||
function fmtDate(d: string | null, tz: string) {
|
||||
if (!d) return 'Never';
|
||||
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz });
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value: string | number; sub?: string; icon?: React.ElementType; cls?: string }) {
|
||||
|
|
@ -35,7 +36,7 @@ function StatCard({ label, value, sub, icon: Icon, cls }: { label: string; value
|
|||
);
|
||||
}
|
||||
|
||||
function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; syncing: boolean }) {
|
||||
function StatusTab({ 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 devs = data.devices ?? {};
|
||||
|
|
@ -46,7 +47,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; s
|
|||
<div className="flex items-center justify-between rounded-lg border p-4 bg-muted/30">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">{data.configured ? 'Connected' : 'Not configured'}</p>
|
||||
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync)}</p>
|
||||
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync, tz)}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<a href="https://concord.rmm.datto.com" target="_blank" rel="noopener noreferrer">
|
||||
|
|
@ -99,7 +100,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: () => void; s
|
|||
);
|
||||
}
|
||||
|
||||
function HistoryTab({ refreshKey }: { refreshKey: number }) {
|
||||
function HistoryTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
|
@ -140,7 +141,7 @@ function HistoryTab({ refreshKey }: { refreshKey: number }) {
|
|||
<StatusBadge tone={tone}>{row.status}</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right num">{row.records_added ?? 0}</TableCell>
|
||||
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at)}</TableCell>
|
||||
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at, tz)}</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground num">
|
||||
{dur != null ? `${dur}s` : '—'}
|
||||
</TableCell>
|
||||
|
|
@ -154,6 +155,7 @@ function HistoryTab({ refreshKey }: { refreshKey: number }) {
|
|||
}
|
||||
|
||||
export default function DattoRmmPage() {
|
||||
const tz = useUserTimezone();
|
||||
const [status, setStatus] = useState<any>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
|
@ -222,10 +224,10 @@ export default function DattoRmmPage() {
|
|||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="status" className="mt-6">
|
||||
<StatusTab data={status} onSync={handleSync} syncing={syncing} />
|
||||
<StatusTab data={status} onSync={handleSync} syncing={syncing} tz={tz} />
|
||||
</TabsContent>
|
||||
<TabsContent value="history" className="mt-6">
|
||||
<HistoryTab refreshKey={refreshKey} />
|
||||
<HistoryTab refreshKey={refreshKey} tz={tz} />
|
||||
</TabsContent>
|
||||
<TabsContent value="about" className="mt-6">
|
||||
<div className="space-y-4 text-sm text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { RefreshCw, ArrowLeft, Loader2, CheckCircle2, AlertTriangle, Shield, Users, Smartphone, ScrollText, Layers, AppWindow, ChevronDown, ChevronUp, ShieldOff, ShieldAlert, ShieldX } from 'lucide-react';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
interface DuoStatus {
|
||||
connected: boolean;
|
||||
|
|
@ -55,6 +56,7 @@ interface DuoAccount {
|
|||
}
|
||||
|
||||
export default function DuoSyncPage() {
|
||||
const tz = useUserTimezone();
|
||||
const [status, setStatus] = useState<DuoStatus | null>(null);
|
||||
const [accounts, setAccounts] = useState<DuoAccount[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -114,7 +116,7 @@ export default function DuoSyncPage() {
|
|||
|
||||
const fmtDate = (d: string | null) => {
|
||||
if (!d) return 'Never';
|
||||
return new Date(d).toLocaleString();
|
||||
return new Date(d).toLocaleString(undefined, { timeZone: tz });
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ import {
|
|||
ExternalLink, CheckCircle2, XCircle, Clock, AlertTriangle,
|
||||
Building2, Monitor, Users, Key, FileText, Globe, Shield, Package,
|
||||
} from 'lucide-react';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
function fmtDate(d: string | null) {
|
||||
function fmtDate(d: string | null, tz: string) {
|
||||
if (!d) return 'Never';
|
||||
return new Date(d).toLocaleString(undefined, {
|
||||
month: 'short', day: 'numeric', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', timeZone: tz,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -65,9 +66,9 @@ function SyncStatusBadge({ status }: { status: string }) {
|
|||
}
|
||||
|
||||
function StatusTab({
|
||||
syncData, onSync, syncing,
|
||||
syncData, onSync, syncing, tz,
|
||||
}: {
|
||||
syncData: any; onSync: () => void; syncing: boolean;
|
||||
syncData: any; onSync: () => void; syncing: boolean; tz: string;
|
||||
}) {
|
||||
if (!syncData) {
|
||||
return (
|
||||
|
|
@ -90,7 +91,7 @@ function StatusTab({
|
|||
Connected to IT Glue
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last sync: {fmtDate(latest?.completed_at ?? null)}
|
||||
Last sync: {fmtDate(latest?.completed_at ?? null, tz)}
|
||||
{latest?.duration_ms && ` · ${fmtDuration(latest.duration_ms)}`}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -165,7 +166,7 @@ function StatusTab({
|
|||
);
|
||||
}
|
||||
|
||||
function HistoryTab({ history }: { history: any[] }) {
|
||||
function HistoryTab({ history, tz }: { history: any[]; tz: string }) {
|
||||
if (!history.length) {
|
||||
return (
|
||||
<div className="text-center py-12 text-muted-foreground text-sm">
|
||||
|
|
@ -192,7 +193,7 @@ function HistoryTab({ history }: { history: any[] }) {
|
|||
<TableCell><SyncStatusBadge status={row.status} /></TableCell>
|
||||
<TableCell className="text-muted-foreground capitalize">{row.triggered_by ?? 'system'}</TableCell>
|
||||
<TableCell className="text-right num">{(row.total_upserted ?? 0).toLocaleString()}</TableCell>
|
||||
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at)}</TableCell>
|
||||
<TableCell className="text-muted-foreground num">{fmtDate(row.started_at, tz)}</TableCell>
|
||||
<TableCell className="text-right text-muted-foreground num">{fmtDuration(row.duration_ms)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
|
@ -261,6 +262,7 @@ function AboutTab() {
|
|||
}
|
||||
|
||||
export default function ITGluePage() {
|
||||
const tz = useUserTimezone();
|
||||
const [syncData, setSyncData] = useState<any>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
||||
|
|
@ -341,11 +343,11 @@ export default function ITGluePage() {
|
|||
</TabsList>
|
||||
|
||||
<TabsContent value="status" className="mt-6">
|
||||
<StatusTab syncData={syncData} onSync={handleSync} syncing={syncing} />
|
||||
<StatusTab syncData={syncData} onSync={handleSync} syncing={syncing} tz={tz} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="history" className="mt-6">
|
||||
<HistoryTab history={syncData?.history ?? []} />
|
||||
<HistoryTab history={syncData?.history ?? []} tz={tz} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="about" className="mt-6">
|
||||
|
|
|
|||
|
|
@ -22,11 +22,12 @@ import {
|
|||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import SyncScheduler from '@/components/admin/SyncScheduler';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
function fmtDate(d: string | null | undefined) {
|
||||
function fmtDate(d: string | null | undefined, tz: string) {
|
||||
if (!d) return 'Never';
|
||||
return new Date(d).toLocaleString(undefined, {
|
||||
month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit',
|
||||
month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -70,7 +71,7 @@ function ThreatLevelBadge({ level }: { level: string }) {
|
|||
}
|
||||
|
||||
// ── Status Tab ────────────────────────────────────────────────────────────────
|
||||
function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) {
|
||||
function StatusTab({ data, onSync, syncing, tz }: { data: any; onSync: (t: string) => 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" />
|
||||
|
|
@ -96,7 +97,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) =
|
|||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Last sync: {fmtDate(stats.lastSync)} · Oldest message: {fmtDate(stats.oldestMessage)}
|
||||
Last sync: {fmtDate(stats.lastSync, tz)} · Oldest message: {fmtDate(stats.oldestMessage, tz)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
|
|
@ -139,7 +140,7 @@ function StatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) =
|
|||
}
|
||||
|
||||
// ── Messages Tab ──────────────────────────────────────────────────────────────
|
||||
function MessagesTab() {
|
||||
function MessagesTab({ tz }: { tz: string }) {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
|
|
@ -232,7 +233,7 @@ function MessagesTab() {
|
|||
<TableCell className="text-xs truncate max-w-[200px]" title={r.subject}>{r.subject ?? '—'}</TableCell>
|
||||
<TableCell className="text-xs capitalize text-muted-foreground">{r.direction ?? '—'}</TableCell>
|
||||
<TableCell><MessageStatusBadge status={r.status} /></TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.sent_datetime)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.sent_datetime, tz)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
|
@ -244,7 +245,7 @@ function MessagesTab() {
|
|||
}
|
||||
|
||||
// ── Threats Tab ───────────────────────────────────────────────────────────────
|
||||
function ThreatsTab() {
|
||||
function ThreatsTab({ tz }: { tz: string }) {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
|
@ -283,7 +284,7 @@ function ThreatsTab() {
|
|||
<TableCell className="text-xs text-muted-foreground truncate max-w-[200px]" title={r.url ?? r.file_name ?? ''}>
|
||||
{r.url ?? r.file_name ?? '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.event_datetime)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap num">{fmtDate(r.event_datetime, tz)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
|
@ -404,7 +405,7 @@ function CloudUserTab() {
|
|||
}
|
||||
|
||||
// ── History Tab ───────────────────────────────────────────────────────────────
|
||||
function HistoryTab() {
|
||||
function HistoryTab({ tz }: { tz: string }) {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
|
@ -453,7 +454,7 @@ function HistoryTab() {
|
|||
</TableCell>
|
||||
<TableCell className="num text-xs">{fmtNum(meta.messagesUpserted ?? r.records_added)}</TableCell>
|
||||
<TableCell className="num text-xs">{fmtNum(meta.threatsUpserted)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(r.started_at)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(r.started_at, tz)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{durStr}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
@ -593,11 +594,12 @@ function analyzeMessage(m: any): Analysis {
|
|||
};
|
||||
}
|
||||
|
||||
function MessageAnalysisDialog({ message, onClose, onRelease, releasing }: {
|
||||
function MessageAnalysisDialog({ message, onClose, onRelease, releasing, tz }: {
|
||||
message: any;
|
||||
onClose: () => void;
|
||||
onRelease: (m: any) => void;
|
||||
releasing: boolean;
|
||||
tz: string;
|
||||
}) {
|
||||
if (!message) return null;
|
||||
const analysis = analyzeMessage(message);
|
||||
|
|
@ -648,7 +650,7 @@ function MessageAnalysisDialog({ message, onClose, onRelease, releasing }: {
|
|||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Received</span>
|
||||
<span>{new Date(message.dateReceived).toLocaleString()}</span>
|
||||
<span>{new Date(message.dateReceived).toLocaleString(undefined, { timeZone: tz })}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Policy</span>
|
||||
|
|
@ -734,7 +736,7 @@ const TENANT_OPTIONS = [
|
|||
{ id: '2', name: 'Seubert & Associates' },
|
||||
];
|
||||
|
||||
function HeldMailTab() {
|
||||
function HeldMailTab({ tz }: { tz: string }) {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [messages, setMessages] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -921,7 +923,7 @@ function HeldMailTab() {
|
|||
{filtered.map((m: any) => (
|
||||
<TableRow key={m.id}>
|
||||
<TableCell className="text-muted-foreground whitespace-nowrap text-xs num">
|
||||
{new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(m.dateReceived).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz })}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs" style={{overflow:'hidden'}}>
|
||||
<div className="truncate">{m.to}</div>
|
||||
|
|
@ -982,6 +984,7 @@ function HeldMailTab() {
|
|||
onClose={() => setAnalysisMessage(null)}
|
||||
onRelease={release}
|
||||
releasing={analysisMessage ? !!releasing[analysisMessage.id] : false}
|
||||
tz={tz}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1112,11 +1115,12 @@ function analyzeDelivered(m: any): { headline: string; explanation: string; seve
|
|||
};
|
||||
}
|
||||
|
||||
function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages }: {
|
||||
function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages, tz }: {
|
||||
message: any;
|
||||
onClose: () => void;
|
||||
onFindSimilar?: (type: 'sender' | 'ip' | 'subject', value: string) => void;
|
||||
allMessages?: any[];
|
||||
tz: string;
|
||||
}) {
|
||||
const [remedStep, setRemedStep] = useState<'idle' | 'searching' | 'confirm' | 'removing' | 'done'>('idle');
|
||||
const [remedMatches, setRemedMatches] = useState<any[]>([]);
|
||||
|
|
@ -1236,7 +1240,7 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
|
|||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Received</span>
|
||||
<span>{new Date(message.received).toLocaleString()}</span>
|
||||
<span>{new Date(message.received).toLocaleString(undefined, { timeZone: tz })}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[72px_1fr] gap-2 px-3 py-2">
|
||||
<span className="text-muted-foreground text-xs pt-0.5">Status</span>
|
||||
|
|
@ -1402,7 +1406,7 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
|
|||
<div className="min-w-0">
|
||||
<div className="text-xs truncate">{m.subject || '(no subject)'}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(m.receivedDateTime).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(m.receivedDateTime).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz })}
|
||||
{!m.isRead && <span className="ml-1 text-blue-500 font-medium">unread</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1457,7 +1461,7 @@ function DeliveredAnalysisDialog({ message, onClose, onFindSimilar, allMessages
|
|||
);
|
||||
}
|
||||
|
||||
function DeliveredMailTab() {
|
||||
function DeliveredMailTab({ tz }: { tz: string }) {
|
||||
const [tenantId, setTenantId] = useState('1');
|
||||
const [to, setTo] = useState('');
|
||||
const [from, setFrom] = useState('');
|
||||
|
|
@ -1837,7 +1841,7 @@ function DeliveredMailTab() {
|
|||
: ''
|
||||
}>
|
||||
<TableCell className="text-muted-foreground whitespace-nowrap text-xs num">
|
||||
{new Date(m.received).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}
|
||||
{new Date(m.received).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz })}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs" style={{overflow:'hidden'}}>
|
||||
<div className="truncate">{m.to}</div>
|
||||
|
|
@ -1887,6 +1891,7 @@ function DeliveredMailTab() {
|
|||
onClose={() => setAnalysisMessage(null)}
|
||||
onFindSimilar={handleFindSimilar}
|
||||
allMessages={messages}
|
||||
tz={tz}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1894,6 +1899,7 @@ function DeliveredMailTab() {
|
|||
|
||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||
export default function MimecastSyncPage() {
|
||||
const tz = useUserTimezone();
|
||||
const [statusData, setStatusData] = useState<any>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [lastResult, setLastResult] = useState<any>(null);
|
||||
|
|
@ -1972,14 +1978,14 @@ export default function MimecastSyncPage() {
|
|||
</TabsList>
|
||||
|
||||
<TabsContent value="status" className="mt-6">
|
||||
<StatusTab data={statusData} onSync={handleSync} syncing={syncing} />
|
||||
<StatusTab data={statusData} onSync={handleSync} syncing={syncing} tz={tz} />
|
||||
</TabsContent>
|
||||
<TabsContent value="held" className="mt-6"><HeldMailTab /></TabsContent>
|
||||
<TabsContent value="delivered" className="mt-6"><DeliveredMailTab /></TabsContent>
|
||||
<TabsContent value="messages" className="mt-6"><MessagesTab /></TabsContent>
|
||||
<TabsContent value="threats" className="mt-6"><ThreatsTab /></TabsContent>
|
||||
<TabsContent value="held" className="mt-6"><HeldMailTab tz={tz} /></TabsContent>
|
||||
<TabsContent value="delivered" className="mt-6"><DeliveredMailTab tz={tz} /></TabsContent>
|
||||
<TabsContent value="messages" className="mt-6"><MessagesTab tz={tz} /></TabsContent>
|
||||
<TabsContent value="threats" className="mt-6"><ThreatsTab tz={tz} /></TabsContent>
|
||||
<TabsContent value="cloudusers" className="mt-6"><CloudUserTab /></TabsContent>
|
||||
<TabsContent value="history" className="mt-6"><HistoryTab /></TabsContent>
|
||||
<TabsContent value="history" className="mt-6"><HistoryTab tz={tz} /></TabsContent>
|
||||
<TabsContent value="schedules" className="mt-6"><SyncScheduler /></TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ import {
|
|||
ArrowLeft, RefreshCw, Play, CheckCircle2, XCircle, Clock,
|
||||
Shield, Monitor, AlertTriangle, Activity,
|
||||
} from 'lucide-react';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
function fmtDate(d: string | null) {
|
||||
function fmtDate(d: string | null, tz: string) {
|
||||
if (!d) return '—';
|
||||
return new Date(d).toLocaleString();
|
||||
return new Date(d).toLocaleString(undefined, { timeZone: tz });
|
||||
}
|
||||
function fmtDuration(ms: number | null) {
|
||||
if (!ms) return '—';
|
||||
|
|
@ -21,6 +22,7 @@ function fmtDuration(ms: number | null) {
|
|||
}
|
||||
|
||||
export default function SentinelOneSyncPage() {
|
||||
const tz = useUserTimezone();
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
|
|
@ -120,7 +122,7 @@ export default function SentinelOneSyncPage() {
|
|||
<div>
|
||||
<div className="font-medium capitalize">{lastSync.status}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{fmtDate(lastSync.completed_at || lastSync.started_at)} · {fmtDuration(lastSync.duration_ms)} · {lastSync.total_upserted?.toLocaleString()} records
|
||||
{fmtDate(lastSync.completed_at || lastSync.started_at, tz)} · {fmtDuration(lastSync.duration_ms)} · {lastSync.total_upserted?.toLocaleString()} records
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -159,7 +161,7 @@ export default function SentinelOneSyncPage() {
|
|||
{h.status === 'completed' ? <CheckCircle2 className="w-4 h-4 text-green-500" />
|
||||
: h.status === 'running' ? <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />
|
||||
: <XCircle className="w-4 h-4 text-red-500" />}
|
||||
<span className="text-muted-foreground">{fmtDate(h.started_at)}</span>
|
||||
<span className="text-muted-foreground">{fmtDate(h.started_at, tz)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<span>{h.total_upserted?.toLocaleString() ?? 0} records</span>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ import {
|
|||
} from '@/components/ui/table';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
import SyncScheduler from '@/components/admin/SyncScheduler';
|
||||
import { useUserTimezone } from '@/lib/hooks/use-user-timezone';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
function fmtDate(d: string | null | undefined) {
|
||||
function fmtDate(d: string | null | undefined, tz: string) {
|
||||
if (!d) return 'Never';
|
||||
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
return new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz });
|
||||
}
|
||||
function fmtDur(ms: number) {
|
||||
if (ms < 60000) return `${Math.round(ms / 1000)}s`;
|
||||
|
|
@ -54,7 +55,7 @@ function SyncStatusBadge({ status }: { status: string }) {
|
|||
}
|
||||
|
||||
// ── Status Tab ────────────────────────────────────────────────────────────────
|
||||
function VeeamStatusTab({ data, onSync, syncing }: { data: any; onSync: (t: string) => void; syncing: boolean }) {
|
||||
function VeeamStatusTab({ data, onSync, syncing, tz }: { data: any; onSync: (t: string) => 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" />
|
||||
|
|
@ -73,7 +74,7 @@ function VeeamStatusTab({ data, onSync, syncing }: { data: any; onSync: (t: stri
|
|||
<div className="flex items-center justify-between rounded-lg border p-4 bg-muted/30">
|
||||
<div className="space-y-0.5">
|
||||
<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>
|
||||
<p className="text-xs text-muted-foreground">Last sync: {fmtDate(data.lastSync, tz)}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => onSync('incremental')} disabled={syncing || !data.configured}>
|
||||
|
|
@ -129,7 +130,7 @@ const ENTITY_LABELS: Record<string, string> = {
|
|||
alarms: 'Alarms',
|
||||
};
|
||||
|
||||
function HistoryRow({ row }: { row: any }) {
|
||||
function HistoryRow({ row, tz }: { row: any; tz: string }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const dur = row.completed_at && row.started_at
|
||||
? new Date(row.completed_at).getTime() - new Date(row.started_at).getTime()
|
||||
|
|
@ -160,7 +161,7 @@ function HistoryRow({ row }: { row: any }) {
|
|||
</TableCell>
|
||||
<TableCell><SyncStatusBadge status={row.status} /></TableCell>
|
||||
<TableCell className="num font-medium">{(row.records_added ?? 0).toLocaleString()}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs num">{fmtDate(row.started_at)}</TableCell>
|
||||
<TableCell className="text-muted-foreground text-xs num">{fmtDate(row.started_at, tz)}</TableCell>
|
||||
<TableCell className="text-muted-foreground num">{dur != null ? fmtDur(dur) : '—'}</TableCell>
|
||||
<TableCell className="text-muted-foreground capitalize">{row.triggered_by ?? '—'}</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -192,7 +193,7 @@ function HistoryRow({ row }: { row: any }) {
|
|||
);
|
||||
}
|
||||
|
||||
function VeeamHistoryTab({ refreshKey }: { refreshKey: number }) {
|
||||
function VeeamHistoryTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ function VeeamHistoryTab({ refreshKey }: { refreshKey: number }) {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rows.map((row, i) => <HistoryRow key={i} row={row} />)}
|
||||
{rows.map((row, i) => <HistoryRow key={i} row={row} tz={tz} />)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
|
@ -323,7 +324,7 @@ function AgentsTab({ refreshKey }: { refreshKey: number }) {
|
|||
}
|
||||
|
||||
// ── Alarms Tab ────────────────────────────────────────────────────────────────
|
||||
function AlarmsTab({ refreshKey }: { refreshKey: number }) {
|
||||
function AlarmsTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
|
@ -386,7 +387,7 @@ function AlarmsTab({ refreshKey }: { refreshKey: number }) {
|
|||
</StatusBadge>
|
||||
</TableCell>
|
||||
<TableCell className="num text-xs">{a.repeat_count ?? 0}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(a.last_activation_time)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(a.last_activation_time, tz)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground max-w-xs truncate" title={a.last_activation_message ?? ''}>
|
||||
{a.last_activation_message?.trim() || '—'}
|
||||
</TableCell>
|
||||
|
|
@ -400,7 +401,7 @@ function AlarmsTab({ refreshKey }: { refreshKey: number }) {
|
|||
}
|
||||
|
||||
// ── RPO Tab ───────────────────────────────────────────────────────────────────
|
||||
function RpoTab({ refreshKey }: { refreshKey: number }) {
|
||||
function RpoTab({ refreshKey, tz }: { refreshKey: number; tz: string }) {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [running, setRunning] = useState(false);
|
||||
|
|
@ -503,7 +504,7 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {
|
|||
<TableRow key={j.job_instance_uid}>
|
||||
<TableCell className="font-medium text-xs">{j.job_name}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">{j.org_name}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time, tz)}</TableCell>
|
||||
<TableCell className="num text-xs font-semibold text-red-600">{display}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground max-w-xs truncate" title={j.failure_category ?? ''}>
|
||||
{j.failure_category ?? '—'}
|
||||
|
|
@ -545,7 +546,7 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {
|
|||
<TableRow key={j.job_instance_uid}>
|
||||
<TableCell className="font-medium text-xs">{j.job_name}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">{j.org_name}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time)}</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground num">{fmtDate(j.last_end_time, tz)}</TableCell>
|
||||
<TableCell className="num text-xs text-green-700">
|
||||
{j.hours_since_backup !== null ? `${j.hours_since_backup}h` : '—'}
|
||||
</TableCell>
|
||||
|
|
@ -562,6 +563,7 @@ function RpoTab({ refreshKey }: { refreshKey: number }) {
|
|||
|
||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||
export default function VeeamSyncPage() {
|
||||
const tz = useUserTimezone();
|
||||
const [status, setStatus] = useState<any>(null);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
|
@ -633,11 +635,11 @@ export default function VeeamSyncPage() {
|
|||
<TabsTrigger value="schedules" className="gap-1.5"><Calendar className="h-4 w-4" />Schedules</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="status" className="mt-6"><VeeamStatusTab data={status} onSync={handleSync} syncing={syncing} /></TabsContent>
|
||||
<TabsContent value="rpo" className="mt-6"><RpoTab refreshKey={refreshKey} /></TabsContent>
|
||||
<TabsContent value="history" className="mt-6"><VeeamHistoryTab refreshKey={refreshKey} /></TabsContent>
|
||||
<TabsContent value="status" className="mt-6"><VeeamStatusTab data={status} onSync={handleSync} syncing={syncing} tz={tz} /></TabsContent>
|
||||
<TabsContent value="rpo" className="mt-6"><RpoTab refreshKey={refreshKey} tz={tz} /></TabsContent>
|
||||
<TabsContent value="history" className="mt-6"><VeeamHistoryTab refreshKey={refreshKey} tz={tz} /></TabsContent>
|
||||
<TabsContent value="agents" className="mt-6"><AgentsTab refreshKey={refreshKey} /></TabsContent>
|
||||
<TabsContent value="alarms" className="mt-6"><AlarmsTab refreshKey={refreshKey} /></TabsContent>
|
||||
<TabsContent value="alarms" className="mt-6"><AlarmsTab refreshKey={refreshKey} tz={tz} /></TabsContent>
|
||||
<TabsContent value="schedules" className="mt-6"><SyncScheduler /></TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue