import {useEffect, useState, type KeyboardEvent} from 'react' import {createPortal} from 'react-dom' import {Maximize2, Minimize2, Play, Terminal} from 'lucide-react' import {useMutation} from '@tanstack/react-query' import {queryLogs} from '@/api/cloudProxyClient' import type {CloudProvider} from '@/types/cloud' import type {LogsInsightsQueryResult} from '@/types/resource' const DEFAULT_QUERY = 'fields @timestamp, @message\n| sort @timestamp desc\n| limit 20' const RANGE_OPTIONS = [ {label: '15m', minutes: 15}, {label: '1h', minutes: 60}, {label: '6h', minutes: 360}, {label: '24h', minutes: 1440}, ] interface LogsQueryPanelProps { cloud: CloudProvider logGroupName?: string runtimeReachable: boolean autoRunKey?: number } /** * Lives inside ResourceInspector next to a selected log group's metadata, * rather than as a full-width section a dev has to scroll the resource table * to find. The inspector column is narrow, so this expands into a full-page * modal on demand via a portal — the same mounted component either way, so * expanding/collapsing keeps the in-progress query text and results. * * Floci's supported StartQuery subset (fields/filter/sort/dedup/limit — see * docs/services/cloudwatch.md) has no UI of its own; this is that surface. * Styled after the real CloudWatch Logs Insights query editor (dark code box, * time-range chips, run-with-shortcut), minus the capabilities Floci doesn't * implement (stats/visualize, pattern analysis, live tail) — this repo's own * rule against decorative, unwired controls. */ export function LogsQueryPanel({cloud, logGroupName, runtimeReachable, autoRunKey = 0}: LogsQueryPanelProps) { const [queryText, setQueryText] = useState(DEFAULT_QUERY) const [rangeMinutes, setRangeMinutes] = useState(RANGE_OPTIONS[1].minutes) const [expanded, setExpanded] = useState(false) const queryMut = useMutation({ mutationFn: async () => { const endTime = Math.floor(Date.now() / 1000) try { return await queryLogs(cloud, logGroupName ?? '', { queryString: queryText, startTime: endTime - rangeMinutes * 60, endTime, }) } catch (error) { if (isMissingLogGroup(error)) return {queryId: '', status: 'Complete', rows: []} throw error } }, }) const {mutate: runQuery, reset: resetQuery} = queryMut // ResourceInspector reuses this instance across row selections rather than // remounting it, so without this the previous group's results linger and // read as if they belong to the newly selected one. useEffect(() => { resetQuery() setQueryText(DEFAULT_QUERY) setRangeMinutes(RANGE_OPTIONS[1].minutes) setExpanded(false) }, [logGroupName, resetQuery]) useEffect(() => { if (autoRunKey > 0 && logGroupName && runtimeReachable) runQuery() }, [autoRunKey, logGroupName, runQuery, runtimeReachable]) if (!logGroupName) return null function submit() { if (!queryText.trim()) return runQuery() } function onEditorKeyDown(event: KeyboardEvent) { if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') { event.preventDefault() submit() } } const panel = (
Insights query {logGroupName} fields / filter / sort / dedup / limit
{RANGE_OPTIONS.map((option) => ( ))}