Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bdbe8d1
feat: import useTableUrlState
fabio-silva Jun 24, 2026
0fe7a2e
chore: lint
fabio-silva Jun 24, 2026
fa98e11
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jun 28, 2026
83984a6
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jun 28, 2026
25d8204
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 6, 2026
e4eeb57
chore: simplify code
fabio-silva Jul 6, 2026
1dd909e
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 7, 2026
a5ef9be
chore: update @percona/percona-ui
fabio-silva Jul 7, 2026
1736744
Merge branch 'PMM-14848-table-url-params' of https://github.com/perco…
fabio-silva Jul 7, 2026
5e5650b
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 7, 2026
1c25d84
chore: remove duplicate const
fabio-silva Jul 7, 2026
5169316
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 7, 2026
7aa1020
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 7, 2026
1dc9fdc
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 13, 2026
c4a58e1
PMM-14848 move sessions URL state constant
cursoragent Jul 13, 2026
0b3e2b0
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 13, 2026
c119f22
chore: format
fabio-silva Jul 13, 2026
c773985
Merge branch 'main' into PMM-14848-table-url-params
mattiasimonato Jul 14, 2026
0e2e8ae
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 14, 2026
a2e1edf
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 14, 2026
16355b0
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 29, 2026
7e08bec
fix: allow search on QAN while typing
fabio-silva Jul 30, 2026
c2ed921
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Jul 30, 2026
963024c
Merge branch 'main' into PMM-14848-table-url-params
fabio-silva Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { act, fireEvent, render } from '@testing-library/react';
import { Search } from './Search';

describe('Search::', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});

it('renders correctly', () => {
const { container } = render(<Search handleSearch={() => {}} />);
const form = container.querySelector('form');

expect(form?.children.length).toBe(2);
});

it('renders correctly with initial value', () => {
const { container } = render(<Search handleSearch={() => {}} initialValue="Test value" />);

expect(container.querySelector('input')?.value).toEqual('Test value');
});

it('submits correctly', () => {
const handleSearch = jest.fn();
const { container } = render(<Search handleSearch={handleSearch} />);
Expand All @@ -26,4 +37,55 @@

expect(handleSearch).toHaveBeenCalled();
});

it('searches after debounce when the input changes', () => {
const handleSearch = jest.fn();
const { container } = render(<Search handleSearch={handleSearch} />);
const input = container.querySelector('input');

fireEvent.change(input!, { target: { value: 'select' } });

expect(handleSearch).not.toHaveBeenCalled();

act(() => {

Check warning on line 50 in dashboards/pmm-app/src/pmm-qan/panel/components/Overview/components/Search/Search.test.tsx

View workflow job for this annotation

GitHub Actions / Tests

'act' is deprecated. https://react.dev/warnings/react-dom-test-utils
jest.advanceTimersByTime(300);
});

expect(handleSearch).toHaveBeenCalledWith({ search: 'select' });
});

it('clears search when the input is emptied', () => {
const handleSearch = jest.fn();
const { container } = render(
<Search handleSearch={handleSearch} initialValue="select" />,
);
const input = container.querySelector('input');

fireEvent.change(input!, { target: { value: '' } });

act(() => {

Check warning on line 66 in dashboards/pmm-app/src/pmm-qan/panel/components/Overview/components/Search/Search.test.tsx

View workflow job for this annotation

GitHub Actions / Tests

'act' is deprecated. https://react.dev/warnings/react-dom-test-utils
jest.advanceTimersByTime(300);
});

expect(handleSearch).toHaveBeenCalledWith({ search: '' });
});

it('submits immediately and cancels pending debounced search', () => {
const handleSearch = jest.fn();
const { container } = render(<Search handleSearch={handleSearch} />);
const input = container.querySelector('input');
const form = container.querySelector('form');

fireEvent.change(input!, { target: { value: 'pending' } });
fireEvent.submit(form!);

expect(handleSearch).toHaveBeenCalledTimes(1);
expect(handleSearch).toHaveBeenCalledWith({ search: 'pending' });

act(() => {

Check warning on line 85 in dashboards/pmm-app/src/pmm-qan/panel/components/Overview/components/Search/Search.test.tsx

View workflow job for this annotation

GitHub Actions / Tests

'act' is deprecated. https://react.dev/warnings/react-dom-test-utils
jest.advanceTimersByTime(300);
});

expect(handleSearch).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -1,29 +1,61 @@
import React, { FC } from 'react';
import React, {
FC, useEffect, useMemo, useRef,
} from 'react';
import { Field, Form } from 'react-final-form';
import {
Button, Icon, Input, useTheme,
} from '@grafana/ui';
import { debounce } from 'lodash';
import { Messages } from 'pmm-qan/panel/QueryAnalytics.messages';
import { getStyles } from './Search.styles';
import { SearchProps } from './Search.types';
import { SearchProps, SearchValues } from './Search.types';

const SEARCH_DEBOUNCE_MS = 300;

export const Search: FC<SearchProps> = ({ dataTestId, initialValue, handleSearch }) => {
const theme = useTheme();
const styles = getStyles(theme);
const {
search: { placeholder },
} = Messages;
const handleSearchRef = useRef(handleSearch);

handleSearchRef.current = handleSearch;

const debouncedSearch = useMemo(
() => debounce((search: string) => {
handleSearchRef.current({ search });
}, SEARCH_DEBOUNCE_MS),
[],
);

useEffect(() => () => {
debouncedSearch.cancel();
}, [debouncedSearch]);

const onSubmit = (values: SearchValues) => {
debouncedSearch.cancel();
handleSearch(values);
};

return (
<Form
onSubmit={handleSearch}
onSubmit={onSubmit}
initialValues={{ search: initialValue }}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit} className={styles.searchWrapper} data-testid={dataTestId}>
<Field
name="search"
render={({ input }) => (
<Input {...input} placeholder={placeholder} className={styles.searchInput} />
<Input
{...input}
placeholder={placeholder}
className={styles.searchInput}
onChange={(event) => {
input.onChange(event);
debouncedSearch(event.currentTarget.value);
}}
/>
)}
/>
<Button type="submit" className={styles.searchButton}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface GrafanaVariables {
query_selected?: string;
details_tab?: string;
[key: string]: any;
dimensionSearchText?: string;
dimensionSearchText?: string | null;
search?: string;
}
export const refreshGrafanaVariables = (state) => {
Expand Down Expand Up @@ -105,9 +105,8 @@ export const refreshGrafanaVariables = (state) => {
variablesQuery.to = rawTime.to;
}

if (dimensionSearchText) {
variablesQuery.dimensionSearchText = dimensionSearchText;
}
// null clears the query param via locationService.partial (omitting keeps the old value)
variablesQuery.dimensionSearchText = dimensionSearchText || null;

if (pageNumber && pageSize) {
variablesQuery.page_number = pageNumber;
Expand Down
100 changes: 100 additions & 0 deletions ui/apps/pmm/src/hooks/utils/useTableUrlState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { renderHook } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import reactRouter from 'react-router-dom';
import { useTableUrlState } from './useTableUrlState';

const { usePerconaTableUrlState } = vi.hoisted(() => ({
usePerconaTableUrlState: vi.fn(),
}));

vi.mock('@percona/percona-ui', async (importOriginal) => {
const actual = await importOriginal<typeof import('@percona/percona-ui')>();
return {
...actual,
usePerconaTableUrlState,
};
});

const setup = (params: string) => {
const searchParams = new URLSearchParams(params);
const setSearchParams = vi.fn();

vi.spyOn(reactRouter, 'useSearchParams').mockReturnValue([
searchParams,
setSearchParams,
]);
vi.mocked(usePerconaTableUrlState).mockReturnValue({
tableState: {
state: {
columnFilters: [],
globalFilter: '',
sorting: [],
pagination: { pageIndex: 0, pageSize: 10 },
},
onColumnFiltersChange: vi.fn(),
onGlobalFilterChange: vi.fn(),
onSortingChange: vi.fn(),
onPaginationChange: vi.fn(),
},
tableProps: {
state: {
columnFilters: [],
globalFilter: '',
sorting: [],
pagination: { pageIndex: 0, pageSize: 10 },
showColumnFilters: false,
showGlobalFilter: false,
},
onColumnFiltersChange: vi.fn(),
onGlobalFilterChange: vi.fn(),
onSortingChange: vi.fn(),
onPaginationChange: vi.fn(),
onShowColumnFiltersChange: vi.fn(),
onShowGlobalFilterChange: vi.fn(),
},
});

return { searchParams, setSearchParams };
};

describe('useTableUrlState', () => {
it('passes react-router search params to percona useTableUrlState', () => {
const { searchParams, setSearchParams } = setup(
'serviceIds=123&overview.sort=queryText:desc'
);

renderHook(() =>
useTableUrlState({
paramPrefix: 'overview',
})
);

expect(usePerconaTableUrlState).toHaveBeenCalledWith({
searchParams,
setSearchParams,
paramPrefix: 'overview',
});
});

it('forwards optional hook configuration', () => {
const { searchParams, setSearchParams } = setup('');

renderHook(() =>
useTableUrlState({
paramPrefix: 'sessions',
debounceMs: 500,
replace: false,
sync: { pagination: false },
})
);

expect(usePerconaTableUrlState).toHaveBeenCalledWith({
searchParams,
setSearchParams,
paramPrefix: 'sessions',
debounceMs: 500,
replace: false,
sync: { pagination: false },
});
});
});
25 changes: 25 additions & 0 deletions ui/apps/pmm/src/hooks/utils/useTableUrlState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useSearchParams } from 'react-router-dom';
import {
usePerconaTableUrlState,
type UsePerconaTableUrlStateOptions,
type UsePerconaTableUrlStateResult,
} from '@percona/percona-ui';

export type UseTableUrlStateOptions = Omit<
UsePerconaTableUrlStateOptions,
'searchParams' | 'setSearchParams'
>;

export type UseTableUrlStateResult = UsePerconaTableUrlStateResult;

export const useTableUrlState = (
options: UseTableUrlStateOptions = {}
): UseTableUrlStateResult => {
const [searchParams, setSearchParams] = useSearchParams();

return usePerconaTableUrlState({
searchParams,
setSearchParams,
...options,
});
};
7 changes: 6 additions & 1 deletion ui/apps/pmm/src/pages/rta/overview/RealtimeOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@ const RealtimeOverviewPage: FC = () => {
});
}

setSearchParams({ serviceIds: newServiceIds });
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.delete('serviceIds');
newServiceIds.forEach((id) => next.append('serviceIds', id));
return next;
});
};

if (isLoading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ export const OVERVIEW_TABLE_COLUMNS: MRT_ColumnDef<QueryData>[] = [
accessorKey: 'queryExecutionDurationMs',
filterVariant: 'range',
filterFn: 'timeRangeFilterFn',
muiTableHeadCellFilterTextFieldProps: {
inputProps: { step: 0.25, type: 'number' },
muiFilterTextFieldProps: {
type: 'text',
inputProps: { inputMode: 'decimal' },
},
Cell: ({ cell }) =>
cell.getValue() ? (
Expand Down
38 changes: 21 additions & 17 deletions ui/apps/pmm/src/pages/rta/overview/table/OverviewTable.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import {
type MRT_Row,
type MaterialReactTableProps,
} from 'material-react-table';
import { FC } from 'react';
import { type MRT_Row, MaterialReactTableProps } from 'material-react-table';
import { Table, useNavigableRows } from '@percona/percona-ui';
import type { FC } from 'react';
import type { QueryData } from 'types/rta.types';
import { QueryData } from 'types/rta.types';
import { OVERVIEW_TABLE_COLUMNS } from './OverviewTable.constants';
import { RealtimeTableWrapper } from 'pages/rta/components/rta-table-wrapper';
import { boxClasses } from '@mui/material/Box';
import { Messages } from './OverviewTable.messages';
import { filterElapsedTime } from './OverviewTable.utils';
import { useTableUrlState } from 'hooks/utils/useTableUrlState';

const OVERVIEW_TABLE_URL_STATE_OPTIONS = {
paramPrefix: 'overview',
defaults: {
pagination: { pageIndex: 0, pageSize: 25 },
},
};

interface Props {
queries: QueryData[];
Expand All @@ -26,21 +31,19 @@ const OverviewTable: FC<Props> = ({
actions,
onRowHover,
}) => {
const { tableProps, refresh } = useNavigableRows<QueryData>({
data: queries,
onChange: onNavigableQueriesChange,
});
const { tableProps: navigableTableProps, refresh } =
useNavigableRows<QueryData>({
data: queries,
onChange: onNavigableQueriesChange,
});
const { tableProps: urlStateTableProps } = useTableUrlState(
OVERVIEW_TABLE_URL_STATE_OPTIONS
);

return (
<RealtimeTableWrapper>
<Table
tableName="realtime-overview-table"
initialState={{
pagination: {
pageSize: 25,
pageIndex: 0,
},
}}
columns={OVERVIEW_TABLE_COLUMNS}
data={queries}
noDataMessage={Messages.noData}
Expand All @@ -54,7 +57,8 @@ const OverviewTable: FC<Props> = ({
},
},
}}
{...tableProps}
{...navigableTableProps}
{...urlStateTableProps}
enableStickyHeader
enableGlobalFilter={false}
enableHiding={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const SESSIONS_TABLE_URL_STATE_OPTIONS = {
paramPrefix: 'sessions',
};
Loading
Loading