Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions galasa-ui/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@
"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"
},
"MethodsTab": {
"title": "Methoden",
Expand Down Expand Up @@ -389,12 +392,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}"
}
Expand Down
5 changes: 4 additions & 1 deletion galasa-ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,10 @@
"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"
},
"MethodsTab": {
"title": "Methods",
Expand Down
11 changes: 11 additions & 0 deletions galasa-ui/src/actions/runsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -92,3 +93,13 @@ export const getExistingTagObjects = async () => {
};
}
};

export const fetchRunLog = async (runId: string) => {
let runLog;
try {
runLog = await fetchRunDetailLogs(runId);
} catch (error: any) {
runLog = 'Error fetching run log: ' + error;
}
return runLog;
};
122 changes: 118 additions & 4 deletions galasa-ui/src/components/test-runs/test-run-details/LogTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ 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';

interface LogLine {
content: string;
Expand All @@ -43,6 +47,7 @@ enum RegexFlags {
interface LogTabProps {
logs: string;
initialLine?: number;
runId: string;
}

interface selectedRange {
Expand All @@ -55,10 +60,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<string>('');
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [processedLines, setProcessedLines] = useState<LogLine[]>([]);
const [searchTerm, setSearchTerm] = useState<string>('');
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState<string>('');
Expand All @@ -74,6 +80,8 @@ export default function LogTab({ logs, initialLine }: LogTabProps) {
TRACE: true,
});
const [selectedRange, setSelectedRange] = useState<selectedRange | null>(null);
const [isAtTop, setIsAtTop] = useState<boolean>(true);
const [isAtBottom, setIsAtBottom] = useState<boolean>(false);

// Cache for search results to avoid recomputation
const [searchCache, setSearchCache] = useState<Map<string, MatchInfo[]>>(new Map());
Expand All @@ -84,6 +92,7 @@ export default function LogTab({ logs, initialLine }: LogTabProps) {
);

const logContainerRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);

const DEBOUNCE_DELAY_MILLISECONDS = 300;
Expand Down Expand Up @@ -162,6 +171,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);

// Reset search and filters
setSearchTerm('');
setDebouncedSearchTerm('');
setCurrentMatchIndex(-1);
setTotalMatches(0);
setSearchCache(new Map());
} catch (error) {
console.error('Error refreshing logs:', error);
// 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: 'smooth',
});
}
};

const scrollToBottom = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTo({
top: scrollContainerRef.current.scrollHeight,
behavior: 'smooth',
});
}
};

// Memoized regex creation to avoid recreating the same regex repeatedly
const searchRegex = useMemo(() => {
let regex: RegExp | null = null;
Expand Down Expand Up @@ -566,6 +627,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');
Expand Down Expand Up @@ -685,11 +767,43 @@ export default function LogTab({ logs, initialLine }: LogTabProps) {
className={!selectedRange?.startLine ? styles.buttonDisabled : ''}
data-testid="icon-button-copy-permalink"
/>
<Button
kind="ghost"
renderIcon={Renew}
hasIconOnly
iconDescription={translations('refreshRunLog')}
onClick={handleRefreshLog}
disabled={isRefreshing}
/>
</div>
<div className={styles.runLog}>
<div className={styles.runLogContent} ref={logContainerRef}>
{renderLogContent()}
<div className={styles.runLogWrapper}>
{!isAtTop && (
<div className={styles.jumpToTopContainer}>
<Button
kind="ghost"
renderIcon={UpToTop}
hasIconOnly
iconDescription={translations('jumpToTop')}
onClick={scrollToTop}
/>
</div>
)}
<div className={styles.runLog} ref={scrollContainerRef}>
<div className={styles.runLogContent} ref={logContainerRef}>
{renderLogContent()}
</div>
</div>
{!isAtBottom && (
<div className={styles.jumpToBottomContainer}>
<Button
kind="ghost"
renderIcon={DownToBottom}
hasIconOnly
iconDescription={translations('jumpToBottom')}
onClick={scrollToBottom}
/>
</div>
)}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ const TestRunDetails = ({
<MethodsTab methods={methods} onMethodClick={handleNavigateToLog} />
</TabPanel>
<TabPanel>
<LogTab logs={logs} initialLine={initialLine} />
<LogTab logs={logs} initialLine={initialLine} runId={runId} />
</TabPanel>
<TabPanel>
<ArtifactsTab
Expand Down
27 changes: 25 additions & 2 deletions galasa-ui/src/styles/test-runs/test-run-details/LogTab.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,16 @@
border-bottom-right-radius: var(--border-radius);
}

.runLog {
.runLogWrapper {
position: relative;
border: 1px solid var(--galasa-border);
border-radius: var(--border-radius);
}

.runLog {
padding: 1rem;
font-family: 'IBM Plex Mono', monospace;
height: 100vh;
height: 70vh;
overflow-y: auto;
}

Expand All @@ -106,3 +110,22 @@
opacity: 0.5;
cursor: not-allowed;
}

.jumpToTopContainer {
top: 1rem;
right: 1rem;
}

.jumpToBottomContainer {
bottom: 1rem;
right: 1rem;
}

.jumpToTopContainer,
.jumpToBottomContainer {
position: absolute;
background-color: var(--cds-layer);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
color: white;
z-index: 10;
}
Loading