mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-22 14:49:50 +08:00
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:
parent
d3b83f5a41
commit
adfe20e88a
@ -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
|
||||||
|
@ -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()}`;
|
||||||
|
50
frontend/src/mocks-server/__mockdata__/dashboards.ts
Normal file
50
frontend/src/mocks-server/__mockdata__/dashboards.ts
Normal 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: [],
|
||||||
|
};
|
@ -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)),
|
||||||
),
|
),
|
||||||
|
@ -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',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -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>(
|
||||||
|
@ -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>
|
||||||
|
@ -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 = (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user