This commit is contained in:
Guillaume CADORET 2026-04-21 10:04:24 +02:00 committed by GitHub
commit 84ace963e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 164 additions and 1 deletions

View file

@ -0,0 +1,9 @@
import * as React from 'react';
import {Button} from '../../../shared/components/button';
interface ClearLogsButtonProps {
disabled?: boolean;
onClear: () => void;
}
export const ClearLogsButton = ({disabled, onClear}: ClearLogsButtonProps) => <Button title='Clear displayed logs' icon='eraser' onClick={onClear} disabled={disabled} />;

View file

@ -0,0 +1,151 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {EMPTY, of} from 'rxjs';
import {act} from 'react-dom/test-utils';
import {LogEntry} from '../../../shared/models';
import {PodsLogsViewer} from './pod-logs-viewer';
const mockGetContainerLogs = jest.fn();
jest.mock('argo-ui', () => ({
DataLoader: ({children}: {children: (data: any) => React.ReactNode}) => children({appDetails: {darkMode: false, wrapLines: false}}),
Tooltip: ({children}: {children: React.ReactNode}) => {
const React = require('react');
return React.createElement(React.Fragment, null, children);
}
}));
jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({
__esModule: true,
default: ({children}: {children: ({width, height}: {width: number; height: number}) => React.ReactNode}) => children({width: 800, height: 400})
}));
jest.mock('ansi-to-react', () => ({
__esModule: true,
default: ({children}: {children: React.ReactNode}) => {
const React = require('react');
return React.createElement(React.Fragment, null, children);
}
}));
jest.mock('../../../shared/services', () => ({
services: {
applications: {
getContainerLogs: (...args: any[]) => mockGetContainerLogs(...args)
},
viewPreferences: {
getPreferences: jest.fn()
}
}
}));
jest.mock('./copy-logs-button', () => ({CopyLogsButton: () => null}));
jest.mock('./download-logs-button', () => ({DownloadLogsButton: () => null}));
jest.mock('./container-selector', () => ({ContainerSelector: () => null}));
jest.mock('./follow-toggle-button', () => ({FollowToggleButton: () => null}));
jest.mock('./show-previous-logs-toggle-button', () => ({ShowPreviousLogsToggleButton: () => null}));
jest.mock('./pod-logs-highlight-button', () => ({PodHighlightButton: () => null}));
jest.mock('./timestamps-toggle-button', () => ({TimestampsToggleButton: () => null}));
jest.mock('./dark-mode-toggle-button', () => ({DarkModeToggleButton: () => null}));
jest.mock('./fullscreen-button', () => ({FullscreenButton: () => null}));
jest.mock('./log-message-filter', () => ({LogMessageFilter: () => null}));
jest.mock('./since-seconds-selector', () => ({SinceSecondsSelector: () => null}));
jest.mock('./tail-selector', () => ({TailSelector: () => null}));
jest.mock('./pod-names-toggle-button', () => ({PodNamesToggleButton: () => null}));
jest.mock('./auto-scroll-button', () => ({AutoScrollButton: () => null}));
jest.mock('./wrap-lines-button', () => ({WrapLinesButton: () => null}));
jest.mock('./match-case-toggle-button', () => ({MatchCaseToggleButton: () => null}));
const logsFixture: LogEntry[] = [
{
content: 'INFO Starting application',
last: false,
podName: 'demo-app-68b8fcf645-pkc5s',
timeStamp: new Date().toISOString(),
timeStampStr: '2026-04-03T19:00:00.000Z'
},
{
content: 'INFO Listening on :8080',
last: false,
podName: 'demo-app-68b8fcf645-pkc5s',
timeStamp: new Date().toISOString(),
timeStampStr: '2026-04-03T19:00:01.000Z'
}
];
const makeComponent = () => (
<PodsLogsViewer
applicationName='demo-app'
applicationNamespace='argocd'
namespace='default'
containerName='glady-app'
podName='demo-app-68b8fcf645-pkc5s'
/>
);
describe('PodsLogsViewer clear logs button', () => {
let container: HTMLDivElement;
beforeEach(() => {
jest.clearAllMocks();
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
ReactDOM.unmountComponentAtNode(container);
container.remove();
});
const renderComponent = () => {
act(() => {
ReactDOM.render(makeComponent(), container);
});
};
const getClearButton = () => {
const clearButtonIcon = container.querySelector('.fa-eraser');
expect(clearButtonIcon).toBeTruthy();
return clearButtonIcon.closest('button') as HTMLButtonElement;
};
it('keeps the clear button disabled when no log has been loaded and ignores clicks', () => {
mockGetContainerLogs.mockReturnValue(EMPTY);
renderComponent();
const clearButton = getClearButton();
expect(clearButton.disabled).toBe(true);
const beforeClick = container.textContent;
act(() => {
clearButton.click();
});
expect(container.textContent).toBe(beforeClick);
});
it('clears displayed logs when the clear button is clicked', () => {
mockGetContainerLogs.mockReturnValue(of(...logsFixture));
renderComponent();
const beforeClear = container.textContent;
expect(beforeClear).toContain('INFO Starting application');
expect(beforeClear).toContain('INFO Listening on :8080');
const clearButton = getClearButton();
expect(clearButton.disabled).toBe(false);
act(() => {
clearButton.click();
});
const afterClear = container.textContent;
expect(afterClear).not.toContain('INFO Starting application');
expect(afterClear).not.toContain('INFO Listening on :8080');
const disabledClearButton = getClearButton();
expect(disabledClearButton.disabled).toBe(true);
});
});

View file

@ -10,6 +10,7 @@ import {services, ViewPreferences} from '../../../shared/services';
import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
import './pod-logs-viewer.scss';
import {ClearLogsButton} from './clear-logs-button';
import {CopyLogsButton} from './copy-logs-button';
import {DownloadLogsButton} from './download-logs-button';
import {ContainerSelector} from './container-selector';
@ -294,6 +295,7 @@ export const PodsLogsViewer = (props: PodLogsProps) => {
</span>
<Spacer />
<span>
<ClearLogsButton disabled={logs.length === 0} onClear={() => setLogs([])} />
<CopyLogsButton logs={logs} />
<DownloadLogsButton {...props} previous={previous} />
<FullscreenButton

View file

@ -30,7 +30,8 @@ export const Button = ({
<button
className={'argo-button ' + (!outline ? 'argo-button--base' : 'argo-button--base-o') + ' ' + (disabled ? 'disabled' : '') + ' ' + (className || '')}
style={style}
onClick={onClick}>
onClick={onClick}
disabled={disabled}>
{icon && <i className={'fa fa-' + icon + ' ' + (beat ? 'fa-beat' : '') + (rotate ? 'fa-rotate-180' : '')} />} {children}
</button>
</Tooltip>