Compare commits

...

3 Commits

Author SHA1 Message Date
ahmadshaheer
22d1b90e2a chore: add optional chaining .keys 2025-12-15 18:35:38 +04:30
ahmadshaheer
aa9a2863af chore: add tests for add/remove columns from log detail drawer 2025-12-15 15:16:06 +04:30
ahmadshaheer
c5fddb2e09 feat: add ability to add/remove columns from log detail page 2025-12-14 16:39:05 +04:30
6 changed files with 270 additions and 8 deletions

View File

@@ -80,12 +80,32 @@ function LogDetailInner({
return stagedQuery.builder.queryData.find((item) => !item.disabled) || null;
}, [stagedQuery]);
const { options } = useOptionsMenu({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});
const handleAddColumn = useCallback(
(fieldName: string): void => {
if (config?.addColumn?.onSelect) {
// onSelect from SelectProps has signature (value, option), but handleSelectColumns only needs value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
config.addColumn.onSelect(fieldName, {} as any);
}
},
[config],
);
const handleRemoveColumn = useCallback(
(fieldName: string): void => {
if (config?.addColumn?.onRemove) {
config.addColumn.onRemove(fieldName);
}
},
[config],
);
const isDarkMode = useIsDarkMode();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
@@ -369,6 +389,8 @@ function LogDetailInner({
isListViewPanel={isListViewPanel}
selectedOptions={options}
listViewPanelSelectedFields={listViewPanelSelectedFields}
onAddColumn={handleAddColumn}
onRemoveColumn={handleRemoveColumn}
/>
)}
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}

View File

@@ -30,6 +30,8 @@ interface OverviewProps {
selectedOptions: OptionsQuery;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onAddColumn?: (fieldName: string) => void;
onRemoveColumn?: (fieldName: string) => void;
}
type Props = OverviewProps &
@@ -44,6 +46,8 @@ function Overview({
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
onAddColumn,
onRemoveColumn,
}: Props): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState<boolean>(true);
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
@@ -213,6 +217,8 @@ function Overview({
isListViewPanel={isListViewPanel}
selectedOptions={selectedOptions}
listViewPanelSelectedFields={listViewPanelSelectedFields}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
/>
</>
),
@@ -228,6 +234,8 @@ Overview.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
onAddColumn: undefined,
onRemoveColumn: undefined,
};
export default Overview;

View File

@@ -48,6 +48,8 @@ interface TableViewProps {
isListViewPanel?: boolean;
listViewPanelSelectedFields?: IField[] | null;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onAddColumn?: (fieldName: string) => void;
onRemoveColumn?: (fieldName: string) => void;
}
type Props = TableViewProps &
@@ -63,6 +65,8 @@ function TableView({
selectedOptions,
onGroupByAttribute,
listViewPanelSelectedFields,
onAddColumn,
onRemoveColumn,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
@@ -292,6 +296,9 @@ function TableView({
isfilterOutLoading={isfilterOutLoading}
onClickHandler={onClickHandler}
onGroupByAttribute={onGroupByAttribute}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
selectedOptions={selectedOptions}
/>
),
},
@@ -335,6 +342,8 @@ TableView.defaultProps = {
isListViewPanel: false,
listViewPanelSelectedFields: null,
onGroupByAttribute: undefined,
onAddColumn: undefined,
onRemoveColumn: undefined,
};
export interface DataType {

View File

@@ -11,7 +11,14 @@ import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
import { OptionsQuery } from 'container/OptionsMenu/types';
import {
ArrowDownToDot,
ArrowUpFromDot,
Ellipsis,
Minus,
Plus,
} from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import React, { useCallback, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
@@ -34,6 +41,9 @@ interface ITableViewActionsProps {
isfilterInLoading: boolean;
isfilterOutLoading: boolean;
onGroupByAttribute?: (fieldKey: string, dataType?: DataTypes) => Promise<void>;
onAddColumn?: (fieldName: string) => void;
onRemoveColumn?: (fieldName: string) => void;
selectedOptions?: OptionsQuery;
onClickHandler: (
operator: string,
fieldKey: string,
@@ -105,6 +115,7 @@ const BodyContent: React.FC<{
BodyContent.displayName = 'BodyContent';
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function TableViewActions(
props: ITableViewActionsProps,
): React.ReactElement {
@@ -116,6 +127,9 @@ export default function TableViewActions(
isfilterOutLoading,
onClickHandler,
onGroupByAttribute,
onAddColumn,
onRemoveColumn,
selectedOptions,
} = props;
const { pathname } = useLocation();
@@ -142,6 +156,13 @@ export default function TableViewActions(
const fieldFilterKey = filterKeyForField(fieldData.field);
const isFieldInSelectedColumns = useMemo(() => {
if (!selectedOptions?.selectColumns) return false;
return selectedOptions.selectColumns.some(
(col) => col.name === fieldFilterKey,
);
}, [selectedOptions, fieldFilterKey]);
// Memoize textToCopy computation
const textToCopy = useMemo(() => {
let text = fieldData.value;
@@ -250,6 +271,32 @@ export default function TableViewActions(
arrow={false}
content={
<div>
{onAddColumn && !isFieldInSelectedColumns && (
<Button
className="group-by-clause"
type="text"
icon={<Plus size={14} />}
onClick={(): void => {
onAddColumn(fieldFilterKey);
setIsOpen(false);
}}
>
Add to Columns
</Button>
)}
{onRemoveColumn && isFieldInSelectedColumns && (
<Button
className="group-by-clause"
type="text"
icon={<Minus size={14} />}
onClick={(): void => {
onRemoveColumn(fieldFilterKey);
setIsOpen(false);
}}
>
Remove from Columns
</Button>
)}
<Button
className="group-by-clause"
type="text"
@@ -330,6 +377,32 @@ export default function TableViewActions(
arrow={false}
content={
<div>
{onAddColumn && !isFieldInSelectedColumns && (
<Button
className="group-by-clause"
type="text"
icon={<Plus size={14} />}
onClick={(): void => {
onAddColumn(fieldFilterKey);
setIsOpen(false);
}}
>
Add to Columns
</Button>
)}
{onRemoveColumn && isFieldInSelectedColumns && (
<Button
className="group-by-clause"
type="text"
icon={<Minus size={14} />}
onClick={(): void => {
onRemoveColumn(fieldFilterKey);
setIsOpen(false);
}}
>
Remove from Columns
</Button>
)}
<Button
className="group-by-clause"
type="text"
@@ -360,4 +433,7 @@ export default function TableViewActions(
TableViewActions.defaultProps = {
onGroupByAttribute: undefined,
onAddColumn: undefined,
onRemoveColumn: undefined,
selectedOptions: undefined,
};

View File

@@ -1,5 +1,7 @@
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { LogViewMode } from 'container/LogsTable';
import { FontSize } from 'container/OptionsMenu/types';
import TableViewActions from '../TableViewActions';
@@ -33,17 +35,32 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('../useAsyncJSONProcessing', () => ({
__esModule: true,
default: (): {
isLoading: boolean;
treeData: unknown[] | null;
error: string | null;
} => ({
isLoading: false,
treeData: null,
error: null,
}),
}));
describe('TableViewActions', () => {
const TEST_VALUE = 'test value';
const ACTION_BUTTON_TEST_ID = '.action-btn';
const TEST_FIELD = 'test-field';
const defaultProps = {
fieldData: {
field: 'test-field',
field: TEST_FIELD,
value: TEST_VALUE,
},
record: {
key: 'test-key',
field: 'test-field',
field: TEST_FIELD,
value: TEST_VALUE,
},
isListViewPanel: false,
@@ -127,4 +144,134 @@ describe('TableViewActions', () => {
container.querySelector(ACTION_BUTTON_TEST_ID),
).not.toBeInTheDocument();
});
describe('Add/Remove Column functionality', () => {
const ADD_TO_COLUMNS_TEXT = 'Add to Columns';
const REMOVE_FROM_COLUMNS_TEXT = 'Remove from Columns';
const getEllipsisButton = (container: HTMLElement): HTMLElement => {
const buttons = container.querySelectorAll('.filter-btn.periscope-btn');
return buttons[buttons.length - 1] as HTMLElement;
};
const defaultSelectedOptions = {
selectColumns: [],
maxLines: 1,
format: 'table' as LogViewMode,
fontSize: FontSize.MEDIUM,
};
it('shows Add to Columns button when field is not selected', async () => {
const onAddColumn = jest.fn();
const { container } = render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
onAddColumn={onAddColumn}
selectedOptions={defaultSelectedOptions}
/>,
);
const ellipsisButton = getEllipsisButton(container);
fireEvent.mouseOver(ellipsisButton);
await waitFor(() => {
expect(screen.getByText(ADD_TO_COLUMNS_TEXT)).toBeInTheDocument();
});
});
it(`calls onAddColumn with correct field key when ${ADD_TO_COLUMNS_TEXT} is clicked`, async () => {
const onAddColumn = jest.fn();
const { container } = render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
onAddColumn={onAddColumn}
selectedOptions={defaultSelectedOptions}
/>,
);
const ellipsisButton = getEllipsisButton(container);
fireEvent.mouseOver(ellipsisButton);
await waitFor(() => {
expect(screen.getByText(ADD_TO_COLUMNS_TEXT)).toBeInTheDocument();
});
const addButton = screen.getByText(ADD_TO_COLUMNS_TEXT);
fireEvent.click(addButton);
expect(onAddColumn).toHaveBeenCalledWith(TEST_FIELD);
});
it('shows Remove from Columns button when field is already selected', async () => {
const onRemoveColumn = jest.fn();
const { container } = render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
onRemoveColumn={onRemoveColumn}
selectedOptions={{
...defaultSelectedOptions,
selectColumns: [{ name: TEST_FIELD }],
}}
/>,
);
const ellipsisButton = getEllipsisButton(container);
fireEvent.mouseOver(ellipsisButton);
await waitFor(() => {
expect(screen.getByText(REMOVE_FROM_COLUMNS_TEXT)).toBeInTheDocument();
});
expect(screen.queryByText(ADD_TO_COLUMNS_TEXT)).not.toBeInTheDocument();
});
it(`calls onRemoveColumn with correct field key when ${REMOVE_FROM_COLUMNS_TEXT} is clicked`, async () => {
const onRemoveColumn = jest.fn();
const { container } = render(
<TableViewActions
fieldData={defaultProps.fieldData}
record={defaultProps.record}
isListViewPanel={defaultProps.isListViewPanel}
isfilterInLoading={defaultProps.isfilterInLoading}
isfilterOutLoading={defaultProps.isfilterOutLoading}
onClickHandler={defaultProps.onClickHandler}
onGroupByAttribute={defaultProps.onGroupByAttribute}
onRemoveColumn={onRemoveColumn}
selectedOptions={{
...defaultSelectedOptions,
selectColumns: [{ name: TEST_FIELD }],
}}
/>,
);
const ellipsisButton = getEllipsisButton(container);
fireEvent.mouseOver(ellipsisButton);
await waitFor(() => {
expect(screen.getByText('Remove from Columns')).toBeInTheDocument();
});
const removeButton = screen.getByText(REMOVE_FROM_COLUMNS_TEXT);
fireEvent.click(removeButton);
expect(onRemoveColumn).toHaveBeenCalledWith(TEST_FIELD);
});
});
});

View File

@@ -170,7 +170,7 @@ const useOptionsMenu = ({
...initialQueryParamsV5,
searchText: debouncedSearchText,
},
{ queryKey: [debouncedSearchText, isFocused], enabled: isFocused },
{ queryKey: [debouncedSearchText, isFocused] },
);
// const {
@@ -186,7 +186,7 @@ const useOptionsMenu = ({
const searchedAttributeKeys: TelemetryFieldKey[] = useMemo(() => {
const searchedAttributesDataList = Object.values(
searchedAttributesDataV5?.data.data.keys || {},
searchedAttributesDataV5?.data.data?.keys || {},
).flat();
if (searchedAttributesDataList.length) {
if (dataSource === DataSource.LOGS) {
@@ -230,7 +230,7 @@ const useOptionsMenu = ({
}
return [];
}, [dataSource, searchedAttributesDataV5?.data.data.keys]);
}, [dataSource, searchedAttributesDataV5?.data.data?.keys]);
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns: TelemetryFieldKey[] = defaultOptionsQuery.selectColumns;