feat: add support for single step funnels while creating from span details (#8492)

* feat: add support for single step funnels while creating from span details

* fix: fix the UI for loading state
This commit is contained in:
Shaheer Kochai 2025-07-22 17:43:09 +04:30 committed by GitHub
parent 24d6d83575
commit b91407416b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 109 additions and 65 deletions

View File

@ -1,5 +1,12 @@
// Modal base styles // Modal base styles
.add-span-to-funnel-modal-container { .add-span-to-funnel-modal {
&__loading-spinner {
display: flex;
align-items: center;
justify-content: center;
height: 400px;
}
&-container {
.ant-modal { .ant-modal {
&-content, &-content,
&-header { &-header {
@ -64,6 +71,7 @@
} }
} }
} }
}
// Main modal styles // Main modal styles
.add-span-to-funnel-modal { .add-span-to-funnel-modal {
@ -89,7 +97,7 @@
} }
.steps-content { .steps-content {
height: 500px; max-height: 500px;
} }
} }
} }

View File

@ -99,6 +99,7 @@ function AddSpanToFunnelModal({
const [triggerSave, setTriggerSave] = useState<boolean>(false); const [triggerSave, setTriggerSave] = useState<boolean>(false);
const [isUnsavedChanges, setIsUnsavedChanges] = useState<boolean>(false); const [isUnsavedChanges, setIsUnsavedChanges] = useState<boolean>(false);
const [triggerDiscard, setTriggerDiscard] = useState<boolean>(false); const [triggerDiscard, setTriggerDiscard] = useState<boolean>(false);
const [isCreatedFromSpan, setIsCreatedFromSpan] = useState<boolean>(false);
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => { const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setSearchQuery(e.target.value); setSearchQuery(e.target.value);
@ -126,6 +127,7 @@ function AddSpanToFunnelModal({
const handleFunnelClick = (funnel: FunnelData): void => { const handleFunnelClick = (funnel: FunnelData): void => {
setSelectedFunnelId(funnel.funnel_id); setSelectedFunnelId(funnel.funnel_id);
setActiveView(ModalView.DETAILS); setActiveView(ModalView.DETAILS);
setIsCreatedFromSpan(false);
}; };
const handleBack = (): void => { const handleBack = (): void => {
@ -133,6 +135,7 @@ function AddSpanToFunnelModal({
setSelectedFunnelId(undefined); setSelectedFunnelId(undefined);
setIsUnsavedChanges(false); setIsUnsavedChanges(false);
setTriggerSave(false); setTriggerSave(false);
setIsCreatedFromSpan(false);
}; };
const handleCreateNewClick = (): void => { const handleCreateNewClick = (): void => {
@ -188,6 +191,7 @@ function AddSpanToFunnelModal({
if (funnelId) { if (funnelId) {
setSelectedFunnelId(funnelId); setSelectedFunnelId(funnelId);
setActiveView(ModalView.DETAILS); setActiveView(ModalView.DETAILS);
setIsCreatedFromSpan(true);
} }
setIsCreateModalOpen(false); setIsCreateModalOpen(false);
}} }}
@ -206,15 +210,18 @@ function AddSpanToFunnelModal({
<ArrowLeft size={14} /> <ArrowLeft size={14} />
All funnels All funnels
</Button> </Button>
<div className="traces-funnel-details">
<div className="traces-funnel-details__steps-config">
<Spin <Spin
style={{ height: 400 }} className="add-span-to-funnel-modal__loading-spinner"
spinning={isFunnelDetailsLoading || isFunnelDetailsFetching} spinning={isFunnelDetailsLoading || isFunnelDetailsFetching}
indicator={<LoadingOutlined spin />} indicator={<LoadingOutlined spin />}
> >
<div className="traces-funnel-details">
<div className="traces-funnel-details__steps-config">
{selectedFunnelId && funnelDetails?.payload && ( {selectedFunnelId && funnelDetails?.payload && (
<FunnelProvider funnelId={selectedFunnelId}> <FunnelProvider
funnelId={selectedFunnelId}
hasSingleStep={isCreatedFromSpan}
>
<FunnelDetailsView <FunnelDetailsView
funnel={funnelDetails.payload} funnel={funnelDetails.payload}
span={span} span={span}
@ -225,10 +232,10 @@ function AddSpanToFunnelModal({
/> />
</FunnelProvider> </FunnelProvider>
)} )}
</div>
</div>
</Spin> </Spin>
</div> </div>
</div>
</div>
); );
return ( return (

View File

@ -1,7 +1,7 @@
import { FunnelStepData, LatencyOptions } from 'types/api/traceFunnels'; import { FunnelStepData, LatencyOptions } from 'types/api/traceFunnels';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
export const initialStepsData: FunnelStepData[] = [ export const createInitialStepsData = (): FunnelStepData[] => [
{ {
id: v4(), id: v4(),
step_order: 1, step_order: 1,
@ -12,7 +12,6 @@ export const initialStepsData: FunnelStepData[] = [
op: 'and', op: 'and',
}, },
latency_pointer: 'start', latency_pointer: 'start',
latency_type: undefined,
has_errors: false, has_errors: false,
}, },
{ {
@ -30,6 +29,21 @@ export const initialStepsData: FunnelStepData[] = [
}, },
]; ];
export const createSingleStepData = (): FunnelStepData[] => [
{
id: v4(),
step_order: 1,
service_name: '',
span_name: '',
filters: {
items: [],
op: 'and',
},
latency_pointer: 'start',
has_errors: false,
},
];
export const LatencyPointers: { export const LatencyPointers: {
value: FunnelStepData['latency_pointer']; value: FunnelStepData['latency_pointer'];
key: string; key: string;

View File

@ -10,7 +10,10 @@ import { normalizeSteps } from 'hooks/TracesFunnels/useFunnelConfiguration';
import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels'; import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
import getStartEndRangeTime from 'lib/getStartEndRangeTime'; import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { initialStepsData } from 'pages/TracesFunnelDetails/constants'; import {
createInitialStepsData,
createSingleStepData,
} from 'pages/TracesFunnelDetails/constants';
import { import {
createContext, createContext,
Dispatch, Dispatch,
@ -68,9 +71,11 @@ const FunnelContext = createContext<FunnelContextType | undefined>(undefined);
export function FunnelProvider({ export function FunnelProvider({
children, children,
funnelId, funnelId,
hasSingleStep = false,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
funnelId: string; funnelId: string;
hasSingleStep?: boolean;
}): JSX.Element { }): JSX.Element {
const { selectedTime } = useSelector<AppState, GlobalReducer>( const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime, (state) => state.globalTime,
@ -89,7 +94,13 @@ export function FunnelProvider({
funnelId, funnelId,
]); ]);
const funnel = data?.payload; const funnel = data?.payload;
const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData;
const defaultSteps = useMemo(
() => (hasSingleStep ? createSingleStepData() : createInitialStepsData()),
[hasSingleStep],
);
const initialSteps = funnel?.steps?.length ? funnel.steps : defaultSteps;
const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps); const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps);
const [triggerSave, setTriggerSave] = useState<boolean>(false); const [triggerSave, setTriggerSave] = useState<boolean>(false);
const [isUpdatingFunnel, setIsUpdatingFunnel] = useState<boolean>(false); const [isUpdatingFunnel, setIsUpdatingFunnel] = useState<boolean>(false);
@ -155,7 +166,7 @@ export function FunnelProvider({
setSteps((prev) => [ setSteps((prev) => [
...prev, ...prev,
{ {
...initialStepsData[0], ...createInitialStepsData()[0],
id: v4(), id: v4(),
step_order: prev.length + 1, step_order: prev.length + 1,
}, },
@ -296,6 +307,10 @@ export function FunnelProvider({
); );
} }
FunnelProvider.defaultProps = {
hasSingleStep: false,
};
export function useFunnelContext(): FunnelContextType { export function useFunnelContext(): FunnelContextType {
const context = useContext(FunnelContext); const context = useContext(FunnelContext);
if (context === undefined) { if (context === undefined) {