mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-12 08:13:19 +00:00
Compare commits
1 Commits
platform-p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
542a648cc3 |
@@ -199,8 +199,6 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
setLayouts: jest.fn(),
|
||||
setSelectedDashboard: jest.fn(),
|
||||
updatedTimeRef: { current: null },
|
||||
toScrollWidgetId: '',
|
||||
setToScrollWidgetId: jest.fn(),
|
||||
updateLocalStorageDashboardVariables: jest.fn(),
|
||||
dashboardQueryRangeCalled: false,
|
||||
setDashboardQueryRangeCalled: jest.fn(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
||||
|
||||
import { useScrollWidgetIntoView } from '../useScrollWidgetIntoView';
|
||||
|
||||
jest.mock('providers/Dashboard/Dashboard');
|
||||
jest.mock('providers/Dashboard/helpers/scrollToWidgetIdHelper');
|
||||
|
||||
type MockHTMLElement = {
|
||||
scrollIntoView: jest.Mock;
|
||||
@@ -18,25 +18,35 @@ function createMockElement(): MockHTMLElement {
|
||||
}
|
||||
|
||||
describe('useScrollWidgetIntoView', () => {
|
||||
const mockedUseDashboard = useDashboard as jest.MockedFunction<
|
||||
typeof useDashboard
|
||||
const mockedUseScrollToWidgetIdStore = useScrollToWidgetIdStore as jest.MockedFunction<
|
||||
typeof useScrollToWidgetIdStore
|
||||
>;
|
||||
|
||||
let mockElement: MockHTMLElement;
|
||||
let ref: React.RefObject<HTMLDivElement>;
|
||||
let setToScrollWidgetId: jest.Mock;
|
||||
|
||||
function mockStore(toScrollWidgetId: string): void {
|
||||
const storeState = { toScrollWidgetId, setToScrollWidgetId };
|
||||
mockedUseScrollToWidgetIdStore.mockImplementation(
|
||||
(selector) =>
|
||||
selector(
|
||||
(storeState as unknown) as Parameters<typeof selector>[0],
|
||||
) as ReturnType<typeof useScrollToWidgetIdStore>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockElement = createMockElement();
|
||||
ref = ({
|
||||
current: mockElement,
|
||||
} as unknown) as React.RefObject<HTMLDivElement>;
|
||||
setToScrollWidgetId = jest.fn();
|
||||
});
|
||||
|
||||
it('scrolls into view and focuses when toScrollWidgetId matches widget id', () => {
|
||||
const setToScrollWidgetId = jest.fn();
|
||||
const mockElement = createMockElement();
|
||||
const ref = ({
|
||||
current: mockElement,
|
||||
} as unknown) as React.RefObject<HTMLDivElement>;
|
||||
|
||||
mockedUseDashboard.mockReturnValue(({
|
||||
toScrollWidgetId: 'widget-id',
|
||||
setToScrollWidgetId,
|
||||
} as unknown) as ReturnType<typeof useDashboard>);
|
||||
mockStore('widget-id');
|
||||
|
||||
renderHook(() => useScrollWidgetIntoView('widget-id', ref));
|
||||
|
||||
@@ -49,16 +59,7 @@ describe('useScrollWidgetIntoView', () => {
|
||||
});
|
||||
|
||||
it('does nothing when toScrollWidgetId does not match widget id', () => {
|
||||
const setToScrollWidgetId = jest.fn();
|
||||
const mockElement = createMockElement();
|
||||
const ref = ({
|
||||
current: mockElement,
|
||||
} as unknown) as React.RefObject<HTMLDivElement>;
|
||||
|
||||
mockedUseDashboard.mockReturnValue(({
|
||||
toScrollWidgetId: 'other-widget',
|
||||
setToScrollWidgetId,
|
||||
} as unknown) as ReturnType<typeof useDashboard>);
|
||||
mockStore('other-widget');
|
||||
|
||||
renderHook(() => useScrollWidgetIntoView('widget-id', ref));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RefObject, useEffect } from 'react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
||||
|
||||
/**
|
||||
* Scrolls the given widget container into view when the dashboard
|
||||
@@ -11,7 +11,10 @@ export function useScrollWidgetIntoView<T extends HTMLElement>(
|
||||
widgetId: string,
|
||||
widgetContainerRef: RefObject<T>,
|
||||
): void {
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const toScrollWidgetId = useScrollToWidgetIdStore((s) => s.toScrollWidgetId);
|
||||
const setToScrollWidgetId = useScrollToWidgetIdStore(
|
||||
(s) => s.setToScrollWidgetId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widgetId) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -34,8 +33,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useScrollWidgetIntoView(widget.id, graphRef);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -32,8 +31,6 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useScrollWidgetIntoView(widget.id, graphRef);
|
||||
|
||||
const config = useMemo(() => {
|
||||
return prepareHistogramPanelConfig({
|
||||
widget,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import ChartManager from 'container/DashboardContainer/visualization/components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
|
||||
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -33,8 +32,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
useScrollWidgetIntoView(widget.id, graphRef);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsPanelWaitingOnVariable } from 'hooks/dashboard/useVariableFetchState';
|
||||
@@ -67,11 +68,7 @@ function GridCardGraph({
|
||||
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const {
|
||||
toScrollWidgetId,
|
||||
setToScrollWidgetId,
|
||||
setDashboardQueryRangeCalled,
|
||||
} = useDashboard();
|
||||
const { setDashboardQueryRangeCalled } = useDashboard();
|
||||
|
||||
const {
|
||||
minTime,
|
||||
@@ -109,20 +106,11 @@ function GridCardGraph({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const widgetContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isVisible = useIntersectionObserver(graphRef, undefined, true);
|
||||
const isVisible = useIntersectionObserver(widgetContainerRef, undefined, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widget.id) {
|
||||
graphRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
graphRef.current?.focus();
|
||||
setToScrollWidgetId('');
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
useScrollWidgetIntoView(widget?.id || '', widgetContainerRef);
|
||||
|
||||
const updatedQuery = widget?.query;
|
||||
|
||||
@@ -306,7 +294,7 @@ function GridCardGraph({
|
||||
: headerMenuList;
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<div style={{ height: '100%', width: '100%' }} ref={widgetContainerRef}>
|
||||
{isEmptyLayout ? (
|
||||
<EmptyWidget />
|
||||
) : (
|
||||
|
||||
@@ -34,6 +34,7 @@ import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
||||
import {
|
||||
clearSelectedRowWidgetId,
|
||||
getSelectedRowWidgetId,
|
||||
@@ -86,10 +87,12 @@ function NewWidget({
|
||||
enableDrillDown = false,
|
||||
}: NewWidgetProps): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const setToScrollWidgetId = useScrollToWidgetIdStore(
|
||||
(s) => s.setToScrollWidgetId,
|
||||
);
|
||||
const {
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
setToScrollWidgetId,
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Uplot from 'components/Uplot';
|
||||
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
|
||||
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUplotHistogramChartOptions } from 'lib/uPlotLib/getUplotHistogramChartOptions';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
|
||||
import { buildHistogramData } from './histogram';
|
||||
import { PanelWrapperProps } from './panelWrapper.types';
|
||||
|
||||
function HistogramPanelWrapper({
|
||||
queryResponse,
|
||||
widget,
|
||||
setGraphVisibility,
|
||||
graphVisibility,
|
||||
isFullViewMode,
|
||||
onToggleModelHandler,
|
||||
onClickHandler,
|
||||
enableDrillDown = false,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const legendScrollPositionRef = useRef<number>(0);
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
const {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
clickedData,
|
||||
onClose,
|
||||
onClick,
|
||||
subMenu,
|
||||
setSubMenu,
|
||||
} = useCoordinates();
|
||||
const { menuItemsConfig } = useGraphContextMenu({
|
||||
widgetId: widget.id || '',
|
||||
query: widget.query,
|
||||
graphData: clickedData,
|
||||
onClose,
|
||||
coordinates,
|
||||
subMenu,
|
||||
setSubMenu,
|
||||
contextLinks: widget.contextLinks,
|
||||
panelType: widget.panelTypes,
|
||||
queryRange: queryResponse,
|
||||
});
|
||||
|
||||
const clickHandlerWithContextMenu = useCallback(
|
||||
(...args: any[]) => {
|
||||
const [
|
||||
,
|
||||
,
|
||||
,
|
||||
,
|
||||
metric,
|
||||
queryData,
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
,
|
||||
focusedSeries,
|
||||
] = args;
|
||||
const data = getUplotClickData({
|
||||
metric,
|
||||
queryData,
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
focusedSeries,
|
||||
});
|
||||
if (data && data?.record?.queryName) {
|
||||
onClick(data.coord, { ...data.record, label: data.label });
|
||||
}
|
||||
},
|
||||
[onClick],
|
||||
);
|
||||
|
||||
const histogramData = buildHistogramData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
widget?.bucketWidth,
|
||||
widget?.bucketCount,
|
||||
widget?.mergeAllActiveQueries,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widget.id) {
|
||||
graphRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
graphRef.current?.focus();
|
||||
setToScrollWidgetId('');
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data?.payload.data.result || [],
|
||||
name: widget.id,
|
||||
});
|
||||
if (setGraphVisibility) {
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [
|
||||
queryResponse?.data?.payload?.data?.result,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
]);
|
||||
|
||||
const histogramOptions = useMemo(
|
||||
() =>
|
||||
getUplotHistogramChartOptions({
|
||||
id: widget.id,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
apiResponse: queryResponse.data?.payload,
|
||||
histogramData,
|
||||
panelType: widget.panelTypes,
|
||||
setGraphsVisibilityStates: setGraphVisibility,
|
||||
graphsVisibilityStates: graphVisibility,
|
||||
mergeAllQueries: widget.mergeAllActiveQueries,
|
||||
onClickHandler: enableDrillDown
|
||||
? clickHandlerWithContextMenu
|
||||
: onClickHandler ?? _noop,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: number) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
}),
|
||||
[
|
||||
containerDimensions,
|
||||
graphVisibility,
|
||||
histogramData,
|
||||
isDarkMode,
|
||||
queryResponse.data?.payload,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
widget.mergeAllActiveQueries,
|
||||
widget.panelTypes,
|
||||
clickHandlerWithContextMenu,
|
||||
enableDrillDown,
|
||||
onClickHandler,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<Uplot options={histogramOptions} data={histogramData} ref={lineChartRef} />
|
||||
<ContextMenu
|
||||
coordinates={coordinates}
|
||||
popoverPosition={popoverPosition}
|
||||
title={menuItemsConfig.header as string}
|
||||
items={menuItemsConfig.items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{isFullViewMode && setGraphVisibility && !widget.mergeAllActiveQueries && (
|
||||
<GraphManager
|
||||
data={histogramData}
|
||||
name={widget.id}
|
||||
options={histogramOptions}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphsVisibilityStates={setGraphVisibility}
|
||||
graphsVisibilityStates={graphVisibility}
|
||||
lineChartRef={lineChartRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistogramPanelWrapper;
|
||||
@@ -1,4 +0,0 @@
|
||||
.info-text {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert } from 'antd';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
|
||||
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
|
||||
import _noop from 'lodash-es/noop';
|
||||
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import uPlot from 'uplot';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { PanelWrapperProps } from './panelWrapper.types';
|
||||
import { getTimeRangeFromStepInterval, isApmMetric } from './utils';
|
||||
|
||||
import './UplotPanelWrapper.styles.scss';
|
||||
|
||||
function UplotPanelWrapper({
|
||||
queryResponse,
|
||||
widget,
|
||||
isFullViewMode,
|
||||
setGraphVisibility,
|
||||
graphVisibility,
|
||||
onToggleModelHandler,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
selectedGraph,
|
||||
customTooltipElement,
|
||||
customSeries,
|
||||
enableDrillDown = false,
|
||||
}: PanelWrapperProps): JSX.Element {
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const legendScrollPositionRef = useRef<{
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}>({
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
});
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (toScrollWidgetId === widget.id) {
|
||||
graphRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
graphRef.current?.focus();
|
||||
setToScrollWidgetId('');
|
||||
}
|
||||
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
|
||||
setMinTimeScale(startTime);
|
||||
setMaxTimeScale(endTime);
|
||||
}, [queryResponse]);
|
||||
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const {
|
||||
coordinates,
|
||||
popoverPosition,
|
||||
clickedData,
|
||||
onClose,
|
||||
onClick,
|
||||
subMenu,
|
||||
setSubMenu,
|
||||
} = useCoordinates();
|
||||
const { menuItemsConfig } = useGraphContextMenu({
|
||||
widgetId: widget.id || '',
|
||||
query: widget.query,
|
||||
graphData: clickedData,
|
||||
onClose,
|
||||
coordinates,
|
||||
subMenu,
|
||||
setSubMenu,
|
||||
contextLinks: widget.contextLinks,
|
||||
panelType: widget.panelTypes,
|
||||
queryRange: queryResponse,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getLocalStorageGraphVisibilityState({
|
||||
apiResponse: queryResponse.data?.payload.data.result || [],
|
||||
name: widget.id,
|
||||
});
|
||||
if (setGraphVisibility) {
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
}, [
|
||||
queryResponse?.data?.payload?.data?.result,
|
||||
setGraphVisibility,
|
||||
widget.id,
|
||||
]);
|
||||
|
||||
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
|
||||
const sortedSeriesData = getSortedSeriesData(
|
||||
queryResponse.data?.payload.data.result,
|
||||
);
|
||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||
}
|
||||
|
||||
const stackedBarChart = useMemo(
|
||||
() =>
|
||||
(selectedGraph
|
||||
? selectedGraph === PANEL_TYPES.BAR
|
||||
: widget?.panelTypes === PANEL_TYPES.BAR) && widget?.stackedBarChart,
|
||||
[selectedGraph, widget?.panelTypes, widget?.stackedBarChart],
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getUPlotChartData(
|
||||
queryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
stackedBarChart,
|
||||
hiddenGraph,
|
||||
),
|
||||
[
|
||||
queryResponse?.data?.payload,
|
||||
widget.fillSpans,
|
||||
stackedBarChart,
|
||||
hiddenGraph,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (widget.panelTypes === PANEL_TYPES.BAR && stackedBarChart) {
|
||||
const graphV = cloneDeep(graphVisibility)?.slice(1);
|
||||
const isSomeSelectedLegend = graphV?.some((v) => v === false);
|
||||
if (isSomeSelectedLegend) {
|
||||
const hiddenIndex = graphV?.findIndex((v) => v === true);
|
||||
if (!isUndefined(hiddenIndex) && hiddenIndex !== -1) {
|
||||
const updatedHiddenGraph = { [hiddenIndex]: true };
|
||||
if (!isEqual(hiddenGraph, updatedHiddenGraph)) {
|
||||
setHiddenGraph(updatedHiddenGraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [graphVisibility, hiddenGraph, widget.panelTypes, stackedBarChart]);
|
||||
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const clickHandlerWithContextMenu = useCallback(
|
||||
(...args: any[]) => {
|
||||
const [
|
||||
xValue,
|
||||
,
|
||||
,
|
||||
,
|
||||
metric,
|
||||
queryData,
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
axesData,
|
||||
focusedSeries,
|
||||
] = args;
|
||||
const data = getUplotClickData({
|
||||
metric,
|
||||
queryData,
|
||||
absoluteMouseX,
|
||||
absoluteMouseY,
|
||||
focusedSeries,
|
||||
});
|
||||
// Compute time range if needed and if axes data is available
|
||||
let timeRange;
|
||||
if (axesData && queryData?.queryName) {
|
||||
// Get the compositeQuery from the response params
|
||||
const compositeQuery = (queryResponse?.data?.params as any)?.compositeQuery;
|
||||
|
||||
if (compositeQuery?.queries) {
|
||||
// Find the specific query by name from the queries array
|
||||
const specificQuery = compositeQuery.queries.find(
|
||||
(query: any) => query.spec?.name === queryData.queryName,
|
||||
);
|
||||
|
||||
// Use the stepInterval from the specific query, fallback to default
|
||||
const stepInterval = specificQuery?.spec?.stepInterval || 60;
|
||||
timeRange = getTimeRangeFromStepInterval(
|
||||
stepInterval,
|
||||
metric?.clickedTimestamp || xValue, // Use the clicked timestamp if available, otherwise use the click position timestamp
|
||||
specificQuery?.spec?.signal === DataSource.METRICS &&
|
||||
isApmMetric(specificQuery?.spec?.aggregations[0]?.metricName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data?.record?.queryName) {
|
||||
onClick(data.coord, { ...data.record, label: data.label, timeRange });
|
||||
}
|
||||
},
|
||||
[onClick, queryResponse],
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
id: widget?.id,
|
||||
apiResponse: queryResponse.data?.payload,
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
yAxisUnit: widget?.yAxisUnit,
|
||||
onClickHandler: enableDrillDown
|
||||
? clickHandlerWithContextMenu
|
||||
: onClickHandler ?? _noop,
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
softMax: widget.softMax === undefined ? null : widget.softMax,
|
||||
softMin: widget.softMin === undefined ? null : widget.softMin,
|
||||
graphsVisibilityStates: graphVisibility,
|
||||
setGraphsVisibilityStates: setGraphVisibility,
|
||||
panelType: selectedGraph || widget.panelTypes,
|
||||
currentQuery,
|
||||
stackBarChart: stackedBarChart,
|
||||
hiddenGraph,
|
||||
setHiddenGraph,
|
||||
customTooltipElement,
|
||||
tzDate: (timestamp: number) =>
|
||||
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||
timezone: timezone.value,
|
||||
customSeries,
|
||||
isLogScale: widget?.isLogScale,
|
||||
colorMapping: widget?.customLegendColors,
|
||||
enhancedLegend: true, // Enable enhanced legend
|
||||
legendPosition: widget?.legendPosition,
|
||||
query: widget?.query || currentQuery,
|
||||
legendScrollPosition: legendScrollPositionRef.current,
|
||||
setLegendScrollPosition: (position: {
|
||||
scrollTop: number;
|
||||
scrollLeft: number;
|
||||
}) => {
|
||||
legendScrollPositionRef.current = position;
|
||||
},
|
||||
decimalPrecision: widget.decimalPrecision,
|
||||
}),
|
||||
[
|
||||
queryResponse.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
clickHandlerWithContextMenu,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
graphVisibility,
|
||||
setGraphVisibility,
|
||||
selectedGraph,
|
||||
currentQuery,
|
||||
hiddenGraph,
|
||||
customTooltipElement,
|
||||
timezone.value,
|
||||
customSeries,
|
||||
enableDrillDown,
|
||||
onClickHandler,
|
||||
widget,
|
||||
stackedBarChart,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<Uplot options={options} data={chartData} ref={lineChartRef} />
|
||||
<ContextMenu
|
||||
coordinates={coordinates}
|
||||
popoverPosition={popoverPosition}
|
||||
title={menuItemsConfig.header as string}
|
||||
items={menuItemsConfig.items}
|
||||
onClose={onClose}
|
||||
/>
|
||||
{stackedBarChart && isFullViewMode && (
|
||||
<Alert
|
||||
message="Selecting multiple legends is currently not supported in case of stacked bar charts"
|
||||
type="info"
|
||||
className="info-text"
|
||||
/>
|
||||
)}
|
||||
{isFullViewMode && setGraphVisibility && !stackedBarChart && (
|
||||
<GraphManager
|
||||
data={chartData}
|
||||
name={widget.id}
|
||||
options={options}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
setGraphsVisibilityStates={setGraphVisibility}
|
||||
graphsVisibilityStates={graphVisibility}
|
||||
lineChartRef={lineChartRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UplotPanelWrapper;
|
||||
@@ -75,8 +75,6 @@ export const DashboardContext = createContext<IDashboardContext>({
|
||||
setLayouts: () => {},
|
||||
setSelectedDashboard: () => {},
|
||||
updatedTimeRef: {} as React.MutableRefObject<Dayjs | null>,
|
||||
toScrollWidgetId: '',
|
||||
setToScrollWidgetId: () => {},
|
||||
updateLocalStorageDashboardVariables: () => {},
|
||||
dashboardQueryRangeCalled: false,
|
||||
setDashboardQueryRangeCalled: () => {},
|
||||
@@ -95,8 +93,6 @@ export function DashboardProvider({
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
|
||||
|
||||
const [toScrollWidgetId, setToScrollWidgetId] = useState<string>('');
|
||||
|
||||
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
|
||||
|
||||
const [
|
||||
@@ -443,7 +439,6 @@ export function DashboardProvider({
|
||||
|
||||
const value: IDashboardContext = useMemo(
|
||||
() => ({
|
||||
toScrollWidgetId,
|
||||
isDashboardSliderOpen,
|
||||
isDashboardLocked,
|
||||
handleToggleDashboardSlider,
|
||||
@@ -457,7 +452,6 @@ export function DashboardProvider({
|
||||
setPanelMap,
|
||||
setSelectedDashboard,
|
||||
updatedTimeRef,
|
||||
setToScrollWidgetId,
|
||||
updateLocalStorageDashboardVariables,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
@@ -474,7 +468,6 @@ export function DashboardProvider({
|
||||
dashboardId,
|
||||
layouts,
|
||||
panelMap,
|
||||
toScrollWidgetId,
|
||||
updateLocalStorageDashboardVariables,
|
||||
currentDashboard,
|
||||
dashboardQueryRangeCalled,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ScrollToWidgetIdState {
|
||||
toScrollWidgetId: string;
|
||||
setToScrollWidgetId: (widgetId: string) => void;
|
||||
}
|
||||
|
||||
export const useScrollToWidgetIdStore = create<ScrollToWidgetIdState>(
|
||||
(set) => ({
|
||||
toScrollWidgetId: '',
|
||||
setToScrollWidgetId: (widgetId): void => set({ toScrollWidgetId: widgetId }),
|
||||
}),
|
||||
);
|
||||
@@ -23,8 +23,6 @@ export interface IDashboardContext {
|
||||
React.SetStateAction<Dashboard | undefined>
|
||||
>;
|
||||
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
|
||||
toScrollWidgetId: string;
|
||||
setToScrollWidgetId: React.Dispatch<React.SetStateAction<string>>;
|
||||
updateLocalStorageDashboardVariables: (
|
||||
id: string,
|
||||
selectedValue:
|
||||
|
||||
@@ -21,7 +21,7 @@ func NewHandler(module tracefunnel.Module) tracefunnel.Handler {
|
||||
return &handler{module: module}
|
||||
}
|
||||
|
||||
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
func (handler *handler) New(rw http.ResponseWriter, r *http.Request) {
|
||||
var req tf.PostableFunnel
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
render.Error(rw, err)
|
||||
@@ -34,7 +34,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
funnel, err := handler.module.Create(r.Context(), req.Name, claims.Email, valuer.MustNewUUID(claims.OrgID))
|
||||
funnel, err := handler.module.Create(r.Context(), req.Timestamp, req.Name, valuer.MustNewUUID(claims.UserID), valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
@@ -42,7 +42,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
response := tf.ConstructFunnelResponse(funnel)
|
||||
response := tf.ConstructFunnelResponse(funnel, &claims)
|
||||
render.Success(rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
funnel, err := handler.module.Get(r.Context(), req.FunnelID, valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||
@@ -73,15 +79,33 @@ func (handler *handler) UpdateSteps(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
funnel.Update(req.Name, req.Description, steps, claims.Email)
|
||||
if err := handler.module.Update(r.Context(), funnel); err != nil {
|
||||
funnel.Steps = steps
|
||||
funnel.UpdatedAt = updatedAt
|
||||
funnel.UpdatedBy = claims.UserID
|
||||
|
||||
if req.Name != "" {
|
||||
funnel.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
funnel.Description = req.Description
|
||||
}
|
||||
|
||||
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"failed to update funnel in database: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response := tf.ConstructFunnelResponse(funnel)
|
||||
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"failed to get updated funnel: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
|
||||
render.Success(rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -98,6 +122,12 @@ func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
updatedAt, err := tf.ValidateAndConvertTimestamp(req.Timestamp)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
funnelID := vars["funnel_id"]
|
||||
|
||||
@@ -109,15 +139,32 @@ func (handler *handler) UpdateFunnel(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
funnel.Update(req.Name, req.Description, nil, claims.Email)
|
||||
if err := handler.module.Update(r.Context(), funnel); err != nil {
|
||||
funnel.UpdatedAt = updatedAt
|
||||
funnel.UpdatedBy = claims.UserID
|
||||
|
||||
if req.Name != "" {
|
||||
funnel.Name = req.Name
|
||||
}
|
||||
if req.Description != "" {
|
||||
funnel.Description = req.Description
|
||||
}
|
||||
|
||||
if err := handler.module.Update(r.Context(), funnel, valuer.MustNewUUID(claims.UserID)); err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"failed to update funnel in database: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response := tf.ConstructFunnelResponse(funnel)
|
||||
updatedFunnel, err := handler.module.Get(r.Context(), funnel.ID, valuer.MustNewUUID(claims.OrgID))
|
||||
if err != nil {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput,
|
||||
errors.CodeInvalidInput,
|
||||
"failed to get updated funnel: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
response := tf.ConstructFunnelResponse(updatedFunnel, &claims)
|
||||
render.Success(rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -138,7 +185,7 @@ func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var response []tf.GettableFunnel
|
||||
for _, f := range funnels {
|
||||
response = append(response, tf.ConstructFunnelResponse(f))
|
||||
response = append(response, tf.ConstructFunnelResponse(f, &claims))
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, response)
|
||||
@@ -162,7 +209,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
"funnel not found: %v", err))
|
||||
return
|
||||
}
|
||||
response := tf.ConstructFunnelResponse(funnel)
|
||||
response := tf.ConstructFunnelResponse(funnel, &claims)
|
||||
render.Success(rw, http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
173
pkg/modules/tracefunnel/impltracefunnel/handler_test.go
Normal file
173
pkg/modules/tracefunnel/impltracefunnel/handler_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package impltracefunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockModule struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockModule) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, timestamp, name, userID, orgID)
|
||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockModule) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, funnelID, orgID)
|
||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockModule) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
|
||||
args := m.Called(ctx, funnel, userID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockModule) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, orgID)
|
||||
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockModule) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||
args := m.Called(ctx, funnelID, orgID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockModule) Save(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID, orgID valuer.UUID) error {
|
||||
args := m.Called(ctx, funnel, userID, orgID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockModule) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||
args := m.Called(ctx, funnelID, orgID)
|
||||
return args.Get(0).(int64), args.Get(1).(int64), args.String(2), args.Error(3)
|
||||
}
|
||||
|
||||
func TestHandler_List(t *testing.T) {
|
||||
mockModule := new(MockModule)
|
||||
handler := NewHandler(mockModule)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/list", nil)
|
||||
|
||||
orgID := valuer.GenerateUUID()
|
||||
claims := authtypes.Claims{
|
||||
OrgID: orgID.String(),
|
||||
}
|
||||
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), claims))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
funnel1ID := valuer.GenerateUUID()
|
||||
funnel2ID := valuer.GenerateUUID()
|
||||
expectedFunnels := []*traceFunnels.StorableFunnel{
|
||||
{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: funnel1ID,
|
||||
},
|
||||
Name: "funnel-1",
|
||||
OrgID: orgID,
|
||||
},
|
||||
{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: funnel2ID,
|
||||
},
|
||||
Name: "funnel-2",
|
||||
OrgID: orgID,
|
||||
},
|
||||
}
|
||||
|
||||
mockModule.On("List", req.Context(), orgID).Return(expectedFunnels, nil)
|
||||
|
||||
handler.List(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response struct {
|
||||
Status string `json:"status"`
|
||||
Data []traceFunnels.GettableFunnel `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", response.Status)
|
||||
assert.Len(t, response.Data, 2)
|
||||
assert.Equal(t, "funnel-1", response.Data[0].FunnelName)
|
||||
assert.Equal(t, "funnel-2", response.Data[1].FunnelName)
|
||||
|
||||
mockModule.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestHandler_Get(t *testing.T) {
|
||||
mockModule := new(MockModule)
|
||||
handler := NewHandler(mockModule)
|
||||
|
||||
funnelID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
||||
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
||||
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
|
||||
OrgID: orgID.String(),
|
||||
}))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: funnelID,
|
||||
},
|
||||
Name: "test-funnel",
|
||||
OrgID: orgID,
|
||||
}
|
||||
|
||||
mockModule.On("Get", req.Context(), funnelID, orgID).Return(expectedFunnel, nil)
|
||||
|
||||
handler.Get(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
var response struct {
|
||||
Status string `json:"status"`
|
||||
Data traceFunnels.GettableFunnel `json:"data"`
|
||||
}
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "success", response.Status)
|
||||
assert.Equal(t, "test-funnel", response.Data.FunnelName)
|
||||
assert.Equal(t, expectedFunnel.OrgID.String(), response.Data.OrgID)
|
||||
|
||||
mockModule.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestHandler_Delete(t *testing.T) {
|
||||
mockModule := new(MockModule)
|
||||
handler := NewHandler(mockModule)
|
||||
|
||||
funnelID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/trace-funnels/"+funnelID.String(), nil)
|
||||
req = mux.SetURLVars(req, map[string]string{"funnel_id": funnelID.String()})
|
||||
req = req.WithContext(authtypes.NewContextWithClaims(req.Context(), authtypes.Claims{
|
||||
OrgID: orgID.String(),
|
||||
}))
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
mockModule.On("Delete", req.Context(), funnelID, orgID).Return(nil)
|
||||
|
||||
handler.Delete(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
mockModule.AssertExpectations(t)
|
||||
}
|
||||
@@ -2,10 +2,11 @@ package impltracefunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -20,25 +21,58 @@ func NewModule(store traceFunnels.FunnelStore) tracefunnel.Module {
|
||||
}
|
||||
}
|
||||
|
||||
func (module *module) Create(ctx context.Context, name string, createdBy string, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
storable := tracefunneltypes.NewStorableFunnel(name, "", nil, "", createdBy, orgID)
|
||||
func (module *module) Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
funnel := &traceFunnels.StorableFunnel{
|
||||
Name: name,
|
||||
OrgID: orgID,
|
||||
}
|
||||
funnel.CreatedAt = time.Unix(0, timestamp*1000000) // Convert to nanoseconds
|
||||
funnel.CreatedBy = userID.String()
|
||||
|
||||
// Set up the user relationship
|
||||
funnel.CreatedByUser = &types.User{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: userID,
|
||||
},
|
||||
}
|
||||
|
||||
if funnel.ID.IsZero() {
|
||||
funnel.ID = valuer.GenerateUUID()
|
||||
}
|
||||
|
||||
if funnel.CreatedAt.IsZero() {
|
||||
funnel.CreatedAt = time.Now()
|
||||
}
|
||||
if funnel.UpdatedAt.IsZero() {
|
||||
funnel.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Set created_by if CreatedByUser is present
|
||||
if funnel.CreatedByUser != nil {
|
||||
funnel.CreatedBy = funnel.CreatedByUser.Identifiable.ID.String()
|
||||
}
|
||||
|
||||
err := module.store.Create(ctx, funnel)
|
||||
|
||||
err := module.store.Create(ctx, storable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storable, nil
|
||||
return funnel, nil
|
||||
}
|
||||
|
||||
func (module *module) Get(ctx context.Context, id valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
return module.store.Get(ctx, id, orgID)
|
||||
// Get gets a funnel by ID
|
||||
func (module *module) Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
return module.store.Get(ctx, funnelID, orgID)
|
||||
}
|
||||
|
||||
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||
// Update updates a funnel
|
||||
func (module *module) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error {
|
||||
funnel.UpdatedBy = userID.String()
|
||||
return module.store.Update(ctx, funnel)
|
||||
}
|
||||
|
||||
// List lists all funnels for an organization
|
||||
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||
funnels, err := module.store.List(ctx, orgID)
|
||||
if err != nil {
|
||||
@@ -48,12 +82,14 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunn
|
||||
return funnels, nil
|
||||
}
|
||||
|
||||
func (module *module) Delete(ctx context.Context, id valuer.UUID, orgID valuer.UUID) error {
|
||||
return module.store.Delete(ctx, id, orgID)
|
||||
// Delete deletes a funnel
|
||||
func (module *module) Delete(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) error {
|
||||
return module.store.Delete(ctx, funnelID, orgID)
|
||||
}
|
||||
|
||||
func (module *module) GetFunnelMetadata(ctx context.Context, id valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||
funnel, err := module.store.Get(ctx, id, orgID)
|
||||
// GetFunnelMetadata gets metadata for a funnel
|
||||
func (module *module) GetFunnelMetadata(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (int64, int64, string, error) {
|
||||
funnel, err := module.store.Get(ctx, funnelID, orgID)
|
||||
if err != nil {
|
||||
return 0, 0, "", err
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ func (store *store) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(funnel).
|
||||
Relation("CreatedByUser").
|
||||
Where("?TableAlias.id = ? AND ?TableAlias.org_id = ?", uuid.String(), orgID.String()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
@@ -126,6 +127,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnel
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(&funnels).
|
||||
Relation("CreatedByUser").
|
||||
Where("?TableAlias.org_id = ?", orgID.String()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package impltracefunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Test that Create method properly validates duplicate names
|
||||
func TestModule_Create_DuplicateNameValidation(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
timestamp := int64(1234567890)
|
||||
name := "Duplicate Funnel"
|
||||
userID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
// Mock store to return "already exists" error
|
||||
expectedErr := errors.Wrapf(nil, errors.TypeAlreadyExists, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", name)
|
||||
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
||||
return f.Name == name && f.OrgID == orgID
|
||||
})).Return(expectedErr)
|
||||
|
||||
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, funnel)
|
||||
assert.Contains(t, err.Error(), fmt.Sprintf("a funnel with name '%s' already exists in this organization", name))
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// Test that Update method properly validates duplicate names
|
||||
func TestModule_Update_DuplicateNameValidation(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
userID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
funnelName := "Duplicate Name"
|
||||
|
||||
funnel := &traceFunnels.StorableFunnel{
|
||||
Name: funnelName,
|
||||
OrgID: orgID,
|
||||
}
|
||||
funnel.ID = valuer.GenerateUUID()
|
||||
|
||||
// Mock store to return "already exists" error
|
||||
expectedErr := errors.Wrapf(nil, errors.TypeAlreadyExists, traceFunnels.ErrFunnelAlreadyExists, "a funnel with name '%s' already exists in this organization", funnelName)
|
||||
mockStore.On("Update", ctx, funnel).Return(expectedErr)
|
||||
|
||||
err := module.Update(ctx, funnel, userID)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), fmt.Sprintf("a funnel with name '%s' already exists in this organization", funnelName))
|
||||
assert.Equal(t, userID.String(), funnel.UpdatedBy) // Should still set UpdatedBy
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// MockStore for testing
|
||||
type MockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||
args := m.Called(ctx, funnel)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, uuid, orgID)
|
||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockStore) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, orgID)
|
||||
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||
args := m.Called(ctx, funnel)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
|
||||
args := m.Called(ctx, uuid, orgID)
|
||||
return args.Error(0)
|
||||
}
|
||||
@@ -2,20 +2,19 @@ package tracefunnel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"net/http"
|
||||
|
||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
)
|
||||
|
||||
// Module defines the interface for trace funnel operations
|
||||
type Module interface {
|
||||
Create(ctx context.Context, name string, userID string, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||
Create(ctx context.Context, timestamp int64, name string, userID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||
|
||||
Get(ctx context.Context, funnelID valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error)
|
||||
|
||||
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error
|
||||
Update(ctx context.Context, funnel *traceFunnels.StorableFunnel, userID valuer.UUID) error
|
||||
|
||||
List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error)
|
||||
|
||||
@@ -25,7 +24,7 @@ type Module interface {
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
Create(http.ResponseWriter, *http.Request)
|
||||
New(http.ResponseWriter, *http.Request)
|
||||
|
||||
UpdateSteps(http.ResponseWriter, *http.Request)
|
||||
|
||||
|
||||
183
pkg/modules/tracefunnel/tracefunneltest/module_test.go
Normal file
183
pkg/modules/tracefunnel/tracefunneltest/module_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package tracefunneltest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracefunnel/impltracefunnel"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
traceFunnels "github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockStore) Create(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||
args := m.Called(ctx, funnel)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockStore) Get(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) (*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, uuid, orgID)
|
||||
return args.Get(0).(*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockStore) List(ctx context.Context, orgID valuer.UUID) ([]*traceFunnels.StorableFunnel, error) {
|
||||
args := m.Called(ctx, orgID)
|
||||
return args.Get(0).([]*traceFunnels.StorableFunnel), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockStore) Update(ctx context.Context, funnel *traceFunnels.StorableFunnel) error {
|
||||
args := m.Called(ctx, funnel)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockStore) Delete(ctx context.Context, uuid valuer.UUID, orgID valuer.UUID) error {
|
||||
args := m.Called(ctx, uuid, orgID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func TestModule_Create(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := impltracefunnel.NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
timestamp := time.Now().UnixMilli()
|
||||
name := "test-funnel"
|
||||
userID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
mockStore.On("Create", ctx, mock.MatchedBy(func(f *traceFunnels.StorableFunnel) bool {
|
||||
return f.Name == name &&
|
||||
f.CreatedBy == userID.String() &&
|
||||
f.OrgID == orgID &&
|
||||
f.CreatedByUser != nil &&
|
||||
f.CreatedByUser.ID == userID &&
|
||||
f.CreatedAt.UnixNano()/1000000 == timestamp
|
||||
})).Return(nil)
|
||||
|
||||
funnel, err := module.Create(ctx, timestamp, name, userID, orgID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, funnel)
|
||||
assert.Equal(t, name, funnel.Name)
|
||||
assert.Equal(t, userID.String(), funnel.CreatedBy)
|
||||
assert.Equal(t, orgID, funnel.OrgID)
|
||||
assert.NotNil(t, funnel.CreatedByUser)
|
||||
assert.Equal(t, userID, funnel.CreatedByUser.ID)
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestModule_Get(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := impltracefunnel.NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
funnelID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||
Name: "test-funnel",
|
||||
}
|
||||
|
||||
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
|
||||
|
||||
funnel, err := module.Get(ctx, funnelID, orgID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedFunnel, funnel)
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestModule_Update(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := impltracefunnel.NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
userID := valuer.GenerateUUID()
|
||||
funnel := &traceFunnels.StorableFunnel{
|
||||
Name: "test-funnel",
|
||||
}
|
||||
|
||||
mockStore.On("Update", ctx, funnel).Return(nil)
|
||||
|
||||
err := module.Update(ctx, funnel, userID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, userID.String(), funnel.UpdatedBy)
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestModule_List(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := impltracefunnel.NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
orgID := valuer.GenerateUUID()
|
||||
expectedFunnels := []*traceFunnels.StorableFunnel{
|
||||
{
|
||||
Name: "funnel-1",
|
||||
OrgID: orgID,
|
||||
},
|
||||
{
|
||||
Name: "funnel-2",
|
||||
OrgID: orgID,
|
||||
},
|
||||
}
|
||||
|
||||
mockStore.On("List", ctx, orgID).Return(expectedFunnels, nil)
|
||||
|
||||
funnels, err := module.List(ctx, orgID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, funnels, 2)
|
||||
assert.Equal(t, expectedFunnels, funnels)
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestModule_Delete(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := impltracefunnel.NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
funnelID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
|
||||
mockStore.On("Delete", ctx, funnelID, orgID).Return(nil)
|
||||
|
||||
err := module.Delete(ctx, funnelID, orgID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestModule_GetFunnelMetadata(t *testing.T) {
|
||||
mockStore := new(MockStore)
|
||||
module := impltracefunnel.NewModule(mockStore)
|
||||
|
||||
ctx := context.Background()
|
||||
funnelID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
now := time.Now()
|
||||
expectedFunnel := &traceFunnels.StorableFunnel{
|
||||
Description: "test description",
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
}
|
||||
|
||||
mockStore.On("Get", ctx, funnelID, orgID).Return(expectedFunnel, nil)
|
||||
|
||||
createdAt, updatedAt, description, err := module.GetFunnelMetadata(ctx, funnelID, orgID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, now.UnixNano()/1000000, createdAt)
|
||||
assert.Equal(t, now.UnixNano()/1000000, updatedAt)
|
||||
assert.Equal(t, "test description", description)
|
||||
|
||||
mockStore.AssertExpectations(t)
|
||||
}
|
||||
@@ -38,7 +38,9 @@ func (r *Repo) GetConfigHistory(
|
||||
var c []opamptypes.AgentConfigVersion
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&c).
|
||||
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at,created_by").
|
||||
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at").
|
||||
ColumnExpr("COALESCE(created_by, '') as created_by").
|
||||
ColumnExpr(`COALESCE((SELECT display_name FROM users WHERE users.id = acv.created_by), 'unknown') as created_by_name`).
|
||||
ColumnExpr("COALESCE(hash, '') as hash, COALESCE(config, '{}') as config").
|
||||
Where("acv.element_type = ?", typ).
|
||||
Where("acv.org_id = ?", orgId).
|
||||
@@ -52,7 +54,6 @@ func (r *Repo) GetConfigHistory(
|
||||
|
||||
incompleteStatuses := []opamptypes.DeployStatus{opamptypes.DeployInitiated, opamptypes.Deploying}
|
||||
for idx := 1; idx < len(c); idx++ {
|
||||
c[idx].CreatedByName = c[idx].CreatedBy
|
||||
if slices.Contains(incompleteStatuses, c[idx].DeployStatus) {
|
||||
c[idx].DeployStatus = opamptypes.DeployStatusUnknown
|
||||
}
|
||||
@@ -67,7 +68,9 @@ func (r *Repo) GetConfigVersion(
|
||||
var c opamptypes.AgentConfigVersion
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&c).
|
||||
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at,created_by").
|
||||
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at").
|
||||
ColumnExpr("COALESCE(created_by, '') as created_by").
|
||||
ColumnExpr(`COALESCE((SELECT display_name FROM users WHERE users.id = acv.created_by), 'unknown') as created_by_name`).
|
||||
ColumnExpr("COALESCE(hash, '') as hash, COALESCE(config, '{}') as config").
|
||||
Where("acv.element_type = ?", typ).
|
||||
Where("acv.version = ?", v).
|
||||
@@ -81,7 +84,6 @@ func (r *Repo) GetConfigVersion(
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get config version")
|
||||
}
|
||||
|
||||
c.CreatedByName = c.CreatedBy
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
@@ -91,7 +93,9 @@ func (r *Repo) GetLatestVersion(
|
||||
var c opamptypes.AgentConfigVersion
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&c).
|
||||
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at,created_by").
|
||||
ColumnExpr("id, version, element_type, deploy_status, deploy_result, created_at").
|
||||
ColumnExpr("COALESCE(created_by, '') as created_by").
|
||||
ColumnExpr(`COALESCE((SELECT display_name FROM users WHERE users.id = acv.created_by), 'unknown') as created_by_name`).
|
||||
Where("acv.element_type = ?", typ).
|
||||
Where("acv.org_id = ?", orgId).
|
||||
Where("version = (SELECT MAX(version) FROM agent_config_version WHERE acv.element_type = ?)", typ).
|
||||
@@ -104,12 +108,11 @@ func (r *Repo) GetLatestVersion(
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to get latest config version")
|
||||
}
|
||||
|
||||
c.CreatedByName = c.CreatedBy
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (r *Repo) insertConfig(
|
||||
ctx context.Context, orgId valuer.UUID, c *opamptypes.AgentConfigVersion, elements []string,
|
||||
ctx context.Context, orgId valuer.UUID, userId valuer.UUID, c *opamptypes.AgentConfigVersion, elements []string,
|
||||
) error {
|
||||
|
||||
if c.ElementType.StringValue() == "" {
|
||||
|
||||
@@ -198,14 +198,14 @@ func GetConfigHistory(
|
||||
|
||||
// StartNewVersion launches a new config version for given set of elements
|
||||
func StartNewVersion(
|
||||
ctx context.Context, orgId valuer.UUID, createdBy string, eleType opamptypes.ElementType, elementIds []string,
|
||||
ctx context.Context, orgId valuer.UUID, userId valuer.UUID, eleType opamptypes.ElementType, elementIds []string,
|
||||
) (*opamptypes.AgentConfigVersion, error) {
|
||||
|
||||
// create a new version
|
||||
cfg := opamptypes.NewAgentConfigVersion(orgId, createdBy, eleType)
|
||||
cfg := opamptypes.NewAgentConfigVersion(orgId, userId, eleType)
|
||||
|
||||
// insert new config and elements into database
|
||||
err := m.insertConfig(ctx, orgId, cfg, elementIds)
|
||||
err := m.insertConfig(ctx, orgId, userId, cfg, elementIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4237,8 +4237,14 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
userID, errv2 := valuer.NewUUID(claims.UserID)
|
||||
if errv2 != nil {
|
||||
render.Error(w, errv2)
|
||||
return
|
||||
}
|
||||
|
||||
req := pipelinetypes.PostablePipelines{}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
@@ -4257,7 +4263,7 @@ func (aH *APIHandler) CreateLogsPipeline(w http.ResponseWriter, r *http.Request)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return aH.LogsParsingPipelineController.ApplyPipelines(ctx, orgID, claims.Email, postable)
|
||||
return aH.LogsParsingPipelineController.ApplyPipelines(ctx, orgID, userID, postable)
|
||||
}
|
||||
|
||||
res, err := createPipeline(r.Context(), req.Pipelines)
|
||||
@@ -5132,7 +5138,7 @@ func (aH *APIHandler) RegisterTraceFunnelsRoutes(router *mux.Router, am *middlew
|
||||
|
||||
// API endpoints
|
||||
traceFunnelsRouter.HandleFunc("/new",
|
||||
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.Create)).
|
||||
am.EditAccess(aH.Signoz.Handlers.TraceFunnel.New)).
|
||||
Methods(http.MethodPost)
|
||||
traceFunnelsRouter.HandleFunc("/list",
|
||||
am.ViewAccess(aH.Signoz.Handlers.TraceFunnel.List)).
|
||||
|
||||
@@ -58,7 +58,7 @@ type PipelinesResponse struct {
|
||||
func (ic *LogParsingPipelineController) ApplyPipelines(
|
||||
ctx context.Context,
|
||||
orgID valuer.UUID,
|
||||
createdBy string,
|
||||
userID valuer.UUID,
|
||||
postable []pipelinetypes.PostablePipeline,
|
||||
) (*PipelinesResponse, error) {
|
||||
var pipelines []pipelinetypes.GettablePipeline
|
||||
@@ -89,7 +89,7 @@ func (ic *LogParsingPipelineController) ApplyPipelines(
|
||||
elements[i] = p.ID.StringValue()
|
||||
}
|
||||
|
||||
cfg, err := agentConf.StartNewVersion(ctx, orgID, createdBy, opamptypes.ElementTypeLogPipelines, elements)
|
||||
cfg, err := agentConf.StartNewVersion(ctx, orgID, userID, opamptypes.ElementTypeLogPipelines, elements)
|
||||
if err != nil || cfg == nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to start new version: %w", err))
|
||||
}
|
||||
|
||||
@@ -172,7 +172,6 @@ func NewSQLMigrationProviderFactories(
|
||||
sqlmigration.NewMigrateRulesV4ToV5Factory(sqlstore, telemetryStore),
|
||||
sqlmigration.NewAddStatusUserFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewDeprecateUserInviteFactory(sqlstore, sqlschema),
|
||||
sqlmigration.NewUpdateCreatedByWithEmailFactory(sqlstore, sqlschema),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
package sqlmigration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/migrate"
|
||||
)
|
||||
|
||||
type updateCreatedByWithEmail struct {
|
||||
sqlstore sqlstore.SQLStore
|
||||
sqlschema sqlschema.SQLSchema
|
||||
}
|
||||
|
||||
func NewUpdateCreatedByWithEmailFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||
return factory.NewProviderFactory(
|
||||
factory.MustNewName("update_created_by_with_email"),
|
||||
func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
|
||||
return &updateCreatedByWithEmail{
|
||||
sqlstore: sqlstore,
|
||||
sqlschema: sqlschema,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (migration *updateCreatedByWithEmail) Register(migrations *migrate.Migrations) error {
|
||||
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *updateCreatedByWithEmail) Up(ctx context.Context, db *bun.DB) error {
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
type userRow struct {
|
||||
ID string `bun:"id"`
|
||||
Email string `bun:"email"`
|
||||
}
|
||||
|
||||
var users []userRow
|
||||
err = tx.NewSelect().TableExpr("users").Column("id", "email").Scan(ctx, &users)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
userEmailMap := make(map[string]string, len(users))
|
||||
for _, u := range users {
|
||||
userEmailMap[u.ID] = u.Email
|
||||
}
|
||||
|
||||
emails := make([]string, 0, len(userEmailMap))
|
||||
for _, email := range userEmailMap {
|
||||
emails = append(emails, email)
|
||||
}
|
||||
|
||||
for id, email := range userEmailMap {
|
||||
_, err = tx.NewUpdate().
|
||||
TableExpr("agent_config_version").
|
||||
Set("created_by = ?", email).
|
||||
Where("created_by = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.NewUpdate().
|
||||
TableExpr("agent_config_version").
|
||||
Set("updated_by = ?", email).
|
||||
Where("updated_by = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
agentCreatedByQuery := tx.NewUpdate().
|
||||
TableExpr("agent_config_version").
|
||||
Set("created_by = ''").
|
||||
Where("created_by != ''")
|
||||
if len(emails) > 0 {
|
||||
agentCreatedByQuery = agentCreatedByQuery.Where("created_by NOT IN (?)", bun.In(emails))
|
||||
}
|
||||
if _, err = agentCreatedByQuery.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agentUpdatedByQuery := tx.NewUpdate().
|
||||
TableExpr("agent_config_version").
|
||||
Set("updated_by = ''").
|
||||
Where("updated_by != ''")
|
||||
if len(emails) > 0 {
|
||||
agentUpdatedByQuery = agentUpdatedByQuery.Where("updated_by NOT IN (?)", bun.In(emails))
|
||||
}
|
||||
if _, err = agentUpdatedByQuery.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, email := range userEmailMap {
|
||||
_, err = tx.NewUpdate().
|
||||
TableExpr("trace_funnel").
|
||||
Set("created_by = ?", email).
|
||||
Where("created_by = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.NewUpdate().
|
||||
TableExpr("trace_funnel").
|
||||
Set("updated_by = ?", email).
|
||||
Where("updated_by = ?", id).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
funnelCreatedByQuery := tx.NewUpdate().
|
||||
TableExpr("trace_funnel").
|
||||
Set("created_by = ''").
|
||||
Where("created_by != ''")
|
||||
if len(emails) > 0 {
|
||||
funnelCreatedByQuery = funnelCreatedByQuery.Where("created_by NOT IN (?)", bun.In(emails))
|
||||
}
|
||||
if _, err = funnelCreatedByQuery.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
funnelUpdatedByQuery := tx.NewUpdate().
|
||||
TableExpr("trace_funnel").
|
||||
Set("updated_by = ''").
|
||||
Where("updated_by != ''")
|
||||
if len(emails) > 0 {
|
||||
funnelUpdatedByQuery = funnelUpdatedByQuery.Where("updated_by NOT IN (?)", bun.In(emails))
|
||||
}
|
||||
if _, err = funnelUpdatedByQuery.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quickFilterTable, _, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("quick_filter"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sqls := [][]byte{}
|
||||
|
||||
createdByCol := &sqlschema.Column{Name: "created_by"}
|
||||
dropSQLS := migration.sqlschema.Operator().DropColumn(quickFilterTable, createdByCol)
|
||||
sqls = append(sqls, dropSQLS...)
|
||||
|
||||
updatedByCol := &sqlschema.Column{Name: "updated_by"}
|
||||
dropSQLS = migration.sqlschema.Operator().DropColumn(quickFilterTable, updatedByCol)
|
||||
sqls = append(sqls, dropSQLS...)
|
||||
|
||||
for _, sql := range sqls {
|
||||
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migration *updateCreatedByWithEmail) Down(ctx context.Context, db *bun.DB) error {
|
||||
return nil
|
||||
}
|
||||
@@ -81,7 +81,10 @@ var (
|
||||
type AgentConfigVersion struct {
|
||||
bun.BaseModel `bun:"table:agent_config_version,alias:acv"`
|
||||
|
||||
// this is only for reading
|
||||
// keeping it here since we query the actual data from users table
|
||||
CreatedByName string `json:"createdByName" bun:"created_by_name,scanonly"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
@@ -95,13 +98,13 @@ type AgentConfigVersion struct {
|
||||
Config string `json:"config" bun:"config,type:text"`
|
||||
}
|
||||
|
||||
func NewAgentConfigVersion(orgId valuer.UUID, createdBy string, elementType ElementType) *AgentConfigVersion {
|
||||
func NewAgentConfigVersion(orgId valuer.UUID, userId valuer.UUID, elementType ElementType) *AgentConfigVersion {
|
||||
return &AgentConfigVersion{
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: createdBy, UpdatedBy: createdBy},
|
||||
UserAuditable: types.UserAuditable{CreatedBy: userId.String(), UpdatedBy: userId.String()},
|
||||
OrgID: orgId,
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
ElementType: elementType,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package tracefunneltypes
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -25,6 +23,7 @@ type StorableFunnel struct {
|
||||
OrgID valuer.UUID `json:"org_id" bun:"org_id,type:varchar,notnull"`
|
||||
Steps []*FunnelStep `json:"steps" bun:"steps,type:text,notnull"`
|
||||
Tags string `json:"tags" bun:"tags,type:text"`
|
||||
CreatedByUser *types.User `json:"user" bun:"rel:belongs-to,join:created_by=id"`
|
||||
}
|
||||
|
||||
type FunnelStep struct {
|
||||
@@ -84,6 +83,12 @@ type StepTransitionRequest struct {
|
||||
StepEnd int64 `json:"step_end,omitempty"`
|
||||
}
|
||||
|
||||
// UserInfo represents basic user information
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type FunnelStepFilter struct {
|
||||
StepNumber int
|
||||
ServiceName string
|
||||
@@ -91,41 +96,3 @@ type FunnelStepFilter struct {
|
||||
LatencyPointer string // "start" or "end"
|
||||
CustomFilters *v3.FilterSet
|
||||
}
|
||||
|
||||
func NewStorableFunnel(name string, description string, steps []*FunnelStep, tags string, createdBy string, orgID valuer.UUID) *StorableFunnel {
|
||||
return &StorableFunnel{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: createdBy,
|
||||
UpdatedBy: createdBy,
|
||||
},
|
||||
Name: name,
|
||||
Description: description,
|
||||
Steps: steps,
|
||||
Tags: tags,
|
||||
OrgID: orgID,
|
||||
}
|
||||
}
|
||||
|
||||
func (tf *StorableFunnel) Update(name string, description string, steps []*FunnelStep, updatedBy string) {
|
||||
if name != "" {
|
||||
tf.Name = name
|
||||
}
|
||||
|
||||
if description != "" {
|
||||
tf.Description = description
|
||||
}
|
||||
|
||||
if steps != nil {
|
||||
tf.Steps = steps
|
||||
}
|
||||
|
||||
tf.UpdatedBy = updatedBy
|
||||
tf.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -93,7 +94,7 @@ func ValidateAndConvertTimestamp(timestamp int64) (time.Time, error) {
|
||||
return time.Unix(0, timestamp*1000000), nil // Convert to nanoseconds
|
||||
}
|
||||
|
||||
func ConstructFunnelResponse(funnel *StorableFunnel) GettableFunnel {
|
||||
func ConstructFunnelResponse(funnel *StorableFunnel, claims *authtypes.Claims) GettableFunnel {
|
||||
resp := GettableFunnel{
|
||||
FunnelName: funnel.Name,
|
||||
FunnelID: funnel.ID.String(),
|
||||
@@ -104,7 +105,12 @@ func ConstructFunnelResponse(funnel *StorableFunnel) GettableFunnel {
|
||||
UpdatedBy: funnel.UpdatedBy,
|
||||
UpdatedAt: funnel.UpdatedAt.UnixNano() / 1000000,
|
||||
Description: funnel.Description,
|
||||
UserEmail: funnel.CreatedBy,
|
||||
}
|
||||
|
||||
if funnel.CreatedByUser != nil {
|
||||
resp.UserEmail = funnel.CreatedByUser.Email.String()
|
||||
} else if claims != nil {
|
||||
resp.UserEmail = claims.Email
|
||||
}
|
||||
|
||||
return resp
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -418,10 +419,12 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
now := time.Now()
|
||||
funnelID := valuer.GenerateUUID()
|
||||
orgID := valuer.GenerateUUID()
|
||||
userID := valuer.GenerateUUID()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
funnel *StorableFunnel
|
||||
claims *authtypes.Claims
|
||||
expected GettableFunnel
|
||||
}{
|
||||
{
|
||||
@@ -435,11 +438,17 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
CreatedBy: userID.String(),
|
||||
UpdatedBy: userID.String(),
|
||||
},
|
||||
Name: "test-funnel",
|
||||
OrgID: orgID,
|
||||
CreatedByUser: &types.User{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: userID,
|
||||
},
|
||||
Email: valuer.MustNewEmail("funnel@example.com"),
|
||||
},
|
||||
Steps: []*FunnelStep{
|
||||
{
|
||||
ID: valuer.GenerateUUID(),
|
||||
@@ -450,6 +459,11 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
claims: &authtypes.Claims{
|
||||
UserID: userID.String(),
|
||||
OrgID: orgID.String(),
|
||||
Email: "claims@example.com",
|
||||
},
|
||||
expected: GettableFunnel{
|
||||
FunnelName: "test-funnel",
|
||||
FunnelID: funnelID.String(),
|
||||
@@ -462,11 +476,11 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
CreatedAt: now.UnixNano() / 1000000,
|
||||
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
CreatedBy: userID.String(),
|
||||
UpdatedAt: now.UnixNano() / 1000000,
|
||||
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
UpdatedBy: userID.String(),
|
||||
OrgID: orgID.String(),
|
||||
UserEmail: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
UserEmail: "funnel@example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -480,8 +494,8 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
UpdatedAt: now,
|
||||
},
|
||||
UserAuditable: types.UserAuditable{
|
||||
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
CreatedBy: userID.String(),
|
||||
UpdatedBy: userID.String(),
|
||||
},
|
||||
Name: "test-funnel",
|
||||
OrgID: orgID,
|
||||
@@ -495,6 +509,11 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
claims: &authtypes.Claims{
|
||||
UserID: userID.String(),
|
||||
OrgID: orgID.String(),
|
||||
Email: "claims@example.com",
|
||||
},
|
||||
expected: GettableFunnel{
|
||||
FunnelName: "test-funnel",
|
||||
FunnelID: funnelID.String(),
|
||||
@@ -507,18 +526,18 @@ func TestConstructFunnelResponse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
CreatedAt: now.UnixNano() / 1000000,
|
||||
CreatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
CreatedBy: userID.String(),
|
||||
UpdatedAt: now.UnixNano() / 1000000,
|
||||
UpdatedBy: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
UpdatedBy: userID.String(),
|
||||
OrgID: orgID.String(),
|
||||
UserEmail: valuer.MustNewEmail("funnel@example.com").String(),
|
||||
UserEmail: "claims@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := ConstructFunnelResponse(tt.funnel)
|
||||
result := ConstructFunnelResponse(tt.funnel, tt.claims)
|
||||
|
||||
// Compare top-level fields
|
||||
assert.Equal(t, tt.expected.FunnelName, result.FunnelName)
|
||||
|
||||
Reference in New Issue
Block a user