mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-02 12:50:37 +01:00
Compare commits
4 Commits
main
...
fix/public
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b9c7b4003 | ||
|
|
55787df825 | ||
|
|
ab9f0ed67d | ||
|
|
33fbc28908 |
@@ -66,6 +66,7 @@ function WidgetGraphComponent({
|
||||
customOnRowClick,
|
||||
customTimeRangeWindowForCoRelation,
|
||||
enableDrillDown,
|
||||
hidePagination,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
@@ -430,6 +431,7 @@ function WidgetGraphComponent({
|
||||
customSeries={customSeries}
|
||||
customOnRowClick={customOnRowClick}
|
||||
enableDrillDown={enableDrillDown}
|
||||
hidePagination={hidePagination}
|
||||
onColumnWidthsChange={onColumnWidthsChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface WidgetGraphComponentProps {
|
||||
customOnRowClick?: (record: RowData) => void;
|
||||
customTimeRangeWindowForCoRelation?: string | undefined;
|
||||
enableDrillDown?: boolean;
|
||||
/** Hide list-panel pagination controls (e.g. public dashboards, where paging isn't supported). */
|
||||
hidePagination?: boolean;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
|
||||
@@ -34,6 +34,7 @@ function LogsPanelComponent({
|
||||
setRequestData,
|
||||
queryResponse,
|
||||
onColumnWidthsChange,
|
||||
hidePagination,
|
||||
}: LogsPanelComponentProps): JSX.Element {
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
const [offset, setOffset] = useState<number>(0);
|
||||
@@ -158,7 +159,7 @@ function LogsPanelComponent({
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
{!widget.query.builder.queryData[0].limit && (
|
||||
{!hidePagination && !widget.query.builder.queryData[0].limit && (
|
||||
<div className="controller">
|
||||
<Controls
|
||||
totalCount={totalCount}
|
||||
@@ -198,6 +199,7 @@ export type LogsPanelComponentProps = {
|
||||
>;
|
||||
widget: Widgets;
|
||||
onColumnWidthsChange?: (widths: Record<string, number>) => void;
|
||||
hidePagination?: boolean;
|
||||
};
|
||||
|
||||
export default LogsPanelComponent;
|
||||
|
||||
@@ -9,6 +9,7 @@ function ListPanelWrapper({
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
onColumnWidthsChange,
|
||||
hidePagination,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const dataSource = widget.query.builder?.queryData[0]?.dataSource;
|
||||
|
||||
@@ -23,6 +24,7 @@ function ListPanelWrapper({
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
onColumnWidthsChange={onColumnWidthsChange}
|
||||
hidePagination={hidePagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -32,6 +34,7 @@ function ListPanelWrapper({
|
||||
queryResponse={queryResponse}
|
||||
setRequestData={setRequestData}
|
||||
onColumnWidthsChange={onColumnWidthsChange}
|
||||
hidePagination={hidePagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ function PanelWrapper({
|
||||
panelMode,
|
||||
enableDrillDown = false,
|
||||
onColumnWidthsChange,
|
||||
hidePagination,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const Component = PanelTypeVsPanelWrapper[
|
||||
selectedGraph || widget.panelTypes
|
||||
@@ -76,6 +77,7 @@ function PanelWrapper({
|
||||
enableDrillDown={enableDrillDown}
|
||||
onColumnWidthsChange={onColumnWidthsChange}
|
||||
groupByPerQuery={groupByPerQuery}
|
||||
hidePagination={hidePagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export type PanelWrapperProps = {
|
||||
panelMode: PanelMode;
|
||||
onColumnWidthsChange?: (widths: Record<string, number>) => void;
|
||||
groupByPerQuery?: Record<string, BaseAutocompleteData[]>;
|
||||
hidePagination?: boolean;
|
||||
};
|
||||
|
||||
export type TooltipData = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import EmptyWidget from 'container/GridCardLayout/EmptyWidget';
|
||||
@@ -6,11 +6,12 @@ import WidgetGraphComponent from 'container/GridCardLayout/GridCard/WidgetGraphC
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
|
||||
import { getPublicPanelRequestData } from './utils';
|
||||
|
||||
function Panel({
|
||||
widget,
|
||||
index,
|
||||
@@ -27,65 +28,36 @@ function Panel({
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const updatedQuery = widget?.query;
|
||||
|
||||
const requestData: GetQueryResultsProps = useMemo(() => {
|
||||
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||
return {
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: getGraphType(widget.panelTypes),
|
||||
// State (not memo) so LIST panels get a setRequestData — ListPanelWrapper
|
||||
// renders nothing without one.
|
||||
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() =>
|
||||
getPublicPanelRequestData({ widget, startTime, endTime }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(updatedQuery, requestData.query)) {
|
||||
setRequestData((prev) => ({
|
||||
...prev,
|
||||
query: updatedQuery,
|
||||
variables: {}, // we are not supporting variables in public dashboards
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
originalGraphType: widget.panelTypes,
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
|
||||
const updatedQueryForList = {
|
||||
...updatedQuery,
|
||||
builder: {
|
||||
...updatedQuery.builder,
|
||||
queryData: updatedQuery.builder.queryData.map((qd, i) =>
|
||||
i === 0 ? { ...qd, pageSize: 10 } : qd,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
query: updatedQueryForList,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
// we do not need select columns in case of logs
|
||||
selectColumns:
|
||||
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
};
|
||||
}, [widget, updatedQuery, startTime, endTime]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedQuery]);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
...requestData,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
originalGraphType: widget?.panelTypes,
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
{
|
||||
queryKey: [
|
||||
widget?.query,
|
||||
widget?.panelTypes,
|
||||
requestData,
|
||||
startTime,
|
||||
endTime,
|
||||
],
|
||||
// Public data is fetched by index and the payload redacts each widget's
|
||||
// filters, so query bodies are identical across panels. Key on panel
|
||||
// identity + time — the only inputs that determine the response — so
|
||||
// panels don't collapse onto one cache entry.
|
||||
queryKey: [widget?.id, index, startTime, endTime],
|
||||
retry(failureCount, error): boolean {
|
||||
if (
|
||||
String(error).includes('status: error') &&
|
||||
@@ -142,6 +114,8 @@ function Panel({
|
||||
headerMenuList={[]}
|
||||
isWarning={false}
|
||||
isFetchingResponse={queryResponse.isFetching || queryResponse.isLoading}
|
||||
setRequestData={setRequestData}
|
||||
hidePagination
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import Panel from '../Panel';
|
||||
|
||||
const useGetQueryRangeMock = jest.fn();
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetQueryRange', () => ({
|
||||
useGetQueryRange: (...args: unknown[]): unknown => {
|
||||
useGetQueryRangeMock(...args);
|
||||
return {
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
const widgetGraphProps = jest.fn();
|
||||
|
||||
jest.mock('container/GridCardLayout/GridCard/WidgetGraphComponent', () => ({
|
||||
__esModule: true,
|
||||
default: (props: { setRequestData?: unknown }): JSX.Element => {
|
||||
widgetGraphProps(props);
|
||||
return <div data-testid="widget-graph" />;
|
||||
},
|
||||
}));
|
||||
|
||||
const buildWidget = (id: string): Widgets =>
|
||||
({
|
||||
id,
|
||||
panelTypes: PANEL_TYPES.LIST,
|
||||
query: {
|
||||
builder: {
|
||||
queryData: [{ dataSource: 'logs', limit: 100, orderBy: [] }],
|
||||
},
|
||||
},
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
}) as unknown as Widgets;
|
||||
|
||||
describe('Public dashboard Panel', () => {
|
||||
beforeEach(() => {
|
||||
useGetQueryRangeMock.mockClear();
|
||||
widgetGraphProps.mockClear();
|
||||
});
|
||||
|
||||
it('forwards a setRequestData setter so LIST panels render (bug 3646)', () => {
|
||||
render(
|
||||
<Panel
|
||||
widget={buildWidget('widget-a')}
|
||||
index={0}
|
||||
dashboardId="dash-1"
|
||||
startTime={100}
|
||||
endTime={200}
|
||||
/>,
|
||||
);
|
||||
|
||||
const props = widgetGraphProps.mock.calls[0][0];
|
||||
expect(typeof props.setRequestData).toBe('function');
|
||||
});
|
||||
|
||||
it('keys each panel by widget id + index so identical queries do not collide (bug 5503)', () => {
|
||||
render(
|
||||
<>
|
||||
<Panel
|
||||
widget={buildWidget('widget-a')}
|
||||
index={2}
|
||||
dashboardId="dash-1"
|
||||
startTime={100}
|
||||
endTime={200}
|
||||
/>
|
||||
<Panel
|
||||
widget={buildWidget('widget-b')}
|
||||
index={62}
|
||||
dashboardId="dash-1"
|
||||
startTime={100}
|
||||
endTime={200}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
|
||||
const [callA, callB] = useGetQueryRangeMock.mock.calls;
|
||||
const queryKeyA = callA[2].queryKey;
|
||||
const metaA = callA[4];
|
||||
const queryKeyB = callB[2].queryKey;
|
||||
const metaB = callB[4];
|
||||
|
||||
// Key is panel identity + time only — the redacted query body is not part
|
||||
// of it, so identical query bodies can't collapse two panels onto one key.
|
||||
expect(queryKeyA).toStrictEqual(['widget-a', 2, 100, 200]);
|
||||
expect(queryKeyB).toStrictEqual(['widget-b', 62, 100, 200]);
|
||||
expect(queryKeyA).not.toStrictEqual(queryKeyB);
|
||||
|
||||
expect(metaA.widgetIndex).toBe(2);
|
||||
expect(metaB.widgetIndex).toBe(62);
|
||||
});
|
||||
});
|
||||
53
frontend/src/container/PublicDashboardContainer/utils.ts
Normal file
53
frontend/src/container/PublicDashboardContainer/utils.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
|
||||
// Builds the useGetQueryRange payload for a public-dashboard panel, mirroring the
|
||||
// authenticated GridCard.
|
||||
export const getPublicPanelRequestData = ({
|
||||
widget,
|
||||
startTime,
|
||||
endTime,
|
||||
}: {
|
||||
widget: Widgets;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}): GetQueryResultsProps => {
|
||||
const updatedQuery = widget?.query;
|
||||
|
||||
if (widget.panelTypes !== PANEL_TYPES.LIST) {
|
||||
return {
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: getGraphType(widget.panelTypes),
|
||||
query: updatedQuery,
|
||||
variables: {}, // we are not supporting variables in public dashboards
|
||||
fillGaps: widget.fillSpans,
|
||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
originalGraphType: widget.panelTypes,
|
||||
};
|
||||
}
|
||||
|
||||
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
|
||||
|
||||
return {
|
||||
query: updatedQuery,
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
|
||||
tableParams: {
|
||||
pagination: {
|
||||
offset: 0,
|
||||
limit: updatedQuery.builder.queryData[0].limit || 0,
|
||||
},
|
||||
// we do not need select columns in case of logs
|
||||
selectColumns:
|
||||
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
|
||||
},
|
||||
fillGaps: widget.fillSpans,
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
};
|
||||
};
|
||||
@@ -37,6 +37,7 @@ function TracesTableComponent({
|
||||
queryResponse,
|
||||
setRequestData,
|
||||
onColumnWidthsChange,
|
||||
hidePagination,
|
||||
}: TracesTableComponentProps): JSX.Element {
|
||||
const [pagination, setPagination] = useState<Pagination>({
|
||||
offset: 0,
|
||||
@@ -139,34 +140,36 @@ function TracesTableComponent({
|
||||
/>
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
<div className="controller">
|
||||
<Controls
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
isLoading={queryResponse.isFetching}
|
||||
offset={pagination.offset}
|
||||
countPerPage={pagination.limit}
|
||||
handleNavigatePrevious={(): void => {
|
||||
handlePaginationChange({
|
||||
...pagination,
|
||||
offset: pagination.offset - pagination.limit,
|
||||
});
|
||||
}}
|
||||
handleNavigateNext={(): void => {
|
||||
handlePaginationChange({
|
||||
...pagination,
|
||||
offset: pagination.offset + pagination.limit,
|
||||
});
|
||||
}}
|
||||
handleCountItemsPerPageChange={(value): void => {
|
||||
handlePaginationChange({
|
||||
...pagination,
|
||||
limit: value,
|
||||
offset: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!hidePagination && (
|
||||
<div className="controller">
|
||||
<Controls
|
||||
totalCount={totalCount}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
isLoading={queryResponse.isFetching}
|
||||
offset={pagination.offset}
|
||||
countPerPage={pagination.limit}
|
||||
handleNavigatePrevious={(): void => {
|
||||
handlePaginationChange({
|
||||
...pagination,
|
||||
offset: pagination.offset - pagination.limit,
|
||||
});
|
||||
}}
|
||||
handleNavigateNext={(): void => {
|
||||
handlePaginationChange({
|
||||
...pagination,
|
||||
offset: pagination.offset + pagination.limit,
|
||||
});
|
||||
}}
|
||||
handleCountItemsPerPageChange={(value): void => {
|
||||
handlePaginationChange({
|
||||
...pagination,
|
||||
limit: value,
|
||||
offset: 0,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -178,6 +181,7 @@ export type TracesTableComponentProps = {
|
||||
>;
|
||||
widget: Widgets;
|
||||
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
|
||||
hidePagination?: boolean;
|
||||
onColumnWidthsChange?: (widths: Record<string, number>) => void;
|
||||
};
|
||||
|
||||
|
||||
30
frontend/src/hooks/__tests__/useLogsData.test.tsx
Normal file
30
frontend/src/hooks/__tests__/useLogsData.test.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { AllTheProviders } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { useLogsData } from '../useLogsData';
|
||||
|
||||
describe('useLogsData', () => {
|
||||
// Public dashboards redact the widget query (orderBy/filter/limit stripped),
|
||||
// so a LIST query can arrive with no orderBy — the hook must not crash on it.
|
||||
it('does not crash when the query has no orderBy', () => {
|
||||
const stagedQuery = {
|
||||
builder: {
|
||||
queryData: [{ dataSource: 'logs', queryName: 'A', disabled: false }],
|
||||
},
|
||||
} as unknown as Query;
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useLogsData({
|
||||
result: undefined,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
stagedQuery,
|
||||
}),
|
||||
{ wrapper: AllTheProviders },
|
||||
);
|
||||
|
||||
expect(result.current.logs).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -71,7 +71,7 @@ export const useLogsData = ({
|
||||
}, [logs.length, listQuery]);
|
||||
|
||||
const orderByTimestamp: OrderByPayload | null = useMemo(() => {
|
||||
const timestampOrderBy = listQuery?.orderBy.find(
|
||||
const timestampOrderBy = listQuery?.orderBy?.find(
|
||||
(item) => item.columnName === 'timestamp',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user