245 lines
6.4 KiB
TypeScript
245 lines
6.4 KiB
TypeScript
import { define } from '@because/forge'
|
|
import type { App, LogLine as LogLineType } from '../../shared/types'
|
|
import { getLogDates, getLogsForDate } from '../api'
|
|
import { LogLine, LogsContainer, LogTime, Section, SectionTitle } from '../styles'
|
|
import { theme } from '../themes'
|
|
import { update } from '../update'
|
|
|
|
type LogsState = {
|
|
dates: string[]
|
|
historicalLogs: string[]
|
|
loadingDates: boolean
|
|
loadingLogs: boolean
|
|
searchFilter: string
|
|
selectedDate: string
|
|
}
|
|
|
|
const logsState = new Map<string, LogsState>()
|
|
|
|
let currentApp: App | null = null
|
|
|
|
const getState = (appName: string): LogsState => {
|
|
if (!logsState.has(appName)) {
|
|
logsState.set(appName, {
|
|
dates: [],
|
|
historicalLogs: [],
|
|
loadingDates: false,
|
|
loadingLogs: false,
|
|
searchFilter: '',
|
|
selectedDate: 'live',
|
|
})
|
|
}
|
|
return logsState.get(appName)!
|
|
}
|
|
|
|
const LogsHeader = define('LogsHeader', {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
marginBottom: 12,
|
|
})
|
|
|
|
const LogsControls = define('LogsControls', {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 8,
|
|
})
|
|
|
|
const SmallSelect = define('SmallSelect', {
|
|
base: 'select',
|
|
padding: '4px 8px',
|
|
background: theme('colors-bgSubtle'),
|
|
border: `1px solid ${theme('colors-border')}`,
|
|
borderRadius: '4px',
|
|
color: theme('colors-text'),
|
|
fontSize: 12,
|
|
cursor: 'pointer',
|
|
selectors: {
|
|
'&:focus': {
|
|
outline: 'none',
|
|
borderColor: theme('colors-primary'),
|
|
},
|
|
},
|
|
})
|
|
|
|
const SmallInput = define('SmallInput', {
|
|
base: 'input',
|
|
padding: '4px 8px',
|
|
background: theme('colors-bgSubtle'),
|
|
border: `1px solid ${theme('colors-border')}`,
|
|
borderRadius: '4px',
|
|
color: theme('colors-text'),
|
|
fontSize: 12,
|
|
width: 120,
|
|
selectors: {
|
|
'&:focus': {
|
|
outline: 'none',
|
|
borderColor: theme('colors-primary'),
|
|
},
|
|
'&::placeholder': {
|
|
color: theme('colors-textFaint'),
|
|
},
|
|
},
|
|
})
|
|
|
|
function filterLogs<T extends string | LogLineType>(
|
|
logs: T[],
|
|
filter: string,
|
|
getText: (log: T) => string
|
|
): T[] {
|
|
if (!filter) return logs
|
|
const lower = filter.toLowerCase()
|
|
return logs.filter(log => getText(log).toLowerCase().includes(lower))
|
|
}
|
|
|
|
function LogsContent() {
|
|
if (!currentApp) return null
|
|
|
|
const state = getState(currentApp.name)
|
|
const isLive = state.selectedDate === 'live'
|
|
const filteredLiveLogs = filterLogs(currentApp.logs ?? [], state.searchFilter, l => l.text)
|
|
const filteredHistoricalLogs = filterLogs(state.historicalLogs, state.searchFilter, l => l)
|
|
|
|
return (
|
|
<>
|
|
{state.loadingLogs && (
|
|
<LogLine>
|
|
<LogTime>--:--:--</LogTime>
|
|
<span style={{ color: theme('colors-textFaint') }}>Loading...</span>
|
|
</LogLine>
|
|
)}
|
|
{!state.loadingLogs && isLive && (
|
|
filteredLiveLogs.length ? (
|
|
filteredLiveLogs.map((line, i) => (
|
|
<LogLine key={i}>
|
|
<LogTime>{new Date(line.time).toLocaleTimeString()}</LogTime>
|
|
<span>{line.text}</span>
|
|
</LogLine>
|
|
))
|
|
) : (
|
|
<LogLine>
|
|
<LogTime>--:--:--</LogTime>
|
|
<span style={{ color: theme('colors-textFaint') }}>
|
|
{state.searchFilter ? 'No matching logs' : 'No logs yet'}
|
|
</span>
|
|
</LogLine>
|
|
)
|
|
)}
|
|
{!state.loadingLogs && !isLive && (
|
|
filteredHistoricalLogs.length ? (
|
|
filteredHistoricalLogs.map((line, i) => (
|
|
<LogLine key={i}>
|
|
<span style={{ color: theme('colors-textFaintest'), marginRight: 12 }}>
|
|
{line.match(/^\[([^\]]+)\]/)?.[1]?.split('T')[1]?.slice(0, 8) ?? '--:--:--'}
|
|
</span>
|
|
<span>{line.replace(/^\[[^\]]+\] \[[^\]]+\] /, '')}</span>
|
|
</LogLine>
|
|
))
|
|
) : (
|
|
<LogLine>
|
|
<LogTime>--:--:--</LogTime>
|
|
<span style={{ color: theme('colors-textFaint') }}>
|
|
{state.searchFilter ? 'No matching logs' : 'No logs for this date'}
|
|
</span>
|
|
</LogLine>
|
|
)
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
const updateLogsContent = () =>
|
|
update('#logs-content', <LogsContent />)
|
|
|
|
async function loadDates(appName: string) {
|
|
const state = getState(appName)
|
|
if (state.loadingDates || state.dates.length > 0) return
|
|
|
|
state.loadingDates = true
|
|
|
|
try {
|
|
const dates = await getLogDates(appName)
|
|
state.dates = dates
|
|
} catch (e) {
|
|
console.error('Failed to load log dates:', e)
|
|
} finally {
|
|
state.loadingDates = false
|
|
}
|
|
}
|
|
|
|
async function loadHistoricalLogs(appName: string, date: string) {
|
|
const state = getState(appName)
|
|
state.loadingLogs = true
|
|
updateLogsContent()
|
|
|
|
try {
|
|
const logs = await getLogsForDate(appName, date)
|
|
state.historicalLogs = logs
|
|
} catch (e) {
|
|
console.error('Failed to load logs:', e)
|
|
state.historicalLogs = []
|
|
} finally {
|
|
state.loadingLogs = false
|
|
updateLogsContent()
|
|
}
|
|
}
|
|
|
|
function handleDateChange(appName: string, date: string) {
|
|
const state = getState(appName)
|
|
state.selectedDate = date
|
|
state.historicalLogs = []
|
|
|
|
if (date !== 'live') {
|
|
loadHistoricalLogs(appName, date)
|
|
} else {
|
|
updateLogsContent()
|
|
}
|
|
}
|
|
|
|
function handleSearchChange(appName: string, value: string) {
|
|
const state = getState(appName)
|
|
state.searchFilter = value
|
|
updateLogsContent()
|
|
}
|
|
|
|
export function LogsSection({ app }: { app: App }) {
|
|
currentApp = app
|
|
const state = getState(app.name)
|
|
|
|
// Load dates on first render
|
|
if (state.dates.length === 0 && !state.loadingDates) {
|
|
loadDates(app.name)
|
|
}
|
|
|
|
return (
|
|
<Section>
|
|
<LogsHeader>
|
|
<SectionTitle style={{ marginBottom: 0 }}>Logs</SectionTitle>
|
|
<LogsControls>
|
|
<SmallInput
|
|
id="logs-filter"
|
|
type="text"
|
|
placeholder="Filter..."
|
|
value={state.searchFilter}
|
|
onInput={(e: Event) => handleSearchChange(app.name, (e.target as HTMLInputElement).value)}
|
|
/>
|
|
<SmallSelect
|
|
id="logs-date"
|
|
value={state.selectedDate}
|
|
onChange={(e: Event) => handleDateChange(app.name, (e.target as HTMLSelectElement).value)}
|
|
>
|
|
<option value="live">Live</option>
|
|
{state.dates.map(date => (
|
|
<option key={date} value={date}>{date}</option>
|
|
))}
|
|
</SmallSelect>
|
|
</LogsControls>
|
|
</LogsHeader>
|
|
<LogsContainer id="logs-content">
|
|
<LogsContent />
|
|
</LogsContainer>
|
|
</Section>
|
|
)
|
|
}
|