diff --git a/web/src/assets/terminal.svg b/web/src/assets/terminal.svg
new file mode 100644
index 0000000000..d06ce18c5a
--- /dev/null
+++ b/web/src/assets/terminal.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/src/components/EventLogPopover/EventLogContent.tsx b/web/src/components/EventLogPopover/EventLogContent.tsx
new file mode 100644
index 0000000000..0ba2469760
--- /dev/null
+++ b/web/src/components/EventLogPopover/EventLogContent.tsx
@@ -0,0 +1,29 @@
+import {useEffect, useRef} from 'react';
+import TestRunEvent from 'models/TestRunEvent.model';
+import EventLogService from 'services/EventLog.service';
+import * as S from './EventLogPopover.styled';
+
+interface IProps {
+ runEvents: TestRunEvent[];
+}
+
+const EventLogContent = ({runEvents}: IProps) => {
+ const bottomRef = useRef(null);
+
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({behavior: 'smooth'});
+ }, [runEvents]);
+
+ return (
+
+ {runEvents.map(event => (
+
+ {EventLogService.typeToString(event)} {EventLogService.detailsToString(event)}
+
+ ))}
+
+
+ );
+};
+
+export default EventLogContent;
diff --git a/web/src/components/EventLogPopover/EventLogPopover.styled.ts b/web/src/components/EventLogPopover/EventLogPopover.styled.ts
new file mode 100644
index 0000000000..a25e902413
--- /dev/null
+++ b/web/src/components/EventLogPopover/EventLogPopover.styled.ts
@@ -0,0 +1,62 @@
+import {CopyOutlined} from '@ant-design/icons';
+import {Typography} from 'antd';
+import styled, {DefaultTheme, createGlobalStyle} from 'styled-components';
+import {LogLevel} from 'constants/TestRunEvents.constants';
+import terminalIcon from 'assets/terminal.svg';
+
+function getLogLevelColor(logLevel: LogLevel, theme: DefaultTheme): string {
+ if (logLevel === LogLevel.Error) return theme.color.error;
+ if (logLevel === LogLevel.Success) return theme.color.success;
+ if (logLevel === LogLevel.Warning) return theme.color.warningYellow;
+ return theme.color.text;
+}
+
+export const GlobalStyle = createGlobalStyle`
+ #eventlog-popover {
+ .ant-popover-inner-content {
+ padding: 5px 16px;
+ }
+ }
+`;
+
+export const Container = styled.div`
+ margin: 0;
+ max-height: calc(100vh - 200px);
+ width: 550px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ border-radius: 2px;
+ position: relative;
+`;
+
+export const CopyIcon = styled(CopyOutlined)`
+ color: ${({theme}) => theme.color.primary};
+ cursor: pointer;
+`;
+
+export const EventEntry = styled(Typography.Text)<{$logLevel?: LogLevel}>`
+ font-size: ${({theme}) => theme.size.sm};
+ color: ${({theme, $logLevel = LogLevel.Info}) => getLogLevelColor($logLevel, theme)};
+ margin: 0;
+`;
+
+export const TerminalIcon = styled.img.attrs({src: terminalIcon})`
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+`;
+
+export const TitleContainer = styled.div`
+ padding: 5px 0;
+ display: flex;
+ justify-content: space-between;
+`;
+
+export const Title = styled(Typography.Title)`
+ && {
+ margin: 0;
+ font-size: ${({theme}) => theme.size.md};
+ }
+`;
diff --git a/web/src/components/EventLogPopover/EventLogPopover.tsx b/web/src/components/EventLogPopover/EventLogPopover.tsx
new file mode 100644
index 0000000000..74369d112d
--- /dev/null
+++ b/web/src/components/EventLogPopover/EventLogPopover.tsx
@@ -0,0 +1,38 @@
+import {Popover, Tooltip} from 'antd';
+import TestRunEvent from 'models/TestRunEvent.model';
+import useCopy from 'hooks/useCopy';
+import EventLogService from 'services/EventLog.service';
+import EventLogContent from './EventLogContent';
+import * as S from './EventLogPopover.styled';
+
+interface IProps {
+ runEvents: TestRunEvent[];
+}
+
+const EventLogPopover = ({runEvents}: IProps) => {
+ const copy = useCopy();
+
+ return (
+ <>
+
+ }
+ trigger="click"
+ placement="bottomLeft"
+ title={
+
+ Event Log
+
+ copy(EventLogService.listToString(runEvents))} />
+
+
+ }
+ >
+
+
+ >
+ );
+};
+
+export default EventLogPopover;
diff --git a/web/src/components/EventLogPopover/index.ts b/web/src/components/EventLogPopover/index.ts
new file mode 100644
index 0000000000..9760459625
--- /dev/null
+++ b/web/src/components/EventLogPopover/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line no-restricted-exports
+export {default} from './EventLogPopover';
diff --git a/web/src/components/RunDetailLayout/HeaderRight.tsx b/web/src/components/RunDetailLayout/HeaderRight.tsx
index 0c2286f056..f2fe32a516 100644
--- a/web/src/components/RunDetailLayout/HeaderRight.tsx
+++ b/web/src/components/RunDetailLayout/HeaderRight.tsx
@@ -10,6 +10,7 @@ import {useTestRun} from 'providers/TestRun/TestRun.provider';
import {useTestSpecs} from 'providers/TestSpecs/TestSpecs.provider';
import {useTestOutput} from 'providers/TestOutput/TestOutput.provider';
import * as S from './RunDetailLayout.styled';
+import EventLogPopover from '../EventLogPopover/EventLogPopover';
interface IProps {
testId: string;
@@ -20,7 +21,7 @@ const HeaderRight = ({testId, testVersion}: IProps) => {
const {isDraftMode: isTestSpecsDraftMode} = useTestSpecs();
const {isDraftMode: isTestOutputsDraftMode} = useTestOutput();
const isDraftMode = isTestSpecsDraftMode || isTestOutputsDraftMode;
- const {isLoadingStop, run, stopRun} = useTestRun();
+ const {isLoadingStop, run, stopRun, runEvents} = useTestRun();
const {onRun} = useTest();
const state = run.state;
@@ -51,6 +52,7 @@ const HeaderRight = ({testId, testVersion}: IProps) => {
Run Test
)}
+
React.ReactElement> = {
[TestState.STOPPED]: StoppedHeader,
};
-type TraceEventTypeWithoutFetching = Exclude<
+export type TraceEventTypeWithoutFetching = Exclude<
TraceEventType,
TraceEventType.FETCHING_START | TraceEventType.FETCHING_ERROR | TraceEventType.FETCHING_SUCCESS
>;
diff --git a/web/src/models/TestRunEvent.model.ts b/web/src/models/TestRunEvent.model.ts
index 0b196e7230..8a8e3e0741 100644
--- a/web/src/models/TestRunEvent.model.ts
+++ b/web/src/models/TestRunEvent.model.ts
@@ -13,7 +13,7 @@ type TestRunEvent = Model<
{logLevel: LogLevel; dataStoreConnection?: ConnectionResult; polling?: PollingInfo; outputs?: OutputInfo[]}
>;
-function PollingInfo({
+export function PollingInfo({
type = PollingInfoType.Periodic,
isComplete = false,
periodic = {},
diff --git a/web/src/services/EventLog.service.ts b/web/src/services/EventLog.service.ts
new file mode 100644
index 0000000000..0e3bc8522d
--- /dev/null
+++ b/web/src/services/EventLog.service.ts
@@ -0,0 +1,55 @@
+import {parseISO, formatISO} from 'date-fns';
+import TestRunEvent, {PollingInfo} from 'models/TestRunEvent.model';
+import ConnectionResult from 'models/ConnectionResult.model';
+import {TraceEventType} from 'constants/TestRunEvents.constants';
+
+type TEventToStringFn = (event: TestRunEvent) => string;
+
+const eventToString = ({title, description}: TestRunEvent): string => {
+ return `${title} - ${description}`;
+};
+
+const dataStoreEventToString = (event: TestRunEvent): string => {
+ const {dataStoreConnection: {allPassed, ...dataStoreConnection} = ConnectionResult({})} = event;
+ const baseText = eventToString(event);
+ const configValidText = allPassed ? 'Data store configuration is valid.' : 'Data store configuration is not valid.';
+
+ const connectionStepsDetailsText = Object.entries(dataStoreConnection || {})
+ .map(([key, {message, error}]) => `${key.toUpperCase()} - ${message} ${error ? ` - ${error}` : ''}`, '')
+ .join(' - ');
+
+ return `${baseText} - ${configValidText} - ${connectionStepsDetailsText}`;
+};
+
+const pollingEventToString = (event: TestRunEvent): string => {
+ const {polling: {type: pollingType, isComplete, periodic} = PollingInfo({})} = event;
+ const baseText = eventToString(event);
+ const pollingTypeText = `Polling type: ${pollingType}`;
+ const pollingCompleteText = `Polling complete: ${isComplete}`;
+ const periodicText = `Periodic polling - number of spans: ${periodic?.numberSpans}, number of iterations: ${periodic?.numberIterations}`;
+
+ return `${baseText} - ${pollingTypeText} - ${pollingCompleteText} - ${periodicText}`;
+};
+
+const eventToStringMap: Record = {
+ [TraceEventType.DATA_STORE_CONNECTION_INFO]: dataStoreEventToString,
+ [TraceEventType.POLLING_ITERATION_INFO]: pollingEventToString,
+};
+
+const EventLogService = () => ({
+ detailsToString(event: TestRunEvent) {
+ const eventToStringFn = eventToStringMap[event.type] || eventToString;
+
+ return eventToStringFn(event);
+ },
+ typeToString({type, createdAt}: TestRunEvent) {
+ const createdAtDate = parseISO(createdAt);
+
+ return `[${formatISO(createdAtDate)} - ${type}]`;
+ },
+ listToString(events: TestRunEvent[]) {
+ return events.map(event => `${this.typeToString(event)} ${this.detailsToString(event)}`).join('\r');
+ },
+});
+
+export default EventLogService();