Skip to content

Commit 82307d0

Browse files
Used LogViewer of PF for pipeline logs
1 parent fd27abc commit 82307d0

File tree

9 files changed

+351
-308
lines changed

9 files changed

+351
-308
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@patternfly/react-component-groups": "^5.1.0",
3939
"@patternfly/react-core": "^5.2.1",
4040
"@patternfly/react-icons": "5.2.1",
41+
"@patternfly/react-log-viewer": "5.3.0",
4142
"@patternfly/react-table": "5.2.1",
4243
"@patternfly/react-tokens": "5.2.1",
4344
"@patternfly/react-topology": "5.2.1",

src/components/logs/Logs.scss

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/components/logs/Logs.tsx

Lines changed: 189 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,151 +1,233 @@
11
import * as React from 'react';
22
import { Alert } from '@patternfly/react-core';
3+
import { LogViewer } from '@patternfly/react-log-viewer';
34
import { Base64 } from 'js-base64';
4-
import { throttle } from 'lodash';
55
import { useTranslation } from 'react-i18next';
66
import { consoleFetchText } from '@openshift-console/dynamic-plugin-sdk';
77
import { LOG_SOURCE_TERMINATED } from '../../consts';
88
import { WSFactory } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/ws-factory';
9-
import { ContainerSpec, PodKind } from '../../types';
9+
import { ContainerSpec, ContainerStatus, PodKind } from '../../types';
1010
import { PodModel } from '../../models';
1111
import { resourceURL } from '../utils/k8s-utils';
12-
import './Logs.scss';
12+
import { containerToLogSourceStatus } from '../utils/pipeline-utils';
13+
import './MultiStreamLogs.scss';
1314

14-
consoleFetchText;
1515
type LogsProps = {
1616
resource: PodKind;
17-
resourceStatus: string;
18-
container: ContainerSpec;
19-
render: boolean;
20-
autoScroll?: boolean;
21-
onComplete: (containerName: string) => void;
17+
containers: ContainerSpec[];
18+
setCurrentLogsGetter?: (getter: () => string) => void;
19+
};
20+
21+
type LogData = {
22+
[containerName: string]: {
23+
logs: string[];
24+
status: string;
25+
};
26+
};
27+
28+
const processLogData = (
29+
logData: LogData,
30+
containers: ContainerSpec[],
31+
): string => {
32+
let result = '';
33+
34+
containers.map(({ name: containerName }) => {
35+
if (logData[containerName]) {
36+
const { logs } = logData[containerName];
37+
const uniqueLogs = Array.from(new Set(logs));
38+
const filteredLogs = uniqueLogs.filter((log) => log.trim() !== '');
39+
const formattedContainerName = `${containerName.toUpperCase()}`;
40+
41+
if (filteredLogs.length === 0) {
42+
result += `${formattedContainerName}\n\n`;
43+
} else {
44+
const joinedLogs = filteredLogs.join('\n');
45+
result += `${formattedContainerName}\n${joinedLogs}\n\n`;
46+
}
47+
}
48+
});
49+
return result;
2250
};
2351

2452
const Logs: React.FC<LogsProps> = ({
2553
resource,
26-
resourceStatus,
27-
container,
28-
onComplete,
29-
render,
30-
autoScroll = true,
54+
containers,
55+
setCurrentLogsGetter,
3156
}) => {
57+
if (!resource) return null;
3258
const { t } = useTranslation('plugin__pipelines-console-plugin');
33-
const { name } = container;
34-
const { kind, metadata = {} } = resource;
59+
const { metadata = {} } = resource;
3560
const { name: resName, namespace: resNamespace } = metadata;
36-
const scrollToRef = React.useRef<HTMLDivElement>(null);
37-
const contentRef = React.useRef<HTMLDivElement>(null);
3861
const [error, setError] = React.useState<boolean>(false);
39-
const resourceStatusRef = React.useRef<string>(resourceStatus);
40-
const onCompleteRef = React.useRef<(name) => void>();
41-
const blockContentRef = React.useRef<string>('');
42-
onCompleteRef.current = onComplete;
43-
44-
const addContentAndScroll = React.useCallback(
45-
throttle(() => {
46-
if (contentRef.current) {
47-
contentRef.current.innerText += blockContentRef.current;
48-
}
49-
if (scrollToRef.current) {
50-
scrollToRef.current.scrollIntoView({
51-
behavior: 'smooth',
52-
block: 'end',
53-
});
54-
}
55-
blockContentRef.current = '';
56-
}, 1000),
57-
[],
62+
const [logData, setLogData] = React.useState<LogData>({});
63+
const [formattedLogString, setFormattedLogString] = React.useState('');
64+
const [scrollToRow, setScrollToRow] = React.useState<number>(0);
65+
const [activeContainers, setActiveContainers] = React.useState<Set<string>>(
66+
new Set(),
5867
);
5968

60-
const appendMessage = React.useRef<(blockContent) => void>();
69+
React.useEffect(() => {
70+
setCurrentLogsGetter(() => {
71+
return formattedLogString;
72+
});
73+
}, [setCurrentLogsGetter, formattedLogString]);
74+
75+
const appendMessage = React.useCallback(
76+
(containerName: string, blockContent: string, resourceStatus: string) => {
77+
if (blockContent) {
78+
setLogData((prevLogData) => {
79+
if (resourceStatus === 'terminated') {
80+
// Replace the entire content with blockContent
81+
return {
82+
...prevLogData,
83+
[containerName]: {
84+
logs: [blockContent],
85+
status: resourceStatus,
86+
},
87+
};
88+
} else {
89+
// Otherwise, append the blockContent to the existing logs
90+
const existingLogs = prevLogData[containerName]?.logs || [];
91+
const updatedLogs = [...existingLogs, blockContent.trimEnd()];
6192

62-
appendMessage.current = React.useCallback(
63-
(blockContent: string) => {
64-
blockContentRef.current += blockContent;
65-
if (scrollToRef.current && blockContent && render && autoScroll) {
66-
addContentAndScroll();
93+
return {
94+
...prevLogData,
95+
[containerName]: {
96+
logs: updatedLogs,
97+
status: resourceStatus,
98+
},
99+
};
100+
}
101+
});
67102
}
68103
},
69-
[autoScroll, render, addContentAndScroll],
104+
[],
70105
);
71106

72-
if (resourceStatusRef.current !== resourceStatus) {
73-
resourceStatusRef.current = resourceStatus;
74-
}
107+
const retryWebSocket = (
108+
watchURL: string,
109+
wsOpts: any,
110+
onMessage: (message: string) => void,
111+
onError: () => void,
112+
retryCount = 0,
113+
) => {
114+
let ws = new WSFactory(watchURL, wsOpts);
115+
const handleError = () => {
116+
if (retryCount < 5) {
117+
setTimeout(() => {
118+
retryWebSocket(watchURL, wsOpts, onMessage, onError, retryCount + 1);
119+
}, 3000); // Retry after 3 seconds
120+
} else {
121+
onError();
122+
}
123+
};
124+
125+
ws.onmessage((msg) => {
126+
const message = Base64.decode(msg);
127+
onMessage(message);
128+
}).onerror(() => {
129+
handleError();
130+
});
131+
132+
return ws;
133+
};
75134

76135
React.useEffect(() => {
77-
let loaded = false;
78-
let ws: WSFactory;
79-
const urlOpts = {
80-
ns: resNamespace,
81-
name: resName,
82-
path: 'log',
83-
queryParams: {
84-
container: name,
85-
follow: 'true',
86-
timestamps: 'true',
87-
},
88-
};
89-
const watchURL = resourceURL(PodModel, urlOpts);
90-
if (resourceStatusRef.current === LOG_SOURCE_TERMINATED) {
91-
consoleFetchText(watchURL)
92-
.then((res) => {
93-
if (loaded) return;
94-
appendMessage.current(res);
95-
onCompleteRef.current(name);
96-
})
97-
.catch(() => {
98-
if (loaded) return;
99-
setError(true);
100-
onCompleteRef.current(name);
101-
});
102-
} else {
103-
const wsOpts = {
104-
host: 'auto',
105-
path: watchURL,
106-
subprotocols: ['base64.binary.k8s.io'],
136+
containers.forEach((container) => {
137+
if (activeContainers.has(container.name)) return;
138+
setActiveContainers((prev) => new Set(prev).add(container.name));
139+
let loaded = false;
140+
let ws: WSFactory;
141+
const { name } = container;
142+
const urlOpts = {
143+
ns: resNamespace,
144+
name: resName,
145+
path: 'log',
146+
queryParams: {
147+
container: name,
148+
follow: 'true',
149+
timestamps: 'true',
150+
},
107151
};
108-
ws = new WSFactory(watchURL, wsOpts);
109-
ws.onmessage((msg) => {
110-
if (loaded) return;
111-
const message = Base64.decode(msg);
112-
appendMessage.current(message);
113-
})
114-
.onclose(() => {
115-
onCompleteRef.current(name);
116-
})
117-
.onerror(() => {
118-
if (loaded) return;
119-
setError(true);
120-
onCompleteRef.current(name);
121-
});
122-
}
123-
return () => {
124-
loaded = true;
125-
ws && ws.destroy();
126-
};
127-
}, [kind, name, resName, resNamespace]);
152+
const watchURL = resourceURL(PodModel, urlOpts);
153+
154+
const containerStatus: ContainerStatus[] =
155+
resource?.status?.containerStatuses ?? [];
156+
const statusIndex = containerStatus.findIndex(
157+
(c) => c.name === container.name,
158+
);
159+
const resourceStatus = containerToLogSourceStatus(
160+
containerStatus[statusIndex],
161+
);
162+
163+
if (resourceStatus === LOG_SOURCE_TERMINATED) {
164+
consoleFetchText(watchURL)
165+
.then((res) => {
166+
if (loaded) return;
167+
appendMessage(name, res, resourceStatus);
168+
})
169+
.catch(() => {
170+
if (loaded) return;
171+
setError(true);
172+
});
173+
} else {
174+
const wsOpts = {
175+
host: 'auto',
176+
path: watchURL,
177+
subprotocols: ['base64.binary.k8s.io'],
178+
};
179+
ws = retryWebSocket(
180+
watchURL,
181+
wsOpts,
182+
(message) => {
183+
if (loaded) return;
184+
setError(false);
185+
appendMessage(name, message, resourceStatus);
186+
},
187+
() => {
188+
if (loaded) return;
189+
setError(true);
190+
},
191+
);
192+
}
193+
return () => {
194+
loaded = true;
195+
if (ws) {
196+
ws.destroy();
197+
}
198+
};
199+
});
200+
}, [
201+
resName,
202+
resNamespace,
203+
resource?.status?.containerStatuses,
204+
activeContainers,
205+
]);
128206

129207
React.useEffect(() => {
130-
if (scrollToRef.current && render && autoScroll) {
131-
addContentAndScroll();
132-
}
133-
}, [autoScroll, render, addContentAndScroll]);
208+
const formattedString = processLogData(logData, containers);
209+
setFormattedLogString(formattedString);
210+
const totalLines = formattedString.split('\n').length;
211+
setScrollToRow(totalLines);
212+
}, [logData]);
134213

135214
return (
136-
<div className="odc-logs" style={{ display: render ? '' : 'none' }}>
137-
<p className="odc-logs__name">{name}</p>
215+
<div className="odc-logs-logviewer">
138216
{error && (
139217
<Alert
140218
variant="danger"
141219
isInline
142220
title={t('An error occurred while retrieving the requested logs.')}
143221
/>
144222
)}
145-
<div>
146-
<div className="odc-logs__content" ref={contentRef} />
147-
<div ref={scrollToRef} />
148-
</div>
223+
<LogViewer
224+
hasLineNumbers={false}
225+
isTextWrapped={false}
226+
data={formattedLogString}
227+
theme="dark"
228+
scrollToRow={scrollToRow}
229+
height="100%"
230+
/>
149231
</div>
150232
);
151233
};

src/components/logs/MultiStreamLogs.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
width: 100%;
3737
}
3838
}
39+
&__logviewer {
40+
background-color: var(--pf-v5-global--palette--black-1000);
41+
color: var(--pf-v5-global--Color--light-100);
42+
font-family: Menlo, Monaco, 'Courier New', monospace;
43+
height: 100%;
44+
}
3945
&__taskName {
4046
background-color: var(--pf-v5-global--BackgroundColor--dark-300);
4147
padding: var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--md);
@@ -45,3 +51,8 @@
4551
margin-left: var(--pf-v5-global--spacer--sm);
4652
}
4753
}
54+
55+
.odc-logs-logviewer {
56+
background-color: var(--pf-v5-global--palette--black-1000);
57+
height: 100%;
58+
}

0 commit comments

Comments
 (0)