mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-26 10:22:35 +00:00
Compare commits
6 Commits
nv/4074
...
chore/issu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db87b30216 | ||
|
|
dfaa09ff8a | ||
|
|
903e2f6e33 | ||
|
|
be8bc019d0 | ||
|
|
7a85ee1602 | ||
|
|
4b0cbb787a |
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
72
frontend/src/hooks/dashboard/useDashboardsListQueryParams.ts
Normal file
72
frontend/src/hooks/dashboard/useDashboardsListQueryParams.ts
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user