@@ -6,26 +6,80 @@
* Sprint 5: 從 /tickets/page.tsx 抽取
* 供原始頁面和整合頁面 (/operations) 共用
*
* 建立時間: 2026-04-09 (台北時區)
* 2026-05-31 Codex: 接上 Incident status-chain / timeline, 讓 Tickets 不再只是清單。
*/
import { useState , useEffect } from 'react'
import { useEffect , useMemo , useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import {
ArrowRight ,
GitBranch ,
ListChecks ,
RefreshCw ,
SearchCheck ,
ShieldCheck ,
TriangleAlert ,
} from 'lucide-react'
import { Link } from '@/i18n/routing'
import {
AwoooPStatusChainPanel ,
type AwoooPStatusChain ,
} from '@/components/awooop/status-chain'
import { cn } from '@/lib/utils'
const API_BASE = process . env . NEXT_PUBLIC_API_URL ? ? ''
interface Incident {
id : string
title : string
incident_id? : string
id? : string
title? : string | null
severity : string
status : string
signal_count? : number
proposal_count? : number
created_at : string
affected_service : string | null
updated_at? : string
affected_services? : string [ ]
affected_service? : string | null
}
interface IncidentListResponse {
incidents : Incident [ ]
total : number
count? : number
total? : number
}
interface IncidentTimelineEvent {
stage : string
status : string
title : string
description? : string | null
actor? : string | null
timestamp? : string | null
source_table? : string | null
data? : Record < string , unknown >
}
type IncidentTimelineStage = IncidentTimelineEvent & {
label : string
events? : IncidentTimelineEvent [ ]
}
interface IncidentTimelineResponse {
incident_id : string
title : string
status : string
severity : string
started_at? : string | null
updated_at? : string | null
resolved_at? : string | null
affected_services? : string [ ]
approval_ids? : string [ ]
timeline : IncidentTimelineStage [ ]
events : IncidentTimelineEvent [ ]
ascii_timeline : string
}
const SEV_COLOR : Record < string , string > = {
@@ -37,84 +91,483 @@ const SEV_COLOR: Record<string, string> = {
const STATUS_COLOR : Record < string , string > = {
open : '#cc2200' ,
investigating : '#F59E0B' ,
in_progress : '#F59E0B' ,
mitigating : '#4A90D9' ,
resolved : '#22C55E' ,
closed : '#87867f' ,
}
async function fetchJson < T > ( url : string , timeoutMs = 10 _000 ) : Promise < T | null > {
const controller = new AbortController ( )
const timeout = window . setTimeout ( ( ) = > controller . abort ( ) , timeoutMs )
try {
const response = await fetch ( url , {
cache : 'no-store' ,
signal : controller.signal ,
} )
if ( ! response . ok ) return null
return ( await response . json ( ) ) as T
} catch {
return null
} finally {
window . clearTimeout ( timeout )
}
}
function incidentId ( incident? : Incident | null ) {
return incident ? . incident_id ? ? incident ? . id ? ? ''
}
function incidentServices ( incident? : Incident | null ) {
const services = incident ? . affected_services ? . filter ( Boolean ) ? ? [ ]
if ( services . length > 0 ) return services
return incident ? . affected_service ? [ incident . affected_service ] : [ ]
}
function formatLocalTime ( value? : string | null ) {
if ( ! value ) return '--'
const date = new Date ( value )
if ( Number . isNaN ( date . getTime ( ) ) ) return '--'
return date . toLocaleString ( 'zh-TW' , {
timeZone : 'Asia/Taipei' ,
month : 'numeric' ,
day : 'numeric' ,
hour : '2-digit' ,
minute : '2-digit' ,
} )
}
function timelineStatusClass ( status? : string | null ) {
const normalized = String ( status ? ? '' ) . toLowerCase ( )
if ( normalized . includes ( 'success' ) || normalized . includes ( 'ok' ) || normalized . includes ( 'resolved' ) ) {
return 'border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]'
}
if ( normalized . includes ( 'fail' ) || normalized . includes ( 'block' ) || normalized . includes ( 'error' ) ) {
return 'border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]'
}
if ( normalized . includes ( 'warn' ) || normalized . includes ( 'pending' ) || normalized . includes ( 'investigating' ) ) {
return 'border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]'
}
return 'border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]'
}
function FocusedIncidentTruthPanel ( {
projectId ,
incident ,
incidentId ,
chain ,
timeline ,
loading ,
error ,
} : {
projectId : string
incident? : Incident | null
incidentId : string | null
chain : AwoooPStatusChain | null
timeline : IncidentTimelineResponse | null
loading : boolean
error : string | null
} ) {
const t = useTranslations ( 'tickets.truth' )
const stages = timeline ? . timeline ? . filter ( ( stage ) = > stage . status !== 'skipped' ) ? ? [ ]
const verifier = timeline ? . timeline ? . find ( ( stage ) = > stage . stage === 'verifier' )
const sourceCorrelation = chain ? . source_refs ? . correlation
const importantEvents = ( timeline ? . events ? ? [ ] )
. filter ( ( event ) = > (
event . source_table === 'automation_operation_log' ||
event . source_table === 'knowledge_entries' ||
event . source_table === 'incident_evidence' ||
event . source_table === 'alert_operation_log' ||
event . stage === 'executor' ||
event . stage === 'verifier' ||
event . stage === 'km' ||
event . stage === 'ai_router'
) )
. slice ( - 5 )
. reverse ( )
const title = incident ? . title || timeline ? . title || incidentServices ( incident ) . join ( ' / ' ) || t ( 'unknownTitle' )
const selectedIncidentId = incidentId || timeline ? . incident_id || incidentId
const encodedProjectId = encodeURIComponent ( projectId )
const encodedIncidentId = selectedIncidentId ? encodeURIComponent ( selectedIncidentId ) : ''
return (
< section className = "border border-[#e0ddd4] bg-white" aria-busy = { loading } >
< div className = "flex flex-wrap items-start justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3" >
< div className = "flex min-w-0 items-start gap-3" >
< span className = "flex h-9 w-9 shrink-0 items-center justify-center border border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]" >
< SearchCheck className = "h-4 w-4" aria-hidden = "true" / >
< / span >
< div className = "min-w-0" >
< h2 className = "text-sm font-semibold text-[#141413]" > { t ( 'title' ) } < / h2 >
< p className = "mt-1 truncate font-mono text-xs text-[#77736a]" >
{ selectedIncidentId || t ( 'emptyIncident' ) }
< / p >
< p className = "mt-1 truncate text-xs text-[#5f5b52]" title = { title } >
{ title }
< / p >
< / div >
< / div >
< div className = "flex flex-wrap items-center gap-2" >
{ loading ? (
< span className = "inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#5f5b52]" >
< RefreshCw className = "h-3.5 w-3.5 animate-spin" aria-hidden = "true" / >
{ t ( 'loading' ) }
< / span >
) : null }
{ selectedIncidentId ? (
< >
< Link
href = { ` /awooop/work-items?project_id= ${ encodedProjectId } &incident_id= ${ encodedIncidentId } ` as never }
className = "inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2.5 py-1 text-xs font-semibold text-[#141413] hover:border-[#d97757]"
>
{ t ( 'openWorkItems' ) }
< ArrowRight className = "h-3.5 w-3.5" aria-hidden = "true" / >
< / Link >
< Link
href = { ` /awooop/runs?project_id= ${ encodedProjectId } &incident_id= ${ encodedIncidentId } ` as never }
className = "inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2.5 py-1 text-xs font-semibold text-[#141413] hover:border-[#1f6feb]"
>
{ t ( 'openRuns' ) }
< ArrowRight className = "h-3.5 w-3.5" aria-hidden = "true" / >
< / Link >
< / >
) : null }
< / div >
< / div >
{ error ? (
< div className = "flex items-start gap-2 border-b border-[#ead9b4] bg-[#fff7e8] px-4 py-3 text-xs leading-5 text-[#8a5a08]" >
< TriangleAlert className = "mt-0.5 h-4 w-4 shrink-0" aria-hidden = "true" / >
{ error }
< / div >
) : null }
< div className = "grid gap-px bg-[#e0ddd4] md:grid-cols-4" >
< div className = "min-w-0 bg-white px-4 py-3" >
< p className = "text-xs font-semibold text-[#77736a]" > { t ( 'metrics.stages' ) } < / p >
< p className = "mt-2 font-mono text-xl font-semibold text-[#141413]" >
{ timeline ? stages . length : '--' }
< / p >
< / div >
< div className = "min-w-0 bg-white px-4 py-3" >
< p className = "text-xs font-semibold text-[#77736a]" > { t ( 'metrics.events' ) } < / p >
< p className = "mt-2 font-mono text-xl font-semibold text-[#141413]" >
{ timeline ? timeline . events . length : '--' }
< / p >
< / div >
< div className = "min-w-0 bg-white px-4 py-3" >
< p className = "text-xs font-semibold text-[#77736a]" > { t ( 'metrics.source' ) } < / p >
< p className = "mt-2 truncate font-mono text-sm font-semibold text-[#141413]" >
{ sourceCorrelation
? ` ${ sourceCorrelation . direct_ref_total ? ? 0 } / ${ sourceCorrelation . candidate_total ? ? 0 } / ${ sourceCorrelation . applied_link_total ? ? 0 } `
: '--' }
< / p >
< / div >
< div className = "min-w-0 bg-white px-4 py-3" >
< p className = "text-xs font-semibold text-[#77736a]" > { t ( 'metrics.verification' ) } < / p >
< span className = { cn ( 'mt-2 inline-flex border px-2 py-0.5 text-xs font-semibold' , timelineStatusClass ( verifier ? . status ? ? chain ? . verification ) ) } >
{ verifier ? . status ? ? chain ? . verification ? ? '--' }
< / span >
< / div >
< / div >
< AwoooPStatusChainPanel chain = { chain } className = "border-x-0 border-t-0" / >
< div className = "grid gap-px bg-[#e0ddd4] lg:grid-cols-[1.15fr_0.85fr]" >
< div className = "min-w-0 bg-white p-4" >
< div className = "flex items-center gap-2" >
< GitBranch className = "h-4 w-4 text-brand-accent" aria-hidden = "true" / >
< h3 className = "text-sm font-semibold text-[#141413]" > { t ( 'flowTitle' ) } < / h3 >
< / div >
{ timeline ? . ascii_timeline ? (
< p className = "mt-3 break-words border border-[#eee9dd] bg-[#faf9f3] px-3 py-2 font-mono text-xs leading-6 text-[#5f5b52]" >
{ timeline . ascii_timeline }
< / p >
) : (
< p className = "mt-3 text-sm text-[#77736a]" >
{ loading ? t ( 'loading' ) : t ( 'timelineEmpty' ) }
< / p >
) }
{ stages . length > 0 ? (
< div className = "mt-3 grid gap-2 md:grid-cols-2" >
{ stages . slice ( 0 , 6 ) . map ( ( stage ) = > (
< div key = { stage . stage } className = "min-w-0 border border-[#eee9dd] bg-white px-3 py-2" >
< div className = "flex items-center justify-between gap-2" >
< span className = "truncate text-xs font-semibold text-[#77736a]" > { stage . label } < / span >
< span className = { cn ( 'shrink-0 border px-2 py-0.5 text-[11px] font-semibold' , timelineStatusClass ( stage . status ) ) } >
{ stage . status }
< / span >
< / div >
< p className = "mt-1 truncate text-xs font-semibold text-[#141413]" title = { stage . title } >
{ stage . title }
< / p >
< / div >
) ) }
< / div >
) : null }
< / div >
< div className = "min-w-0 bg-white p-4" >
< div className = "flex items-center gap-2" >
< ListChecks className = "h-4 w-4 text-brand-accent" aria-hidden = "true" / >
< h3 className = "text-sm font-semibold text-[#141413]" > { t ( 'evidenceTitle' ) } < / h3 >
< / div >
< div className = "mt-3 grid gap-px border border-[#e0ddd4] bg-[#e0ddd4]" >
{ [
[ t ( 'executor' ) , timeline ? . timeline ? . find ( ( stage ) = > stage . stage === 'executor' ) ? . title ? ? chain ? . execution ? . latest_operation_type ? ? '--' ] ,
[ t ( 'ansible' ) , chain ? . execution ? . ansible ? . latest_playbook_path ? ? chain ? . execution ? . ansible ? . latest_catalog_id ? ? '--' ] ,
[ t ( 'mcp' ) , timeline ? . timeline ? . find ( ( stage ) = > stage . stage === 'investigator' ) ? . title ? ? '--' ] ,
[ t ( 'km' ) , timeline ? . timeline ? . find ( ( stage ) = > stage . stage === 'km' ) ? . title ? ? '--' ] ,
] . map ( ( [ label , value ] ) = > (
< div key = { label } className = "min-w-0 bg-white px-3 py-2" >
< p className = "text-xs font-semibold text-[#77736a]" > { label } < / p >
< p className = "mt-1 truncate font-mono text-xs text-[#141413]" title = { value } >
{ value }
< / p >
< / div >
) ) }
< / div >
{ importantEvents . length > 0 ? (
< div className = "mt-3 divide-y divide-[#eee9dd] border border-[#eee9dd]" >
{ importantEvents . map ( ( event , index ) = > (
< div key = { ` ${ event . stage } - ${ event . timestamp } - ${ index } ` } className = "px-3 py-2" >
< div className = "flex flex-wrap items-center gap-2" >
< span className = { cn ( 'border px-2 py-0.5 text-[11px] font-semibold' , timelineStatusClass ( event . status ) ) } >
{ event . status }
< / span >
< span className = "font-mono text-[11px] text-[#77736a]" > { event . source_table ? ? '--' } < / span >
< / div >
< p className = "mt-1 truncate text-xs font-semibold text-[#141413]" title = { event . title } >
{ event . title }
< / p >
< / div >
) ) }
< / div >
) : null }
< / div >
< / div >
< / section >
)
}
export function TicketsPanel() {
const t = useTranslations ( 'tickets' )
const searchParams = useSearchParams ( )
const queryIncidentId = searchParams . get ( 'incident_id' )
const projectId = searchParams . get ( 'project_id' ) ? ? 'awoooi'
const [ incidents , setIncidents ] = useState < Incident [ ] > ( [ ] )
const [ total , setTotal ] = useState ( 0 )
const [ loading , setLoading ] = useState ( true )
const [ error , setError ] = useState < string | null > ( null )
const [ chain , setChain ] = useState < AwoooPStatusChain | null > ( null )
const [ timeline , setTimeline ] = useState < IncidentTimelineResponse | null > ( null )
const [ detailLoading , setDetailLoading ] = useState ( false )
const [ detailError , setDetailError ] = useState < string | null > ( null )
useEffect ( ( ) = > {
fetch ( ` ${ API_BASE } /api/v1/incidents ` )
. then ( r = > r . json ( ) )
. then ( ( data : IncidentListResponse ) = > {
let cancelled = false
setLoading ( true )
setError ( null )
fetchJson < IncidentListResponse > ( ` ${ API_BASE } /api/v1/incidents ` , 12 _000 )
. then ( ( data ) = > {
if ( cancelled ) return
if ( ! data ) {
setError ( t ( 'error' ) )
setIncidents ( [ ] )
setTotal ( 0 )
return
}
setIncidents ( data . incidents ? ? [ ] )
setTotal ( data . total ? ? 0 )
setLoading ( false )
setTotal ( data . count ? ? data . total ? ? data . incidents ? . length ? ? 0 )
} )
. catch ( err = > { setError ( String ( err ) ) ; setLoading ( false ) } )
} , [ ] )
. catch ( ( err ) = > {
if ( ! cancelled ) setError ( err instanceof Error ? err.message : t ( 'error' ) )
} )
. finally ( ( ) = > {
if ( ! cancelled ) setLoading ( false )
} )
return ( ) = > {
cancelled = true
}
} , [ t ] )
const selectedIncidentId = useMemo ( ( ) = > {
if ( queryIncidentId ) return queryIncidentId
return incidentId ( incidents [ 0 ] ) || null
} , [ incidents , queryIncidentId ] )
const selectedIncident = useMemo (
( ) = > incidents . find ( ( incident ) = > incidentId ( incident ) === selectedIncidentId ) ? ? null ,
[ incidents , selectedIncidentId ]
)
useEffect ( ( ) = > {
let cancelled = false
if ( ! selectedIncidentId ) {
setChain ( null )
setTimeline ( null )
setDetailError ( null )
setDetailLoading ( false )
return ( ) = > {
cancelled = true
}
}
const targetIncidentId = selectedIncidentId
async function loadIncidentTruth() {
setDetailLoading ( true )
setDetailError ( null )
const encodedProjectId = encodeURIComponent ( projectId )
const encodedIncidentId = encodeURIComponent ( targetIncidentId )
const [ statusChain , incidentTimeline ] = await Promise . all ( [
fetchJson < AwoooPStatusChain > (
` ${ API_BASE } /api/v1/platform/status-chain?project_id= ${ encodedProjectId } &incident_id= ${ encodedIncidentId } ` ,
12 _000
) ,
fetchJson < IncidentTimelineResponse > (
` ${ API_BASE } /api/v1/incidents/ ${ encodedIncidentId } /timeline ` ,
12 _000
) ,
] )
if ( cancelled ) return
setChain ( statusChain )
setTimeline ( incidentTimeline )
if ( ! statusChain && ! incidentTimeline ) {
setDetailError ( t ( 'truth.loadFailed' ) )
}
setDetailLoading ( false )
}
loadIncidentTruth ( )
return ( ) = > {
cancelled = true
}
} , [ projectId , selectedIncidentId , t ] )
return (
< div style = { { padding : '24px' , background : '#f5f4ed' , minHeight : '100%' } } >
< div style = { { marginBottom : '20px' } } >
< h1 style = { { fontSize : 18 , fontWeight : 700 , color : '#141413' , margin : 0 , fontFamily : 'var(--font-body), monospace' } } > { t ( 'title' ) } < / h1 >
< p style = { { fontSize : 12 , color : '#87867f' , margin : '4px 0 0' , fontFamily : 'var(--font-body), monospace' } } > { t ( 'subtitle' ) } < / p >
< div className = "min-h-full space-y-5 bg-[#f5f4ed] p-6" >
< div >
< h1 className = "m-0 text-lg font-bold text-[#141413]" > { t ( 'title' ) } < / h1 >
< p className = "mt-1 text-xs text-[#87867f]" > { t ( 'subtitle' ) } < / p >
< / div >
< div style = { { background : '#fff' , border : '0.5px solid #e0ddd4' , borderRadius : 12 , overflow : 'hidden' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , fontSize : 14 , fontWeight : 700 , padding : '10px 14px' , borderBottom : '0.5px solid #e0ddd4' , background : '#faf9f3' , fontFamily : 'var(--font-body), monospace' , color : '#141413' } } >
< span style = { { width : 6 , height : 6 , borderRadius : '50%' , background : '#d97757' , display : 'inline-block' } } / >
{ t ( 'title' ) } ( { loading ? '...' : total } )
< FocusedIncidentTruthPanel
projectId = { projectId }
incident = { selectedIncident }
incidentId = { selectedIncidentId }
chain = { chain }
timeline = { timeline }
loading = { detailLoading }
error = { detailError }
/ >
< section className = "overflow-hidden border border-[#e0ddd4] bg-white" >
< div className = "flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3" >
< div className = "flex items-center gap-2 text-sm font-bold text-[#141413]" >
< span className = "h-1.5 w-1.5 rounded-full bg-[#d97757]" / >
{ t ( 'title' ) } ( { loading ? '...' : total } )
< / div >
< span className = "inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#5f5b52]" >
< ShieldCheck className = "h-3.5 w-3.5" aria-hidden = "true" / >
{ t ( 'readOnly' ) }
< / span >
< / div >
{ loading ? (
< div style = { { padding : '32px' , textAlign : 'center' , color : '#87867f' , fontFamily : 'var(--font-body), monospace' , fontSize : 13 } } > { t ( 'loading' ) } < / div >
< div className = "px-8 py-10 text-center text-sm text-[#87867f]" > { t ( 'loading' ) } < / div >
) : error ? (
< div style = { { padding : '32px' , textAlign : 'center' , color : '#cc2200' , fontFamily : 'var(--font-body), monospace' , fontSize : 13 } } > { t ( ' error' ) } < / div >
< div className = "px-8 py-10 text-center text-sm text-[#cc2200]" > { error } < / div >
) : incidents . length === 0 ? (
< div style = { { padding : '32px' , textAlign : 'center' , color : '#87867f' , fontFamily : 'var(--font-body), monospace' , fontSize : 13 } } > { t ( 'noTickets' ) } < / div >
< div className = "px-8 py-10 text-center text-sm text-[#87867f]" > { t ( 'noTickets' ) } < / div >
) : (
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontFamily : 'var(--font-body), monospace' , fontSize : 13 } } >
< thead >
< tr style = { { background : '#faf9f3' } } >
{ [ t ( 'id' ) , t ( 'title_col' ) , t ( 'priority' ) , t ( 'status' ) , t ( 'createdAt' ) ] . map ( col = > (
< th key = { col } style = { { padding : '8px 14px' , textAlign : 'left' , fontWeight : 600 , color : '#87867f' , borderBottom : '0.5px solid #e0ddd4' , fontSize : 11 } } > { col } < / th >
) ) }
< / tr >
< / thead >
< tbody >
{ incidents . map ( ( inc ) = > (
< tr key = { inc . id } style = { { borderBottom : '0.5px solid #f0ede4' } } >
< td style = { { padding : '8px 14px' , color : '#87867f' , fontSize : 11 , fontFamily : 'monospace' } } > { inc . id . slice ( 0 , 8 ) } < / td >
< td style = { { padding : '8px 14px' , fontWeight : 500 , color : '#141413' , maxWidth : 300 } } >
< div style = { { overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } > { inc . title } < / div >
{ inc . affected_service && (
< div style = { { fontSize : 11 , color : '#87867f' , marginTop : 2 } } > { inc . affected_service } < / div >
) }
< / td >
< td style = { { padding : '8px 14px' } } >
< span style = { { fontSize : 11 , fontWeight : 700 , color : SEV_COLOR [ inc . severity ] ? ? '#87867f' , background : ` ${ SEV_COLOR [ inc . severity ] ? ? '#87867f' } 18 ` , border : ` 0.5px solid ${ SEV_COLOR [ inc . severity ] ? ? '#87867f' } 40 ` , borderRadius : 4 , padding : '1px 6px' } } >
{ inc . severity }
< / span >
< / td >
< td style = { { padding : '8px 14px' } } >
< span style = { { fontSize : 11 , fontWeight : 600 , color : STATUS_COLOR [ inc . status ] ? ? '#87867f' } } >
{ inc . status }
< / span >
< / td >
< td style = { { padding : '8px 14px' , color : '#87867f' , fontSize : 11 } } >
{ new Date ( inc . created_at ) . toLocaleString ( 'zh-TW' , { timeZone : 'Asia/Taipei' , month : 'numeric' , day : 'numeric' , hour : '2-digit' , minute : '2-digit' } ) }
< / td >
< div className = "overflow-x-auto" >
< table className = "w-full border-collapse text-sm" >
< thead >
< tr className = "bg-[#faf9f3]" >
{ [ t ( 'id' ) , t ( 'title_col' ) , t ( 'priority' ) , t ( 'status' ) , t ( 'signals' ) , t ( 'createdAt' ) , t ( 'actions' ) ] . map ( ( col ) = > (
< th key = { col } className = "border-b border-[#e0ddd4] px-4 py-3 text-left text-xs font-semibold text-[#87867f]" >
{ col }
< / th >
) ) }
< / tr >
) ) }
</ tbody >
< / table >
< / thead >
< tbody >
{ incidents . map ( ( incident ) = > {
const rowIncidentId = incidentId ( incident )
const isSelected = rowIncidentId === selectedIncidentId
const services = incidentServices ( incident )
const title = incident . title || services . join ( ' / ' ) || t ( 'unknownService' )
return (
< tr
key = { rowIncidentId || ` ${ incident . created_at } - ${ title } ` }
className = { cn (
'border-b border-[#f0ede4]' ,
isSelected && 'bg-[#fff7e8]'
) }
>
< td className = "px-4 py-3 align-top font-mono text-xs text-[#5f5b52]" >
< Link
href = { ` /tickets?project_id= ${ encodeURIComponent ( projectId ) } &incident_id= ${ encodeURIComponent ( rowIncidentId ) } ` as never }
className = "inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 font-semibold text-[#141413] hover:border-[#d97757]"
>
{ rowIncidentId . slice ( 0 , 16 ) }
< ArrowRight className = "h-3 w-3" aria-hidden = "true" / >
< / Link >
< / td >
< td className = "max-w-[360px] px-4 py-3 align-top font-medium text-[#141413]" >
< div className = "truncate" title = { title } > { title } < / div >
{ services . length > 1 ? (
< div className = "mt-1 truncate text-xs text-[#87867f]" >
{ t ( 'serviceCount' , { count : services.length } ) }
< / div >
) : null }
< / td >
< td className = "px-4 py-3 align-top" >
< span
style = { {
color : SEV_COLOR [ incident . severity ] ? ? '#87867f' ,
background : ` ${ SEV_COLOR [ incident . severity ] ? ? '#87867f' } 18 ` ,
borderColor : ` ${ SEV_COLOR [ incident . severity ] ? ? '#87867f' } 40 ` ,
} }
className = "inline-flex border px-2 py-0.5 text-xs font-bold"
>
{ incident . severity }
< / span >
< / td >
< td className = "px-4 py-3 align-top" >
< span
style = { { color : STATUS_COLOR [ incident . status ] ? ? '#87867f' } }
className = "text-xs font-semibold"
>
{ incident . status }
< / span >
< / td >
< td className = "px-4 py-3 align-top font-mono text-xs text-[#5f5b52]" >
{ t ( 'signalProposal' , {
signals : incident.signal_count ? ? 0 ,
proposals : incident.proposal_count ? ? 0 ,
} ) }
< / td >
< td className = "px-4 py-3 align-top text-xs text-[#87867f]" >
{ formatLocalTime ( incident . created_at ) }
< / td >
< td className = "px-4 py-3 align-top" >
< Link
href = { ` /awooop/work-items?project_id= ${ encodeURIComponent ( projectId ) } &incident_id= ${ encodeURIComponent ( rowIncidentId ) } ` as never }
className = "inline-flex items-center gap-1.5 border border-[#d8d3c7] bg-white px-2 py-1 text-xs font-semibold text-[#2e2b26] hover:border-[#1f6feb] hover:bg-[#edf4ff] hover:text-[#0f4fa8]"
>
< SearchCheck className = "h-3.5 w-3.5" aria-hidden = "true" / >
{ t ( 'openTruth' ) }
< / Link >
< / td >
< / tr >
)
} ) }
< / tbody >
< / table >
< / div >
) }
< / div >
< / section >
< / div >
)
}