Skip to content

Commit 16fd72e

Browse files
committed
frontend: Add new LogsViewer component
1 parent ce95d2a commit 16fd72e

File tree

15 files changed

+1294
-11
lines changed

15 files changed

+1294
-11
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
.ansi-bold {
2+
font-weight: bold;
3+
}
4+
.ansi-italic {
5+
font-style: italic;
6+
}
7+
.ansi-underline {
8+
text-decoration: underline;
9+
}
10+
11+
/* Foreground colors */
12+
.ansi-black-fg {
13+
color: #000;
14+
}
15+
.ansi-red-fg {
16+
color: #cc0000;
17+
}
18+
.ansi-green-fg {
19+
color: #00aa00;
20+
}
21+
.ansi-yellow-fg {
22+
color: #aa5500;
23+
}
24+
.ansi-blue-fg {
25+
color: #3465a4;
26+
}
27+
.ansi-magenta-fg {
28+
color: #aa00aa;
29+
}
30+
.ansi-cyan-fg {
31+
color: #00aaaa;
32+
}
33+
.ansi-white-fg {
34+
color: #aaaaaa;
35+
}
36+
.ansi-bright-black-fg {
37+
color: #555555;
38+
}
39+
.ansi-bright-red-fg {
40+
color: #ff5555;
41+
}
42+
.ansi-bright-green-fg {
43+
color: #55ff55;
44+
}
45+
.ansi-bright-yellow-fg {
46+
color: #ffff55;
47+
}
48+
.ansi-bright-blue-fg {
49+
color: #5555ff;
50+
}
51+
.ansi-bright-magenta-fg {
52+
color: #ff55ff;
53+
}
54+
.ansi-bright-cyan-fg {
55+
color: #55ffff;
56+
}
57+
.ansi-bright-white-fg {
58+
color: #ffffff;
59+
}
60+
61+
/* Background colors */
62+
.ansi-black-bg {
63+
background-color: #000;
64+
}
65+
.ansi-red-bg {
66+
background-color: #aa0000;
67+
}
68+
.ansi-green-bg {
69+
background-color: #00aa00;
70+
}
71+
.ansi-yellow-bg {
72+
background-color: #aa5500;
73+
}
74+
.ansi-blue-bg {
75+
background-color: #0000aa;
76+
}
77+
.ansi-magenta-bg {
78+
background-color: #aa00aa;
79+
}
80+
.ansi-cyan-bg {
81+
background-color: #00aaaa;
82+
}
83+
.ansi-white-bg {
84+
background-color: #aaaaaa;
85+
}
86+
.ansi-bright-black-bg {
87+
background-color: #555555;
88+
}
89+
.ansi-bright-red-bg {
90+
background-color: #ff5555;
91+
}
92+
.ansi-bright-green-bg {
93+
background-color: #55ff55;
94+
}
95+
.ansi-bright-yellow-bg {
96+
background-color: #ffff55;
97+
}
98+
.ansi-bright-blue-bg {
99+
background-color: #5555ff;
100+
}
101+
.ansi-bright-magenta-bg {
102+
background-color: #ff55ff;
103+
}
104+
.ansi-bright-cyan-bg {
105+
background-color: #55ffff;
106+
}
107+
.ansi-bright-white-bg {
108+
background-color: #ffffff;
109+
}
110+
111+
.search-highlight {
112+
background-color: #fceb92;
113+
color: #333;
114+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2025 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import './AnsiText.css';
18+
19+
export const AnsiText = ({ text, searchQuery }: { text: string; searchQuery?: string }) => {
20+
const ansiCodes: Record<string, string> = {
21+
'1': 'ansi-bold',
22+
'3': 'ansi-italic',
23+
'4': 'ansi-underline',
24+
'30': 'ansi-black-fg',
25+
'31': 'ansi-red-fg',
26+
'32': 'ansi-green-fg',
27+
'33': 'ansi-yellow-fg',
28+
'34': 'ansi-blue-fg',
29+
'35': 'ansi-magenta-fg',
30+
'36': 'ansi-cyan-fg',
31+
'37': 'ansi-white-fg',
32+
'90': 'ansi-bright-black-fg',
33+
'91': 'ansi-bright-red-fg',
34+
'92': 'ansi-bright-green-fg',
35+
'93': 'ansi-bright-yellow-fg',
36+
'94': 'ansi-bright-blue-fg',
37+
'95': 'ansi-bright-magenta-fg',
38+
'96': 'ansi-bright-cyan-fg',
39+
'97': 'ansi-bright-white-fg',
40+
'40': 'ansi-black-bg',
41+
'41': 'ansi-red-bg',
42+
'42': 'ansi-green-bg',
43+
'43': 'ansi-yellow-bg',
44+
'44': 'ansi-blue-bg',
45+
'45': 'ansi-magenta-bg',
46+
'46': 'ansi-cyan-bg',
47+
'47': 'ansi-white-bg',
48+
'100': 'ansi-bright-black-bg',
49+
'101': 'ansi-bright-red-bg',
50+
'102': 'ansi-bright-green-bg',
51+
'103': 'ansi-bright-yellow-bg',
52+
'104': 'ansi-bright-blue-bg',
53+
'105': 'ansi-bright-magenta-bg',
54+
'106': 'ansi-bright-cyan-bg',
55+
'107': 'ansi-bright-white-bg',
56+
};
57+
58+
const parseAnsi = (inputText: string) => {
59+
const ansiRegex = /\u001b\[([0-9;]*)m/g;
60+
const parts = [];
61+
let lastIndex = 0;
62+
let match;
63+
const currentClasses = new Set<string>();
64+
65+
while ((match = ansiRegex.exec(inputText)) !== null) {
66+
const textBefore = inputText.substring(lastIndex, match.index);
67+
if (textBefore) {
68+
parts.push({ text: textBefore, classes: Array.from(currentClasses) });
69+
}
70+
71+
lastIndex = ansiRegex.lastIndex;
72+
73+
const codes = match[1].split(';').filter(Boolean);
74+
75+
// An empty code sequence is a reset.
76+
if (codes.length === 0) {
77+
currentClasses.clear();
78+
continue;
79+
}
80+
81+
codes.forEach(code => {
82+
if (code === '0') {
83+
// Full reset.
84+
currentClasses.clear();
85+
} else if (code === '39') {
86+
// Reset foreground color.
87+
currentClasses.forEach(cls => {
88+
if (cls.endsWith('-fg')) {
89+
currentClasses.delete(cls);
90+
}
91+
});
92+
} else if (code === '49') {
93+
// Reset background color.
94+
currentClasses.forEach(cls => {
95+
if (cls.endsWith('-bg')) {
96+
currentClasses.delete(cls);
97+
}
98+
});
99+
} else {
100+
const newClass = ansiCodes[code];
101+
if (newClass) {
102+
// If setting a new color, remove any existing color of the same type.
103+
const isFg = newClass.endsWith('-fg');
104+
const isBg = newClass.endsWith('-bg');
105+
106+
if (isFg || isBg) {
107+
const typeSuffix = isFg ? '-fg' : '-bg';
108+
currentClasses.forEach(cls => {
109+
if (cls.endsWith(typeSuffix)) {
110+
currentClasses.delete(cls);
111+
}
112+
});
113+
}
114+
currentClasses.add(newClass);
115+
}
116+
}
117+
});
118+
}
119+
120+
// Add any remaining text after the last ANSI code.
121+
const remainingText = inputText.substring(lastIndex);
122+
if (remainingText) {
123+
parts.push({ text: remainingText, classes: Array.from(currentClasses) });
124+
}
125+
126+
return parts;
127+
};
128+
129+
/**
130+
* Highlight matches of a search query in a text.
131+
* @param text The text to search within.
132+
* @param query The search query.
133+
* @returns An array of strings and JSX elements with matches wrapped in <b> tags.
134+
*/
135+
const highlightMatches = (text: string, query?: string) => {
136+
if (!query) {
137+
return text;
138+
}
139+
140+
// Escape special characters for regex
141+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142+
const regex = new RegExp(`(${escapedQuery})`, 'gi');
143+
const parts = text.split(regex);
144+
145+
return parts.map((part, index) =>
146+
part.toLowerCase() === query.toLowerCase() ? (
147+
<b key={index} className="search-highlight">
148+
{part}
149+
</b>
150+
) : (
151+
part
152+
)
153+
);
154+
};
155+
156+
const textParts = parseAnsi(text);
157+
158+
return (
159+
<>
160+
{textParts.map((part, index) => (
161+
<span key={index} className={part.classes.join(' ')}>
162+
{highlightMatches(part.text, searchQuery)}
163+
</span>
164+
))}
165+
</>
166+
);
167+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.LogsDisplay__row:hover {
2+
background-color: rgba(255, 255, 255, 0.1);
3+
}
4+
5+
.LogsDisplay__row {
6+
word-break: break-all;
7+
border-left: 5px solid;
8+
padding-left: 5px;
9+
}

0 commit comments

Comments
 (0)