diff --git a/galasa-ui/messages/de.json b/galasa-ui/messages/de.json index df5543a4..ddbecb6a 100644 --- a/galasa-ui/messages/de.json +++ b/galasa-ui/messages/de.json @@ -228,7 +228,12 @@ "filterTrace": "Trace", "downloadButton": "Laufprotokoll herunterladen", "copyPermalinkButton": "Permalink mit ausgewählten Zeilen kopieren", - "selectLinesToCreatePermalink": "Zeilen für Permalink wählen" + "selectLinesToCreatePermalink": "Zeilen für Permalink wählen", + "refreshRunLog": "Aktualisiere das Ausführungsprotokoll", + "jumpToTop": "Zum Anfang springen", + "jumpToBottom": "Zum Ende springen", + "errorTitle": "Fehler beim Aktualisieren des Ausführungsprotokolls", + "errorFailedMessage": "Fehler beim Aktualisieren des Ausführungsprotokolls, Fehlermeldung: {errorMessage}" }, "MethodsTab": { "title": "Methoden", @@ -389,12 +394,10 @@ "isloading": "Diagramm wird geladen...", "errorLoadingGraph": "Beim Laden des Diagramms ist ein Fehler aufgetreten.", "noTestRunsFound": "Keine Testläufe gefunden.", - "limitExceeded": { "title": "Grenzwert überschritten", "subtitle": "Ihre Abfrage hat mehr als {maxRecords} Ergebnisse zurückgegeben. Es werden die ersten {maxRecords} Datensätze angezeigt. Um dies in Zukunft zu vermeiden, schränken Sie Ihren Zeitrahmen ein oder ändern Sie Ihre Suchkriterien, um weniger Ergebnisse zu erhalten." }, - "timeFrameText": { "range": "Zeige Testläufe von {from} bis {to}" } diff --git a/galasa-ui/messages/en.json b/galasa-ui/messages/en.json index b64c83d4..e0827c42 100644 --- a/galasa-ui/messages/en.json +++ b/galasa-ui/messages/en.json @@ -207,7 +207,12 @@ "filterTrace": "Trace", "downloadButton": "Download Run Log", "copyPermalinkButton": "Copy permalink with selected lines", - "selectLinesToCreatePermalink": "Select log lines for permalink" + "selectLinesToCreatePermalink": "Select log lines for permalink", + "refreshRunLog": "Refresh the Run Log", + "jumpToTop": "Jump to top", + "jumpToBottom": "Jump to bottom", + "errorTitle": "Failed to refresh run log", + "errorFailedMessage": "Failed to refresh run log, error message: {errorMessage}" }, "MethodsTab": { "title": "Methods", diff --git a/galasa-ui/src/actions/runsAction.ts b/galasa-ui/src/actions/runsAction.ts index d0169d24..a41e4225 100644 --- a/galasa-ui/src/actions/runsAction.ts +++ b/galasa-ui/src/actions/runsAction.ts @@ -7,6 +7,7 @@ import { ResultArchiveStoreAPIApi, TagsAPIApi } from '@/generated/galasaapi'; import { createAuthenticatedApiConfiguration } from '@/utils/api'; +import { fetchRunDetailLogs } from '@/utils/testRuns'; import { CLIENT_API_VERSION } from '@/utils/constants/common'; export const downloadArtifactFromServer = async (runId: string, artifactUrl: string) => { @@ -92,3 +93,12 @@ export const getExistingTagObjects = async () => { }; } }; + +export const fetchRunLog = async (runId: string) => { + try { + const runLog = await fetchRunDetailLogs(runId); + return runLog; + } catch (error: any) { + throw new Error(error); + } +}; diff --git a/galasa-ui/src/components/test-runs/test-run-details/LogTab.tsx b/galasa-ui/src/components/test-runs/test-run-details/LogTab.tsx index 0739c212..eb7c358a 100644 --- a/galasa-ui/src/components/test-runs/test-run-details/LogTab.tsx +++ b/galasa-ui/src/components/test-runs/test-run-details/LogTab.tsx @@ -6,7 +6,7 @@ 'use client'; import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react'; -import { Search, OverflowMenu, Button } from '@carbon/react'; +import { Search, OverflowMenu, Button, InlineNotification } from '@carbon/react'; import styles from '@/styles/test-runs/test-run-details/LogTab.module.css'; import { Checkbox } from '@carbon/react'; import { @@ -17,9 +17,15 @@ import { Term, LetterAa, Copy, + Renew, + UpToTop, + DownToBottom, } from '@carbon/icons-react'; import { handleDownload } from '@/utils/artifacts'; import { useTranslations } from 'next-intl'; +import { fetchRunLog } from '@/actions/runsAction'; +import { NotificationType } from '@/utils/types/common'; +import { NOTIFICATION_VISIBLE_MILLISECS } from '@/utils/constants/common'; interface LogLine { content: string; @@ -43,6 +49,7 @@ enum RegexFlags { interface LogTabProps { logs: string; initialLine?: number; + runId: string; } interface selectedRange { @@ -55,10 +62,11 @@ interface selectedRange { const SELECTION_CHANGE_EVENT = 'selectionchange'; const HASH_CHANGE_EVENT = 'hashchange'; -export default function LogTab({ logs, initialLine }: LogTabProps) { +export default function LogTab({ logs, initialLine, runId }: LogTabProps) { const translations = useTranslations('LogTab'); const [logContent, setLogContent] = useState(''); + const [isRefreshing, setIsRefreshing] = useState(false); const [processedLines, setProcessedLines] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(''); @@ -74,6 +82,8 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { TRACE: true, }); const [selectedRange, setSelectedRange] = useState(null); + const [isAtTop, setIsAtTop] = useState(true); + const [isAtBottom, setIsAtBottom] = useState(false); // Cache for search results to avoid recomputation const [searchCache, setSearchCache] = useState>(new Map()); @@ -84,9 +94,15 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { ); const logContainerRef = useRef(null); + const scrollContainerRef = useRef(null); const debounceTimeoutRef = useRef(null); + const [notification, setNotification] = useState(null); + const DEBOUNCE_DELAY_MILLISECONDS = 300; + const ANIMATION_BEHAVIOUR = window.matchMedia('(prefers-reduced-motion: reduce)').matches + ? 'instant' + : 'smooth'; const handleSearchChange = (e: any) => { const value = e.target?.value || ''; @@ -162,6 +178,58 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { } }; + const handleRefreshLog = async () => { + setIsRefreshing(true); + + try { + // Fetch fresh log from the server + const newRunLog = await fetchRunLog(runId); + + setLogContent(newRunLog); + } catch (error: any) { + console.error('Error refreshing logs:', error); + setNotification?.({ + kind: 'error', + title: translations('errorTitle'), + subtitle: translations('errorFailedMessage', { errorMessage: error.message }), + }); + setTimeout(() => setNotification(null), NOTIFICATION_VISIBLE_MILLISECS); + + // Fallback to existing logs if fetch fails + setLogContent(logs); + } finally { + setIsRefreshing(false); + } + }; + + const checkScrollPosition = useCallback(() => { + if (scrollContainerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current; + const THRESHOLD_PIXELS = 40; + + setIsAtTop(scrollTop <= THRESHOLD_PIXELS); + setIsAtBottom(scrollTop + clientHeight >= scrollHeight - THRESHOLD_PIXELS); + } + }, []); + + const scrollToTop = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + top: 0, + behavior: ANIMATION_BEHAVIOUR, + }); + } + }; + + const scrollToBottom = () => { + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight, + behavior: ANIMATION_BEHAVIOUR, + }); + } + }; + // Memoized regex creation to avoid recreating the same regex repeatedly const searchRegex = useMemo(() => { let regex: RegExp | null = null; @@ -465,7 +533,7 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { if (initialLine && processedLines.length > 0) { const lineElement = document.getElementById(`log-line-${initialLine - 1}`); if (lineElement) { - lineElement.scrollIntoView({ behavior: 'smooth', block: 'start' }); + lineElement.scrollIntoView({ behavior: ANIMATION_BEHAVIOUR, block: 'start' }); } } }, [initialLine, processedLines]); @@ -476,7 +544,7 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { const currentMatchElement = document.getElementById('current-match'); if (currentMatchElement) { currentMatchElement.scrollIntoView({ - behavior: 'smooth', + behavior: ANIMATION_BEHAVIOUR, block: 'center', }); } @@ -566,6 +634,27 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { }; }, []); + // Track scroll position in log tab + useEffect(() => { + const scrollContainer = scrollContainerRef.current; + if (!scrollContainer) return; + + // Check initial position + checkScrollPosition(); + + // Add scroll event listener + scrollContainer.addEventListener('scroll', checkScrollPosition); + + return () => { + scrollContainer.removeEventListener('scroll', checkScrollPosition); + }; + }, [checkScrollPosition]); + + // Re-check scroll position when content changes + useEffect(() => { + checkScrollPosition(); + }, [processedLines, checkScrollPosition]); + const copyPermalinkText = selectedRange?.startLine ? translations('copyPermalinkButton') : translations('selectLinesToCreatePermalink'); @@ -685,12 +774,55 @@ export default function LogTab({ logs, initialLine }: LogTabProps) { className={!selectedRange?.startLine ? styles.buttonDisabled : ''} data-testid="icon-button-copy-permalink" /> +