mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-23 21:00:28 +00:00
Compare commits
2 Commits
refactor/c
...
tvats-vali
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecfeee35dc | ||
|
|
c360e4498d |
@@ -7,7 +7,7 @@ import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
@@ -79,7 +79,7 @@ export function useNavigateToExplorer(): (
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
|
||||
@@ -86,8 +86,8 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): { selectedDashboard: undefined } => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { selectedDashboard: undefined } => ({
|
||||
selectedDashboard: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
import {
|
||||
getDashboardById,
|
||||
getNonIntegrationDashboardById,
|
||||
@@ -6,10 +8,9 @@ import {
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
DashboardContext,
|
||||
DashboardProvider,
|
||||
} from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardContext } from 'providers/Dashboard/types';
|
||||
resetDashboard,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
@@ -21,6 +22,18 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
import DashboardDescription from '..';
|
||||
|
||||
function DashboardBootstrapWrapper({
|
||||
dashboardId,
|
||||
children,
|
||||
}: {
|
||||
dashboardId: string;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
useDashboardBootstrap(dashboardId);
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
interface MockSafeNavigateReturn {
|
||||
safeNavigate: jest.MockedFunction<(url: string) => void>;
|
||||
}
|
||||
@@ -54,6 +67,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
beforeEach(() => {
|
||||
mockSafeNavigate.mockClear();
|
||||
sessionStorage.clear();
|
||||
resetDashboard();
|
||||
});
|
||||
|
||||
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||
@@ -64,7 +78,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -73,7 +87,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -105,7 +119,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -114,7 +128,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -144,7 +158,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -153,7 +167,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -181,37 +195,26 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
|
||||
const mockContextValue: IDashboardContext = {
|
||||
isDashboardLocked: false,
|
||||
handleDashboardLockToggle: jest.fn(),
|
||||
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
||||
useDashboardStore.setState({
|
||||
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
||||
layouts: [],
|
||||
panelMap: {},
|
||||
setPanelMap: jest.fn(),
|
||||
setLayouts: jest.fn(),
|
||||
setSelectedDashboard: jest.fn(),
|
||||
updatedTimeRef: { current: null },
|
||||
updateLocalStorageDashboardVariables: jest.fn(),
|
||||
dashboardQueryRangeCalled: false,
|
||||
setDashboardQueryRangeCalled: jest.fn(),
|
||||
isDashboardFetching: false,
|
||||
columnWidths: {},
|
||||
setColumnWidths: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardContext.Provider value={mockContextValue}>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardContext.Provider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteBu
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useLockDashboard } from 'hooks/dashboard/useLockDashboard';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -39,8 +40,11 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
@@ -79,10 +83,11 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
setPanelMap,
|
||||
layouts,
|
||||
setLayouts,
|
||||
isDashboardLocked,
|
||||
setSelectedDashboard,
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
} = useDashboardStore();
|
||||
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
const handleDashboardLockToggle = useLockDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
Pyramid,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import {
|
||||
IDashboardVariable,
|
||||
@@ -239,7 +239,7 @@ function VariableItem({
|
||||
|
||||
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { CustomMultiSelect } from 'components/NewSelect';
|
||||
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
|
||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { WidgetRow, Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function WidgetSelector({
|
||||
@@ -12,7 +12,7 @@ export function WidgetSelector({
|
||||
selectedWidgets: string[];
|
||||
setSelectedWidgets: (widgets: string[]) => void;
|
||||
}): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
// Get layout IDs for cross-referencing
|
||||
const layoutIds = new Set(
|
||||
|
||||
@@ -19,8 +19,8 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { PenLine, Trash2 } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { TVariableMode } from './types';
|
||||
@@ -87,7 +87,7 @@ function VariablesSettings({
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@@ -5,7 +5,7 @@ import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddT
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { Button } from './styles';
|
||||
import { Base64Icons } from './utils';
|
||||
@@ -15,7 +15,7 @@ import './GeneralSettings.styles.scss';
|
||||
const { Option } = Select;
|
||||
|
||||
function GeneralDashboardSettings(): JSX.Element {
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
unpublishedPublicDashboardMeta,
|
||||
} from 'mocks-server/__mockdata__/publicDashboard';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import PublicDashboardSetting from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('providers/Dashboard/Dashboard');
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore');
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useCopyToClipboard: jest.fn(),
|
||||
@@ -26,7 +26,7 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseDashboard = jest.mocked(useDashboard);
|
||||
const mockUseDashboard = jest.mocked(useDashboardStore);
|
||||
const mockUseCopyToClipboard = jest.mocked(useCopyToClipboard);
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
@@ -67,10 +67,10 @@ beforeEach(() => {
|
||||
// Mock window.open
|
||||
window.open = jest.fn();
|
||||
|
||||
// Mock useDashboard
|
||||
// Mock useDashboardStore
|
||||
mockUseDashboard.mockReturnValue(({
|
||||
selectedDashboard: mockSelectedDashboard,
|
||||
} as unknown) as ReturnType<typeof useDashboard>);
|
||||
} as unknown) as ReturnType<typeof useDashboardStore>);
|
||||
|
||||
// Mock useCopyToClipboard
|
||||
mockUseCopyToClipboard.mockReturnValue(([
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboard
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
|
||||
import APIError from 'types/api/error';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
@@ -59,7 +59,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
const [defaultTimeRange, setDefaultTimeRange] = useState('30m');
|
||||
const [, setCopyPublicDashboardURL] = useCopyToClipboard();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Row } from 'antd';
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import {
|
||||
useDashboardVariables,
|
||||
useDashboardVariablesSelector,
|
||||
} from 'hooks/dashboard/useDashboardVariables';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import {
|
||||
enqueueDescendantsOfVariable,
|
||||
enqueueFetchOfAllVariables,
|
||||
@@ -18,23 +19,23 @@ import {
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
const {
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboard();
|
||||
const { dashboardId, setSelectedDashboard } = useDashboardStore(
|
||||
useShallow((s) => ({
|
||||
dashboardId: s.selectedDashboard?.id ?? '',
|
||||
setSelectedDashboard: s.setSelectedDashboard,
|
||||
})),
|
||||
);
|
||||
|
||||
const { updateUrlVariable } = useVariablesFromUrl();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const dashboardId = useDashboardVariablesSelector(
|
||||
(state) => state.dashboardId,
|
||||
);
|
||||
const sortedVariablesArray = useDashboardVariablesSelector(
|
||||
(state) => state.sortedVariablesArray,
|
||||
);
|
||||
@@ -82,7 +83,13 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
// This makes localStorage much lighter by avoiding storing all individual values
|
||||
const variable = dashboardVariables[id] || dashboardVariables[name];
|
||||
const isDynamic = variable.type === 'DYNAMIC';
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
|
||||
updateLocalStorageDashboardVariable(
|
||||
dashboardId,
|
||||
name,
|
||||
value,
|
||||
allSelected,
|
||||
isDynamic,
|
||||
);
|
||||
|
||||
if (allSelected) {
|
||||
updateUrlVariable(name || id, ALL_SELECTED_VALUE);
|
||||
@@ -150,13 +157,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
// Safe to call synchronously now that the store already has the updated value.
|
||||
enqueueDescendantsOfVariable(name);
|
||||
},
|
||||
[
|
||||
dashboardId,
|
||||
dashboardVariables,
|
||||
updateLocalStorageDashboardVariables,
|
||||
updateUrlVariable,
|
||||
setSelectedDashboard,
|
||||
],
|
||||
[dashboardId, dashboardVariables, updateUrlVariable, setSelectedDashboard],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,11 +32,22 @@ const mockVariableItemCallbacks: {
|
||||
// Mock providers/Dashboard/Dashboard
|
||||
const mockSetSelectedDashboard = jest.fn();
|
||||
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): Record<string, unknown> => ({
|
||||
setSelectedDashboard: mockSetSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
|
||||
}),
|
||||
interface MockDashboardStoreState {
|
||||
selectedDashboard?: { id: string };
|
||||
setSelectedDashboard: typeof mockSetSelectedDashboard;
|
||||
updateLocalStorageDashboardVariables: typeof mockUpdateLocalStorageDashboardVariables;
|
||||
}
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (
|
||||
selector?: (s: Record<string, unknown>) => MockDashboardStoreState,
|
||||
): MockDashboardStoreState => {
|
||||
const state = {
|
||||
selectedDashboard: { id: 'dash-1' },
|
||||
setSelectedDashboard: mockSetSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
|
||||
};
|
||||
return selector ? selector(state) : state;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock hooks/dashboard/useVariablesFromUrl
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback } from 'react';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
|
||||
@@ -37,11 +39,16 @@ interface UseDashboardVariableUpdateReturn {
|
||||
|
||||
export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn => {
|
||||
const {
|
||||
dashboardId,
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboard();
|
||||
|
||||
} = useDashboardStore(
|
||||
useShallow((s) => ({
|
||||
dashboardId: s.selectedDashboard?.id ?? '',
|
||||
selectedDashboard: s.selectedDashboard,
|
||||
setSelectedDashboard: s.setSelectedDashboard,
|
||||
})),
|
||||
);
|
||||
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
|
||||
const updateMutation = useUpdateDashboard();
|
||||
|
||||
@@ -59,7 +66,13 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
// This makes localStorage much lighter and more efficient.
|
||||
// currently all the variables are dynamic
|
||||
const isDynamic = true;
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
|
||||
updateLocalStorageDashboardVariable(
|
||||
dashboardId,
|
||||
name,
|
||||
value,
|
||||
allSelected,
|
||||
isDynamic,
|
||||
);
|
||||
|
||||
if (selectedDashboard) {
|
||||
setSelectedDashboard((prev) => {
|
||||
@@ -97,11 +110,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
],
|
||||
[dashboardId, selectedDashboard, setSelectedDashboard],
|
||||
);
|
||||
|
||||
const updateVariables = useCallback(
|
||||
|
||||
@@ -49,8 +49,8 @@ const mockDashboard = {
|
||||
// Mock the dashboard provider with stable functions to prevent infinite loops
|
||||
const mockSetSelectedDashboard = jest.fn();
|
||||
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: mockDashboard,
|
||||
setSelectedDashboard: mockSetSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
|
||||
|
||||
@@ -56,8 +56,8 @@ const mockDashboard = {
|
||||
},
|
||||
};
|
||||
// Mock dependencies
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: mockDashboard,
|
||||
}),
|
||||
}));
|
||||
@@ -152,8 +152,8 @@ describe('Panel Management Tests', () => {
|
||||
};
|
||||
|
||||
// Temporarily mock the dashboard
|
||||
jest.doMock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.doMock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: modifiedDashboard,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -4,7 +4,7 @@ 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';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { Base64Icons } from '../../DashboardSettings/General/utils';
|
||||
@@ -13,7 +13,7 @@ import './DashboardBreadcrumbs.styles.scss';
|
||||
|
||||
function DashboardBreadcrumbs(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const updatedAtRef = useRef(selectedDashboard?.updatedAt);
|
||||
|
||||
const selectedData = selectedDashboard
|
||||
|
||||
@@ -6,7 +6,10 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
|
||||
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { getChartManagerColumns } from './getChartMangerColumns';
|
||||
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
|
||||
@@ -50,7 +53,7 @@ export default function ChartManager({
|
||||
onToggleSeriesVisibility,
|
||||
syncSeriesVisibilityToLocalStorage,
|
||||
} = usePlotContext();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
|
||||
getDefaultTableDataSet(
|
||||
|
||||
@@ -32,10 +32,18 @@ jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): { isDashboardLocked: boolean } => ({
|
||||
isDashboardLocked: false,
|
||||
}),
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (
|
||||
selector?: (s: {
|
||||
selectedDashboard: { locked: boolean } | undefined;
|
||||
}) => { selectedDashboard: { locked: boolean } },
|
||||
): { selectedDashboard: { locked: boolean } } => {
|
||||
const mockState = { selectedDashboard: { locked: false } };
|
||||
return selector ? selector(mockState) : mockState;
|
||||
},
|
||||
selectIsDashboardLocked: (s: {
|
||||
selectedDashboard: { locked: boolean } | undefined;
|
||||
}): boolean => s.selectedDashboard?.locked ?? false,
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
|
||||
@@ -8,8 +8,11 @@ import { VariablesSettingsTab } from 'container/DashboardContainer/DashboardDesc
|
||||
import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -20,7 +23,8 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Button, Input } from 'antd';
|
||||
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||
@@ -34,7 +37,7 @@ function GraphManager({
|
||||
}, [data, options]);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
|
||||
@@ -39,7 +39,10 @@ import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariab
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -81,11 +84,8 @@ function FullView({
|
||||
setCurrentGraphRef(fullViewRef);
|
||||
}, [setCurrentGraphRef]);
|
||||
|
||||
const {
|
||||
selectedDashboard,
|
||||
isDashboardLocked,
|
||||
setColumnWidths,
|
||||
} = useDashboard();
|
||||
const { selectedDashboard, setColumnWidths } = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const onColumnWidthsChange = useCallback(
|
||||
(widths: Record<string, number>) => {
|
||||
|
||||
@@ -161,8 +161,8 @@ const mockProps: WidgetGraphComponentProps = {
|
||||
};
|
||||
|
||||
// Mock useDashabord hook
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: {
|
||||
data: {
|
||||
variables: [],
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
getCustomTimeRangeWindowSweepInMS,
|
||||
getStartAndEndTimesInMilliseconds,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -106,7 +106,7 @@ function WidgetGraphComponent({
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
setColumnWidths,
|
||||
} = useDashboard();
|
||||
} = useDashboardStore();
|
||||
|
||||
const onColumnWidthsChange = useCallback(
|
||||
(widths: Record<string, number>) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -17,7 +18,6 @@ import { getVariableReferencesInQuery } from 'lib/dashboardVariables/variableRef
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -68,7 +68,19 @@ function GridCardGraph({
|
||||
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const { setDashboardQueryRangeCalled } = useDashboard();
|
||||
const queryRangeCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!queryRangeCalledRef.current) {
|
||||
Sentry.captureEvent({
|
||||
message: `Dashboard query range not called within expected timeframe for widget ${widget?.id}`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
}, 120000);
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
}, [widget?.id]);
|
||||
|
||||
const {
|
||||
minTime,
|
||||
@@ -260,14 +272,14 @@ function GridCardGraph({
|
||||
});
|
||||
}
|
||||
}
|
||||
setDashboardQueryRangeCalled(true);
|
||||
queryRangeCalledRef.current = true;
|
||||
},
|
||||
onSettled: (data) => {
|
||||
dataAvailable?.(
|
||||
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
|
||||
);
|
||||
getGraphData?.(data?.payload?.data);
|
||||
setDashboardQueryRangeCalled(true);
|
||||
queryRangeCalledRef.current = true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
||||
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||
import { useIsFetching } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -12,6 +12,7 @@ import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { DEFAULT_ROW_NAME } from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
@@ -31,7 +32,10 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -61,6 +65,9 @@ interface GraphLayoutProps {
|
||||
function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
const { handle, enableDrillDown = false } = props;
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const isDashboardFetching =
|
||||
useIsFetching([REACT_QUERY_KEY.DASHBOARD_BY_ID]) > 0;
|
||||
|
||||
const {
|
||||
selectedDashboard,
|
||||
layouts,
|
||||
@@ -68,12 +75,9 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
panelMap,
|
||||
setPanelMap,
|
||||
setSelectedDashboard,
|
||||
isDashboardLocked,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
} = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
const { data } = selectedDashboard || {};
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
@@ -137,25 +141,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
setDashboardLayout(sortLayout(layouts));
|
||||
}, [layouts]);
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardQueryRangeCalled(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Send Sentry event if query_range is not called within expected timeframe (2 mins) when there are widgets
|
||||
if (!dashboardQueryRangeCalled && data?.widgets?.length) {
|
||||
Sentry.captureEvent({
|
||||
message: `Dashboard query range not called within expected timeframe even when there are ${data?.widgets?.length} widgets`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
}, 120000);
|
||||
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
}, [dashboardQueryRangeCalled, data?.widgets?.length]);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
|
||||
@@ -4,9 +4,12 @@ import { Button, Popover } from 'antd';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -39,7 +42,8 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const permissions: ComponentTypes[] = ['add_panel'];
|
||||
const { user } = useAppContext();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
@@ -121,7 +121,7 @@ function useNavigateToExplorerPages(): (
|
||||
) => Promise<{
|
||||
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
|
||||
}> {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
|
||||
@@ -92,8 +92,8 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): { selectedDashboard: undefined } => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { selectedDashboard: undefined } => ({
|
||||
selectedDashboard: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -6,11 +6,24 @@
|
||||
// - Handling multiple rows correctly
|
||||
// - Handling widgets with different heights
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
|
||||
function DashboardBootstrapWrapper({
|
||||
dashboardId,
|
||||
children,
|
||||
}: {
|
||||
dashboardId: string;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
useDashboardBootstrap(dashboardId);
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import i18n from 'ReactI18';
|
||||
import {
|
||||
@@ -309,7 +322,7 @@ describe('Stacking bar in new panel', () => {
|
||||
|
||||
const { container, getByText } = render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<DashboardProvider dashboardId="">
|
||||
<DashboardBootstrapWrapper dashboardId="">
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
@@ -317,7 +330,7 @@ describe('Stacking bar in new panel', () => {
|
||||
selectedGraph={PANEL_TYPES.BAR}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
|
||||
@@ -362,13 +375,13 @@ describe('when switching to BAR panel type', () => {
|
||||
});
|
||||
|
||||
const { getByTestId, getByText, container } = render(
|
||||
<DashboardProvider dashboardId="">
|
||||
<DashboardBootstrapWrapper dashboardId="">
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
selectedDashboard={undefined}
|
||||
selectedGraph={PANEL_TYPES.BAR}
|
||||
/>
|
||||
</DashboardProvider>,
|
||||
</DashboardBootstrapWrapper>,
|
||||
);
|
||||
|
||||
expect(getByTestId('panel-change-select')).toHaveAttribute(
|
||||
|
||||
@@ -2,16 +2,15 @@ import { Dispatch, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { IDashboardContext } from 'providers/Dashboard/types';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { timePreferance } from './RightContainer/timeItems';
|
||||
|
||||
export interface NewWidgetProps {
|
||||
dashboardId: string;
|
||||
selectedDashboard: IDashboardContext['selectedDashboard'];
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
selectedGraph: PANEL_TYPES;
|
||||
enableDrillDown?: boolean;
|
||||
}
|
||||
@@ -35,7 +34,7 @@ export interface WidgetGraphProps {
|
||||
>
|
||||
>;
|
||||
enableDrillDown?: boolean;
|
||||
selectedDashboard: IDashboardContext['selectedDashboard'];
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
isNewPanel?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import useContextVariables from 'hooks/dashboard/useContextVariables';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ContextLinksData } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -66,7 +66,7 @@ const useBaseAggregateOptions = ({
|
||||
getUpdatedQuery,
|
||||
isLoading: isResolveQueryLoading,
|
||||
} = useUpdatedQuery();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!aggregateData) {
|
||||
|
||||
@@ -12,8 +12,8 @@ jest.mock('react-router-dom', () => ({
|
||||
}));
|
||||
|
||||
// Mock useDashabord hook
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: {
|
||||
data: {
|
||||
variables: [],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { getLocalStorageDashboardVariables } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
@@ -7,8 +7,8 @@ import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
|
||||
jest.mock('hooks/dashboard/useVariablesFromUrl');
|
||||
|
||||
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
|
||||
typeof useDashboardVariablesFromLocalStorage
|
||||
const mockGetLocalStorageDashboardVariables = getLocalStorageDashboardVariables as jest.MockedFunction<
|
||||
typeof getLocalStorageDashboardVariables
|
||||
>;
|
||||
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
|
||||
typeof useVariablesFromUrl
|
||||
@@ -46,10 +46,7 @@ const setupHook = (
|
||||
currentDashboard: Record<string, any> = {},
|
||||
urlVariables: Record<string, any> = {},
|
||||
): ReturnType<typeof useTransformDashboardVariables> => {
|
||||
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
|
||||
currentDashboard,
|
||||
updateLocalStorageDashboardVariables: jest.fn(),
|
||||
});
|
||||
mockGetLocalStorageDashboardVariables.mockReturnValue(currentDashboard as any);
|
||||
mockUseVariablesFromUrl.mockReturnValue({
|
||||
getUrlVariables: () => urlVariables,
|
||||
setUrlVariables: jest.fn(),
|
||||
|
||||
164
frontend/src/hooks/dashboard/useDashboardBootstrap.ts
Normal file
164
frontend/src/hooks/dashboard/useDashboardBootstrap.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Modal } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { useDashboardQuery } from './useDashboardQuery';
|
||||
import { useDashboardVariablesSync } from './useDashboardVariablesSync';
|
||||
|
||||
interface UseDashboardBootstrapOptions {
|
||||
/** Pass `onModal.confirm` from `Modal.useModal()` to get theme-aware modals. Falls back to static `Modal.confirm`. */
|
||||
confirm?: typeof Modal.confirm;
|
||||
}
|
||||
|
||||
export interface UseDashboardBootstrapReturn {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export function useDashboardBootstrap(
|
||||
dashboardId: string,
|
||||
options: UseDashboardBootstrapOptions = {},
|
||||
): UseDashboardBootstrapReturn {
|
||||
const confirm = options.confirm ?? Modal.confirm;
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const {
|
||||
setSelectedDashboard,
|
||||
setLayouts,
|
||||
setPanelMap,
|
||||
resetDashboardStore,
|
||||
} = useDashboardStore();
|
||||
|
||||
const dashboardRef = useRef<Dashboard>();
|
||||
const modalRef = useRef<ReturnType<typeof Modal.confirm>>();
|
||||
|
||||
const isVisible = useTabVisibility();
|
||||
|
||||
const {
|
||||
getUrlVariables,
|
||||
updateUrlVariable,
|
||||
transformDashboardVariables,
|
||||
} = useTransformDashboardVariables(dashboardId);
|
||||
|
||||
// Keep the external variables store in sync with selectedDashboard
|
||||
useDashboardVariablesSync(dashboardId);
|
||||
|
||||
const dashboardQuery = useDashboardQuery(dashboardId);
|
||||
|
||||
// Handle new dashboard data: initialize on first load, detect changes on subsequent fetches.
|
||||
// React Query's structural sharing means this effect only fires when data actually changes.
|
||||
useEffect(() => {
|
||||
if (!dashboardQuery.data?.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedDashboardData = transformDashboardVariables(
|
||||
dashboardQuery.data.data,
|
||||
);
|
||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||
|
||||
// First load: initialize store and URL variables, then return
|
||||
if (!dashboardRef.current) {
|
||||
const variables = updatedDashboardData?.data?.variables;
|
||||
if (variables) {
|
||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||
}
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
setLayouts(sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)));
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
|
||||
return;
|
||||
}
|
||||
|
||||
// Subsequent fetches: skip if updatedAt hasn't advanced
|
||||
if (!updatedDate.isAfter(dayjs(dashboardRef.current.updatedAt))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Data has changed: prompt user if tab is visible
|
||||
if (isVisible && dashboardRef.current.id === updatedDashboardData?.id) {
|
||||
const modal = confirm({
|
||||
centered: true,
|
||||
title: t('dashboard_has_been_updated'),
|
||||
content: t('do_you_want_to_refresh_the_dashboard'),
|
||||
onOk() {
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: { maxTime, minTime, selectedTime: globalTime.selectedTime },
|
||||
});
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
);
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
|
||||
},
|
||||
});
|
||||
modalRef.current = modal;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardQuery.data]);
|
||||
|
||||
// Refetch when tab becomes visible (after initial load)
|
||||
useEffect(() => {
|
||||
if (isVisible && dashboardRef.current && !!dashboardId) {
|
||||
dashboardQuery.refetch();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVisible]);
|
||||
|
||||
// Dismiss stale modal when tab is hidden
|
||||
useEffect(() => {
|
||||
if (!isVisible && modalRef.current) {
|
||||
modalRef.current.destroy();
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
// Reset store on unmount so stale state doesn't bleed across dashboards
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
resetDashboardStore();
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading: dashboardQuery.isLoading,
|
||||
isError: dashboardQuery.isError,
|
||||
isFetching: dashboardQuery.isFetching,
|
||||
error: dashboardQuery.error,
|
||||
};
|
||||
}
|
||||
68
frontend/src/hooks/dashboard/useDashboardFromLocalStorage.ts
Normal file
68
frontend/src/hooks/dashboard/useDashboardFromLocalStorage.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface LocalStoreDashboardVariables {
|
||||
[id: string]: {
|
||||
selectedValue: IDashboardVariable['selectedValue'];
|
||||
allSelected: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardLocalStorageVariables {
|
||||
[id: string]: LocalStoreDashboardVariables;
|
||||
}
|
||||
|
||||
function readAll(): DashboardLocalStorageVariables {
|
||||
const raw = getLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
console.error('Failed to parse dashboard variables from local storage');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeAll(data: DashboardLocalStorageVariables): void {
|
||||
try {
|
||||
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, JSON.stringify(data));
|
||||
} catch {
|
||||
console.error('Failed to set dashboard variables in local storage');
|
||||
}
|
||||
}
|
||||
|
||||
/** Read the saved variable selections for a dashboard from localStorage. */
|
||||
export function getLocalStorageDashboardVariables(
|
||||
dashboardId: string,
|
||||
): LocalStoreDashboardVariables {
|
||||
return readAll()[dashboardId] ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Write one variable's selection for a dashboard to localStorage.
|
||||
* All call sites write to the same store with no React state coordination.
|
||||
*/
|
||||
export function updateLocalStorageDashboardVariable(
|
||||
dashboardId: string,
|
||||
id: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
isDynamic?: boolean,
|
||||
): void {
|
||||
const all = readAll();
|
||||
all[dashboardId] = {
|
||||
...(all[dashboardId] ?? {}),
|
||||
[id]:
|
||||
isDynamic && allSelected
|
||||
? {
|
||||
selectedValue: (undefined as unknown) as IDashboardVariable['selectedValue'],
|
||||
allSelected: true,
|
||||
}
|
||||
: { selectedValue, allSelected },
|
||||
};
|
||||
writeAll(all);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
interface LocalStoreDashboardVariables {
|
||||
[id: string]: {
|
||||
selectedValue: IDashboardVariable['selectedValue'];
|
||||
allSelected: boolean;
|
||||
};
|
||||
}
|
||||
interface DashboardLocalStorageVariables {
|
||||
[id: string]: LocalStoreDashboardVariables;
|
||||
}
|
||||
|
||||
export interface UseDashboardVariablesFromLocalStorageReturn {
|
||||
currentDashboard: LocalStoreDashboardVariables;
|
||||
updateLocalStorageDashboardVariables: (
|
||||
id: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
isDynamic?: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const useDashboardVariablesFromLocalStorage = (
|
||||
dashboardId: string,
|
||||
): UseDashboardVariablesFromLocalStorageReturn => {
|
||||
const [
|
||||
allDashboards,
|
||||
setAllDashboards,
|
||||
] = useState<DashboardLocalStorageVariables>({});
|
||||
|
||||
const [
|
||||
currentDashboard,
|
||||
setCurrentDashboard,
|
||||
] = useState<LocalStoreDashboardVariables>({});
|
||||
|
||||
useEffect(() => {
|
||||
const localStoreDashboardVariablesString = getLocalStorageKey(
|
||||
LOCALSTORAGE.DASHBOARD_VARIABLES,
|
||||
);
|
||||
let localStoreDashboardVariables: DashboardLocalStorageVariables = {};
|
||||
if (localStoreDashboardVariablesString === null) {
|
||||
try {
|
||||
const serialzedData = JSON.stringify({
|
||||
[dashboardId]: {},
|
||||
});
|
||||
|
||||
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, serialzedData);
|
||||
} catch {
|
||||
console.error('Failed to seralise the data');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
localStoreDashboardVariables = JSON.parse(
|
||||
localStoreDashboardVariablesString,
|
||||
);
|
||||
} catch {
|
||||
console.error('Failed to parse dashboards from local storage');
|
||||
localStoreDashboardVariables = {};
|
||||
} finally {
|
||||
setAllDashboards(localStoreDashboardVariables);
|
||||
}
|
||||
}
|
||||
setCurrentDashboard(defaultTo(localStoreDashboardVariables[dashboardId], {}));
|
||||
}, [dashboardId]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const serializedData = JSON.stringify(allDashboards);
|
||||
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, serializedData);
|
||||
} catch {
|
||||
console.error('Failed to set dashboards in local storage');
|
||||
}
|
||||
}, [allDashboards]);
|
||||
|
||||
useEffect(() => {
|
||||
setAllDashboards((prev) => ({
|
||||
...prev,
|
||||
[dashboardId]: { ...currentDashboard },
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentDashboard]);
|
||||
|
||||
const updateLocalStorageDashboardVariables = (
|
||||
id: string,
|
||||
selectedValue: IDashboardVariable['selectedValue'],
|
||||
allSelected: boolean,
|
||||
isDynamic?: boolean,
|
||||
): void => {
|
||||
setCurrentDashboard((prev) => ({
|
||||
...prev,
|
||||
[id]:
|
||||
isDynamic && allSelected
|
||||
? {
|
||||
selectedValue: (undefined as unknown) as IDashboardVariable['selectedValue'],
|
||||
allSelected: true,
|
||||
}
|
||||
: { selectedValue, allSelected },
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
currentDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
};
|
||||
};
|
||||
49
frontend/src/hooks/dashboard/useDashboardQuery.ts
Normal file
49
frontend/src/hooks/dashboard/useDashboardQuery.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import {
|
||||
DASHBOARD_CACHE_TIME,
|
||||
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
|
||||
} from 'constants/queryCacheTime';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
/**
|
||||
* Fetches a dashboard by ID. Handles auth gating, cache time based on
|
||||
* auto-refresh setting, and surfaces API errors via the error modal.
|
||||
*/
|
||||
export function useDashboardQuery(
|
||||
dashboardId: string,
|
||||
): UseQueryResult<SuccessResponseV2<Dashboard>> {
|
||||
const { isLoggedIn } = useAppContext();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
return useQuery(
|
||||
[
|
||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||
dashboardId,
|
||||
globalTime.isAutoRefreshDisabled,
|
||||
],
|
||||
{
|
||||
enabled: !!dashboardId && isLoggedIn,
|
||||
queryFn: () => getDashboard({ id: dashboardId }),
|
||||
refetchOnWindowFocus: false,
|
||||
cacheTime: globalTime.isAutoRefreshDisabled
|
||||
? DASHBOARD_CACHE_TIME
|
||||
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
33
frontend/src/hooks/dashboard/useDashboardVariablesSync.ts
Normal file
33
frontend/src/hooks/dashboard/useDashboardVariablesSync.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from 'react';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import {
|
||||
setDashboardVariablesStore,
|
||||
updateDashboardVariablesStore,
|
||||
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import {
|
||||
DashboardStore,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { useDashboardVariablesSelector } from './useDashboardVariables';
|
||||
|
||||
/**
|
||||
* Keeps the external variables store in sync with the zustand dashboard store.
|
||||
* When selectedDashboard changes, propagates variable updates to the variables store.
|
||||
*/
|
||||
export function useDashboardVariablesSync(dashboardId: string): void {
|
||||
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
|
||||
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
|
||||
const selectedDashboard = useDashboardStore(
|
||||
(s: DashboardStore) => s.selectedDashboard,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const updatedVariables = selectedDashboard?.data.variables || {};
|
||||
if (savedDashboardId !== dashboardId) {
|
||||
setDashboardVariablesStore({ dashboardId, variables: updatedVariables });
|
||||
} else if (!isEqual(dashboardVariables, updatedVariables)) {
|
||||
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
|
||||
}
|
||||
}, [selectedDashboard]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}
|
||||
42
frontend/src/hooks/dashboard/useLockDashboard.ts
Normal file
42
frontend/src/hooks/dashboard/useLockDashboard.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useMutation } from 'react-query';
|
||||
import locked from 'api/v1/dashboards/id/lock';
|
||||
import {
|
||||
getSelectedDashboard,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
/**
|
||||
* Hook for toggling dashboard locked state.
|
||||
* Calls the lock API and syncs the result into the Zustand store.
|
||||
*/
|
||||
export function useLockDashboard(): (value: boolean) => Promise<void> {
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const { setSelectedDashboard } = useDashboardStore();
|
||||
|
||||
const { mutate: lockDashboard } = useMutation(locked, {
|
||||
onSuccess: (_, props) => {
|
||||
setSelectedDashboard((prev) =>
|
||||
prev ? { ...prev, locked: props.lock } : prev,
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
});
|
||||
|
||||
return async (value: boolean): Promise<void> => {
|
||||
const selectedDashboard = getSelectedDashboard();
|
||||
if (selectedDashboard) {
|
||||
try {
|
||||
await lockDashboard({
|
||||
id: selectedDashboard.id,
|
||||
lock: value,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import {
|
||||
useDashboardVariablesFromLocalStorage,
|
||||
UseDashboardVariablesFromLocalStorageReturn,
|
||||
} from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { getLocalStorageDashboardVariables } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import useVariablesFromUrl, {
|
||||
UseVariablesFromUrlReturn,
|
||||
} from 'hooks/dashboard/useVariablesFromUrl';
|
||||
@@ -13,14 +10,10 @@ import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
export function useTransformDashboardVariables(
|
||||
dashboardId: string,
|
||||
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
|
||||
UseDashboardVariablesFromLocalStorageReturn & {
|
||||
transformDashboardVariables: (data: Dashboard) => Dashboard;
|
||||
} {
|
||||
const {
|
||||
currentDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
||||
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> & {
|
||||
transformDashboardVariables: (data: Dashboard) => Dashboard;
|
||||
currentDashboard: ReturnType<typeof getLocalStorageDashboardVariables>;
|
||||
} {
|
||||
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
||||
|
||||
const mergeDBWithLocalStorage = (
|
||||
@@ -80,7 +73,7 @@ export function useTransformDashboardVariables(
|
||||
if (data && data.data && data.data.variables) {
|
||||
const clonedDashboardData = mergeDBWithLocalStorage(
|
||||
JSON.parse(JSON.stringify(data)),
|
||||
currentDashboard,
|
||||
getLocalStorageDashboardVariables(dashboardId),
|
||||
);
|
||||
const { variables } = clonedDashboardData.data;
|
||||
const existingOrders: Set<number> = new Set();
|
||||
@@ -122,7 +115,6 @@ export function useTransformDashboardVariables(
|
||||
transformDashboardVariables,
|
||||
getUrlVariables,
|
||||
updateUrlVariable,
|
||||
currentDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
currentDashboard: getLocalStorageDashboardVariables(dashboardId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import update from 'api/v1/dashboards/id/update';
|
||||
import dayjs from 'dayjs';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
@@ -9,14 +7,8 @@ import { Props } from 'types/api/dashboard/update';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export const useUpdateDashboard = (): UseUpdateDashboard => {
|
||||
const { updatedTimeRef } = useDashboard();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
return useMutation(update, {
|
||||
onSuccess: (data) => {
|
||||
if (data.data) {
|
||||
updatedTimeRef.current = dayjs(data.data.updatedAt);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorModal(error);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
|
||||
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { useDashboardVariablesByType } from './useDashboardVariablesByType';
|
||||
@@ -12,7 +12,7 @@ import { useDashboardVariablesByType } from './useDashboardVariablesByType';
|
||||
*/
|
||||
export function useWidgetsByDynamicVariableId(): Record<string, string[]> {
|
||||
const dynamicVariables = useDashboardVariablesByType('DYNAMIC', 'values');
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
return useMemo(() => {
|
||||
const widgets =
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -33,7 +33,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import DashboardContainer from 'container/DashboardContainer';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { ErrorType } from 'types/common';
|
||||
|
||||
function DashboardPage(): JSX.Element {
|
||||
const { dashboardResponse } = useDashboard();
|
||||
|
||||
const { isFetching, isError, isLoading } = dashboardResponse;
|
||||
|
||||
const errorMessage = isError
|
||||
? (dashboardResponse?.error as AxiosError<{ errorType: string }>)?.response
|
||||
?.data?.errorType
|
||||
: 'Something went wrong';
|
||||
|
||||
useEffect(() => {
|
||||
const dashboardTitle = dashboardResponse.data?.data.data.title;
|
||||
document.title = dashboardTitle || document.title;
|
||||
}, [dashboardResponse.data?.data.data.title, isFetching]);
|
||||
|
||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
if (isError && errorMessage) {
|
||||
return <Typography>{errorMessage}</Typography>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
return <DashboardContainer />;
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -1,16 +1,56 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { Modal, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import DashboardContainer from 'container/DashboardContainer';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ErrorType } from 'types/common';
|
||||
|
||||
import DashboardPage from './DashboardPage';
|
||||
|
||||
function DashboardPageWithProvider(): JSX.Element {
|
||||
function DashboardPage(): JSX.Element {
|
||||
const { dashboardId } = useParams<{ dashboardId: string }>();
|
||||
|
||||
const [onModal, Content] = Modal.useModal();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
isFetching,
|
||||
error,
|
||||
} = useDashboardBootstrap(dashboardId, { confirm: onModal.confirm });
|
||||
|
||||
const dashboardTitle = useDashboardStore(
|
||||
(s) => s.selectedDashboard?.data.title,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = dashboardTitle || document.title;
|
||||
}, [dashboardTitle]);
|
||||
|
||||
const errorMessage = isError
|
||||
? (error as AxiosError<{ errorType: string }>)?.response?.data?.errorType
|
||||
: 'Something went wrong';
|
||||
|
||||
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
if (isError && errorMessage) {
|
||||
return <Typography>{errorMessage}</Typography>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardProvider dashboardId={dashboardId}>
|
||||
<DashboardPage />
|
||||
</DashboardProvider>
|
||||
<>
|
||||
{Content}
|
||||
<DashboardContainer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPageWithProvider;
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
import {
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery, UseQueryResult } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Modal } from 'antd';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import locked from 'api/v1/dashboards/id/lock';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import isUndefined from 'lodash-es/isUndefined';
|
||||
import omitBy from 'lodash-es/omitBy';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import {
|
||||
DASHBOARD_CACHE_TIME,
|
||||
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
|
||||
} from '../../constants/queryCacheTime';
|
||||
import { useDashboardVariablesSelector } from '../../hooks/dashboard/useDashboardVariables';
|
||||
import {
|
||||
setDashboardVariablesStore,
|
||||
updateDashboardVariablesStore,
|
||||
} from './store/dashboardVariables/dashboardVariablesStore';
|
||||
import { IDashboardContext, WidgetColumnWidths } from './types';
|
||||
import { sortLayout } from './util';
|
||||
|
||||
export const DashboardContext = createContext<IDashboardContext>({
|
||||
isDashboardLocked: false,
|
||||
handleDashboardLockToggle: () => {},
|
||||
dashboardResponse: {} as UseQueryResult<
|
||||
SuccessResponseV2<Dashboard>,
|
||||
APIError
|
||||
>,
|
||||
selectedDashboard: {} as Dashboard,
|
||||
layouts: [],
|
||||
panelMap: {},
|
||||
setPanelMap: () => {},
|
||||
|
||||
setLayouts: () => {},
|
||||
setSelectedDashboard: () => {},
|
||||
updatedTimeRef: {} as React.MutableRefObject<Dayjs | null>,
|
||||
updateLocalStorageDashboardVariables: () => {},
|
||||
dashboardQueryRangeCalled: false,
|
||||
setDashboardQueryRangeCalled: () => {},
|
||||
isDashboardFetching: false,
|
||||
columnWidths: {},
|
||||
setColumnWidths: () => {},
|
||||
});
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function DashboardProvider({
|
||||
children,
|
||||
dashboardId,
|
||||
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
|
||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||
|
||||
const [
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const [onModal, Content] = Modal.useModal();
|
||||
|
||||
const [layouts, setLayouts] = useState<Layout[]>([]);
|
||||
|
||||
const [panelMap, setPanelMap] = useState<
|
||||
Record<string, { widgets: Layout[]; collapsed: boolean }>
|
||||
>({});
|
||||
|
||||
const { isLoggedIn } = useAppContext();
|
||||
|
||||
const [selectedDashboard, setSelectedDashboard] = useState<Dashboard>();
|
||||
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
|
||||
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
|
||||
|
||||
useEffect(() => {
|
||||
const existingVariables = dashboardVariables;
|
||||
const updatedVariables = selectedDashboard?.data.variables || {};
|
||||
|
||||
if (savedDashboardId !== dashboardId) {
|
||||
setDashboardVariablesStore({
|
||||
dashboardId,
|
||||
variables: updatedVariables,
|
||||
});
|
||||
} else if (!isEqual(existingVariables, updatedVariables)) {
|
||||
updateDashboardVariablesStore({
|
||||
dashboardId,
|
||||
variables: updatedVariables,
|
||||
});
|
||||
}
|
||||
}, [selectedDashboard]);
|
||||
|
||||
const {
|
||||
currentDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
getUrlVariables,
|
||||
updateUrlVariable,
|
||||
transformDashboardVariables,
|
||||
} = useTransformDashboardVariables(dashboardId);
|
||||
|
||||
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
||||
const modalRef = useRef<any>(null);
|
||||
|
||||
const isVisible = useTabVisibility();
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
const dashboardRef = useRef<Dashboard>();
|
||||
|
||||
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
||||
|
||||
const dashboardResponse = useQuery(
|
||||
[
|
||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||
dashboardId,
|
||||
globalTime.isAutoRefreshDisabled,
|
||||
],
|
||||
{
|
||||
enabled: !!dashboardId && isLoggedIn,
|
||||
queryFn: async () => {
|
||||
setIsDashboardFetching(true);
|
||||
try {
|
||||
return await getDashboard({
|
||||
id: dashboardId,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
return;
|
||||
} finally {
|
||||
setIsDashboardFetching(false);
|
||||
}
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
cacheTime: globalTime.isAutoRefreshDisabled
|
||||
? DASHBOARD_CACHE_TIME
|
||||
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
|
||||
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||
|
||||
// initialize URL variables after dashboard state is set to avoid race conditions
|
||||
const variables = updatedDashboardData?.data?.variables;
|
||||
if (variables) {
|
||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||
}
|
||||
|
||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||
|
||||
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||
|
||||
// on first render
|
||||
if (updatedTimeRef.current === null) {
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
|
||||
updatedTimeRef.current = updatedDate;
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
);
|
||||
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
|
||||
}
|
||||
|
||||
if (
|
||||
updatedTimeRef.current !== null &&
|
||||
updatedDate.isAfter(updatedTimeRef.current) &&
|
||||
isVisible &&
|
||||
dashboardRef.current?.id === updatedDashboardData?.id
|
||||
) {
|
||||
// show modal when state is out of sync
|
||||
const modal = onModal.confirm({
|
||||
centered: true,
|
||||
title: t('dashboard_has_been_updated'),
|
||||
content: t('do_you_want_to_refresh_the_dashboard'),
|
||||
onOk() {
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
|
||||
const { maxTime, minTime } = getMinMaxForSelectedTime(
|
||||
globalTime.selectedTime,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_TIME_INTERVAL,
|
||||
payload: {
|
||||
maxTime,
|
||||
minTime,
|
||||
selectedTime: globalTime.selectedTime,
|
||||
},
|
||||
});
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData?.updatedAt);
|
||||
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
);
|
||||
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
|
||||
},
|
||||
});
|
||||
|
||||
modalRef.current = modal;
|
||||
} else {
|
||||
// normal flow
|
||||
updatedTimeRef.current = dayjs(updatedDashboardData?.updatedAt);
|
||||
|
||||
dashboardRef.current = updatedDashboardData;
|
||||
|
||||
if (!isEqual(selectedDashboard, updatedDashboardData)) {
|
||||
setSelectedDashboard(updatedDashboardData);
|
||||
}
|
||||
|
||||
if (
|
||||
!isEqual(
|
||||
[omitBy(layouts, (value): boolean => isUndefined(value))[0]],
|
||||
updatedDashboardData?.data.layout,
|
||||
)
|
||||
) {
|
||||
setLayouts(
|
||||
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
|
||||
);
|
||||
|
||||
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// make the call on tab visibility only if the user is on dashboard / widget page
|
||||
if (isVisible && updatedTimeRef.current && !!dashboardId) {
|
||||
dashboardResponse.refetch();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible && modalRef.current) {
|
||||
modalRef.current.destroy();
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
const { mutate: lockDashboard } = useMutation(locked, {
|
||||
onSuccess: (_, props) => {
|
||||
setIsDashboardLocked(props.lock);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
});
|
||||
|
||||
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
|
||||
if (selectedDashboard) {
|
||||
try {
|
||||
await lockDashboard({
|
||||
id: selectedDashboard.id,
|
||||
lock: value,
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<WidgetColumnWidths>({});
|
||||
|
||||
const value: IDashboardContext = useMemo(
|
||||
() => ({
|
||||
isDashboardLocked,
|
||||
handleDashboardLockToggle,
|
||||
dashboardResponse,
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
layouts,
|
||||
panelMap,
|
||||
setLayouts,
|
||||
setPanelMap,
|
||||
setSelectedDashboard,
|
||||
updatedTimeRef,
|
||||
updateLocalStorageDashboardVariables,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
isDashboardLocked,
|
||||
dashboardResponse,
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
layouts,
|
||||
panelMap,
|
||||
updateLocalStorageDashboardVariables,
|
||||
currentDashboard,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
setColumnWidths,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardContext.Provider value={value}>
|
||||
{Content}
|
||||
{children}
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useDashboard = (): IDashboardContext => {
|
||||
const context = useContext(DashboardContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Should be used inside the context');
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -6,7 +7,20 @@ import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import getDashboard from 'api/v1/dashboards/id/get';
|
||||
import { DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED } from 'constants/queryCacheTime';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { DashboardProvider, useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
|
||||
function DashboardBootstrapWrapper({
|
||||
dashboardId,
|
||||
children,
|
||||
}: {
|
||||
dashboardId: string;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
useDashboardBootstrap(dashboardId);
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { useDashboardVariables } from '../../../hooks/dashboard/useDashboardVariables';
|
||||
@@ -55,17 +69,12 @@ jest.mock('react-redux', () => ({
|
||||
jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
|
||||
|
||||
function TestComponent(): JSX.Element {
|
||||
const { dashboardResponse, selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="dashboard-id">{selectedDashboard?.id}</div>
|
||||
<div data-testid="query-status">{dashboardResponse.status}</div>
|
||||
<div data-testid="is-loading">{dashboardResponse.isLoading.toString()}</div>
|
||||
<div data-testid="is-fetching">
|
||||
{dashboardResponse.isFetching.toString()}
|
||||
</div>
|
||||
<div data-testid="dashboard-variables">
|
||||
{dashboardVariables ? JSON.stringify(dashboardVariables) : 'null'}
|
||||
</div>
|
||||
@@ -89,7 +98,7 @@ function createTestQueryClient(): QueryClient {
|
||||
}
|
||||
|
||||
// Helper to render with dashboard provider
|
||||
function renderWithDashboardProvider(
|
||||
function renderWithDashboardBootstrap(
|
||||
dashboardId = 'test-dashboard-id',
|
||||
): RenderResult {
|
||||
const queryClient = createTestQueryClient();
|
||||
@@ -98,9 +107,9 @@ function renderWithDashboardProvider(
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[initialRoute]}>
|
||||
<DashboardProvider dashboardId={dashboardId}>
|
||||
<DashboardBootstrapWrapper dashboardId={dashboardId}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
@@ -172,7 +181,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
describe('Query Key Behavior', () => {
|
||||
it('should include route params in query key when on dashboard page', async () => {
|
||||
const dashboardId = 'test-dashboard-id';
|
||||
renderWithDashboardProvider(dashboardId);
|
||||
renderWithDashboardBootstrap(dashboardId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId });
|
||||
@@ -187,7 +196,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
const newDashboardId = 'new-dashboard-id';
|
||||
|
||||
// First render with initial dashboard ID
|
||||
const { rerender } = renderWithDashboardProvider(initialDashboardId);
|
||||
const { rerender } = renderWithDashboardBootstrap(initialDashboardId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: initialDashboardId });
|
||||
@@ -197,9 +206,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
rerender(
|
||||
<QueryClientProvider client={createTestQueryClient()}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${newDashboardId}`]}>
|
||||
<DashboardProvider dashboardId={newDashboardId}>
|
||||
<DashboardBootstrapWrapper dashboardId={newDashboardId}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
@@ -213,7 +222,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
});
|
||||
|
||||
it('should not fetch when no dashboardId is provided', () => {
|
||||
renderWithDashboardProvider('');
|
||||
renderWithDashboardBootstrap('');
|
||||
|
||||
// Should not call the API
|
||||
expect(mockGetDashboard).not.toHaveBeenCalled();
|
||||
@@ -229,9 +238,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
const { rerender } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${dashboardId1}`]}>
|
||||
<DashboardProvider dashboardId={dashboardId1}>
|
||||
<DashboardBootstrapWrapper dashboardId={dashboardId1}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
@@ -243,9 +252,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
rerender(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${dashboardId2}`]}>
|
||||
<DashboardProvider dashboardId={dashboardId2}>
|
||||
<DashboardBootstrapWrapper dashboardId={dashboardId2}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
@@ -286,9 +295,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/dashboard/${dashboardId}`]}>
|
||||
<DashboardProvider dashboardId={dashboardId}>
|
||||
<DashboardBootstrapWrapper dashboardId={dashboardId}>
|
||||
<TestComponent />
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
@@ -365,7 +374,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
// Empty URL variables - tests initialization flow
|
||||
mockGetUrlVariables.mockReturnValue({});
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -421,7 +430,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
.mockReturnValueOnce('development')
|
||||
.mockReturnValueOnce(['db', 'cache']);
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -481,7 +490,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
|
||||
mockGetUrlVariables.mockReturnValue(urlVariables);
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -517,7 +526,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
||||
.mockReturnValueOnce('development')
|
||||
.mockReturnValueOnce(['api']);
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify normalization was called with the specific values and variable configs
|
||||
@@ -584,7 +593,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -626,7 +635,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -669,7 +678,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
@@ -711,7 +720,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
|
||||
} as any);
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
renderWithDashboardProvider(DASHBOARD_ID);
|
||||
renderWithDashboardBootstrap(DASHBOARD_ID);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { Layout } from 'react-grid-layout';
|
||||
import type { StateCreator } from 'zustand';
|
||||
|
||||
import type { DashboardStore } from '../useDashboardStore';
|
||||
|
||||
export interface DashboardLayoutSlice {
|
||||
//
|
||||
layouts: Layout[];
|
||||
setLayouts: (updater: Layout[] | ((prev: Layout[]) => Layout[])) => void;
|
||||
//
|
||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||
setPanelMap: (
|
||||
updater:
|
||||
| Record<string, { widgets: Layout[]; collapsed: boolean }>
|
||||
| ((
|
||||
prev: Record<string, { widgets: Layout[]; collapsed: boolean }>,
|
||||
) => Record<string, { widgets: Layout[]; collapsed: boolean }>),
|
||||
) => void;
|
||||
// resetDashboardLayout: () => void;
|
||||
}
|
||||
|
||||
export const initialDashboardLayoutState = {
|
||||
layouts: [] as Layout[],
|
||||
panelMap: {} as Record<string, { widgets: Layout[]; collapsed: boolean }>,
|
||||
};
|
||||
|
||||
export const createDashboardLayoutSlice: StateCreator<
|
||||
DashboardStore,
|
||||
[['zustand/immer', never]],
|
||||
[],
|
||||
DashboardLayoutSlice
|
||||
> = (set) => ({
|
||||
...initialDashboardLayoutState,
|
||||
|
||||
setLayouts: (updater): void =>
|
||||
set((state) => {
|
||||
state.layouts =
|
||||
typeof updater === 'function' ? updater(state.layouts) : updater;
|
||||
}),
|
||||
|
||||
setPanelMap: (updater): void =>
|
||||
set((state) => {
|
||||
state.panelMap =
|
||||
typeof updater === 'function' ? updater(state.panelMap) : updater;
|
||||
}),
|
||||
|
||||
// resetDashboardLayout: () =>
|
||||
// set((state) => {
|
||||
// Object.assign(state, initialDashboardLayoutState);
|
||||
// }),
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import type { StateCreator } from 'zustand';
|
||||
|
||||
import type { DashboardStore } from '../useDashboardStore';
|
||||
|
||||
export type WidgetColumnWidths = {
|
||||
[widgetId: string]: Record<string, number>;
|
||||
};
|
||||
|
||||
export interface DashboardUISlice {
|
||||
//
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
setSelectedDashboard: (
|
||||
updater:
|
||||
| Dashboard
|
||||
| undefined
|
||||
| ((prev: Dashboard | undefined) => Dashboard | undefined),
|
||||
) => void;
|
||||
//
|
||||
columnWidths: WidgetColumnWidths;
|
||||
setColumnWidths: (
|
||||
updater:
|
||||
| WidgetColumnWidths
|
||||
| ((prev: WidgetColumnWidths) => WidgetColumnWidths),
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const initialDashboardUIState = {
|
||||
selectedDashboard: undefined as Dashboard | undefined,
|
||||
columnWidths: {} as WidgetColumnWidths,
|
||||
};
|
||||
|
||||
export const createDashboardUISlice: StateCreator<
|
||||
DashboardStore,
|
||||
[['zustand/immer', never]],
|
||||
[],
|
||||
DashboardUISlice
|
||||
> = (set) => ({
|
||||
...initialDashboardUIState,
|
||||
|
||||
setSelectedDashboard: (updater): void =>
|
||||
set((state: DashboardUISlice): void => {
|
||||
state.selectedDashboard =
|
||||
typeof updater === 'function' ? updater(state.selectedDashboard) : updater;
|
||||
}),
|
||||
|
||||
setColumnWidths: (updater): void =>
|
||||
set((state: DashboardUISlice): void => {
|
||||
state.columnWidths =
|
||||
typeof updater === 'function' ? updater(state.columnWidths) : updater;
|
||||
}),
|
||||
|
||||
resetDashboardUI: (): void =>
|
||||
set((state: DashboardUISlice): void => {
|
||||
Object.assign(state, initialDashboardUIState);
|
||||
}),
|
||||
});
|
||||
50
frontend/src/providers/Dashboard/store/useDashboardStore.ts
Normal file
50
frontend/src/providers/Dashboard/store/useDashboardStore.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Layout } from 'react-grid-layout';
|
||||
import type { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { create } from 'zustand';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
import {
|
||||
createDashboardLayoutSlice,
|
||||
DashboardLayoutSlice,
|
||||
initialDashboardLayoutState,
|
||||
} from './slices/dashboardLayoutSlice';
|
||||
import {
|
||||
createDashboardUISlice,
|
||||
DashboardUISlice,
|
||||
initialDashboardUIState,
|
||||
} from './slices/dashboardUISlice';
|
||||
|
||||
export type DashboardStore = DashboardUISlice &
|
||||
DashboardLayoutSlice & {
|
||||
resetDashboardStore: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 'select*' is a redux naming convention that can be carried over to zustand.
|
||||
* It is used to select a piece of state from the store.
|
||||
* In this case, we are selecting the locked state of the selected dashboard.
|
||||
* */
|
||||
export const selectIsDashboardLocked = (s: DashboardStore): boolean =>
|
||||
s.selectedDashboard?.locked ?? false;
|
||||
|
||||
export const useDashboardStore = create<DashboardStore>()(
|
||||
immer((set, get, api) => ({
|
||||
...createDashboardUISlice(set, get, api),
|
||||
...createDashboardLayoutSlice(set, get, api),
|
||||
|
||||
resetDashboardStore: (): void =>
|
||||
set((state: DashboardStore) => {
|
||||
Object.assign(state, initialDashboardUIState, initialDashboardLayoutState);
|
||||
}),
|
||||
})),
|
||||
);
|
||||
|
||||
// Standalone imperative accessors — use these instead of calling useDashboardStore.getState() at call sites.
|
||||
export const getSelectedDashboard = (): Dashboard | undefined =>
|
||||
useDashboardStore.getState().selectedDashboard;
|
||||
|
||||
export const getDashboardLayouts = (): Layout[] =>
|
||||
useDashboardStore.getState().layouts;
|
||||
|
||||
export const resetDashboard = (): void =>
|
||||
useDashboardStore.getState().resetDashboardStore();
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import dayjs from 'dayjs';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export type WidgetColumnWidths = {
|
||||
[widgetId: string]: Record<string, number>;
|
||||
};
|
||||
|
||||
export interface IDashboardContext {
|
||||
isDashboardLocked: boolean;
|
||||
handleDashboardLockToggle: (value: boolean) => void;
|
||||
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
|
||||
selectedDashboard: Dashboard | undefined;
|
||||
layouts: Layout[];
|
||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: React.Dispatch<
|
||||
React.SetStateAction<Dashboard | undefined>
|
||||
>;
|
||||
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
|
||||
updateLocalStorageDashboardVariables: (
|
||||
id: string,
|
||||
selectedValue:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| (string | number | boolean)[]
|
||||
| null
|
||||
| undefined,
|
||||
allSelected: boolean,
|
||||
isDynamic?: boolean,
|
||||
) => void;
|
||||
dashboardQueryRangeCalled: boolean;
|
||||
setDashboardQueryRangeCalled: (value: boolean) => void;
|
||||
isDashboardFetching: boolean;
|
||||
columnWidths: WidgetColumnWidths;
|
||||
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;
|
||||
}
|
||||
@@ -1,5 +1,97 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
)
|
||||
|
||||
type chTableCheck struct {
|
||||
pattern *regexp.Regexp
|
||||
errMsg string
|
||||
}
|
||||
|
||||
func buildDeprecatedChecks(entries []struct{ name, replacement string }) []chTableCheck {
|
||||
result := make([]chTableCheck, len(entries))
|
||||
for i, e := range entries {
|
||||
var msg string
|
||||
if e.replacement != "" {
|
||||
msg = fmt.Sprintf("ClickHouse query references deprecated table %q, use %q instead", e.name, e.replacement)
|
||||
} else {
|
||||
msg = fmt.Sprintf("ClickHouse query references deprecated table %q", e.name)
|
||||
}
|
||||
result[i] = chTableCheck{
|
||||
pattern: regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(e.name) + `\b`),
|
||||
errMsg: msg,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildLocalChecks(entries []struct{ name, replacement string }) []chTableCheck {
|
||||
result := make([]chTableCheck, len(entries))
|
||||
for i, e := range entries {
|
||||
result[i] = chTableCheck{
|
||||
pattern: regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(e.name) + `\b`),
|
||||
errMsg: fmt.Sprintf("ClickHouse query references local table %q, use distributed table %q instead", e.name, e.replacement),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// chTableChecks combines deprecated and local table checks. Both sets reject
|
||||
// queries that reference disallowed tables; they differ only in error message.
|
||||
// Word-boundary patterns prevent false positives (e.g. "distributed_logs"
|
||||
// must not match "distributed_logs_v2").
|
||||
var chTableChecks = append(
|
||||
buildDeprecatedChecks([]struct{ name, replacement string }{
|
||||
// Traces V2 → V3
|
||||
{"distributed_signoz_index_v2", "distributed_signoz_index_v3"},
|
||||
{"signoz_index_v2", "distributed_signoz_index_v3"},
|
||||
{"distributed_signoz_error_index_v2", "distributed_signoz_index_v3"},
|
||||
{"signoz_error_index_v2", "distributed_signoz_index_v3"},
|
||||
{"distributed_dependency_graph_minutes_v2", ""},
|
||||
{"dependency_graph_minutes_v2", ""},
|
||||
{"distributed_signoz_operations", "distributed_top_level_operations"},
|
||||
{"signoz_operations", "distributed_top_level_operations"},
|
||||
{"distributed_durationSort", "distributed_signoz_index_v3"},
|
||||
{"durationSort", "distributed_signoz_index_v3"},
|
||||
{"distributed_usage_explorer", ""},
|
||||
{"usage_explorer", ""},
|
||||
{"distributed_signoz_spans", ""},
|
||||
{"signoz_spans", ""},
|
||||
// Logs V1 → V2
|
||||
{"distributed_logs", "distributed_logs_v2"},
|
||||
{"logs", "distributed_logs_v2"},
|
||||
}),
|
||||
buildLocalChecks([]struct{ name, replacement string }{
|
||||
// Traces
|
||||
{"signoz_index_v3", "distributed_signoz_index_v3"},
|
||||
{"tag_attributes_v2", "distributed_tag_attributes_v2"},
|
||||
// Logs
|
||||
{"logs_v2", "distributed_logs_v2"},
|
||||
{"logs_v2_resource", "distributed_logs_v2_resource"},
|
||||
// Metrics
|
||||
{"samples_v4", "distributed_samples_v4"},
|
||||
{"samples_v4_agg_5m", "distributed_samples_v4_agg_5m"},
|
||||
{"samples_v4_agg_30m", "distributed_samples_v4_agg_30m"},
|
||||
{"exp_hist", "distributed_exp_hist"},
|
||||
{"time_series_v4", "distributed_time_series_v4"},
|
||||
{"time_series_v4_6hrs", "distributed_time_series_v4_6hrs"},
|
||||
{"time_series_v4_1day", "distributed_time_series_v4_1day"},
|
||||
{"time_series_v4_1week", "distributed_time_series_v4_1week"},
|
||||
{"updated_metadata", "distributed_updated_metadata"},
|
||||
{"metadata", "distributed_metadata"},
|
||||
// Meter
|
||||
{"samples", "distributed_samples"},
|
||||
{"samples_agg_1d", "distributed_samples_agg_1d"},
|
||||
// Metadata
|
||||
{"attributes_metadata", "distributed_attributes_metadata"},
|
||||
})...,
|
||||
)
|
||||
|
||||
type ClickHouseQuery struct {
|
||||
// name of the query
|
||||
Name string `json:"name"`
|
||||
@@ -15,3 +107,30 @@ type ClickHouseQuery struct {
|
||||
func (q ClickHouseQuery) Copy() ClickHouseQuery {
|
||||
return q
|
||||
}
|
||||
|
||||
// Validate performs basic validation on ClickHouseQuery.
|
||||
func (q ClickHouseQuery) Validate() error {
|
||||
if q.Name == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"name is required for ClickHouse query",
|
||||
)
|
||||
}
|
||||
|
||||
if q.Query == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"ClickHouse SQL query is required",
|
||||
)
|
||||
}
|
||||
|
||||
trimmed := strings.TrimSpace(q.Query)
|
||||
|
||||
for _, check := range chTableChecks {
|
||||
if check.pattern.MatchString(trimmed) {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "%s", check.errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package querybuildertypesv5
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClickHouseQuery_Copy(t *testing.T) {
|
||||
q := ClickHouseQuery{Name: "A", Query: "SELECT 1", Disabled: true, Legend: "my legend"}
|
||||
got := q.Copy()
|
||||
if got != q {
|
||||
t.Errorf("Copy() = %+v, want %+v", got, q)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClickHouseQuery_Validate_DeprecatedTables covers every deprecated table entry,
|
||||
// verifying both rejection and the correct error message (with or without a replacement hint).
|
||||
func TestClickHouseQuery_Validate_DeprecatedTables(t *testing.T) {
|
||||
tests := []struct {
|
||||
table string
|
||||
query string
|
||||
wantErrMsg string // substring expected in error
|
||||
}{
|
||||
// Traces V2 → V3 (distributed)
|
||||
{
|
||||
"distributed_signoz_index_v2",
|
||||
"SELECT * FROM distributed_signoz_index_v2 LIMIT 10",
|
||||
`use "distributed_signoz_index_v3"`,
|
||||
},
|
||||
{
|
||||
"distributed_signoz_error_index_v2",
|
||||
"SELECT * FROM distributed_signoz_error_index_v2",
|
||||
`use "distributed_signoz_index_v3"`,
|
||||
},
|
||||
{
|
||||
"distributed_dependency_graph_minutes_v2",
|
||||
"SELECT * FROM distributed_dependency_graph_minutes_v2",
|
||||
`deprecated table "distributed_dependency_graph_minutes_v2"`,
|
||||
},
|
||||
{
|
||||
"distributed_signoz_operations",
|
||||
"SELECT * FROM distributed_signoz_operations",
|
||||
`use "distributed_top_level_operations"`,
|
||||
},
|
||||
{
|
||||
"distributed_durationSort",
|
||||
"SELECT * FROM distributed_durationSort",
|
||||
`use "distributed_signoz_index_v3"`,
|
||||
},
|
||||
{
|
||||
"distributed_usage_explorer",
|
||||
"SELECT * FROM distributed_usage_explorer",
|
||||
`deprecated table "distributed_usage_explorer"`,
|
||||
},
|
||||
{
|
||||
"distributed_signoz_spans",
|
||||
"SELECT * FROM distributed_signoz_spans",
|
||||
`deprecated table "distributed_signoz_spans"`,
|
||||
},
|
||||
// Traces V2 → V3 (local)
|
||||
{
|
||||
"signoz_index_v2",
|
||||
"SELECT * FROM signoz_index_v2",
|
||||
`use "distributed_signoz_index_v3"`,
|
||||
},
|
||||
{
|
||||
"signoz_error_index_v2",
|
||||
"SELECT * FROM signoz_error_index_v2",
|
||||
`use "distributed_signoz_index_v3"`,
|
||||
},
|
||||
{
|
||||
"dependency_graph_minutes_v2",
|
||||
"SELECT * FROM dependency_graph_minutes_v2",
|
||||
`deprecated table "dependency_graph_minutes_v2"`,
|
||||
},
|
||||
{
|
||||
"signoz_operations",
|
||||
"SELECT * FROM signoz_operations",
|
||||
`use "distributed_top_level_operations"`,
|
||||
},
|
||||
{
|
||||
"durationSort",
|
||||
"SELECT * FROM durationSort",
|
||||
`use "distributed_signoz_index_v3"`,
|
||||
},
|
||||
{
|
||||
"usage_explorer",
|
||||
"SELECT * FROM usage_explorer",
|
||||
`deprecated table "usage_explorer"`,
|
||||
},
|
||||
{
|
||||
"signoz_spans",
|
||||
"SELECT * FROM signoz_spans LIMIT 10",
|
||||
`deprecated table "signoz_spans"`,
|
||||
},
|
||||
// Logs V1 → V2
|
||||
{
|
||||
"distributed_logs",
|
||||
"SELECT * FROM signoz_logs.distributed_logs WHERE timestamp > now() - INTERVAL 1 HOUR",
|
||||
`use "distributed_logs_v2"`,
|
||||
},
|
||||
{
|
||||
"logs",
|
||||
"SELECT body FROM logs LIMIT 100",
|
||||
`use "distributed_logs_v2"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.table, func(t *testing.T) {
|
||||
q := ClickHouseQuery{Name: "A", Query: tt.query}
|
||||
err := q.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("Validate() expected error for deprecated table %q but got none", tt.table)
|
||||
}
|
||||
if !contains(err.Error(), tt.wantErrMsg) {
|
||||
t.Errorf("Validate() error = %q, want to contain %q", err.Error(), tt.wantErrMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClickHouseQuery_Validate_LocalTables covers every local-table entry,
|
||||
// verifying rejection and the correct "use distributed table X instead" message.
|
||||
func TestClickHouseQuery_Validate_LocalTables(t *testing.T) {
|
||||
tests := []struct {
|
||||
table string
|
||||
query string
|
||||
dist string // expected distributed replacement in error
|
||||
}{
|
||||
// Traces
|
||||
{"signoz_index_v3", "SELECT * FROM signoz_index_v3", "distributed_signoz_index_v3"},
|
||||
{"tag_attributes_v2", "SELECT * FROM tag_attributes_v2", "distributed_tag_attributes_v2"},
|
||||
// Logs
|
||||
{"logs_v2", "SELECT body FROM logs_v2 LIMIT 50", "distributed_logs_v2"},
|
||||
{"logs_v2_resource", "SELECT * FROM logs_v2_resource", "distributed_logs_v2_resource"},
|
||||
// Metrics
|
||||
{"samples_v4", "SELECT * FROM samples_v4 WHERE unix_milli >= 1000", "distributed_samples_v4"},
|
||||
{"samples_v4_agg_5m", "SELECT * FROM samples_v4_agg_5m", "distributed_samples_v4_agg_5m"},
|
||||
{"samples_v4_agg_30m", "SELECT * FROM samples_v4_agg_30m", "distributed_samples_v4_agg_30m"},
|
||||
{"exp_hist", "SELECT * FROM exp_hist", "distributed_exp_hist"},
|
||||
{"time_series_v4", "SELECT * FROM time_series_v4", "distributed_time_series_v4"},
|
||||
{"time_series_v4_6hrs", "SELECT * FROM time_series_v4_6hrs", "distributed_time_series_v4_6hrs"},
|
||||
{"time_series_v4_1day", "SELECT * FROM time_series_v4_1day", "distributed_time_series_v4_1day"},
|
||||
{"time_series_v4_1week", "SELECT * FROM time_series_v4_1week", "distributed_time_series_v4_1week"},
|
||||
{"updated_metadata", "SELECT * FROM updated_metadata", "distributed_updated_metadata"},
|
||||
{"metadata", "SELECT * FROM metadata", "distributed_metadata"},
|
||||
// Meter
|
||||
{"samples", "SELECT * FROM samples WHERE unix_milli >= 1000", "distributed_samples"},
|
||||
{"samples_agg_1d", "SELECT * FROM samples_agg_1d", "distributed_samples_agg_1d"},
|
||||
// Metadata
|
||||
{"attributes_metadata", "SELECT * FROM attributes_metadata", "distributed_attributes_metadata"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.table, func(t *testing.T) {
|
||||
q := ClickHouseQuery{Name: "A", Query: tt.query}
|
||||
err := q.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("Validate() expected error for local table %q but got none", tt.table)
|
||||
}
|
||||
wantFragment := `use distributed table "` + tt.dist + `"`
|
||||
if !contains(err.Error(), wantFragment) {
|
||||
t.Errorf("Validate() error = %q, want to contain %q", err.Error(), wantFragment)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClickHouseQuery_Validate_AllowedTables verifies that current-generation
|
||||
// distributed tables are accepted without error.
|
||||
func TestClickHouseQuery_Validate_AllowedTables(t *testing.T) {
|
||||
queries := []string{
|
||||
"SELECT * FROM distributed_signoz_index_v3",
|
||||
"SELECT * FROM distributed_tag_attributes_v2",
|
||||
"SELECT * FROM distributed_top_level_operations",
|
||||
"SELECT * FROM distributed_logs_v2",
|
||||
"SELECT * FROM distributed_logs_v2_resource",
|
||||
"SELECT * FROM distributed_samples_v4",
|
||||
"SELECT * FROM distributed_samples_v4_agg_5m",
|
||||
"SELECT * FROM distributed_samples_v4_agg_30m",
|
||||
"SELECT * FROM distributed_exp_hist",
|
||||
"SELECT * FROM distributed_time_series_v4",
|
||||
"SELECT * FROM distributed_time_series_v4_6hrs",
|
||||
"SELECT * FROM distributed_time_series_v4_1day",
|
||||
"SELECT * FROM distributed_time_series_v4_1week",
|
||||
"SELECT * FROM distributed_updated_metadata",
|
||||
"SELECT * FROM distributed_metadata",
|
||||
"SELECT * FROM distributed_samples",
|
||||
"SELECT * FROM distributed_samples_agg_1d",
|
||||
"SELECT * FROM distributed_attributes_metadata",
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
t.Run(q, func(t *testing.T) {
|
||||
err := (ClickHouseQuery{Name: "A", Query: q}).Validate()
|
||||
if err != nil {
|
||||
t.Errorf("Validate() unexpected error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClickHouseQuery_Validate_WordBoundary ensures that table name patterns
|
||||
// do not produce false positives when a deprecated/local name is a prefix of
|
||||
// a longer allowed name.
|
||||
func TestClickHouseQuery_Validate_WordBoundary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
wantErr bool
|
||||
}{
|
||||
// "logs" must not match "distributed_logs_v2" or "logs_v2"
|
||||
{"distributed_logs_v2 not flagged by 'logs' pattern", "SELECT * FROM distributed_logs_v2", false},
|
||||
// "distributed_logs" must not match "distributed_logs_v2"
|
||||
{"distributed_logs_v2 not flagged by 'distributed_logs' pattern", "SELECT * FROM signoz_logs.distributed_logs_v2", false},
|
||||
// "metadata" must not match "attributes_metadata" or "distributed_attributes_metadata"
|
||||
{"distributed_attributes_metadata not flagged by 'metadata' pattern", "SELECT * FROM distributed_attributes_metadata", false},
|
||||
{"attributes_metadata flagged by its own pattern", "SELECT * FROM attributes_metadata", true},
|
||||
// "samples" must not match "samples_v4" (caught by samples_v4 entry instead)
|
||||
{"distributed_samples_v4 not flagged by 'samples' pattern", "SELECT * FROM distributed_samples_v4", false},
|
||||
// "time_series_v4" must not match "time_series_v4_6hrs"
|
||||
{"distributed_time_series_v4_6hrs not flagged by 'time_series_v4' pattern", "SELECT * FROM distributed_time_series_v4_6hrs", false},
|
||||
// "signoz_index_v2" must not match "distributed_signoz_index_v2" (separate entry)
|
||||
{"distributed_signoz_index_v2 flagged by its own pattern", "SELECT * FROM distributed_signoz_index_v2", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := (ClickHouseQuery{Name: "A", Query: tt.query}).Validate()
|
||||
if tt.wantErr && err == nil {
|
||||
t.Errorf("Validate() expected error but got none")
|
||||
} else if !tt.wantErr && err != nil {
|
||||
t.Errorf("Validate() unexpected error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestClickHouseQuery_Validate_CaseInsensitive verifies that table pattern
|
||||
// matching is case-insensitive.
|
||||
func TestClickHouseQuery_Validate_CaseInsensitive(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
}{
|
||||
{"deprecated table uppercase", "SELECT * FROM DISTRIBUTED_SIGNOZ_INDEX_V2"},
|
||||
{"deprecated table mixed case", "SELECT * FROM Distributed_SignoZ_Index_V2"},
|
||||
{"local table uppercase", "SELECT * FROM SAMPLES_V4"},
|
||||
{"local table mixed case", "SELECT * FROM Time_Series_V4"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := (ClickHouseQuery{Name: "A", Query: tt.query}).Validate()
|
||||
if err == nil {
|
||||
t.Errorf("Validate() expected error for %q but got none", tt.query)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -608,13 +608,7 @@ func validateQueryEnvelope(envelope QueryEnvelope, requestType RequestType) erro
|
||||
"invalid ClickHouse SQL spec",
|
||||
)
|
||||
}
|
||||
if spec.Query == "" {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"ClickHouse SQL query is required",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
return spec.Validate()
|
||||
default:
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
|
||||
@@ -279,7 +279,7 @@ func TestQueryRangeRequest_ValidateAllQueriesNotDisabled(t *testing.T) {
|
||||
Type: QueryTypeClickHouseSQL,
|
||||
Spec: ClickHouseQuery{
|
||||
Name: "CH1",
|
||||
Query: "SELECT count() FROM logs",
|
||||
Query: "SELECT count() FROM distributed_logs_v2",
|
||||
Disabled: true,
|
||||
},
|
||||
},
|
||||
@@ -559,7 +559,7 @@ func TestQueryRangeRequest_ValidateCompositeQuery(t *testing.T) {
|
||||
Type: QueryTypeClickHouseSQL,
|
||||
Spec: ClickHouseQuery{
|
||||
Name: "CH1",
|
||||
Query: "SELECT count() FROM logs",
|
||||
Query: "SELECT count() FROM distributed_logs_v2",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1150,6 +1150,22 @@ func TestRequestType_IsAggregation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClickHouseQuery_Validate(t *testing.T) {
|
||||
t.Run("empty name", func(t *testing.T) {
|
||||
err := (ClickHouseQuery{Name: "", Query: "SELECT 1"}).Validate()
|
||||
if err == nil || !contains(err.Error(), "name is required") {
|
||||
t.Errorf("Validate() expected 'name is required' error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty query", func(t *testing.T) {
|
||||
err := (ClickHouseQuery{Name: "A", Query: ""}).Validate()
|
||||
if err == nil || !contains(err.Error(), "ClickHouse SQL query is required") {
|
||||
t.Errorf("Validate() expected 'ClickHouse SQL query is required' error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonAggregationFieldsSkipped(t *testing.T) {
|
||||
// Fields that only apply to aggregation queries (groupBy, having, aggregations)
|
||||
// should be silently skipped for non-aggregation request types.
|
||||
|
||||
84
tests/integration/src/querier/12_clickhouse_sql.py
Normal file
84
tests/integration/src/querier/12_clickhouse_sql.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from datetime import datetime, timezone
|
||||
from http import HTTPStatus
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import make_query_request
|
||||
|
||||
|
||||
def test_clickhouse_sql_valid_query(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
"""
|
||||
A valid ClickHouse SQL query referencing a distributed table is accepted.
|
||||
No data insertion required — we only verify the request is not rejected.
|
||||
"""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
end = int(datetime.now(tz=timezone.utc).timestamp() * 1000)
|
||||
start = end - 60_000
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start,
|
||||
end,
|
||||
[
|
||||
{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": "SELECT count() AS value FROM signoz_logs.distributed_logs_v2",
|
||||
},
|
||||
}
|
||||
],
|
||||
request_type="scalar",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"query, expected_error",
|
||||
[
|
||||
(
|
||||
"SELECT * FROM signoz_logs.distributed_logs LIMIT 10",
|
||||
"deprecated table",
|
||||
),
|
||||
(
|
||||
"SELECT count() AS value FROM signoz_logs.logs_v2",
|
||||
"local table",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_clickhouse_sql_rejected_tables(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
query: str,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""
|
||||
ClickHouse SQL queries referencing deprecated or local (non-distributed)
|
||||
tables are rejected with HTTP 400.
|
||||
"""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
end = int(datetime.now(tz=timezone.utc).timestamp() * 1000)
|
||||
start = end - 60_000
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start,
|
||||
end,
|
||||
[{"type": "clickhouse_sql", "spec": {"name": "A", "query": query}}],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST
|
||||
body = response.json()
|
||||
assert body["status"] == "error"
|
||||
assert expected_error in body["error"]["message"]
|
||||
Reference in New Issue
Block a user