Files
floci-ui/packages/frontend/src/components/KinesisPanel.tsx
T
2026-10-05 11:06:57 -04:00

45 lines
9.1 KiB
TypeScript

import {useEffect, useState} from 'react'
import {Loader2, Plus, RefreshCw, Trash2} from 'lucide-react'
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {createLambdaEventSourceMapping, deleteLambdaEventSourceMapping, listCloudResources, listKinesisRecords, listLambdaEventSourceMappings, putKinesisRecord, updateLambdaEventSourceMapping, type LambdaEventSourceMappingOverview} from '@/api/cloudProxyClient'
import type {CloudProvider} from '@/types/cloud'
import type {CloudResource} from '@/types/resource'
interface KinesisPanelProps {cloud: CloudProvider; resource?: CloudResource; runtimeReachable: boolean; mode: 'stream' | 'lambda'}
export function KinesisPanel({cloud, resource, runtimeReachable, mode}: KinesisPanelProps) {
const queryClient = useQueryClient()
const [partitionKey, setPartitionKey] = useState('console')
const [data, setData] = useState('{}')
const [functionName, setFunctionName] = useState('')
const [streamArn, setStreamArn] = useState('')
const [batchSize, setBatchSize] = useState('100')
const [startingPosition, setStartingPosition] = useState<'TRIM_HORIZON' | 'LATEST'>('LATEST')
const [deleteTarget, setDeleteTarget] = useState<LambdaEventSourceMappingOverview | null>(null)
const streamsQuery = useQuery({queryKey: ['cloud-resources', cloud, 'kinesis'], queryFn: ({signal}) => listCloudResources(cloud, 'kinesis', undefined, signal), enabled: mode === 'lambda' && cloud === 'aws' && runtimeReachable})
const functionsQuery = useQuery({queryKey: ['cloud-resources', cloud, 'serverless'], queryFn: ({signal}) => listCloudResources(cloud, 'serverless', undefined, signal), enabled: mode === 'stream' && cloud === 'aws' && runtimeReachable})
const streamName = mode === 'stream' ? resource?.id : undefined
const sourceArn = mode === 'stream' ? resource?.metadata.arn as string | undefined : streamArn
const lambdaName = mode === 'lambda' ? resource?.name : functionName
const recordsQuery = useQuery({queryKey: ['kinesis-records', cloud, streamName], queryFn: ({signal}) => listKinesisRecords(cloud, streamName!, signal), enabled: mode === 'stream' && Boolean(streamName) && runtimeReachable})
const mappingsQuery = useQuery({queryKey: ['kinesis-lambda-mappings', cloud, sourceArn, lambdaName], queryFn: ({signal}) => listLambdaEventSourceMappings(cloud, mode === 'stream' ? {eventSourceArn: sourceArn} : {functionName: lambdaName}, signal), enabled: Boolean(sourceArn || lambdaName) && runtimeReachable})
useEffect(() => { setFunctionName(''); setStreamArn(''); setDeleteTarget(null) }, [resource?.id])
const putMutation = useMutation({mutationFn: () => putKinesisRecord(cloud, streamName!, {partitionKey, data}), onSuccess: () => queryClient.invalidateQueries({queryKey: ['kinesis-records', cloud, streamName]})})
const createMutation = useMutation({mutationFn: () => createLambdaEventSourceMapping(cloud, {functionName: lambdaName!, eventSourceArn: sourceArn!, batchSize: Number(batchSize), startingPosition}), onSuccess: () => queryClient.invalidateQueries({queryKey: ['kinesis-lambda-mappings']})})
const updateMutation = useMutation({mutationFn: ({id, enabled}: {id: string; enabled: boolean}) => updateLambdaEventSourceMapping(cloud, id, {enabled}), onSuccess: () => queryClient.invalidateQueries({queryKey: ['kinesis-lambda-mappings']})})
const deleteMutation = useMutation({mutationFn: (id: string) => deleteLambdaEventSourceMapping(cloud, id), onSuccess: () => { setDeleteTarget(null); queryClient.invalidateQueries({queryKey: ['kinesis-lambda-mappings']}) }})
if (!resource || (mode === 'stream' && resource.service !== 'kinesis') || (mode === 'lambda' && resource.type !== 'lambda')) return null
const canCreate = Boolean(lambdaName && sourceArn) && Number.isInteger(Number(batchSize)) && Number(batchSize) >= 1 && Number(batchSize) <= 10_000
const mappings = (mappingsQuery.data ?? []).filter((mapping) => mapping.eventSourceArn.includes(':kinesis:'))
return <div className="kinesis-records-grid">
<section className="table-panel">
{mode === 'stream' && <><div className="dynamic-stage-header"><div><p className="eyebrow">Records</p><h3>Produce records</h3></div></div><form className="inspector-action-form apigateway-route-form" onSubmit={(event) => { event.preventDefault(); putMutation.mutate() }}><label className="dynamic-field"><span>Partition key</span><input className="input" value={partitionKey} onChange={(event) => setPartitionKey(event.target.value)} required/></label><label className="dynamic-field"><span>Data</span><textarea className="input" value={data} onChange={(event) => setData(event.target.value)} required/></label><button className="button primary" type="submit" disabled={!partitionKey || putMutation.isPending}>{putMutation.isPending ? <Loader2 size={13}/> : <Plus size={13}/>} Put record</button></form>{putMutation.isError && <p className="error-text compact-text">{putMutation.error instanceof Error ? putMutation.error.message : 'Unable to put record.'}</p>}</>}
<div className="dynamic-stage-header"><div><p className="eyebrow">Event Source</p><h3>Add Kinesis trigger</h3><p className="muted compact-text">Configure Lambda polling through an AWS Lambda event source mapping.</p></div></div>
<form className="inspector-action-form apigateway-route-form" onSubmit={(event) => { event.preventDefault(); if (canCreate) createMutation.mutate() }}>
{mode === 'stream' ? <label className="dynamic-field"><span>Lambda function</span><select className="input" value={functionName} onChange={(event) => setFunctionName(event.target.value)} required><option value="">Select a function</option>{functionsQuery.data?.filter((candidate) => candidate.type === 'lambda').map((fn) => <option key={fn.id} value={fn.name}>{fn.name}</option>)}</select></label> : <label className="dynamic-field"><span>Kinesis stream</span><select className="input" value={streamArn} onChange={(event) => setStreamArn(event.target.value)} required><option value="">Select a stream</option>{streamsQuery.data?.map((stream) => typeof stream.metadata.arn === 'string' && <option key={stream.id} value={stream.metadata.arn}>{stream.name}</option>)}</select></label>}
<label className="dynamic-field"><span>Starting position</span><select className="input" value={startingPosition} onChange={(event) => setStartingPosition(event.target.value as 'TRIM_HORIZON' | 'LATEST')}><option value="LATEST">Latest</option><option value="TRIM_HORIZON">Trim horizon</option></select></label><label className="dynamic-field"><span>Batch size</span><input className="input" type="number" min="1" max="10000" value={batchSize} onChange={(event) => setBatchSize(event.target.value)} required/></label><button className="button primary" type="submit" disabled={!canCreate || createMutation.isPending}>{createMutation.isPending ? <Loader2 size={13}/> : <Plus size={13}/>} Add trigger</button>
</form>{createMutation.isError && <p className="error-text compact-text">{createMutation.error instanceof Error ? createMutation.error.message : 'Unable to add Kinesis trigger.'}</p>}<div className="apigateway-route-explorer"><div className="dynamic-stage-header"><div><p className="eyebrow">Configured Triggers</p><h3>Kinesis event source mappings</h3></div></div>{mappingsQuery.isSuccess && mappings.length === 0 && <p className="muted compact-text">No Kinesis triggers configured.</p>}{mappings.map((mapping) => <div className="apigateway-route-item" key={mapping.id}><div><code>{mapping.eventSourceArn}</code><small>{mapping.functionName ?? mapping.functionArn ?? 'Lambda'} · Batch size: {mapping.batchSize ?? 'default'} · {mapping.state ?? 'unknown'}</small></div>{deleteTarget?.id === mapping.id ? <div className="apigateway-route-confirm"><span>Remove trigger?</span><button className="button danger compact" type="button" onClick={() => deleteMutation.mutate(mapping.id)}>Remove</button><button className="button compact" type="button" onClick={() => setDeleteTarget(null)}>Cancel</button></div> : <div><button className="button compact" type="button" onClick={() => updateMutation.mutate({id: mapping.id, enabled: mapping.state !== 'Enabled'})}>{mapping.state === 'Enabled' ? 'Disable' : 'Enable'}</button><button className="icon-btn" type="button" aria-label="Remove Kinesis trigger" onClick={() => setDeleteTarget(mapping)}><Trash2 size={13}/></button></div>}</div>)}</div></section>
{mode === 'stream' && <section className="table-panel"><div className="apigateway-route-explorer"><div className="dynamic-stage-header"><div><p className="eyebrow">Inspection</p><h3>Stream records</h3></div><button className="button compact" type="button" onClick={() => recordsQuery.refetch()}><RefreshCw size={13}/> Refresh</button></div>{recordsQuery.data?.length === 0 && <p className="muted compact-text">No records in this stream.</p>}<div className="kinesis-record-list">{recordsQuery.data?.map((record) => <div className="apigateway-route-item" key={record.sequenceNumber}><div><code>{record.partitionKey ?? 'no partition key'}</code><small>{record.sequenceNumber}</small><pre className="compact-text">{record.data}</pre></div></div>)}</div></div></section>}
</div>
}