/* * Copyright (c) 2021-2026 ahriman team. * * This file is part of ahriman * (see https://github.com/arcan1s/ahriman). * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import ListIcon from "@mui/icons-material/List"; import { Box, Button, Menu, MenuItem, Typography } from "@mui/material"; import { keepPreviousData, skipToken, useQuery } from "@tanstack/react-query"; import CodeBlock from "components/common/CodeBlock"; import { QueryKeys } from "hooks/QueryKeys"; import { useAutoScroll } from "hooks/useAutoScroll"; import { useClient } from "hooks/useClient"; import type { LogRecord } from "models/LogRecord"; import type { RepositoryId } from "models/RepositoryId"; import React, { useEffect, useMemo, useState } from "react"; interface Logs { version: string; processId: string; created: number; logs: string; } interface BuildLogsTabProps { packageBase: string; repository: RepositoryId; refreshInterval: number; } function convertLogs(records: LogRecord[], filter?: (record: LogRecord) => boolean): string { const filtered = filter ? records.filter(filter) : records; return filtered .map(record => `[${new Date(record.created * 1000).toISOString()}] ${record.message}`) .join("\n"); } export default function BuildLogsTab({ packageBase, repository, refreshInterval, }: BuildLogsTabProps): React.JSX.Element { const client = useClient(); const [selectedVersionKey, setSelectedVersionKey] = useState(null); const [anchorEl, setAnchorEl] = useState(null); const { data: allLogs } = useQuery({ queryKey: QueryKeys.logs(packageBase, repository), queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository), enabled: !!packageBase, refetchInterval: refreshInterval > 0 ? refreshInterval : false, }); // Build version selectors from all logs const versions = useMemo(() => { if (!allLogs || allLogs.length === 0) { return []; } const grouped: Record = {}; for (const record of allLogs) { const key = `${record.version}-${record.process_id}`; const existing = grouped[key]; if (!existing) { grouped[key] = { ...record, minCreated: record.created }; } else { existing.minCreated = Math.min(existing.minCreated, record.created); } } return Object.values(grouped) .sort((left, right) => right.minCreated - left.minCreated) .map(record => ({ version: record.version, processId: record.process_id, created: record.minCreated, logs: convertLogs( allLogs, right => record.version === right.version && record.process_id === right.process_id, ), })); }, [allLogs]); // Compute active index from selected version key, defaulting to newest (index 0) const activeIndex = useMemo(() => { if (selectedVersionKey) { const index = versions.findIndex(record => `${record.version}-${record.processId}` === selectedVersionKey); if (index >= 0) { return index; } } return 0; }, [versions, selectedVersionKey]); const activeVersion = versions[activeIndex]; const activeVersionKey = activeVersion ? `${activeVersion.version}-${activeVersion.processId}` : null; // Refresh active version logs const { data: versionLogs } = useQuery({ queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""), queryFn: activeVersion ? () => client.fetch.fetchPackageLogs( packageBase, repository, activeVersion.version, activeVersion.processId, ) : skipToken, placeholderData: keepPreviousData, refetchInterval: refreshInterval > 0 ? refreshInterval : false, }); // Derive displayed logs: prefer fresh polled data when available const displayedLogs = useMemo(() => { if (versionLogs && versionLogs.length > 0) { return convertLogs(versionLogs); } return activeVersion?.logs ?? ""; }, [versionLogs, activeVersion]); const { preRef, handleScroll, scrollToBottom, resetScroll } = useAutoScroll(); // Reset scroll tracking when active version changes useEffect(() => { resetScroll(); }, [activeVersionKey, resetScroll]); // Scroll to bottom on new logs useEffect(() => { scrollToBottom(); }, [displayedLogs, scrollToBottom]); return