Add Ivanti Workflows panel with API key auth and SQLite cache
- New panel below Archer tickets showing workflow count and list - Backend proxies platform4.risksense.com workflowBatch/search via x-api-key - SQLite cache table (ivanti_sync_state) stores latest sync result - Auto-syncs on server startup if >24h stale, then every 24h via setInterval - POST /api/ivanti/workflows/sync for on-demand sync with spinner feedback - GET /api/ivanti/workflows returns cached data instantly (no live API call) - Displays id.value, name, currentState, type, createdOn per workflow - Shows last-synced timestamp and error messages inline - IVANTI_SKIP_TLS flag for Charter SSL proxy environments Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown, Shield } from 'lucide-react';
|
||||
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown, Shield, Activity } from 'lucide-react';
|
||||
import { useAuth } from './contexts/AuthContext';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import UserMenu from './components/UserMenu';
|
||||
@@ -221,6 +221,15 @@ export default function App() {
|
||||
});
|
||||
const [addArcherTicketContext, setAddArcherTicketContext] = useState(null); // { cve_id, vendor }
|
||||
|
||||
// Ivanti workflows state
|
||||
const [ivantiTotal, setIvantiTotal] = useState(null);
|
||||
const [ivantiWorkflows, setIvantiWorkflows] = useState([]);
|
||||
const [ivantiSyncedAt, setIvantiSyncedAt] = useState(null);
|
||||
const [ivantiSyncStatus, setIvantiSyncStatus] = useState(null);
|
||||
const [ivantiSyncError, setIvantiSyncError] = useState(null);
|
||||
const [ivantiLoading, setIvantiLoading] = useState(false);
|
||||
const [ivantiSyncing, setIvantiSyncing] = useState(false);
|
||||
|
||||
const toggleCVEExpand = (cveId) => {
|
||||
setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] }));
|
||||
};
|
||||
@@ -333,6 +342,43 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const applyIvantiState = (data) => {
|
||||
setIvantiTotal(data.total ?? 0);
|
||||
setIvantiWorkflows(data.workflows || []);
|
||||
setIvantiSyncedAt(data.synced_at || null);
|
||||
setIvantiSyncStatus(data.sync_status || null);
|
||||
setIvantiSyncError(data.error_message || null);
|
||||
};
|
||||
|
||||
const fetchIvantiWorkflows = async () => {
|
||||
setIvantiLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/ivanti/workflows`, { credentials: 'include' });
|
||||
const data = await response.json();
|
||||
if (response.ok) applyIvantiState(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading Ivanti workflows:', err);
|
||||
} finally {
|
||||
setIvantiLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const syncIvantiWorkflows = async () => {
|
||||
setIvantiSyncing(true);
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/ivanti/workflows/sync`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) applyIvantiState(data);
|
||||
} catch (err) {
|
||||
console.error('Error syncing Ivanti workflows:', err);
|
||||
} finally {
|
||||
setIvantiSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDocuments = async (cveId, vendor) => {
|
||||
const key = `${cveId}-${vendor}`;
|
||||
if (cveDocuments[key]) return;
|
||||
@@ -861,6 +907,7 @@ export default function App() {
|
||||
fetchVendors();
|
||||
fetchJiraTickets();
|
||||
fetchArcherTickets();
|
||||
fetchIvantiWorkflows();
|
||||
fetchKnowledgeBaseArticles();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -2347,6 +2394,97 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ivanti Workflows */}
|
||||
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #0D9488'}} className="rounded-lg">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#0D9488', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(13, 148, 136, 0.4)' }}>
|
||||
<Activity className="w-5 h-5" />
|
||||
Ivanti Workflows
|
||||
</h2>
|
||||
<button
|
||||
onClick={syncIvantiWorkflows}
|
||||
disabled={ivantiSyncing || ivantiLoading}
|
||||
className="intel-button intel-button-primary flex items-center gap-1 text-xs px-2 py-1"
|
||||
title="Sync now"
|
||||
>
|
||||
<RefreshCw className={`w-3 h-3 ${ivantiSyncing ? 'animate-spin' : ''}`} />
|
||||
{ivantiSyncing ? 'Syncing…' : 'Sync'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Last synced line */}
|
||||
<div className="text-xs text-gray-500 font-mono mb-4">
|
||||
{ivantiSyncedAt
|
||||
? `Synced ${new Date(ivantiSyncedAt.replace(' ', 'T') + 'Z').toLocaleString()}`
|
||||
: 'Never synced'}
|
||||
</div>
|
||||
|
||||
{ivantiLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<Loader className="w-6 h-6 text-teal-400 animate-spin mx-auto mb-2" />
|
||||
<p className="text-xs text-gray-400 font-mono">Loading...</p>
|
||||
</div>
|
||||
) : ivantiSyncStatus === 'error' ? (
|
||||
<>
|
||||
<div className="text-center mb-3">
|
||||
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#0D9488', textShadow: '0 0 16px rgba(13, 148, 136, 0.4)' }}>
|
||||
{ivantiTotal ?? '—'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-2 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.3)' }}>
|
||||
<AlertCircle className="w-4 h-4 text-intel-danger mt-0.5 shrink-0" />
|
||||
<p className="text-xs text-red-400 font-mono">{ivantiSyncError}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center mb-3">
|
||||
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#0D9488', textShadow: '0 0 16px rgba(13, 148, 136, 0.4)' }}>
|
||||
{ivantiSyncStatus === 'never' ? '—' : (ivantiTotal ?? '—')}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">Total Workflows</div>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{ivantiWorkflows.slice(0, 10).map((wf, idx) => (
|
||||
<div key={wf.uuid ?? idx} style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(13, 148, 136, 0.25)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.03)' }}>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<span className="font-mono text-xs font-semibold text-teal-300">
|
||||
{wf.id?.value || wf.uuid?.slice(0, 8)}
|
||||
</span>
|
||||
{wf.currentState && (
|
||||
<span style={{ fontSize: '0.65rem', padding: '0.2rem 0.4rem', borderRadius: '0.25rem', background: 'rgba(13, 148, 136, 0.2)', border: '1px solid #0D9488', color: '#0D9488', whiteSpace: 'nowrap', fontFamily: 'monospace' }}>
|
||||
{wf.currentState}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-white truncate mb-1">{wf.name}</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{wf.type && (
|
||||
<span className="text-xs text-gray-400 font-mono">{wf.type.replace(/_/g, ' ')}</span>
|
||||
)}
|
||||
{wf.createdOn && (
|
||||
<span className="text-xs text-gray-500">{wf.createdOn}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ivantiSyncStatus !== 'never' && ivantiTotal === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<CheckCircle className="w-8 h-8 text-intel-success mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-400 italic font-mono">No workflows found</p>
|
||||
</div>
|
||||
)}
|
||||
{ivantiSyncStatus === 'never' && (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-xs text-gray-500 font-mono">Click Sync to load workflow data</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* End Right Panel */}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user