chore: funnel run and save flow changes (#8231)

* feat: while the funnel steps are invalid, handle auto save in local storage

* chore: handle lightmode style in 'add span to funnel' modal

* fix: don't save incomplete steps state in local storage if last saved configuration has valid steps

* chore: close the 'Add span to funnel' modal on clicking save or discard

* chore: deprecate the run funnel flow for unexecuted funnel

* feat: change the funnel configuration save logic, and deprecate auto save

* refactor: send all steps in the payload of analytics/overview

* refactor: send all steps in the payload of analytics/steps (graph API)

* chore: send all steps in the payload of analytics/steps/overview API

* chore: send funnel steps with slow and error traces + deprecate the refetch on latency type change

* chore: overall improvements

* chore: change the save funnel icon + increase the width of funnel steps

* fix: make the changes w.r.t. the updated funnel steps validation API + bugfixes

* fix: remove funnelId from funnel results APIs

* fix: handle edge case i.e. refetch funnel results on deleting a funnel step

* chore: remove funnel steps configuration cache on removing funnel

* chore: don't refetch the results on changing the latency type

* fix: fix the edge cases of save funnel button being enabled even after saving the funnel steps

* chore: remove the span count column from top traces tables

* fix: fix the failing CI check by removing unnecessary props / fixing the types
This commit is contained in:
Shaheer Kochai 2025-06-18 10:38:41 +04:30 committed by GitHub
parent 66affb0ece
commit bed3dbc698
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 253 additions and 267 deletions

View File

@ -119,6 +119,7 @@ export const updateFunnelSteps = async (
export interface ValidateFunnelPayload { export interface ValidateFunnelPayload {
start_time: number; start_time: number;
end_time: number; end_time: number;
steps: FunnelStepData[];
} }
export interface ValidateFunnelResponse { export interface ValidateFunnelResponse {
@ -132,12 +133,11 @@ export interface ValidateFunnelResponse {
} }
export const validateFunnelSteps = async ( export const validateFunnelSteps = async (
funnelId: string,
payload: ValidateFunnelPayload, payload: ValidateFunnelPayload,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => { ): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
const response = await axios.post( const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`, `${FUNNELS_BASE_PATH}/analytics/validate`,
payload, payload,
{ signal }, { signal },
); );
@ -185,6 +185,7 @@ export interface FunnelOverviewPayload {
end_time: number; end_time: number;
step_start?: number; step_start?: number;
step_end?: number; step_end?: number;
steps: FunnelStepData[];
} }
export interface FunnelOverviewResponse { export interface FunnelOverviewResponse {
@ -202,12 +203,11 @@ export interface FunnelOverviewResponse {
} }
export const getFunnelOverview = async ( export const getFunnelOverview = async (
funnelId: string,
payload: FunnelOverviewPayload, payload: FunnelOverviewPayload,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => { ): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
const response = await axios.post( const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`, `${FUNNELS_BASE_PATH}/analytics/overview`,
payload, payload,
{ {
signal, signal,
@ -235,12 +235,11 @@ export interface SlowTraceData {
} }
export const getFunnelSlowTraces = async ( export const getFunnelSlowTraces = async (
funnelId: string,
payload: FunnelOverviewPayload, payload: FunnelOverviewPayload,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => { ): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
const response = await axios.post( const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`, `${FUNNELS_BASE_PATH}/analytics/slow-traces`,
payload, payload,
{ {
signal, signal,
@ -273,7 +272,7 @@ export const getFunnelErrorTraces = async (
signal?: AbortSignal, signal?: AbortSignal,
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => { ): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
const response: AxiosResponse = await axios.post( const response: AxiosResponse = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`, `${FUNNELS_BASE_PATH}/analytics/error-traces`,
payload, payload,
{ {
signal, signal,
@ -291,6 +290,7 @@ export const getFunnelErrorTraces = async (
export interface FunnelStepsPayload { export interface FunnelStepsPayload {
start_time: number; start_time: number;
end_time: number; end_time: number;
steps: FunnelStepData[];
} }
export interface FunnelStepGraphMetrics { export interface FunnelStepGraphMetrics {
@ -307,12 +307,11 @@ export interface FunnelStepsResponse {
} }
export const getFunnelSteps = async ( export const getFunnelSteps = async (
funnelId: string,
payload: FunnelStepsPayload, payload: FunnelStepsPayload,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => { ): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
const response = await axios.post( const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`, `${FUNNELS_BASE_PATH}/analytics/steps`,
payload, payload,
{ signal }, { signal },
); );
@ -330,6 +329,7 @@ export interface FunnelStepsOverviewPayload {
end_time: number; end_time: number;
step_start?: number; step_start?: number;
step_end?: number; step_end?: number;
steps: FunnelStepData[];
} }
export interface FunnelStepsOverviewResponse { export interface FunnelStepsOverviewResponse {
@ -341,12 +341,11 @@ export interface FunnelStepsOverviewResponse {
} }
export const getFunnelStepsOverview = async ( export const getFunnelStepsOverview = async (
funnelId: string,
payload: FunnelStepsOverviewPayload, payload: FunnelStepsOverviewPayload,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => { ): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
const response = await axios.post( const response = await axios.post(
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`, `${FUNNELS_BASE_PATH}/analytics/steps/overview`,
payload, payload,
{ signal }, { signal },
); );

View File

@ -30,5 +30,5 @@ export enum LOCALSTORAGE {
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS', SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
BANNER_DISMISSED = 'BANNER_DISMISSED', BANNER_DISMISSED = 'BANNER_DISMISSED',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT', QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS', FUNNEL_STEPS = 'FUNNEL_STEPS',
} }

View File

@ -241,6 +241,15 @@
&-title { &-title {
color: var(--bg-ink-500); color: var(--bg-ink-500);
} }
&-footer {
border-top-color: var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.add-span-to-funnel-modal__discard-button {
background: var(--bg-vanilla-200);
color: var(--bg-ink-500);
}
}
} }
} }

View File

@ -72,7 +72,6 @@ function FunnelDetailsView({
funnel={funnel} funnel={funnel}
isTraceDetailsPage isTraceDetailsPage
span={span} span={span}
disableAutoSave
triggerAutoSave={triggerAutoSave} triggerAutoSave={triggerAutoSave}
showNotifications={showNotifications} showNotifications={showNotifications}
/> />
@ -143,13 +142,19 @@ function AddSpanToFunnelModal({
const handleSaveFunnel = (): void => { const handleSaveFunnel = (): void => {
setTriggerSave(true); setTriggerSave(true);
// Reset trigger after a brief moment to allow the save to be processed // Reset trigger after a brief moment to allow the save to be processed
setTimeout(() => setTriggerSave(false), 100); setTimeout(() => {
setTriggerSave(false);
onClose();
}, 100);
}; };
const handleDiscard = (): void => { const handleDiscard = (): void => {
setTriggerDiscard(true); setTriggerDiscard(true);
// Reset trigger after a brief moment // Reset trigger after a brief moment
setTimeout(() => setTriggerDiscard(false), 100); setTimeout(() => {
setTriggerDiscard(false);
onClose();
}, 100);
}; };
const renderListView = (): JSX.Element => ( const renderListView = (): JSX.Element => (
@ -239,9 +244,6 @@ function AddSpanToFunnelModal({
footer={ footer={
activeView === ModalView.DETAILS activeView === ModalView.DETAILS
? [ ? [
<Button key="close" onClick={onClose}>
Close
</Button>,
<Button <Button
type="default" type="default"
key="discard" key="discard"

View File

@ -1,10 +1,13 @@
import { LOCALSTORAGE } from 'constants/localStorage';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useDebounce from 'hooks/useDebounce'; import useDebounce from 'hooks/useDebounce';
import { useLocalStorage } from 'hooks/useLocalStorage';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext'; import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FunnelData, FunnelStepData } from 'types/api/traceFunnels'; import { FunnelData, FunnelStepData } from 'types/api/traceFunnels';
import { useUpdateFunnelSteps } from './useFunnels'; import { useUpdateFunnelSteps } from './useFunnels';
@ -13,22 +16,30 @@ interface UseFunnelConfiguration {
isPopoverOpen: boolean; isPopoverOpen: boolean;
setIsPopoverOpen: (isPopoverOpen: boolean) => void; setIsPopoverOpen: (isPopoverOpen: boolean) => void;
steps: FunnelStepData[]; steps: FunnelStepData[];
isSaving: boolean;
} }
// Add this helper function // Add this helper function
const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => { export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
if (steps.some((step) => !step.filters)) return steps; if (steps.some((step) => !step.filters)) return steps;
return steps.map((step) => ({ return steps.map((step) => ({
...step, ...step,
filters: { filters: {
...step.filters, ...step.filters,
items: step.filters.items.map((item) => ({ items: step.filters.items.map((item) => {
id: '', const {
key: item.key, id: unusedId,
value: item.value, isIndexed,
op: item.op, ...keyObj
})), } = item.key as BaseAutocompleteData;
return {
id: '',
key: keyObj,
value: item.value,
op: item.op,
};
}),
}, },
})); }));
}; };
@ -36,22 +47,22 @@ const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
export default function useFunnelConfiguration({ export default function useFunnelConfiguration({
funnel, funnel,
disableAutoSave = false,
triggerAutoSave = false, triggerAutoSave = false,
showNotifications = false, showNotifications = false,
}: { }: {
funnel: FunnelData; funnel: FunnelData;
disableAutoSave?: boolean;
triggerAutoSave?: boolean; triggerAutoSave?: boolean;
showNotifications?: boolean; showNotifications?: boolean;
}): UseFunnelConfiguration { }): UseFunnelConfiguration {
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const queryClient = useQueryClient();
const { const {
steps, steps,
initialSteps, lastUpdatedSteps,
hasIncompleteStepFields, setLastUpdatedSteps,
handleRestoreSteps, handleRestoreSteps,
handleRunFunnel, selectedTime,
setIsUpdatingFunnel,
} = useFunnelContext(); } = useFunnelContext();
// State management // State management
@ -59,10 +70,6 @@ export default function useFunnelConfiguration({
const debouncedSteps = useDebounce(steps, 200); const debouncedSteps = useDebounce(steps, 200);
const [lastValidatedSteps, setLastValidatedSteps] = useState<FunnelStepData[]>(
initialSteps,
);
// Mutation hooks // Mutation hooks
const updateStepsMutation = useUpdateFunnelSteps( const updateStepsMutation = useUpdateFunnelSteps(
funnel.funnel_id, funnel.funnel_id,
@ -71,6 +78,15 @@ export default function useFunnelConfiguration({
// Derived state // Derived state
const lastSavedStepsStateRef = useRef<FunnelStepData[]>(steps); const lastSavedStepsStateRef = useRef<FunnelStepData[]>(steps);
const hasRestoredFromLocalStorage = useRef(false);
// localStorage hook for funnel steps
const localStorageKey = `${LOCALSTORAGE.FUNNEL_STEPS}_${funnel.funnel_id}`;
const [
localStorageSavedSteps,
setLocalStorageSavedSteps,
clearLocalStorageSavedSteps,
] = useLocalStorage<FunnelStepData[] | null>(localStorageKey, null);
const hasStepsChanged = useCallback(() => { const hasStepsChanged = useCallback(() => {
const normalizedLastSavedSteps = normalizeSteps( const normalizedLastSavedSteps = normalizeSteps(
@ -80,6 +96,34 @@ export default function useFunnelConfiguration({
return !isEqual(normalizedDebouncedSteps, normalizedLastSavedSteps); return !isEqual(normalizedDebouncedSteps, normalizedLastSavedSteps);
}, [debouncedSteps]); }, [debouncedSteps]);
// Handle localStorage for funnel steps
useEffect(() => {
// Restore from localStorage on first run if
if (!hasRestoredFromLocalStorage.current) {
const savedSteps = localStorageSavedSteps;
if (savedSteps) {
handleRestoreSteps(savedSteps);
hasRestoredFromLocalStorage.current = true;
return;
}
}
// Save steps to localStorage
if (hasStepsChanged()) {
setLocalStorageSavedSteps(debouncedSteps);
}
}, [
debouncedSteps,
funnel.funnel_id,
hasStepsChanged,
handleRestoreSteps,
localStorageSavedSteps,
setLocalStorageSavedSteps,
queryClient,
selectedTime,
lastUpdatedSteps,
]);
const hasFunnelStepDefinitionsChanged = useCallback( const hasFunnelStepDefinitionsChanged = useCallback(
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean => { (prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean => {
if (prevSteps.length !== nextSteps.length) return true; if (prevSteps.length !== nextSteps.length) return true;
@ -97,15 +141,6 @@ export default function useFunnelConfiguration({
[], [],
); );
const hasFunnelLatencyTypeChanged = useCallback(
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean =>
prevSteps.some((step, index) => {
const nextStep = nextSteps[index];
return step.latency_type !== nextStep.latency_type;
}),
[],
);
// Mutation payload preparation // Mutation payload preparation
const getUpdatePayload = useCallback( const getUpdatePayload = useCallback(
() => ({ () => ({
@ -116,33 +151,19 @@ export default function useFunnelConfiguration({
[funnel.funnel_id, debouncedSteps], [funnel.funnel_id, debouncedSteps],
); );
const queryClient = useQueryClient();
const { selectedTime } = useFunnelContext();
const validateStepsQueryKey = useMemo(
() => [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnel.funnel_id, selectedTime],
[funnel.funnel_id, selectedTime],
);
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => { useEffect(() => {
// Determine if we should save based on the mode if (triggerAutoSave && !isEqual(debouncedSteps, lastUpdatedSteps)) {
let shouldSave = false; setIsUpdatingFunnel(true);
if (disableAutoSave) {
// Manual save mode: only save when explicitly triggered
shouldSave = triggerAutoSave;
} else {
// Auto-save mode: save when steps have changed and no incomplete fields
shouldSave = hasStepsChanged() && !hasIncompleteStepFields;
}
if (shouldSave && !isEqual(debouncedSteps, lastValidatedSteps)) {
updateStepsMutation.mutate(getUpdatePayload(), { updateStepsMutation.mutate(getUpdatePayload(), {
onSuccess: (data) => { onSuccess: (data) => {
const updatedFunnelSteps = data?.payload?.steps; const updatedFunnelSteps = data?.payload?.steps;
if (!updatedFunnelSteps) return; if (!updatedFunnelSteps) return;
// Clear localStorage since steps are saved successfully
clearLocalStorageSavedSteps();
queryClient.setQueryData( queryClient.setQueryData(
[REACT_QUERY_KEY.GET_FUNNEL_DETAILS, funnel.funnel_id], [REACT_QUERY_KEY.GET_FUNNEL_DETAILS, funnel.funnel_id],
(oldData: any) => { (oldData: any) => {
@ -163,17 +184,9 @@ export default function useFunnelConfiguration({
(step) => step.service_name === '' || step.span_name === '', (step) => step.service_name === '' || step.span_name === '',
); );
if (hasFunnelLatencyTypeChanged(lastValidatedSteps, debouncedSteps)) {
handleRunFunnel();
setLastValidatedSteps(debouncedSteps);
}
// Only validate if funnel steps definitions // Only validate if funnel steps definitions
else if ( if (!hasIncompleteStepFields) {
!hasIncompleteStepFields && setLastUpdatedSteps(debouncedSteps);
hasFunnelStepDefinitionsChanged(lastValidatedSteps, debouncedSteps)
) {
queryClient.refetchQueries(validateStepsQueryKey);
setLastValidatedSteps(debouncedSteps);
} }
// Show success notification only when requested // Show success notification only when requested
@ -216,17 +229,18 @@ export default function useFunnelConfiguration({
getUpdatePayload, getUpdatePayload,
hasFunnelStepDefinitionsChanged, hasFunnelStepDefinitionsChanged,
hasStepsChanged, hasStepsChanged,
lastValidatedSteps, lastUpdatedSteps,
queryClient, queryClient,
validateStepsQueryKey,
triggerAutoSave, triggerAutoSave,
showNotifications, showNotifications,
disableAutoSave, localStorageSavedSteps,
clearLocalStorageSavedSteps,
]); ]);
return { return {
isPopoverOpen, isPopoverOpen,
setIsPopoverOpen, setIsPopoverOpen,
steps, steps,
isSaving: updateStepsMutation.isLoading,
}; };
} }

View File

@ -20,10 +20,11 @@ export function useFunnelMetrics({
metricsData: MetricItem[]; metricsData: MetricItem[];
conversionRate: number; conversionRate: number;
} { } {
const { startTime, endTime } = useFunnelContext(); const { startTime, endTime, steps } = useFunnelContext();
const payload = { const payload = {
start_time: startTime, start_time: startTime,
end_time: endTime, end_time: endTime,
steps,
}; };
const { const {
@ -81,6 +82,7 @@ export function useFunnelStepsMetrics({
end_time: endTime, end_time: endTime,
step_start: stepStart, step_start: stepStart,
step_end: stepEnd, step_end: stepEnd,
steps,
}; };
const { const {

View File

@ -7,6 +7,7 @@ import {
FunnelOverviewResponse, FunnelOverviewResponse,
FunnelStepsOverviewPayload, FunnelStepsOverviewPayload,
FunnelStepsOverviewResponse, FunnelStepsOverviewResponse,
FunnelStepsPayload,
FunnelStepsResponse, FunnelStepsResponse,
getFunnelById, getFunnelById,
getFunnelErrorTraces, getFunnelErrorTraces,
@ -37,6 +38,7 @@ import {
CreateFunnelPayload, CreateFunnelPayload,
CreateFunnelResponse, CreateFunnelResponse,
FunnelData, FunnelData,
FunnelStepData,
} from 'types/api/traceFunnels'; } from 'types/api/traceFunnels';
export const useFunnelsList = (): UseQueryResult< export const useFunnelsList = (): UseQueryResult<
@ -117,12 +119,14 @@ export const useValidateFunnelSteps = ({
startTime, startTime,
endTime, endTime,
enabled, enabled,
steps,
}: { }: {
funnelId: string; funnelId: string;
selectedTime: string; selectedTime: string;
startTime: number; startTime: number;
endTime: number; endTime: number;
enabled: boolean; enabled: boolean;
steps: FunnelStepData[];
}): UseQueryResult< }): UseQueryResult<
SuccessResponse<ValidateFunnelResponse> | ErrorResponse, SuccessResponse<ValidateFunnelResponse> | ErrorResponse,
Error Error
@ -130,11 +134,19 @@ export const useValidateFunnelSteps = ({
useQuery({ useQuery({
queryFn: ({ signal }) => queryFn: ({ signal }) =>
validateFunnelSteps( validateFunnelSteps(
funnelId, { start_time: startTime, end_time: endTime, steps },
{ start_time: startTime, end_time: endTime },
signal, signal,
), ),
queryKey: [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime], queryKey: [
REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS,
funnelId,
selectedTime,
steps.map((step) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { latency_type, ...rest } = step;
return rest;
}),
],
enabled, enabled,
staleTime: 0, staleTime: 0,
}); });
@ -168,18 +180,17 @@ export const useFunnelOverview = (
const { const {
selectedTime, selectedTime,
validTracesCount, validTracesCount,
hasFunnelBeenExecuted, isUpdatingFunnel,
} = useFunnelContext(); } = useFunnelContext();
return useQuery({ return useQuery({
queryFn: ({ signal }) => getFunnelOverview(funnelId, payload, signal), queryFn: ({ signal }) => getFunnelOverview(payload, signal),
queryKey: [ queryKey: [
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW, REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
funnelId, funnelId,
selectedTime, selectedTime,
payload.step_start ?? '', payload.steps,
payload.step_end ?? '',
], ],
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted, enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
}); });
}; };
@ -190,18 +201,19 @@ export const useFunnelSlowTraces = (
const { const {
selectedTime, selectedTime,
validTracesCount, validTracesCount,
hasFunnelBeenExecuted, isUpdatingFunnel,
} = useFunnelContext(); } = useFunnelContext();
return useQuery<SuccessResponse<SlowTraceData> | ErrorResponse, Error>({ return useQuery<SuccessResponse<SlowTraceData> | ErrorResponse, Error>({
queryFn: ({ signal }) => getFunnelSlowTraces(funnelId, payload, signal), queryFn: ({ signal }) => getFunnelSlowTraces(payload, signal),
queryKey: [ queryKey: [
REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES, REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES,
funnelId, funnelId,
selectedTime, selectedTime,
payload.step_start ?? '', payload.step_start ?? '',
payload.step_end ?? '', payload.step_end ?? '',
payload.steps,
], ],
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted, enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
}); });
}; };
@ -212,7 +224,7 @@ export const useFunnelErrorTraces = (
const { const {
selectedTime, selectedTime,
validTracesCount, validTracesCount,
hasFunnelBeenExecuted, isUpdatingFunnel,
} = useFunnelContext(); } = useFunnelContext();
return useQuery({ return useQuery({
queryFn: ({ signal }) => getFunnelErrorTraces(funnelId, payload, signal), queryFn: ({ signal }) => getFunnelErrorTraces(funnelId, payload, signal),
@ -222,35 +234,31 @@ export const useFunnelErrorTraces = (
selectedTime, selectedTime,
payload.step_start ?? '', payload.step_start ?? '',
payload.step_end ?? '', payload.step_end ?? '',
payload.steps,
], ],
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted, enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
}); });
}; };
export function useFunnelStepsGraphData( export function useFunnelStepsGraphData(
funnelId: string, funnelId: string,
payload: FunnelStepsPayload,
): UseQueryResult<SuccessResponse<FunnelStepsResponse> | ErrorResponse, Error> { ): UseQueryResult<SuccessResponse<FunnelStepsResponse> | ErrorResponse, Error> {
const { const {
startTime,
endTime,
selectedTime, selectedTime,
validTracesCount, validTracesCount,
hasFunnelBeenExecuted, isUpdatingFunnel,
} = useFunnelContext(); } = useFunnelContext();
return useQuery({ return useQuery({
queryFn: ({ signal }) => queryFn: ({ signal }) => getFunnelSteps(payload, signal),
getFunnelSteps(
funnelId,
{ start_time: startTime, end_time: endTime },
signal,
),
queryKey: [ queryKey: [
REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA, REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA,
funnelId, funnelId,
selectedTime, selectedTime,
payload.steps,
], ],
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted, enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
}); });
} }
@ -264,17 +272,18 @@ export const useFunnelStepsOverview = (
const { const {
selectedTime, selectedTime,
validTracesCount, validTracesCount,
hasFunnelBeenExecuted, isUpdatingFunnel,
} = useFunnelContext(); } = useFunnelContext();
return useQuery({ return useQuery({
queryFn: ({ signal }) => getFunnelStepsOverview(funnelId, payload, signal), queryFn: ({ signal }) => getFunnelStepsOverview(payload, signal),
queryKey: [ queryKey: [
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW, REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
funnelId, funnelId,
selectedTime, selectedTime,
payload.step_start ?? '', payload.step_start ?? '',
payload.step_end ?? '', payload.step_end ?? '',
payload.steps,
], ],
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted, enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
}); });
}; };

View File

@ -2,6 +2,7 @@ import './DeleteFunnelStep.styles.scss';
import SignozModal from 'components/SignozModal/SignozModal'; import SignozModal from 'components/SignozModal/SignozModal';
import { Trash2, X } from 'lucide-react'; import { Trash2, X } from 'lucide-react';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
interface DeleteFunnelStepProps { interface DeleteFunnelStepProps {
isOpen: boolean; isOpen: boolean;
@ -14,8 +15,10 @@ function DeleteFunnelStep({
onClose, onClose,
onStepRemove, onStepRemove,
}: DeleteFunnelStepProps): JSX.Element { }: DeleteFunnelStepProps): JSX.Element {
const { handleRunFunnel } = useFunnelContext();
const handleStepRemoval = (): void => { const handleStepRemoval = (): void => {
onStepRemove(); onStepRemove();
handleRunFunnel();
onClose(); onClose();
}; };

View File

@ -6,6 +6,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import useFunnelConfiguration from 'hooks/TracesFunnels/useFunnelConfiguration'; import useFunnelConfiguration from 'hooks/TracesFunnels/useFunnelConfiguration';
import { PencilLine } from 'lucide-react'; import { PencilLine } from 'lucide-react';
import FunnelItemPopover from 'pages/TracesFunnels/components/FunnelsList/FunnelItemPopover'; import FunnelItemPopover from 'pages/TracesFunnels/components/FunnelsList/FunnelItemPopover';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import CopyToClipboard from 'periscope/components/CopyToClipboard'; import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { memo, useState } from 'react'; import { memo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2'; import { Span } from 'types/api/trace/getTraceV2';
@ -21,7 +22,6 @@ interface FunnelConfigurationProps {
funnel: FunnelData; funnel: FunnelData;
isTraceDetailsPage?: boolean; isTraceDetailsPage?: boolean;
span?: Span; span?: Span;
disableAutoSave?: boolean;
triggerAutoSave?: boolean; triggerAutoSave?: boolean;
showNotifications?: boolean; showNotifications?: boolean;
} }
@ -30,15 +30,19 @@ function FunnelConfiguration({
funnel, funnel,
isTraceDetailsPage, isTraceDetailsPage,
span, span,
disableAutoSave,
triggerAutoSave, triggerAutoSave,
showNotifications, showNotifications,
}: FunnelConfigurationProps): JSX.Element { }: FunnelConfigurationProps): JSX.Element {
const { isPopoverOpen, setIsPopoverOpen, steps } = useFunnelConfiguration({ const { triggerSave } = useFunnelContext();
const {
isPopoverOpen,
setIsPopoverOpen,
steps,
isSaving,
} = useFunnelConfiguration({
funnel, funnel,
disableAutoSave, triggerAutoSave: triggerAutoSave || triggerSave,
triggerAutoSave, showNotifications: showNotifications || triggerSave,
showNotifications,
}); });
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState<boolean>( const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState<boolean>(
false, false,
@ -106,7 +110,7 @@ function FunnelConfiguration({
{!isTraceDetailsPage && ( {!isTraceDetailsPage && (
<> <>
<StepsFooter stepsCount={steps.length} /> <StepsFooter stepsCount={steps.length} isSaving={isSaving || false} />
<AddFunnelDescriptionModal <AddFunnelDescriptionModal
isOpen={isDescriptionModalOpen} isOpen={isDescriptionModalOpen}
onClose={handleDescriptionModalClose} onClose={handleDescriptionModalClose}
@ -122,7 +126,6 @@ function FunnelConfiguration({
FunnelConfiguration.defaultProps = { FunnelConfiguration.defaultProps = {
isTraceDetailsPage: false, isTraceDetailsPage: false,
span: undefined, span: undefined,
disableAutoSave: false,
triggerAutoSave: false, triggerAutoSave: false,
showNotifications: false, showNotifications: false,
}; };

View File

@ -9,6 +9,7 @@
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
border: 1px solid var(--bg-slate-500); border: 1px solid var(--bg-slate-500);
border-radius: 6px; border-radius: 6px;
width: 100%;
.step-popover { .step-popover {
opacity: 0; opacity: 0;
width: 22px; width: 22px;

View File

@ -40,11 +40,6 @@
letter-spacing: 0.12px; letter-spacing: 0.12px;
border-radius: 2px; border-radius: 2px;
&--sync {
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-vanilla-400);
}
&--run { &--run {
background-color: var(--bg-robin-500); background-color: var(--bg-robin-500);
} }

View File

@ -1,53 +1,14 @@
import './StepsFooter.styles.scss'; import './StepsFooter.styles.scss';
import { LoadingOutlined } from '@ant-design/icons'; import { Button, Skeleton } from 'antd';
import { Button, Skeleton, Spin } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Cone, Play, RefreshCcw } from 'lucide-react'; import { Check, Cone } from 'lucide-react';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext'; import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useMemo } from 'react'; import { useIsMutating } from 'react-query';
import { useIsFetching, useIsMutating } from 'react-query';
const useFunnelResultsLoading = (): boolean => {
const { funnelId } = useFunnelContext();
const isFetchingFunnelOverview = useIsFetching({
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW, funnelId],
});
const isFetchingStepsGraphData = useIsFetching({
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA, funnelId],
});
const isFetchingErrorTraces = useIsFetching({
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_ERROR_TRACES, funnelId],
});
const isFetchingSlowTraces = useIsFetching({
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES, funnelId],
});
return useMemo(() => {
if (!funnelId) {
return false;
}
return (
!!isFetchingFunnelOverview ||
!!isFetchingStepsGraphData ||
!!isFetchingErrorTraces ||
!!isFetchingSlowTraces
);
}, [
funnelId,
isFetchingFunnelOverview,
isFetchingStepsGraphData,
isFetchingErrorTraces,
isFetchingSlowTraces,
]);
};
interface StepsFooterProps { interface StepsFooterProps {
stepsCount: number; stepsCount: number;
isSaving: boolean;
} }
function ValidTracesCount(): JSX.Element { function ValidTracesCount(): JSX.Element {
@ -93,21 +54,13 @@ function ValidTracesCount(): JSX.Element {
return <span className="steps-footer__valid-traces">Valid traces found</span>; return <span className="steps-footer__valid-traces">Valid traces found</span>;
} }
function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element { function StepsFooter({ stepsCount, isSaving }: StepsFooterProps): JSX.Element {
const { const {
validTracesCount, hasIncompleteStepFields,
handleRunFunnel, handleSaveFunnel,
hasFunnelBeenExecuted, hasUnsavedChanges,
funnelId,
} = useFunnelContext(); } = useFunnelContext();
const isFunnelResultsLoading = useFunnelResultsLoading();
const isFunnelUpdateMutating = useIsMutating([
REACT_QUERY_KEY.UPDATE_FUNNEL_STEPS,
funnelId,
]);
return ( return (
<div className="steps-footer"> <div className="steps-footer">
<div className="steps-footer__left"> <div className="steps-footer__left">
@ -117,38 +70,16 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
<ValidTracesCount /> <ValidTracesCount />
</div> </div>
<div className="steps-footer__right"> <div className="steps-footer__right">
{!!isFunnelUpdateMutating && ( <Button
<div className="steps-footer__button steps-footer__button--updating"> disabled={hasIncompleteStepFields || !hasUnsavedChanges}
<Spin onClick={handleSaveFunnel}
indicator={<LoadingOutlined style={{ color: 'grey' }} />} type="primary"
size="small" className="steps-footer__button steps-footer__button--run"
/> icon={<Check size={14} />}
Updating loading={isSaving}
</div> >
)} Save funnel
</Button>
{!hasFunnelBeenExecuted ? (
<Button
disabled={validTracesCount === 0}
onClick={handleRunFunnel}
type="primary"
className="steps-footer__button steps-footer__button--run"
icon={<Play size={16} />}
>
Run funnel
</Button>
) : (
<Button
type="text"
className="steps-footer__button steps-footer__button--sync"
icon={<RefreshCcw size={16} />}
onClick={handleRunFunnel}
loading={isFunnelResultsLoading}
disabled={validTracesCount === 0}
>
Refresh
</Button>
)}
</div> </div>
</div> </div>
); );

View File

@ -29,13 +29,20 @@ Chart.register(
); );
function FunnelGraph(): JSX.Element { function FunnelGraph(): JSX.Element {
const { funnelId } = useFunnelContext(); const { funnelId, startTime, endTime, steps } = useFunnelContext();
const payload = {
start_time: startTime,
end_time: endTime,
steps,
};
const { const {
data: stepsData, data: stepsData,
isLoading, isLoading,
isFetching, isFetching,
isError, isError,
} = useFunnelStepsGraphData(funnelId); } = useFunnelStepsGraphData(funnelId, payload);
const data = useMemo(() => stepsData?.payload?.data?.[0]?.data, [ const data = useMemo(() => stepsData?.payload?.data?.[0]?.data, [
stepsData?.payload?.data, stepsData?.payload?.data,

View File

@ -16,7 +16,6 @@ function FunnelResults(): JSX.Element {
isValidateStepsLoading, isValidateStepsLoading,
hasIncompleteStepFields, hasIncompleteStepFields,
hasAllEmptyStepFields, hasAllEmptyStepFields,
hasFunnelBeenExecuted,
funnelId, funnelId,
} = useFunnelContext(); } = useFunnelContext();
@ -47,14 +46,6 @@ function FunnelResults(): JSX.Element {
/> />
); );
} }
if (!hasFunnelBeenExecuted) {
return (
<EmptyFunnelResults
title="Funnel has not been run yet."
description="Run the funnel to see the results"
/>
);
}
return ( return (
<div className="funnel-results"> <div className="funnel-results">

View File

@ -7,6 +7,7 @@ import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { FunnelStepData } from 'types/api/traceFunnels';
import FunnelTable from './FunnelTable'; import FunnelTable from './FunnelTable';
import { topTracesTableColumns } from './utils'; import { topTracesTableColumns } from './utils';
@ -24,6 +25,7 @@ interface FunnelTopTracesTableProps {
SuccessResponse<SlowTraceData | ErrorTraceData> | ErrorResponse, SuccessResponse<SlowTraceData | ErrorTraceData> | ErrorResponse,
Error Error
>; >;
steps: FunnelStepData[];
} }
function FunnelTopTracesTable({ function FunnelTopTracesTable({
@ -32,6 +34,7 @@ function FunnelTopTracesTable({
stepBOrder, stepBOrder,
title, title,
tooltip, tooltip,
steps,
useQueryHook, useQueryHook,
}: FunnelTopTracesTableProps): JSX.Element { }: FunnelTopTracesTableProps): JSX.Element {
const { startTime, endTime } = useFunnelContext(); const { startTime, endTime } = useFunnelContext();
@ -41,8 +44,9 @@ function FunnelTopTracesTable({
end_time: endTime, end_time: endTime,
step_start: stepAOrder, step_start: stepAOrder,
step_end: stepBOrder, step_end: stepBOrder,
steps,
}), }),
[startTime, endTime, stepAOrder, stepBOrder], [startTime, endTime, stepAOrder, stepBOrder, steps],
); );
const { data: response, isLoading, isFetching } = useQueryHook( const { data: response, isLoading, isFetching } = useQueryHook(

View File

@ -6,7 +6,7 @@ import FunnelMetricsTable from './FunnelMetricsTable';
function OverallMetrics(): JSX.Element { function OverallMetrics(): JSX.Element {
const { funnelId } = useParams<{ funnelId: string }>(); const { funnelId } = useParams<{ funnelId: string }>();
const { isLoading, metricsData, conversionRate, isError } = useFunnelMetrics({ const { isLoading, metricsData, conversionRate, isError } = useFunnelMetrics({
funnelId: funnelId || '', funnelId,
}); });
return ( return (

View File

@ -52,11 +52,13 @@ function StepsTransitionResults(): JSX.Element {
funnelId={funnelId} funnelId={funnelId}
stepAOrder={stepAOrder} stepAOrder={stepAOrder}
stepBOrder={stepBOrder} stepBOrder={stepBOrder}
steps={steps}
/> />
<TopTracesWithErrors <TopTracesWithErrors
funnelId={funnelId} funnelId={funnelId}
stepAOrder={stepAOrder} stepAOrder={stepAOrder}
stepBOrder={stepBOrder} stepBOrder={stepBOrder}
steps={steps}
/> />
</div> </div>
</div> </div>

View File

@ -1,4 +1,5 @@
import { useFunnelSlowTraces } from 'hooks/TracesFunnels/useFunnels'; import { useFunnelSlowTraces } from 'hooks/TracesFunnels/useFunnels';
import { FunnelStepData } from 'types/api/traceFunnels';
import FunnelTopTracesTable from './FunnelTopTracesTable'; import FunnelTopTracesTable from './FunnelTopTracesTable';
@ -6,6 +7,7 @@ interface TopSlowestTracesProps {
funnelId: string; funnelId: string;
stepAOrder: number; stepAOrder: number;
stepBOrder: number; stepBOrder: number;
steps: FunnelStepData[];
} }
function TopSlowestTraces(props: TopSlowestTracesProps): JSX.Element { function TopSlowestTraces(props: TopSlowestTracesProps): JSX.Element {

View File

@ -1,4 +1,5 @@
import { useFunnelErrorTraces } from 'hooks/TracesFunnels/useFunnels'; import { useFunnelErrorTraces } from 'hooks/TracesFunnels/useFunnels';
import { FunnelStepData } from 'types/api/traceFunnels';
import FunnelTopTracesTable from './FunnelTopTracesTable'; import FunnelTopTracesTable from './FunnelTopTracesTable';
@ -6,6 +7,7 @@ interface TopTracesWithErrorsProps {
funnelId: string; funnelId: string;
stepAOrder: number; stepAOrder: number;
stepBOrder: number; stepBOrder: number;
steps: FunnelStepData[];
} }
function TopTracesWithErrors(props: TopTracesWithErrorsProps): JSX.Element { function TopTracesWithErrors(props: TopTracesWithErrorsProps): JSX.Element {

View File

@ -18,10 +18,4 @@ export const topTracesTableColumns = [
key: 'duration_ms', key: 'duration_ms',
render: (value: string): string => getYAxisFormattedValue(value, 'ms'), render: (value: string): string => getYAxisFormattedValue(value, 'ms'),
}, },
{
title: 'SPAN COUNT',
dataIndex: 'span_count',
key: 'span_count',
render: (value: number): string => value.toString(),
},
]; ];

View File

@ -14,8 +14,6 @@ export const initialStepsData: FunnelStepData[] = [
latency_pointer: 'start', latency_pointer: 'start',
latency_type: undefined, latency_type: undefined,
has_errors: false, has_errors: false,
name: '',
description: '',
}, },
{ {
id: v4(), id: v4(),
@ -29,8 +27,6 @@ export const initialStepsData: FunnelStepData[] = [
latency_pointer: 'start', latency_pointer: 'start',
latency_type: LatencyOptions.P95, latency_type: LatencyOptions.P95,
has_errors: false, has_errors: false,
name: '',
description: '',
}, },
]; ];

View File

@ -1,15 +1,15 @@
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { ValidateFunnelResponse } from 'api/traceFunnels'; import { ValidateFunnelResponse } from 'api/traceFunnels';
import { LOCALSTORAGE } from 'constants/localStorage';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Time } from 'container/TopNav/DateTimeSelection/config'; import { Time } from 'container/TopNav/DateTimeSelection/config';
import { import {
CustomTimeType, CustomTimeType,
Time as TimeV2, Time as TimeV2,
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import { normalizeSteps } from 'hooks/TracesFunnels/useFunnelConfiguration';
import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels'; import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
import { useLocalStorage } from 'hooks/useLocalStorage';
import getStartEndRangeTime from 'lib/getStartEndRangeTime'; import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { isEqual } from 'lodash-es';
import { initialStepsData } from 'pages/TracesFunnelDetails/constants'; import { initialStepsData } from 'pages/TracesFunnelDetails/constants';
import { import {
createContext, createContext,
@ -41,6 +41,9 @@ interface FunnelContextType {
handleStepChange: (index: number, newStep: Partial<FunnelStepData>) => void; handleStepChange: (index: number, newStep: Partial<FunnelStepData>) => void;
handleStepRemoval: (index: number) => void; handleStepRemoval: (index: number) => void;
handleRunFunnel: () => void; handleRunFunnel: () => void;
handleSaveFunnel: () => void;
triggerSave: boolean;
hasUnsavedChanges: boolean;
validationResponse: validationResponse:
| SuccessResponse<ValidateFunnelResponse> | SuccessResponse<ValidateFunnelResponse>
| ErrorResponse | ErrorResponse
@ -54,8 +57,10 @@ interface FunnelContextType {
spanName: string, spanName: string,
) => void; ) => void;
handleRestoreSteps: (oldSteps: FunnelStepData[]) => void; handleRestoreSteps: (oldSteps: FunnelStepData[]) => void;
hasFunnelBeenExecuted: boolean; isUpdatingFunnel: boolean;
setHasFunnelBeenExecuted: Dispatch<SetStateAction<boolean>>; setIsUpdatingFunnel: Dispatch<SetStateAction<boolean>>;
lastUpdatedSteps: FunnelStepData[];
setLastUpdatedSteps: Dispatch<SetStateAction<FunnelStepData[]>>;
} }
const FunnelContext = createContext<FunnelContextType | undefined>(undefined); const FunnelContext = createContext<FunnelContextType | undefined>(undefined);
@ -86,6 +91,19 @@ export function FunnelProvider({
const funnel = data?.payload; const funnel = data?.payload;
const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData; const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData;
const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps); const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps);
const [triggerSave, setTriggerSave] = useState<boolean>(false);
const [isUpdatingFunnel, setIsUpdatingFunnel] = useState<boolean>(false);
const [lastUpdatedSteps, setLastUpdatedSteps] = useState<FunnelStepData[]>(
initialSteps,
);
// Check if there are unsaved changes by comparing with initial steps from API
const hasUnsavedChanges = useMemo(() => {
const normalizedCurrentSteps = normalizeSteps(steps);
const normalizedInitialSteps = normalizeSteps(lastUpdatedSteps);
return !isEqual(normalizedCurrentSteps, normalizedInitialSteps);
}, [steps, lastUpdatedSteps]);
const { hasIncompleteStepFields, hasAllEmptyStepFields } = useMemo( const { hasIncompleteStepFields, hasAllEmptyStepFields } = useMemo(
() => ({ () => ({
hasAllEmptyStepFields: steps.every( hasAllEmptyStepFields: steps.every(
@ -98,15 +116,6 @@ export function FunnelProvider({
[steps], [steps],
); );
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
LOCALSTORAGE.UNEXECUTED_FUNNELS,
[],
);
const [hasFunnelBeenExecuted, setHasFunnelBeenExecuted] = useState(
!unexecutedFunnels.includes(funnelId),
);
const { const {
data: validationResponse, data: validationResponse,
isLoading: isValidationLoading, isLoading: isValidationLoading,
@ -116,7 +125,13 @@ export function FunnelProvider({
selectedTime, selectedTime,
startTime, startTime,
endTime, endTime,
enabled: !!funnelId && !!selectedTime && !!startTime && !!endTime, enabled:
!!funnelId &&
!!selectedTime &&
!!startTime &&
!!endTime &&
!hasIncompleteStepFields,
steps,
}); });
const validTracesCount = useMemo( const validTracesCount = useMemo(
@ -185,11 +200,7 @@ export function FunnelProvider({
const handleRunFunnel = useCallback(async (): Promise<void> => { const handleRunFunnel = useCallback(async (): Promise<void> => {
if (validTracesCount === 0) return; if (validTracesCount === 0) return;
if (!hasFunnelBeenExecuted) {
setUnexecutedFunnels(unexecutedFunnels.filter((id) => id !== funnelId));
setHasFunnelBeenExecuted(true);
}
queryClient.refetchQueries([ queryClient.refetchQueries([
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW, REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
funnelId, funnelId,
@ -215,15 +226,13 @@ export function FunnelProvider({
funnelId, funnelId,
selectedTime, selectedTime,
]); ]);
}, [ }, [funnelId, queryClient, selectedTime, validTracesCount]);
funnelId,
hasFunnelBeenExecuted, const handleSaveFunnel = useCallback(() => {
unexecutedFunnels, setTriggerSave(true);
queryClient, // Reset the trigger after a brief moment to allow useFunnelConfiguration to pick it up
selectedTime, setTimeout(() => setTriggerSave(false), 100);
setUnexecutedFunnels, }, []);
validTracesCount,
]);
const value = useMemo<FunnelContextType>( const value = useMemo<FunnelContextType>(
() => ({ () => ({
@ -239,14 +248,19 @@ export function FunnelProvider({
handleAddStep: addNewStep, handleAddStep: addNewStep,
handleStepRemoval, handleStepRemoval,
handleRunFunnel, handleRunFunnel,
handleSaveFunnel,
triggerSave,
validationResponse, validationResponse,
isValidateStepsLoading: isValidationLoading || isValidationFetching, isValidateStepsLoading: isValidationLoading || isValidationFetching,
hasIncompleteStepFields, hasIncompleteStepFields,
hasAllEmptyStepFields, hasAllEmptyStepFields,
handleReplaceStep, handleReplaceStep,
handleRestoreSteps, handleRestoreSteps,
hasFunnelBeenExecuted, hasUnsavedChanges,
setHasFunnelBeenExecuted, setIsUpdatingFunnel,
isUpdatingFunnel,
lastUpdatedSteps,
setLastUpdatedSteps,
}), }),
[ [
funnelId, funnelId,
@ -260,6 +274,8 @@ export function FunnelProvider({
addNewStep, addNewStep,
handleStepRemoval, handleStepRemoval,
handleRunFunnel, handleRunFunnel,
handleSaveFunnel,
triggerSave,
validationResponse, validationResponse,
isValidationLoading, isValidationLoading,
isValidationFetching, isValidationFetching,
@ -267,8 +283,11 @@ export function FunnelProvider({
hasAllEmptyStepFields, hasAllEmptyStepFields,
handleReplaceStep, handleReplaceStep,
handleRestoreSteps, handleRestoreSteps,
hasFunnelBeenExecuted, hasUnsavedChanges,
setHasFunnelBeenExecuted, setIsUpdatingFunnel,
isUpdatingFunnel,
lastUpdatedSteps,
setLastUpdatedSteps,
], ],
); );

View File

@ -4,11 +4,9 @@ import { Input } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import SignozModal from 'components/SignozModal/SignozModal'; import SignozModal from 'components/SignozModal/SignozModal';
import { LOCALSTORAGE } from 'constants/localStorage';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useCreateFunnel } from 'hooks/TracesFunnels/useFunnels'; import { useCreateFunnel } from 'hooks/TracesFunnels/useFunnels';
import { useLocalStorage } from 'hooks/useLocalStorage';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
@ -34,11 +32,6 @@ function CreateFunnel({
const { safeNavigate } = useSafeNavigate(); const { safeNavigate } = useSafeNavigate();
const { pathname } = useLocation(); const { pathname } = useLocation();
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
LOCALSTORAGE.UNEXECUTED_FUNNELS,
[],
);
const handleCreate = (): void => { const handleCreate = (): void => {
createFunnelMutation.mutate( createFunnelMutation.mutate(
{ {
@ -61,9 +54,6 @@ function CreateFunnel({
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]); queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
const funnelId = data?.payload?.funnel_id; const funnelId = data?.payload?.funnel_id;
if (funnelId) {
setUnexecutedFunnels([...unexecutedFunnels, funnelId]);
}
onClose(funnelId); onClose(funnelId);
if (funnelId && redirectToDetails) { if (funnelId && redirectToDetails) {

View File

@ -2,13 +2,16 @@ import '../RenameFunnel/RenameFunnel.styles.scss';
import './DeleteFunnel.styles.scss'; import './DeleteFunnel.styles.scss';
import SignozModal from 'components/SignozModal/SignozModal'; import SignozModal from 'components/SignozModal/SignozModal';
import { LOCALSTORAGE } from 'constants/localStorage';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useDeleteFunnel } from 'hooks/TracesFunnels/useFunnels'; import { useDeleteFunnel } from 'hooks/TracesFunnels/useFunnels';
import { useLocalStorage } from 'hooks/useLocalStorage';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { Trash2, X } from 'lucide-react'; import { Trash2, X } from 'lucide-react';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { FunnelStepData } from 'types/api/traceFunnels';
interface DeleteFunnelProps { interface DeleteFunnelProps {
isOpen: boolean; isOpen: boolean;
@ -29,6 +32,13 @@ function DeleteFunnel({
const history = useHistory(); const history = useHistory();
const { pathname } = history.location; const { pathname } = history.location;
// localStorage hook for funnel steps
const localStorageKey = `${LOCALSTORAGE.FUNNEL_STEPS}_${funnelId}`;
const [, , clearLocalStorageSavedSteps] = useLocalStorage<
FunnelStepData[] | null
>(localStorageKey, null);
const handleDelete = (): void => { const handleDelete = (): void => {
deleteFunnelMutation.mutate( deleteFunnelMutation.mutate(
{ {
@ -39,6 +49,7 @@ function DeleteFunnel({
notifications.success({ notifications.success({
message: 'Funnel deleted successfully', message: 'Funnel deleted successfully',
}); });
clearLocalStorageSavedSteps();
onClose(); onClose();
if ( if (