Skip to content

Commit 2940beb

Browse files
authored
Rework tailing of logs (#1048)
1 parent 9afe2f9 commit 2940beb

File tree

18 files changed

+764
-477
lines changed

18 files changed

+764
-477
lines changed

src/main/frontend/common/RestClient.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface StepLogBufferInfo {
5151
lastFetched?: number;
5252
stopTailing?: boolean;
5353
exceptionText?: string[];
54-
pendingExceptionText?: Promise<string[]>;
54+
pendingExceptionText?: Promise<void>;
5555
}
5656

5757
// Returned from API, gets converted to 'StepLogBufferInfo'.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { LocalizedMessageKey, useMessages } from "../i18n";
2+
import Tooltip from "./tooltip.tsx";
3+
4+
export interface TailLogsButtonProps {
5+
complete: boolean;
6+
loading: boolean;
7+
tailLogs: boolean;
8+
startTailingLogs: () => void;
9+
stopTailingLogs: () => void;
10+
}
11+
12+
export default function TailLogsButton({
13+
complete,
14+
loading,
15+
tailLogs,
16+
startTailingLogs,
17+
stopTailingLogs,
18+
}: TailLogsButtonProps) {
19+
const messages = useMessages();
20+
if (loading || complete) return null;
21+
22+
return (
23+
<Tooltip
24+
content={
25+
tailLogs
26+
? messages.format(LocalizedMessageKey.tailLogsPause)
27+
: messages.format(LocalizedMessageKey.tailLogsResume)
28+
}
29+
>
30+
<button
31+
className={"jenkins-button jenkins-!-info-color"}
32+
onClick={tailLogs ? stopTailingLogs : startTailingLogs}
33+
>
34+
{tailLogs ? pauseIcon() : playIcon()}
35+
</button>
36+
</Tooltip>
37+
);
38+
}
39+
40+
// play-circle-outline.svg
41+
function playIcon() {
42+
return (
43+
<svg
44+
xmlns="http://www.w3.org/2000/svg"
45+
className="ionicon"
46+
viewBox="0 0 512 512"
47+
>
48+
<path
49+
d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z"
50+
fill="none"
51+
stroke="currentColor"
52+
strokeMiterlimit="10"
53+
strokeWidth="32"
54+
/>
55+
<path
56+
d="M216.32 334.44l114.45-69.14a10.89 10.89 0 000-18.6l-114.45-69.14a10.78 10.78 0 00-16.32 9.31v138.26a10.78 10.78 0 0016.32 9.31z"
57+
fill="currentColor"
58+
/>
59+
</svg>
60+
);
61+
}
62+
63+
// pause-circle-outline.svg
64+
function pauseIcon() {
65+
return (
66+
<svg
67+
xmlns="http://www.w3.org/2000/svg"
68+
className="ionicon"
69+
viewBox="0 0 512 512"
70+
>
71+
<path
72+
d="M448 256c0-106-86-192-192-192S64 150 64 256s86 192 192 192 192-86 192-192z"
73+
fill="none"
74+
stroke="currentColor"
75+
strokeMiterlimit="10"
76+
strokeWidth="32"
77+
/>
78+
<path
79+
fill="none"
80+
stroke="currentColor"
81+
strokeLinecap="round"
82+
strokeMiterlimit="10"
83+
strokeWidth="32"
84+
d="M208 192v128M304 192v128"
85+
/>
86+
</svg>
87+
);
88+
}

src/main/frontend/common/i18n/messages.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export enum LocalizedMessageKey {
7979
showNames = "settings.showStageName",
8080
showDuration = "settings.showStageDuration",
8181
consoleNewTab = "console.newTab",
82+
tailLogsResume = "tailLogs.resume",
83+
tailLogsPause = "tailLogs.pause",
8284
}
8385

8486
const DEFAULT_MESSAGES: ResourceBundle = {
@@ -92,6 +94,8 @@ const DEFAULT_MESSAGES: ResourceBundle = {
9294
[LocalizedMessageKey.showNames]: "Show stage names",
9395
[LocalizedMessageKey.showDuration]: "Show stage duration",
9496
[LocalizedMessageKey.consoleNewTab]: "View step as plain text",
97+
[LocalizedMessageKey.tailLogsResume]: "Resume tailing logs",
98+
[LocalizedMessageKey.tailLogsPause]: "Pause tailing logs",
9599
};
96100

97101
export function defaultMessages(locale: string): Messages {

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLine.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { linkifyJsOptions } from "../../../common/utils/linkify-js.ts";
55
import { makeReactChildren, tokenizeANSIString } from "./Ansi.tsx";
66

77
export interface ConsoleLineProps {
8+
stopTailingLogs: () => void;
89
lineNumber: string;
910
content: string;
1011
stepId: string;
@@ -26,6 +27,7 @@ export const ConsoleLine = memo(function ConsoleLine(props: ConsoleLineProps) {
2627
id={id}
2728
href={`${baseURL}#${id}`}
2829
onClick={() => {
30+
props.stopTailingLogs();
2931
// Avoid an actual page navigation by swapping the current URL for
3032
// the baseURL (query without hash) before the default "click"
3133
// behavior (the browsers page navigation logic) runs. The

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.spec.tsx

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
/** * @vitest-environment jsdom */
22

33
import { render } from "@testing-library/react";
4-
import { vi } from "vitest";
4+
import { beforeEach, Mock, vi } from "vitest";
55

66
import ConsoleLogCard, { ConsoleLogCardProps } from "./ConsoleLogCard.tsx";
77
import { ConsoleLogStreamProps } from "./ConsoleLogStream.tsx";
88
import {
99
Result,
1010
StepInfo,
1111
StepLogBufferInfo,
12+
TAIL_CONSOLE_LOG,
1213
} from "./PipelineConsoleModel.tsx";
1314

1415
vi.mock("./ConsoleLogStream.tsx", () => {
@@ -41,18 +42,21 @@ describe("ConsoleLogCard", () => {
4142
endByte: 13,
4243
};
4344

44-
const DefaultTestProps = {
45+
const DefaultTestProps: ConsoleLogCardProps = {
4546
step: baseStep,
46-
stepBuffer: baseBuffer,
47+
stepBuffers: new Map().set(baseStep.id, baseBuffer),
4748
isExpanded: false,
48-
onStepToggle: () => {
49-
console.log("onStepToggle triggered");
50-
},
51-
onMoreConsoleClick: () => {
52-
console.log("onMoreConsoleClick triggered");
53-
},
54-
fetchExceptionText: () => {},
49+
onStepToggle: vi.fn(),
50+
fetchLogText: vi.fn().mockResolvedValue(baseBuffer),
51+
fetchExceptionText: vi.fn().mockResolvedValue(baseBuffer),
52+
tailLogs: true,
53+
scrollToTail: () => {},
54+
stopTailingLogs: () => {},
5555
} as ConsoleLogCardProps;
56+
beforeEach(function () {
57+
(DefaultTestProps.fetchLogText as Mock).mockReset();
58+
(DefaultTestProps.fetchLogText as Mock).mockResolvedValue(baseBuffer);
59+
});
5660

5761
it("renders step header only when not expanded", async () => {
5862
const { getByText } = render(<ConsoleLogCard {...DefaultTestProps} />);
@@ -67,17 +71,16 @@ describe("ConsoleLogCard", () => {
6771
expect(findByText(/Hello, world!/));
6872
});
6973

70-
it("calls onMoreConsoleClick on load was card isExpanded set", async () => {
71-
console.log = vi.fn();
74+
it("calls fetchLogText on load was card isExpanded set", async () => {
7275
render(<ConsoleLogCard {...DefaultTestProps} isExpanded />);
73-
expect(console.log).toHaveBeenCalledWith("onMoreConsoleClick triggered");
76+
expect(DefaultTestProps.fetchLogText as Mock).toHaveBeenCalledWith(
77+
DefaultTestProps.step.id,
78+
TAIL_CONSOLE_LOG,
79+
);
7480
});
7581

76-
it("does not call onMoreConsoleClick on load was card isExpanded set", async () => {
77-
console.log = vi.fn();
82+
it("does not call fetchLogText on load was card isExpanded set", async () => {
7883
render(<ConsoleLogCard {...DefaultTestProps} />);
79-
expect(console.log).not.toHaveBeenCalledWith(
80-
"onMoreConsoleClick triggered",
81-
);
84+
expect(DefaultTestProps.fetchLogText as Mock).not.toHaveBeenCalled();
8285
});
8386
});

0 commit comments

Comments
 (0)