Compare commits

...

6 Commits

Author SHA1 Message Date
Ashwin Bhatkal
db87b30216 chore: cleanup on unmount of dashboardContainer 2026-02-26 15:38:20 +05:30
Ashwin Bhatkal
dfaa09ff8a fix: navigation from edit widget to dashboard to list 2026-02-26 14:56:35 +05:30
Ashwin Bhatkal
903e2f6e33 chore: updated tests 2026-02-26 11:35:40 +05:30
Ashwin Bhatkal
be8bc019d0 chore: update approach 2026-02-26 11:03:40 +05:30
Ashwin Bhatkal
7a85ee1602 test: handle both cases of back navigation 2026-02-25 14:12:35 +05:30
Ashwin Bhatkal
4b0cbb787a refactor: dashboard list to not use dashboard provider 2026-02-25 14:12:35 +05:30
8 changed files with 445 additions and 222 deletions

View File

@@ -1,10 +1,18 @@
/* eslint-disable sonarjs/no-identical-functions */
import { MemoryRouter, useLocation } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import { getNonIntegrationDashboardById } from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import {
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import DashboardDescription from '..';
@@ -17,6 +25,7 @@ const DASHBOARD_TITLE_TEXT = 'thor';
const DASHBOARD_PATH = '/dashboard/4';
const mockSafeNavigate = jest.fn();
const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
@@ -28,6 +37,11 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('react-router-dom-v5-compat', () => ({
...jest.requireActual('react-router-dom-v5-compat'),
useNavigate: (): jest.Mock => mockNavigate,
}));
jest.mock(
'container/TopNav/DateTimeSelectionV2/index.tsx',
() =>
@@ -45,6 +59,8 @@ jest.mock('hooks/useSafeNavigate', () => ({
describe('Dashboard landing page actions header tests', () => {
beforeEach(() => {
mockSafeNavigate.mockClear();
mockNavigate.mockClear();
sessionStorage.clear();
});
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
@@ -124,17 +140,26 @@ describe('Dashboard landing page actions header tests', () => {
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
});
it('should navigate to dashboard list with correct params and exclude variables', async () => {
const dashboardUrlWithVariables = `${DASHBOARD_PATH}?variables=%7B%22var1%22%3A%22value1%22%7D&otherParam=test`;
it('should navigate to the dashboard list with stored query params when clicking the dashboard breadcrumb', async () => {
const storedParams = JSON.stringify({
columnKey: 'createdAt',
order: 'ascend',
page: '2',
search: 'test',
});
sessionStorage.setItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
storedParams,
);
const mockLocation = {
pathname: DASHBOARD_PATH,
search: '?variables=%7B%22var1%22%3A%22value1%22%7D&otherParam=test',
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText } = render(
<MemoryRouter initialEntries={[dashboardUrlWithVariables]}>
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardDescription
handle={{
@@ -154,27 +179,49 @@ describe('Dashboard landing page actions header tests', () => {
),
);
// Click the dashboard breadcrumb to navigate back to list
const dashboardButton = getByText('Dashboard /');
fireEvent.click(dashboardButton);
await userEvent.click(dashboardButton);
// Verify navigation was called with correct URL
expect(mockSafeNavigate).toHaveBeenCalledWith(
'/dashboard?columnKey=updatedAt&order=descend&page=1&search=',
expect(mockSafeNavigate).toHaveBeenCalledWith({
pathname: ROUTES.ALL_DASHBOARD,
search: `?${storedParams}`,
});
expect(mockNavigate).not.toHaveBeenCalled();
});
it('should navigate to dashboard list page when there are no stored query params', async () => {
const mockLocation = {
pathname: DASHBOARD_PATH,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardProvider>
</MemoryRouter>,
);
// Ensure the URL contains only essential dashboard list params
const calledUrl = mockSafeNavigate.mock.calls[0][0] as string;
const urlParams = new URLSearchParams(calledUrl.split('?')[1]);
await waitFor(() =>
expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
DASHBOARD_TITLE_TEXT,
),
);
// Should have essential dashboard list params
expect(urlParams.get('columnKey')).toBe('updatedAt');
expect(urlParams.get('order')).toBe('descend');
expect(urlParams.get('page')).toBe('1');
expect(urlParams.get('search')).toBe('');
const dashboardButton = getByText('Dashboard /');
await userEvent.click(dashboardButton);
// Should NOT have variables or other dashboard-specific params
expect(urlParams.has('variables')).toBeFalsy();
expect(urlParams.has('relativeTime')).toBeFalsy();
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.ALL_DASHBOARD);
expect(mockNavigate).not.toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { LayoutGrid } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -12,7 +13,7 @@ import './DashboardBreadcrumbs.styles.scss';
function DashboardBreadcrumbs(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedDashboard, listSortOrder } = useDashboard();
const { selectedDashboard } = useDashboard();
const selectedData = selectedDashboard
? {
@@ -24,15 +25,24 @@ function DashboardBreadcrumbs(): JSX.Element {
const { title = '', image = Base64Icons[0] } = selectedData || {};
const goToListPage = useCallback(() => {
const urlParams = new URLSearchParams();
urlParams.set('columnKey', listSortOrder.columnKey as string);
urlParams.set('order', listSortOrder.order as string);
urlParams.set('page', listSortOrder.pagination as string);
urlParams.set('search', listSortOrder.search as string);
const dashboardsListQueryParamsObject = sessionStorage.getItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
safeNavigate(generatedUrl);
}, [listSortOrder, safeNavigate]);
if (dashboardsListQueryParamsObject) {
const queryParamsString = Object.entries(
JSON.parse(dashboardsListQueryParamsObject),
)
.map(([key, value]) => `${key}=${value}`)
.join('&');
safeNavigate({
pathname: ROUTES.ALL_DASHBOARD,
search: `?${queryParamsString}`,
});
} else {
safeNavigate(ROUTES.ALL_DASHBOARD);
}
}, [safeNavigate]);
return (
<div className="dashboard-breadcrumbs">

View File

@@ -1,10 +1,19 @@
import { useEffect } from 'react';
import { useFullScreenHandle } from 'react-full-screen';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import Description from './DashboardDescription';
import GridGraphs from './GridGraphs';
function DashboardContainer(): JSX.Element {
const handle = useFullScreenHandle();
useEffect(() => {
return (): void => {
sessionStorage.removeItem(DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY);
};
}, []);
return (
<div>
<Description handle={handle} />

View File

@@ -45,6 +45,7 @@ import {
} from 'container/DashboardContainer/DashboardDescription/utils';
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
import dayjs from 'dayjs';
import useDashboardsListQueryParams from 'hooks/dashboard/useDashboardsListQueryParams';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -76,7 +77,6 @@ import {
// see more: https://github.com/lucide-icons/lucide/issues/94
import { handleContactSupport } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
import {
@@ -104,7 +104,7 @@ function DashboardsList(): JSX.Element {
const {
data: dashboardListResponse,
isLoading: isDashboardListLoading,
isRefetching: isDashboardListRefetching,
isFetching: isDashboardListFetching,
error: dashboardFetchError,
refetch: refetchDashboardList,
} = useGetAllDashboard();
@@ -112,14 +112,14 @@ function DashboardsList(): JSX.Element {
const { user } = useAppContext();
const { safeNavigate } = useSafeNavigate();
const {
listSortOrder: sortOrder,
setListSortOrder: setSortOrder,
} = useDashboard();
dashboardsListQueryParams,
updateDashboardsListQueryParams,
} = useDashboardsListQueryParams();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const [searchString, setSearchString] = useState<string>(
sortOrder.search || '',
dashboardsListQueryParams.search || '',
);
const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'],
@@ -139,7 +139,6 @@ function DashboardsList(): JSX.Element {
] = useState<boolean>(false);
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
const [isConfigureMetadataOpen, setIsConfigureMetadata] = useState<boolean>(
false,
);
@@ -187,76 +186,41 @@ function DashboardsList(): JSX.Element {
}
}
const [dashboards, setDashboards] = useState<Dashboard[]>();
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
const sortedDashboards = dashboards.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
setDashboards(sortedDashboards);
};
const sortDashboardsByUpdatedAt = (dashboards: Dashboard[]): void => {
const sortedDashboards = dashboards.sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
);
setDashboards(sortedDashboards);
};
const sortHandle = (key: string): void => {
if (!dashboards) {
return;
}
if (key === 'createdAt') {
sortDashboardsByCreatedAt(dashboards);
setSortOrder({
columnKey: 'createdAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
} else if (key === 'updatedAt') {
sortDashboardsByUpdatedAt(dashboards);
setSortOrder({
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
}
};
function handlePageSizeUpdate(page: number): void {
setSortOrder({ ...sortOrder, pagination: String(page) });
}
useEffect(() => {
const filteredDashboards = filterDashboard(
const dashboards = useMemo((): Dashboard[] => {
const filtered = filterDashboard(
searchString,
dashboardListResponse?.data || [],
);
if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []);
} else if (sortOrder.columnKey === 'createdAt') {
sortDashboardsByCreatedAt(filteredDashboards || []);
} else if (sortOrder.columnKey === 'null') {
setSortOrder({
columnKey: 'updatedAt',
order: 'descend',
pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
});
sortDashboardsByUpdatedAt(filteredDashboards || []);
if (dashboardsListQueryParams.columnKey === 'createdAt') {
return filtered.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
}
return filtered.sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
);
}, [
dashboardListResponse,
dashboardListResponse?.data,
searchString,
setSortOrder,
sortOrder.columnKey,
sortOrder.pagination,
sortOrder.search,
dashboardsListQueryParams.columnKey,
]);
const sortHandle = (key: string): void => {
updateDashboardsListQueryParams({
columnKey: key,
order: 'descend',
page: dashboardsListQueryParams.page || '1',
search: dashboardsListQueryParams.search || '',
});
};
function handlePageSizeUpdate(page: number): void {
updateDashboardsListQueryParams({
...dashboardsListQueryParams,
page: String(page),
});
}
const [newDashboardState, setNewDashboardState] = useState({
loading: false,
error: false,
@@ -265,26 +229,25 @@ function DashboardsList(): JSX.Element {
const { showErrorModal } = useErrorModal();
const data: Data[] =
dashboards?.map((e) => ({
createdAt: e.createdAt,
description: e.data.description || '',
id: e.id,
lastUpdatedTime: e.updatedAt,
name: e.data.title,
tags: e.data.tags || [],
key: e.id,
createdBy: e.createdBy,
isLocked: !!e.locked || false,
lastUpdatedBy: e.updatedBy,
image: e.data.image || Base64Icons[0],
variables: e.data.variables,
widgets: e.data.widgets,
layout: e.data.layout,
panelMap: e.data.panelMap,
version: e.data.version,
refetchDashboardList,
})) || [];
const data: Data[] = dashboards.map((e) => ({
createdAt: e.createdAt,
description: e.data.description || '',
id: e.id,
lastUpdatedTime: e.updatedAt,
name: e.data.title,
tags: e.data.tags || [],
key: e.id,
createdBy: e.createdBy,
isLocked: !!e.locked || false,
lastUpdatedBy: e.updatedBy,
image: e.data.image || Base64Icons[0],
variables: e.data.variables,
widgets: e.data.widgets,
layout: e.data.layout,
panelMap: e.data.panelMap,
version: e.data.version,
refetchDashboardList,
}));
const onNewDashboardHandler = useCallback(async () => {
try {
@@ -324,16 +287,12 @@ function DashboardsList(): JSX.Element {
};
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
setIsFilteringDashboards(true);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse?.data || [],
);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
setSearchString(searchText);
setSortOrder({ ...sortOrder, search: searchText });
updateDashboardsListQueryParams({
...dashboardsListQueryParams,
search: searchText,
});
};
const [state, setCopy] = useCopyToClipboard();
@@ -671,8 +630,8 @@ function DashboardsList(): JSX.Element {
showTotal: showPaginationItem,
showSizeChanger: false,
onChange: (page: any): void => handlePageSizeUpdate(page),
current: Number(sortOrder.pagination),
defaultCurrent: Number(sortOrder.pagination) || 1,
current: Number(dashboardsListQueryParams.page),
defaultCurrent: Number(dashboardsListQueryParams.page) || 1,
hideOnSinglePage: true,
};
@@ -710,9 +669,7 @@ function DashboardsList(): JSX.Element {
)}
</div>
{isDashboardListLoading ||
isFilteringDashboards ||
isDashboardListRefetching ? (
{isDashboardListFetching ? (
<div className="loading-dashboard-details">
<Skeleton.Input active size="large" className="skeleton-1" />
<Skeleton.Input active size="large" className="skeleton-1" />
@@ -749,7 +706,7 @@ function DashboardsList(): JSX.Element {
<ArrowUpRight size={16} className="learn-more-arrow" />
</section>
</div>
) : dashboards?.length === 0 && !searchString ? (
) : dashboards.length === 0 && !searchString ? (
<div className="dashboard-empty-state">
<img
src="/Icons/dashboards.svg"
@@ -831,7 +788,7 @@ function DashboardsList(): JSX.Element {
)}
</div>
{dashboards?.length === 0 ? (
{dashboards.length === 0 ? (
<div className="no-search">
<img src="/Icons/emptyState.svg" alt="img" className="img" />
<Typography.Text className="text">
@@ -860,7 +817,9 @@ function DashboardsList(): JSX.Element {
data-testid="sort-by-last-created"
>
Last created
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
{dashboardsListQueryParams.columnKey === 'createdAt' && (
<Check size={14} />
)}
</Button>
<Button
type="text"
@@ -869,7 +828,9 @@ function DashboardsList(): JSX.Element {
data-testid="sort-by-last-updated"
>
Last updated
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
{dashboardsListQueryParams.columnKey === 'updatedAt' && (
<Check size={14} />
)}
</Button>
</div>
}
@@ -911,11 +872,7 @@ function DashboardsList(): JSX.Element {
columns={columns}
dataSource={data}
showSorterTooltip
loading={
isDashboardListLoading ||
isFilteringDashboards ||
isDashboardListRefetching
}
loading={isDashboardListFetching}
showHeader={false}
pagination={paginationConfig}
/>
@@ -964,12 +921,12 @@ function DashboardsList(): JSX.Element {
<div className="configure-preview">
<section className="header">
<img
src={dashboards?.[0]?.data?.image || Base64Icons[0]}
src={dashboards[0]?.data?.image || Base64Icons[0]}
alt="dashboard-image"
style={{ height: '14px', width: '14px' }}
/>
<Typography.Text className="title">
{dashboards?.[0]?.data?.title}
{dashboards[0]?.data?.title}
</Typography.Text>
</section>
<section className="details">
@@ -977,16 +934,16 @@ function DashboardsList(): JSX.Element {
{visibleColumns.createdAt && (
<Typography.Text className="formatted-time">
<CalendarClock size={14} />
{getFormattedTime(dashboards?.[0] as Dashboard, 'created_at')}
{getFormattedTime(dashboards[0] as Dashboard, 'created_at')}
</Typography.Text>
)}
{visibleColumns.createdBy && (
<div className="user">
<Typography.Text className="user-tag">
{dashboards?.[0]?.createdBy?.substring(0, 1).toUpperCase()}
{dashboards[0]?.createdBy?.substring(0, 1).toUpperCase()}
</Typography.Text>
<Typography.Text className="dashboard-created-by">
{dashboards?.[0]?.createdBy}
{dashboards[0]?.createdBy}
</Typography.Text>
</div>
)}
@@ -995,16 +952,16 @@ function DashboardsList(): JSX.Element {
{visibleColumns.updatedAt && (
<Typography.Text className="formatted-time">
<CalendarClock size={14} />
{onLastUpdated(dashboards?.[0]?.updatedAt || '')}
{onLastUpdated(dashboards[0]?.updatedAt || '')}
</Typography.Text>
)}
{visibleColumns.updatedBy && (
<div className="user">
<Typography.Text className="user-tag">
{dashboards?.[0]?.updatedBy?.substring(0, 1).toUpperCase()}
{dashboards[0]?.updatedBy?.substring(0, 1).toUpperCase()}
</Typography.Text>
<Typography.Text className="dashboard-created-by">
{dashboards?.[0]?.updatedBy}
{dashboards[0]?.updatedBy}
</Typography.Text>
</div>
)}

View File

@@ -0,0 +1,198 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { act, renderHook } from '@testing-library/react';
import useDashboardsListQueryParams, {
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
IDashboardsListQueryParams,
} from '../useDashboardsListQueryParams';
const mockSafeNavigate = jest.fn();
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
function makeWrapper(
initialUrl: string,
): ({ children }: { children: React.ReactNode }) => JSX.Element {
return function Wrapper({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
return React.createElement(
MemoryRouter,
{ initialEntries: [initialUrl] },
children,
) as JSX.Element;
};
}
describe('useDashboardsListQueryParams', () => {
beforeEach(() => {
mockSafeNavigate.mockClear();
sessionStorage.clear();
});
describe('initialisation from URL params', () => {
it('returns default params when no URL query params are present', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
expect(result.current.dashboardsListQueryParams).toEqual({
columnKey: 'updatedAt',
order: 'descend',
page: '1',
search: '',
});
});
it('reads valid columnKey, order, page and search from the URL', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper(
'/dashboard?columnKey=createdAt&order=ascend&page=3&search=foo',
),
});
expect(result.current.dashboardsListQueryParams).toEqual({
columnKey: 'createdAt',
order: 'ascend',
page: '3',
search: 'foo',
});
});
it('falls back to updatedAt for an unsupported columnKey', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard?columnKey=name&order=ascend'),
});
expect(result.current.dashboardsListQueryParams.columnKey).toBe('updatedAt');
});
it('falls back to descend for an unsupported order', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard?columnKey=createdAt&order=invalid'),
});
expect(result.current.dashboardsListQueryParams.order).toBe('descend');
});
it('defaults page to 1 when page param is absent', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
expect(result.current.dashboardsListQueryParams.page).toBe('1');
});
it('defaults search to empty string when search param is absent', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
expect(result.current.dashboardsListQueryParams.search).toBe('');
});
});
describe('updateDashboardsListQueryParams', () => {
it('updates the state when params change', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
const updated: IDashboardsListQueryParams = {
columnKey: 'createdAt',
order: 'ascend',
page: '2',
search: 'signoz',
};
act(() => {
result.current.updateDashboardsListQueryParams(updated);
});
expect(result.current.dashboardsListQueryParams).toEqual(updated);
});
it('does not update state when params are identical', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
const initial = result.current.dashboardsListQueryParams;
act(() => {
result.current.updateDashboardsListQueryParams({ ...initial });
});
// Reference equality confirms no re-render-triggering state update.
expect(result.current.dashboardsListQueryParams).toBe(initial);
});
it('calls safeNavigate with the updated search string', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
const updated: IDashboardsListQueryParams = {
columnKey: 'createdAt',
order: 'ascend',
page: '2',
search: 'test',
};
act(() => {
result.current.updateDashboardsListQueryParams(updated);
});
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
const [navigateArg] = mockSafeNavigate.mock.calls[0];
const searchParams = new URLSearchParams(navigateArg.search);
expect(searchParams.get('columnKey')).toBe('createdAt');
expect(searchParams.get('order')).toBe('ascend');
expect(searchParams.get('page')).toBe('2');
expect(searchParams.get('search')).toBe('test');
});
it('persists params to sessionStorage', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
const updated: IDashboardsListQueryParams = {
columnKey: 'updatedAt',
order: 'descend',
page: '1',
search: 'signoz',
};
act(() => {
result.current.updateDashboardsListQueryParams(updated);
});
const stored = sessionStorage.getItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
);
expect(JSON.parse(stored || '')).toEqual(updated);
});
it('still calls safeNavigate even when params are unchanged', () => {
const { result } = renderHook(() => useDashboardsListQueryParams(), {
wrapper: makeWrapper('/dashboard'),
});
const initial = result.current.dashboardsListQueryParams;
act(() => {
result.current.updateDashboardsListQueryParams({ ...initial });
});
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,72 @@
import { useState } from 'react';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import isEqual from 'lodash-es/isEqual';
export interface IDashboardsListQueryParams {
columnKey: string;
order: string;
page: string;
search: string;
}
export const DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY =
'dashboardsListQueryParams';
const SUPPORTED_COLUMN_KEYS = ['createdAt', 'updatedAt'];
const SUPPORTED_ORDER_KEYS = ['ascend', 'descend'];
function useDashboardsListQueryParams(): {
dashboardsListQueryParams: IDashboardsListQueryParams;
updateDashboardsListQueryParams: (
dashboardsListQueryParams: IDashboardsListQueryParams,
) => void;
} {
const { safeNavigate } = useSafeNavigate();
const params = useUrlQuery();
const orderColumnKeyQueryParam = params.get('columnKey');
const orderQueryParam = params.get('order');
const pageQueryParam = params.get('page');
const searchQueryParam = params.get('search');
const [
dashboardsListQueryParams,
setDashboardsListQueryParams,
] = useState<IDashboardsListQueryParams>({
columnKey:
orderColumnKeyQueryParam &&
SUPPORTED_COLUMN_KEYS.includes(orderColumnKeyQueryParam)
? orderColumnKeyQueryParam
: 'updatedAt',
order:
orderQueryParam && SUPPORTED_ORDER_KEYS.includes(orderQueryParam)
? orderQueryParam
: 'descend',
page: pageQueryParam || '1',
search: searchQueryParam || '',
});
function updateDashboardsListQueryParams(
updatedQueryParams: IDashboardsListQueryParams,
): void {
if (!isEqual(updatedQueryParams, dashboardsListQueryParams)) {
setDashboardsListQueryParams(updatedQueryParams);
}
params.set('columnKey', updatedQueryParams.columnKey);
params.set('order', updatedQueryParams.order);
params.set('page', updatedQueryParams.page || '1');
params.set('search', updatedQueryParams.search || '');
safeNavigate({ search: params.toString() });
sessionStorage.setItem(
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
JSON.stringify(updatedQueryParams),
);
}
return { dashboardsListQueryParams, updateDashboardsListQueryParams };
}
export default useDashboardsListQueryParams;

View File

@@ -22,9 +22,7 @@ import ROUTES from 'constants/routes';
import dayjs, { Dayjs } from 'dayjs';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useTabVisibility from 'hooks/useTabFocus';
import useUrlQuery from 'hooks/useUrlQuery';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { defaultTo, isEmpty } from 'lodash-es';
@@ -50,11 +48,7 @@ import {
setDashboardVariablesStore,
updateDashboardVariablesStore,
} from './store/dashboardVariables/dashboardVariablesStore';
import {
DashboardSortOrder,
IDashboardContext,
WidgetColumnWidths,
} from './types';
import { IDashboardContext, WidgetColumnWidths } from './types';
import { sortLayout } from './util';
const DashboardContext = createContext<IDashboardContext>({
@@ -71,13 +65,7 @@ const DashboardContext = createContext<IDashboardContext>({
layouts: [],
panelMap: {},
setPanelMap: () => {},
listSortOrder: {
columnKey: 'createdAt',
order: 'descend',
pagination: '1',
search: '',
},
setListSortOrder: () => {},
setLayouts: () => {},
setSelectedDashboard: () => {},
updatedTimeRef: {} as React.MutableRefObject<Dayjs | null>,
@@ -101,7 +89,6 @@ interface Props {
export function DashboardProvider({
children,
}: PropsWithChildren): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
const [toScrollWidgetId, setToScrollWidgetId] = useState<string>('');
@@ -122,52 +109,8 @@ export function DashboardProvider({
exact: true,
});
const isDashboardListPage = useRouteMatch<Props>({
path: ROUTES.ALL_DASHBOARD,
exact: true,
});
const { showErrorModal } = useErrorModal();
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
const supportedOrderKeys = ['ascend', 'descend'];
const params = useUrlQuery();
// since the dashboard provider is wrapped at the very top of the application hence it initialises these values from other pages as well.
// pick the below params from URL only if the user is on the dashboards list page.
const orderColumnParam = isDashboardListPage && params.get('columnKey');
const orderQueryParam = isDashboardListPage && params.get('order');
const paginationParam = isDashboardListPage && params.get('page');
const searchParam = isDashboardListPage && params.get('search');
const [listSortOrder, setListOrder] = useState({
columnKey: orderColumnParam
? supportedOrderColumnKeys.includes(orderColumnParam)
? orderColumnParam
: 'updatedAt'
: 'updatedAt',
order: orderQueryParam
? supportedOrderKeys.includes(orderQueryParam)
? orderQueryParam
: 'descend'
: 'descend',
pagination: paginationParam || '1',
search: searchParam || '',
});
function setListSortOrder(sortOrder: DashboardSortOrder): void {
if (!isEqual(sortOrder, listSortOrder)) {
setListOrder(sortOrder);
}
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
params.set('search', sortOrder.search || '');
safeNavigate({ search: params.toString() });
}
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
@@ -502,8 +445,6 @@ export function DashboardProvider({
selectedDashboard,
dashboardId,
layouts,
listSortOrder,
setListSortOrder,
panelMap,
setLayouts,
setPanelMap,
@@ -527,8 +468,6 @@ export function DashboardProvider({
selectedDashboard,
dashboardId,
layouts,
listSortOrder,
setListSortOrder,
panelMap,
toScrollWidgetId,
updateLocalStorageDashboardVariables,

View File

@@ -4,13 +4,6 @@ import dayjs from 'dayjs';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
export interface DashboardSortOrder {
columnKey: string;
order: string;
pagination: string;
search: string;
}
export type WidgetColumnWidths = {
[widgetId: string]: Record<string, number>;
};
@@ -26,8 +19,6 @@ export interface IDashboardContext {
layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
listSortOrder: DashboardSortOrder;
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
setSelectedDashboard: React.Dispatch<
React.SetStateAction<Dashboard | undefined>