Files
floci-ui/packages/frontend/src/components/LambdaInvocationHistoryPanel.tsx
T
2026-09-24 22:34:04 -04:00

70 lines
4.1 KiB
TypeScript

import {Activity, RefreshCw, Timer, TriangleAlert} from 'lucide-react'
import {useQuery} from '@tanstack/react-query'
import {queryLogs} from '@/api/cloudProxyClient'
import type {CloudProvider} from '@/types/cloud'
interface LambdaInvocationHistoryPanelProps {
cloud: CloudProvider
functionName: string
runtimeReachable: boolean
}
interface InvocationRecord {
requestId: string
timestamp: string
durationMs: number
errorType: string | null
}
const HISTORY_QUERY = 'fields @timestamp, @message\n| filter @message like /REPORT RequestId:/\n| sort @timestamp desc\n| limit 20'
export function LambdaInvocationHistoryPanel({cloud, functionName, runtimeReachable}: LambdaInvocationHistoryPanelProps) {
const history = useQuery({
queryKey: ['lambda-invocation-history', cloud, functionName],
queryFn: async () => {
const endTime = Math.floor(Date.now() / 1000)
try {
const result = await queryLogs(cloud, `/aws/lambda/${functionName}`, {
queryString: HISTORY_QUERY,
startTime: endTime - 24 * 60 * 60,
endTime,
})
return result.rows.map(toInvocationRecord).filter((record): record is InvocationRecord => record !== null)
} catch (error) {
if (isMissingLogGroup(error)) return []
throw error
}
},
enabled: cloud === 'aws' && runtimeReachable,
})
const records = history.data ?? []
const errors = records.filter((record) => record.errorType !== null).length
const averageDuration = records.length === 0 ? null : records.reduce((sum, record) => sum + record.durationMs, 0) / records.length
return <section className="table-panel">
<div className="dynamic-stage-header">
<div><p className="eyebrow">Observability</p><h3><Activity size={15}/> Invocation history</h3><p className="muted compact-text">Real Lambda platform records from the last 24 hours.</p></div>
<button className="button" type="button" disabled={history.isFetching} onClick={() => history.refetch()}><RefreshCw size={13}/>{history.isFetching ? 'Refreshing' : 'Refresh'}</button>
</div>
<div className="apigateway-request-flow">
<div className="apigateway-flow-step"><Timer size={16}/><div><strong>{averageDuration === null ? 'No duration' : `${averageDuration.toFixed(1)} ms`}</strong><small>Average duration</small></div></div>
<div className="apigateway-flow-step"><TriangleAlert size={16}/><div><strong>{errors}</strong><small>Function errors</small></div></div>
<div className="apigateway-flow-step"><Activity size={16}/><div><strong>{records.length}</strong><small>Recorded invocations</small></div></div>
</div>
{history.isError && <p className="error-text compact-text">{history.error instanceof Error ? history.error.message : 'Unable to load invocation history.'}</p>}
{!history.isLoading && records.length === 0 && <p className="muted compact-text">No Lambda platform records yet. Invoke the function or trigger it from S3, then refresh.</p>}
{records.length > 0 && <table className="table"><thead><tr><th>Time</th><th>Request ID</th><th>Duration</th><th>Outcome</th></tr></thead><tbody>{records.map((record) => <tr key={record.requestId}><td>{record.timestamp}</td><td><code>{record.requestId}</code></td><td>{record.durationMs.toFixed(2)} ms</td><td>{record.errorType ? <span className="error-text">{record.errorType}</span> : 'Success'}</td></tr>)}</tbody></table>}
</section>
}
function toInvocationRecord(row: Record<string, string>): InvocationRecord | null {
const message = row['@message'] ?? ''
const match = /REPORT RequestId: (\S+) Duration: ([\d.]+) ms.*?(?: Error Type: (.+))?$/.exec(message)
if (!match) return null
return {requestId: match[1], timestamp: row['@timestamp'] ?? '', durationMs: Number(match[2]), errorType: match[3] ?? null}
}
function isMissingLogGroup(error: unknown): boolean {
return error instanceof Error && /log group does not exist|resource_not_found/i.test(error.message)
}