fix: url params should not propagate across pages (#5417)

* fix: dashboards list url query params isolation

* feat: order query param old logs explorer isolation

* feat: added extra checks in place

* fix: refactor the dashboards list page for better performance

* chore: add test cases for the dashboards list page

* fix: added test cases for dashboards list page

* fix: added code comments

* fix: added empty state for dashboards and no search state
This commit is contained in:
Vikrant Gupta 2024-07-18 12:25:31 +05:30 committed by GitHub
parent d3b83f5a41
commit adfe20e88a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 376 additions and 56 deletions

View File

@ -73,7 +73,6 @@ import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import useUrlQuery from '../../hooks/useUrlQuery';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal'; import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON'; import ImportJSON from './ImportJSON';
import { DeleteButton } from './TableComponents/DeleteButton'; import { DeleteButton } from './TableComponents/DeleteButton';
@ -86,7 +85,7 @@ import {
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardsList(): JSX.Element { function DashboardsList(): JSX.Element {
const { const {
data: dashboardListResponse = [], data: dashboardListResponse,
isLoading: isDashboardListLoading, isLoading: isDashboardListLoading,
error: dashboardFetchError, error: dashboardFetchError,
refetch: refetchDashboardList, refetch: refetchDashboardList,
@ -99,12 +98,14 @@ function DashboardsList(): JSX.Element {
setListSortOrder: setSortOrder, setListSortOrder: setSortOrder,
} = useDashboard(); } = useDashboard();
const [searchString, setSearchString] = useState<string>(
sortOrder.search || '',
);
const [action, createNewDashboard] = useComponentPermission( const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'], ['action', 'create_new_dashboards'],
role, role,
); );
const [searchValue, setSearchValue] = useState<string>('');
const [ const [
showNewDashboardTemplatesModal, showNewDashboardTemplatesModal,
setShowNewDashboardTemplatesModal, setShowNewDashboardTemplatesModal,
@ -123,10 +124,6 @@ function DashboardsList(): JSX.Element {
false, false,
); );
const params = useUrlQuery();
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => { const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = localStorage.getItem('dashboard'); const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = { let dashboardDynamicColumns: DashboardDynamicColumns = {
@ -188,14 +185,6 @@ function DashboardsList(): JSX.Element {
setDashboards(sortedDashboards); setDashboards(sortedDashboards);
}; };
useEffect(() => {
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
history.replace({ search: params.toString() });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortOrder]);
const sortHandle = (key: string): void => { const sortHandle = (key: string): void => {
if (!dashboards) return; if (!dashboards) return;
if (key === 'createdAt') { if (key === 'createdAt') {
@ -204,6 +193,7 @@ function DashboardsList(): JSX.Element {
columnKey: 'createdAt', columnKey: 'createdAt',
order: 'descend', order: 'descend',
pagination: sortOrder.pagination || '1', pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
}); });
} else if (key === 'updatedAt') { } else if (key === 'updatedAt') {
sortDashboardsByUpdatedAt(dashboards); sortDashboardsByUpdatedAt(dashboards);
@ -211,21 +201,19 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt', columnKey: 'updatedAt',
order: 'descend', order: 'descend',
pagination: sortOrder.pagination || '1', pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
}); });
} }
}; };
function handlePageSizeUpdate(page: number): void { function handlePageSizeUpdate(page: number): void {
setSortOrder((order) => ({ setSortOrder({ ...sortOrder, pagination: String(page) });
...order,
pagination: String(page),
}));
} }
useEffect(() => { useEffect(() => {
const filteredDashboards = filterDashboard( const filteredDashboards = filterDashboard(
searchString, searchString,
dashboardListResponse, dashboardListResponse || [],
); );
if (sortOrder.columnKey === 'updatedAt') { if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []); sortDashboardsByUpdatedAt(filteredDashboards || []);
@ -236,6 +224,7 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt', columnKey: 'updatedAt',
order: 'descend', order: 'descend',
pagination: sortOrder.pagination || '1', pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
}); });
sortDashboardsByUpdatedAt(filteredDashboards || []); sortDashboardsByUpdatedAt(filteredDashboards || []);
} }
@ -245,6 +234,7 @@ function DashboardsList(): JSX.Element {
setSortOrder, setSortOrder,
sortOrder.columnKey, sortOrder.columnKey,
sortOrder.pagination, sortOrder.pagination,
sortOrder.search,
]); ]);
const [newDashboardState, setNewDashboardState] = useState({ const [newDashboardState, setNewDashboardState] = useState({
@ -316,12 +306,15 @@ function DashboardsList(): JSX.Element {
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => { const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
setIsFilteringDashboards(true); setIsFilteringDashboards(true);
setSearchValue(event.target.value);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(searchText, dashboardListResponse); const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse || [],
);
setDashboards(filteredDashboards); setDashboards(filteredDashboards);
setIsFilteringDashboards(false); setIsFilteringDashboards(false);
setSearchString(searchText); setSearchString(searchText);
setSortOrder({ ...sortOrder, search: searchText });
}; };
const [state, setCopy] = useCopyToClipboard(); const [state, setCopy] = useCopyToClipboard();
@ -412,7 +405,7 @@ function DashboardsList(): JSX.Element {
{ {
title: 'Dashboards', title: 'Dashboards',
key: 'dashboard', key: 'dashboard',
render: (dashboard: Data): JSX.Element => { render: (dashboard: Data, _, index): JSX.Element => {
const timeOptions: Intl.DateTimeFormatOptions = { const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
@ -461,7 +454,9 @@ function DashboardsList(): JSX.Element {
style={{ height: '14px', width: '14px' }} style={{ height: '14px', width: '14px' }}
alt="dashboard-image" alt="dashboard-image"
/> />
<Typography.Text>{dashboard.name}</Typography.Text> <Typography.Text data-testid={`dashboard-title-${index}`}>
{dashboard.name}
</Typography.Text>
</div> </div>
<div className="tags-with-actions"> <div className="tags-with-actions">
@ -701,7 +696,7 @@ function DashboardsList(): JSX.Element {
<ArrowUpRight size={16} className="learn-more-arrow" /> <ArrowUpRight size={16} className="learn-more-arrow" />
</section> </section>
</div> </div>
) : dashboards?.length === 0 && !searchValue ? ( ) : dashboards?.length === 0 && !searchString ? (
<div className="dashboard-empty-state"> <div className="dashboard-empty-state">
<img <img
src="/Icons/dashboards.svg" src="/Icons/dashboards.svg"
@ -739,6 +734,7 @@ function DashboardsList(): JSX.Element {
<Button <Button
type="text" type="text"
className="learn-more" className="learn-more"
data-testid="learn-more"
onClick={(): void => { onClick={(): void => {
window.open( window.open(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state', 'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
@ -758,7 +754,7 @@ function DashboardsList(): JSX.Element {
<Input <Input
placeholder="Search by name, description, or tags..." placeholder="Search by name, description, or tags..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />} prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue} value={searchString}
onChange={handleSearch} onChange={handleSearch}
/> />
{createNewDashboard && ( {createNewDashboard && (
@ -786,7 +782,7 @@ function DashboardsList(): JSX.Element {
<div className="no-search"> <div className="no-search">
<img src="/Icons/emptyState.svg" alt="img" className="img" /> <img src="/Icons/emptyState.svg" alt="img" className="img" />
<Typography.Text className="text"> <Typography.Text className="text">
No dashboards found for {searchValue}. Create a new dashboard? No dashboards found for {searchString}. Create a new dashboard?
</Typography.Text> </Typography.Text>
</div> </div>
) : ( ) : (
@ -808,6 +804,7 @@ function DashboardsList(): JSX.Element {
type="text" type="text"
className={cx('sort-btns')} className={cx('sort-btns')}
onClick={(): void => sortHandle('createdAt')} onClick={(): void => sortHandle('createdAt')}
data-testid="sort-by-last-created"
> >
Last created Last created
{sortOrder.columnKey === 'createdAt' && <Check size={14} />} {sortOrder.columnKey === 'createdAt' && <Check size={14} />}
@ -816,6 +813,7 @@ function DashboardsList(): JSX.Element {
type="text" type="text"
className={cx('sort-btns')} className={cx('sort-btns')}
onClick={(): void => sortHandle('updatedAt')} onClick={(): void => sortHandle('updatedAt')}
data-testid="sort-by-last-updated"
> >
Last updated Last updated
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />} {sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
@ -826,7 +824,7 @@ function DashboardsList(): JSX.Element {
placement="bottomRight" placement="bottomRight"
arrow={false} arrow={false}
> >
<ArrowDownWideNarrow size={14} /> <ArrowDownWideNarrow size={14} data-testid="sort-by" />
</Popover> </Popover>
</Tooltip> </Tooltip>
<Popover <Popover

View File

@ -266,6 +266,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
urlQuery.set('columnKey', listSortOrder.columnKey as string); urlQuery.set('columnKey', listSortOrder.columnKey as string);
urlQuery.set('order', listSortOrder.order as string); urlQuery.set('order', listSortOrder.order as string);
urlQuery.set('page', listSortOrder.pagination as string); urlQuery.set('page', listSortOrder.pagination as string);
urlQuery.set('search', listSortOrder.search as string);
urlQuery.delete(QueryParams.relativeTime); urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`; const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;

View File

@ -0,0 +1,50 @@
export const dashboardSuccessResponse = {
status: 'success',
data: [
{
id: 1,
uuid: '1',
created_at: '2022-11-16T13:29:47.064874419Z',
created_by: null,
updated_at: '2024-05-21T06:41:30.546630961Z',
updated_by: 'thor@avengers.io',
isLocked: 0,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: ['linux'],
title: 'thor',
uploadedGrafana: false,
uuid: '',
version: '',
},
},
{
id: 2,
uuid: '2',
created_at: '2022-11-16T13:20:47.064874419Z',
created_by: null,
updated_at: '2024-05-21T06:42:30.546630961Z',
updated_by: 'captain-america@avengers.io',
isLocked: 0,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: ['linux'],
title: 'captain america',
uploadedGrafana: false,
uuid: '',
version: '',
},
},
],
};
export const dashboardEmptyState = {
status: 'sucsess',
data: [],
};

View File

@ -1,6 +1,7 @@
import { rest } from 'msw'; import { rest } from 'msw';
import { billingSuccessResponse } from './__mockdata__/billing'; import { billingSuccessResponse } from './__mockdata__/billing';
import { dashboardSuccessResponse } from './__mockdata__/dashboards';
import { inviteUser } from './__mockdata__/invite_user'; import { inviteUser } from './__mockdata__/invite_user';
import { licensesSuccessResponse } from './__mockdata__/licenses'; import { licensesSuccessResponse } from './__mockdata__/licenses';
import { membersResponse } from './__mockdata__/members'; import { membersResponse } from './__mockdata__/members';
@ -91,6 +92,10 @@ export const handlers = [
res(ctx.status(200), ctx.json(billingSuccessResponse)), res(ctx.status(200), ctx.json(billingSuccessResponse)),
), ),
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
res(ctx.status(200), ctx.json(dashboardSuccessResponse)),
),
rest.get('http://localhost/api/v1/invite', (_, res, ctx) => rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
res(ctx.status(200), ctx.json(inviteUser)), res(ctx.status(200), ctx.json(inviteUser)),
), ),

View File

@ -0,0 +1,207 @@
/* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes';
import DashboardsList from 'container/ListOfDashboard';
import { dashboardEmptyState } from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { fireEvent, render, waitFor } from 'tests/test-utils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useRouteMatch: jest.fn().mockReturnValue({
params: {
dashboardId: 4,
},
}),
}));
const mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
describe('dashboard list page', () => {
// should render on updatedAt and descend when the column key and order is messed up
it('should render the list even when the columnKey or the order is mismatched', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=asgard&order=stones&page=1`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={['/dashbords?columnKey=asgard&order=stones&page=1']}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('captain america');
const secondElement = getByTestId('dashboard-title-1');
expect(secondElement.textContent).toBe('thor');
});
// should render correctly when the column key is createdAt and order is descend
it('should render the list even when the columnKey and the order are given', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={['/dashbords?columnKey=createdAt&order=descend&page=1']}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('thor');
const secondElement = getByTestId('dashboard-title-1');
expect(secondElement.textContent).toBe('captain america');
});
// change the sort by order and dashboards list ot be updated accordingly
it('dashboards list should be correctly updated on choosing the different sortBy from dropdown values', async () => {
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('thor');
const secondElement = getByTestId('dashboard-title-1');
expect(secondElement.textContent).toBe('captain america');
// click on the sort button
const sortByButton = getByTestId('sort-by');
expect(sortByButton).toBeInTheDocument();
fireEvent.click(sortByButton!);
// change the sort order
const sortByUpdatedBy = getByTestId('sort-by-last-updated');
await waitFor(() => expect(sortByUpdatedBy).toBeInTheDocument());
fireEvent.click(sortByUpdatedBy!);
// expect the new order
const updatedFirstElement = getByTestId('dashboard-title-0');
expect(updatedFirstElement.textContent).toBe('captain america');
const updatedSecondElement = getByTestId('dashboard-title-1');
expect(updatedSecondElement.textContent).toBe('thor');
});
// should filter correctly on search string
it('should filter dashboards based on search string', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1&search=tho`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText, getByTestId, queryByText } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('thor');
expect(queryByText('captain america')).not.toBeInTheDocument();
// the pagination item should not be present in the list when number of items are less than one page size
expect(
document.querySelector('.ant-table-pagination'),
).not.toBeInTheDocument();
});
it('dashboard empty search state', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1&search=someRandomString`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(
getByText(
'No dashboards found for someRandomString. Create a new dashboard?',
),
).toBeInTheDocument(),
);
});
it('dashboard empty state', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
server.use(
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
res(ctx.status(200), ctx.json(dashboardEmptyState)),
),
);
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(getByText('No dashboards yet.')).toBeInTheDocument(),
);
const learnMoreButton = getByTestId('learn-more');
expect(learnMoreButton).toBeInTheDocument();
fireEvent.click(learnMoreButton);
// test the correct link to be added for the dashboards empty state
await waitFor(() =>
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
'_blank',
),
);
});
});

View File

@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { Modal } from 'antd'; import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get'; import getDashboard from 'api/dashboard/get';
import lockDashboardApi from 'api/dashboard/lockDashboard'; import lockDashboardApi from 'api/dashboard/lockDashboard';
@ -11,6 +12,7 @@ import useAxiosError from 'hooks/useAxiosError';
import useTabVisibility from 'hooks/useTabFocus'; import useTabVisibility from 'hooks/useTabFocus';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
import { defaultTo } from 'lodash-es'; import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined'; import isUndefined from 'lodash-es/isUndefined';
@ -38,7 +40,7 @@ import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { IDashboardContext } from './types'; import { DashboardSortOrder, IDashboardContext } from './types';
import { sortLayout } from './util'; import { sortLayout } from './util';
const DashboardContext = createContext<IDashboardContext>({ const DashboardContext = createContext<IDashboardContext>({
@ -52,7 +54,12 @@ const DashboardContext = createContext<IDashboardContext>({
layouts: [], layouts: [],
panelMap: {}, panelMap: {},
setPanelMap: () => {}, setPanelMap: () => {},
listSortOrder: { columnKey: 'createdAt', order: 'descend', pagination: '1' }, listSortOrder: {
columnKey: 'createdAt',
order: 'descend',
pagination: '1',
search: '',
},
setListSortOrder: () => {}, setListSortOrder: () => {},
setLayouts: () => {}, setLayouts: () => {},
setSelectedDashboard: () => {}, setSelectedDashboard: () => {},
@ -68,6 +75,7 @@ interface Props {
dashboardId: string; dashboardId: string;
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
export function DashboardProvider({ export function DashboardProvider({
children, children,
}: PropsWithChildren): JSX.Element { }: PropsWithChildren): JSX.Element {
@ -82,17 +90,50 @@ export function DashboardProvider({
exact: true, exact: true,
}); });
const params = useUrlQuery(); const isDashboardListPage = useRouteMatch<Props>({
const orderColumnParam = params.get('columnKey'); path: ROUTES.ALL_DASHBOARD,
const orderQueryParam = params.get('order'); exact: true,
const paginationParam = params.get('page');
const [listSortOrder, setListSortOrder] = useState({
columnKey: orderColumnParam || 'updatedAt',
order: orderQueryParam || 'descend',
pagination: paginationParam || '1',
}); });
// 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 || '');
history.replace({ search: params.toString() });
}
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>( const globalTime = useSelector<AppState, GlobalReducer>(

View File

@ -1,9 +1,15 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Dispatch, SetStateAction } from 'react';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
export interface DashboardSortOrder {
columnKey: string;
order: string;
pagination: string;
search: string;
}
export interface IDashboardContext { export interface IDashboardContext {
isDashboardSliderOpen: boolean; isDashboardSliderOpen: boolean;
isDashboardLocked: boolean; isDashboardLocked: boolean;
@ -15,18 +21,8 @@ export interface IDashboardContext {
layouts: Layout[]; layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>; panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>; setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
listSortOrder: { listSortOrder: DashboardSortOrder;
columnKey: string; setListSortOrder: (sortOrder: DashboardSortOrder) => void;
order: string;
pagination: string;
};
setListSortOrder: Dispatch<
SetStateAction<{
columnKey: string;
order: string;
pagination: string;
}>
>;
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>; setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
setSelectedDashboard: React.Dispatch< setSelectedDashboard: React.Dispatch<
React.SetStateAction<Dashboard | undefined> React.SetStateAction<Dashboard | undefined>

View File

@ -1,3 +1,4 @@
import ROUTES from 'constants/routes';
import { parseQuery } from 'lib/logql'; import { parseQuery } from 'lib/logql';
import { OrderPreferenceItems } from 'pages/Logs/config'; import { OrderPreferenceItems } from 'pages/Logs/config';
import { import {
@ -29,6 +30,30 @@ import {
} from 'types/actions/logs'; } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
const supportedLogsOrder = [
OrderPreferenceItems.ASC,
OrderPreferenceItems.DESC,
];
function getLogsOrder(): OrderPreferenceItems {
// set the value of order from the URL only when order query param is present and the user is landing on the old logs explorer page
if (window.location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
const orderParam = new URLSearchParams(window.location.search).get('order');
if (orderParam) {
// check if the order passed is supported else pass the default order
if (supportedLogsOrder.includes(orderParam as OrderPreferenceItems)) {
return orderParam as OrderPreferenceItems;
}
return OrderPreferenceItems.DESC;
}
return OrderPreferenceItems.DESC;
}
return OrderPreferenceItems.DESC;
}
const initialState: ILogsReducer = { const initialState: ILogsReducer = {
fields: { fields: {
interesting: [], interesting: [],
@ -51,10 +76,7 @@ const initialState: ILogsReducer = {
liveTailStartRange: 15, liveTailStartRange: 15,
selectedLogId: null, selectedLogId: null,
detailedLog: null, detailedLog: null,
order: order: getLogsOrder(),
(new URLSearchParams(window.location.search).get(
'order',
) as ILogsReducer['order']) ?? OrderPreferenceItems.DESC,
}; };
export const LogsReducer = ( export const LogsReducer = (