Skip to content

Commit 21246ff

Browse files
Guillaume Cadoretgcadoret
authored andcommitted
feat: add a button to clear logs in the pod logs viewer
Signed-off-by: Guillaume Cadoret <guillaume.cadoret@pluxeegroup.com> Signed-off-by: Guillaume Cadoret <gcadoret@gmail.com>
1 parent 84553d3 commit 21246ff

4 files changed

Lines changed: 164 additions & 1 deletion

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as React from 'react';
2+
import {Button} from '../../../shared/components/button';
3+
4+
interface ClearLogsButtonProps {
5+
disabled?: boolean;
6+
onClear: () => void;
7+
}
8+
9+
export const ClearLogsButton = ({disabled, onClear}: ClearLogsButtonProps) => <Button title='Clear displayed logs' icon='eraser' onClick={onClear} disabled={disabled} />;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import * as React from 'react';
2+
import * as ReactDOM from 'react-dom';
3+
import {EMPTY, of} from 'rxjs';
4+
import {act} from 'react-dom/test-utils';
5+
import {LogEntry} from '../../../shared/models';
6+
import {PodsLogsViewer} from './pod-logs-viewer';
7+
8+
const mockGetContainerLogs = jest.fn();
9+
10+
jest.mock('argo-ui', () => ({
11+
DataLoader: ({children}: {children: (data: any) => React.ReactNode}) => children({appDetails: {darkMode: false, wrapLines: false}}),
12+
Tooltip: ({children}: {children: React.ReactNode}) => {
13+
const React = require('react');
14+
return React.createElement(React.Fragment, null, children);
15+
}
16+
}));
17+
18+
jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
19+
__esModule: true,
20+
default: ({children}: {children: ({width, height}: {width: number; height: number}) => React.ReactNode}) => children({width: 800, height: 400})
21+
}));
22+
23+
jest.mock('ansi-to-react', () => ({
24+
__esModule: true,
25+
default: ({children}: {children: React.ReactNode}) => {
26+
const React = require('react');
27+
return React.createElement(React.Fragment, null, children);
28+
}
29+
}));
30+
31+
jest.mock('../../../shared/services', () => ({
32+
services: {
33+
applications: {
34+
getContainerLogs: (...args: any[]) => mockGetContainerLogs(...args)
35+
},
36+
viewPreferences: {
37+
getPreferences: jest.fn()
38+
}
39+
}
40+
}));
41+
42+
jest.mock('./copy-logs-button', () => ({CopyLogsButton: () => null}));
43+
jest.mock('./download-logs-button', () => ({DownloadLogsButton: () => null}));
44+
jest.mock('./container-selector', () => ({ContainerSelector: () => null}));
45+
jest.mock('./follow-toggle-button', () => ({FollowToggleButton: () => null}));
46+
jest.mock('./show-previous-logs-toggle-button', () => ({ShowPreviousLogsToggleButton: () => null}));
47+
jest.mock('./pod-logs-highlight-button', () => ({PodHighlightButton: () => null}));
48+
jest.mock('./timestamps-toggle-button', () => ({TimestampsToggleButton: () => null}));
49+
jest.mock('./dark-mode-toggle-button', () => ({DarkModeToggleButton: () => null}));
50+
jest.mock('./fullscreen-button', () => ({FullscreenButton: () => null}));
51+
jest.mock('./log-message-filter', () => ({LogMessageFilter: () => null}));
52+
jest.mock('./since-seconds-selector', () => ({SinceSecondsSelector: () => null}));
53+
jest.mock('./tail-selector', () => ({TailSelector: () => null}));
54+
jest.mock('./pod-names-toggle-button', () => ({PodNamesToggleButton: () => null}));
55+
jest.mock('./auto-scroll-button', () => ({AutoScrollButton: () => null}));
56+
jest.mock('./wrap-lines-button', () => ({WrapLinesButton: () => null}));
57+
jest.mock('./match-case-toggle-button', () => ({MatchCaseToggleButton: () => null}));
58+
59+
const logsFixture: LogEntry[] = [
60+
{
61+
content: 'INFO Starting application',
62+
last: false,
63+
podName: 'demo-app-68b8fcf645-pkc5s',
64+
timeStamp: new Date().toISOString(),
65+
timeStampStr: '2026-04-03T19:00:00.000Z'
66+
},
67+
{
68+
content: 'INFO Listening on :8080',
69+
last: false,
70+
podName: 'demo-app-68b8fcf645-pkc5s',
71+
timeStamp: new Date().toISOString(),
72+
timeStampStr: '2026-04-03T19:00:01.000Z'
73+
}
74+
];
75+
76+
const makeComponent = () => (
77+
<PodsLogsViewer
78+
applicationName='demo-app'
79+
applicationNamespace='argocd'
80+
namespace='default'
81+
containerName='glady-app'
82+
podName='demo-app-68b8fcf645-pkc5s'
83+
/>
84+
);
85+
86+
describe('PodsLogsViewer clear logs button', () => {
87+
let container: HTMLDivElement;
88+
89+
beforeEach(() => {
90+
jest.clearAllMocks();
91+
container = document.createElement('div');
92+
document.body.appendChild(container);
93+
});
94+
95+
afterEach(() => {
96+
ReactDOM.unmountComponentAtNode(container);
97+
container.remove();
98+
});
99+
100+
const renderComponent = () => {
101+
act(() => {
102+
ReactDOM.render(makeComponent(), container);
103+
});
104+
};
105+
106+
const getClearButton = () => {
107+
const clearButtonIcon = container.querySelector('.fa-eraser');
108+
expect(clearButtonIcon).toBeTruthy();
109+
return clearButtonIcon.closest('button') as HTMLButtonElement;
110+
};
111+
112+
it('keeps the clear button disabled when no log has been loaded and ignores clicks', () => {
113+
mockGetContainerLogs.mockReturnValue(EMPTY);
114+
115+
renderComponent();
116+
117+
const clearButton = getClearButton();
118+
expect(clearButton.disabled).toBe(true);
119+
120+
const beforeClick = container.textContent;
121+
act(() => {
122+
clearButton.click();
123+
});
124+
125+
expect(container.textContent).toBe(beforeClick);
126+
});
127+
128+
it('clears displayed logs when the clear button is clicked', () => {
129+
mockGetContainerLogs.mockReturnValue(of(...logsFixture));
130+
131+
renderComponent();
132+
133+
const beforeClear = container.textContent;
134+
expect(beforeClear).toContain('INFO Starting application');
135+
expect(beforeClear).toContain('INFO Listening on :8080');
136+
137+
const clearButton = getClearButton();
138+
expect(clearButton.disabled).toBe(false);
139+
140+
act(() => {
141+
clearButton.click();
142+
});
143+
144+
const afterClear = container.textContent;
145+
expect(afterClear).not.toContain('INFO Starting application');
146+
expect(afterClear).not.toContain('INFO Listening on :8080');
147+
148+
const disabledClearButton = getClearButton();
149+
expect(disabledClearButton.disabled).toBe(true);
150+
});
151+
});

ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {services, ViewPreferences} from '../../../shared/services';
1010
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
1111

1212
import './pod-logs-viewer.scss';
13+
import {ClearLogsButton} from './clear-logs-button';
1314
import {CopyLogsButton} from './copy-logs-button';
1415
import {DownloadLogsButton} from './download-logs-button';
1516
import {ContainerSelector} from './container-selector';
@@ -295,6 +296,7 @@ export const PodsLogsViewer = (props: PodLogsProps) => {
295296
</span>
296297
<Spacer />
297298
<span>
299+
<ClearLogsButton disabled={logs.length === 0} onClear={() => setLogs([])} />
298300
<CopyLogsButton logs={logs} />
299301
<DownloadLogsButton {...props} previous={previous} />
300302
<FullscreenButton

ui/src/app/shared/components/button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export const Button = ({
3030
<button
3131
className={'argo-button ' + (!outline ? 'argo-button--base' : 'argo-button--base-o') + ' ' + (disabled ? 'disabled' : '') + ' ' + (className || '')}
3232
style={style}
33-
onClick={onClick}>
33+
onClick={onClick}
34+
disabled={disabled}>
3435
{icon && <i className={'fa fa-' + icon + ' ' + (beat ? 'fa-beat' : '') + (rotate ? 'fa-rotate-180' : '')} />} {children}
3536
</button>
3637
</Tooltip>

0 commit comments

Comments
 (0)