Fix: Opening logs link broken (Pref framework) (#9182)

* fix: logs popover content logic extracted out

* fix: logs popover content in live view

* fix: destory popover on close

* feat: add logs format tests

* feat: minor refactor

* feat: test case refactor

* feat: remove menu refs in logs live view
This commit is contained in:
Aditya Singh 2025-09-25 19:14:05 +05:30 committed by GitHub
parent 1aa5f5d0e1
commit 96cdf21a92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 183 additions and 37 deletions

View File

@ -26,7 +26,7 @@ interface LogsFormatOptionsMenuProps {
config: OptionsMenuConfig; config: OptionsMenuConfig;
} }
export default function LogsFormatOptionsMenu({ function OptionsMenu({
items, items,
selectedOptionFormat, selectedOptionFormat,
config, config,
@ -49,7 +49,6 @@ export default function LogsFormatOptionsMenu({
const [selectedValue, setSelectedValue] = useState<string | null>(null); const [selectedValue, setSelectedValue] = useState<string | null>(null);
const listRef = useRef<HTMLDivElement>(null); const listRef = useRef<HTMLDivElement>(null);
const initialMouseEnterRef = useRef<boolean>(false); const initialMouseEnterRef = useRef<boolean>(false);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const onChange = useCallback( const onChange = useCallback(
(key: LogViewMode) => { (key: LogViewMode) => {
@ -209,7 +208,7 @@ export default function LogsFormatOptionsMenu({
}; };
}, [selectedValue]); }, [selectedValue]);
const popoverContent = ( return (
<div <div
className={cx( className={cx(
'nested-menu-container', 'nested-menu-container',
@ -447,15 +446,30 @@ export default function LogsFormatOptionsMenu({
)} )}
</div> </div>
); );
}
function LogsFormatOptionsMenu({
items,
selectedOptionFormat,
config,
}: LogsFormatOptionsMenuProps): JSX.Element {
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
return ( return (
<Popover <Popover
content={popoverContent} content={
<OptionsMenu
items={items}
selectedOptionFormat={selectedOptionFormat}
config={config}
/>
}
trigger="click" trigger="click"
placement="bottomRight" placement="bottomRight"
arrow={false} arrow={false}
open={isPopoverOpen} open={isPopoverOpen}
onOpenChange={setIsPopoverOpen} onOpenChange={setIsPopoverOpen}
rootClassName="format-options-popover" rootClassName="format-options-popover"
destroyTooltipOnHide
> >
<Button <Button
className="periscope-btn ghost" className="periscope-btn ghost"
@ -465,3 +479,5 @@ export default function LogsFormatOptionsMenu({
</Popover> </Popover>
); );
} }
export default LogsFormatOptionsMenu;

View File

@ -0,0 +1,157 @@
import { FontSize } from 'container/OptionsMenu/types';
import { fireEvent, render, waitFor } from 'tests/test-utils';
import LogsFormatOptionsMenu from '../LogsFormatOptionsMenu';
const mockUpdateFormatting = jest.fn();
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: mockUpdateFormatting,
}),
}));
describe('LogsFormatOptionsMenu (unit)', () => {
beforeEach(() => {
mockUpdateFormatting.mockClear();
});
function setup(): {
getByTestId: ReturnType<typeof render>['getByTestId'];
findItemByLabel: (label: string) => Element | undefined;
formatOnChange: jest.Mock<any, any>;
maxLinesOnChange: jest.Mock<any, any>;
fontSizeOnChange: jest.Mock<any, any>;
} {
const items = [
{ key: 'raw', label: 'Raw', data: { title: 'max lines per row' } },
{ key: 'list', label: 'Default' },
{ key: 'table', label: 'Column', data: { title: 'columns' } },
];
const formatOnChange = jest.fn();
const maxLinesOnChange = jest.fn();
const fontSizeOnChange = jest.fn();
const { getByTestId } = render(
<LogsFormatOptionsMenu
items={items}
selectedOptionFormat="table"
config={{
format: { value: 'table', onChange: formatOnChange },
maxLines: { value: 2, onChange: maxLinesOnChange },
fontSize: { value: FontSize.SMALL, onChange: fontSizeOnChange },
addColumn: {
isFetching: false,
value: [],
options: [],
onFocus: jest.fn(),
onBlur: jest.fn(),
onSearch: jest.fn(),
onSelect: jest.fn(),
onRemove: jest.fn(),
},
}}
/>,
);
// Open the popover menu by default for each test
const formatButton = getByTestId('periscope-btn-format-options');
fireEvent.click(formatButton);
const getMenuItems = (): Element[] =>
Array.from(document.querySelectorAll('.menu-items .item'));
const findItemByLabel = (label: string): Element | undefined =>
getMenuItems().find((el) => (el.textContent || '').includes(label));
return {
getByTestId,
findItemByLabel,
formatOnChange,
maxLinesOnChange,
fontSizeOnChange,
};
}
// Covers: opens menu, changes format selection, updates max-lines, changes font size
it('opens and toggles format selection', async () => {
const { findItemByLabel, formatOnChange } = setup();
// Assert initial selection
const columnItem = findItemByLabel('Column') as Element;
expect(document.querySelectorAll('.menu-items .item svg')).toHaveLength(1);
expect(columnItem.querySelector('svg')).toBeInTheDocument();
// Change selection to 'Raw'
const rawItem = findItemByLabel('Raw') as Element;
fireEvent.click(rawItem as HTMLElement);
await waitFor(() => {
const rawEl = findItemByLabel('Raw') as Element;
expect(document.querySelectorAll('.menu-items .item svg')).toHaveLength(1);
expect(rawEl.querySelector('svg')).toBeInTheDocument();
});
expect(formatOnChange).toHaveBeenCalledWith('raw');
});
it('increments max-lines and calls onChange', async () => {
const { maxLinesOnChange } = setup();
// Increment max lines
const input = document.querySelector(
'.max-lines-per-row-input input',
) as HTMLInputElement;
const initial = Number(input.value);
const buttons = document.querySelectorAll(
'.max-lines-per-row-input .periscope-btn',
);
const incrementBtn = buttons[1] as HTMLElement;
fireEvent.click(incrementBtn);
await waitFor(() => {
expect(Number(input.value)).toBe(initial + 1);
});
await waitFor(() => {
expect(maxLinesOnChange).toHaveBeenCalledWith(initial + 1);
});
});
it('changes font size to MEDIUM and calls onChange', async () => {
const { fontSizeOnChange } = setup();
// Open font dropdown
const fontButton = document.querySelector(
'.font-size-container .value',
) as HTMLElement;
fireEvent.click(fontButton);
// Choose MEDIUM
const optionButtons = Array.from(
document.querySelectorAll('.font-size-dropdown .option-btn'),
);
const mediumBtn = optionButtons[1] as HTMLElement;
fireEvent.click(mediumBtn);
await waitFor(() => {
expect(
document.querySelectorAll('.font-size-dropdown .option-btn .icon'),
).toHaveLength(1);
expect(
(optionButtons[1] as Element).querySelector('.icon'),
).toBeInTheDocument();
});
await waitFor(() => {
expect(fontSizeOnChange).toHaveBeenCalledWith(FontSize.MEDIUM);
});
});
});

View File

@ -1,6 +1,6 @@
import './LiveLogsContainer.styles.scss'; import './LiveLogsContainer.styles.scss';
import { Button, Switch, Typography } from 'antd'; import { Switch, Typography } from 'antd';
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu'; import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail'; import { MAX_LOGS_LIST_SIZE } from 'constants/liveTail';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
@ -8,10 +8,8 @@ import GoToTop from 'container/GoToTop';
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useClickOutside from 'hooks/useClickOutside';
import useDebouncedFn from 'hooks/useDebouncedFunction'; import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useEventSourceEvent } from 'hooks/useEventSourceEvent'; import { useEventSourceEvent } from 'hooks/useEventSourceEvent';
import { Sliders } from 'lucide-react';
import { useEventSource } from 'providers/EventSource'; import { useEventSource } from 'providers/EventSource';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@ -41,9 +39,6 @@ function LiveLogsContainer(): JSX.Element {
const batchedEventsRef = useRef<ILiveLogsLog[]>([]); const batchedEventsRef = useRef<ILiveLogsLog[]>([]);
const [showFormatMenuItems, setShowFormatMenuItems] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const prevFilterExpressionRef = useRef<string | null>(null); const prevFilterExpressionRef = useRef<string | null>(null);
const { options, config } = useOptionsMenu({ const { options, config } = useOptionsMenu({
@ -73,18 +68,6 @@ function LiveLogsContainer(): JSX.Element {
}, },
]; ];
const handleToggleShowFormatOptions = (): void =>
setShowFormatMenuItems(!showFormatMenuItems);
useClickOutside({
ref: menuRef,
onClickOutside: () => {
if (showFormatMenuItems) {
setShowFormatMenuItems(false);
}
},
});
const { const {
handleStartOpenConnection, handleStartOpenConnection,
handleCloseConnection, handleCloseConnection,
@ -231,21 +214,11 @@ function LiveLogsContainer(): JSX.Element {
/> />
</div> </div>
<div className="format-options-container" ref={menuRef}> <LogsFormatOptionsMenu
<Button items={formatItems}
className="periscope-btn ghost" selectedOptionFormat={options.format}
onClick={handleToggleShowFormatOptions} config={config}
icon={<Sliders size={14} />} />
/>
{showFormatMenuItems && (
<LogsFormatOptionsMenu
items={formatItems}
selectedOptionFormat={options.format}
config={config}
/>
)}
</div>
</div> </div>
{showLiveLogsFrequencyChart && ( {showLiveLogsFrequencyChart && (