diff --git a/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.styles.scss b/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.styles.scss
new file mode 100644
index 0000000000..8ac9e6d8f3
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.styles.scss
@@ -0,0 +1,28 @@
+.traces-funnel-details {
+ display: flex;
+ // 45px -> height of the tab bar
+ height: calc(100vh - 45px);
+
+ &__steps-config {
+ flex-shrink: 0;
+ width: 600px;
+ border-right: 1px solid var(--bg-slate-400);
+ position: relative;
+ }
+ &__steps-results {
+ width: 100%;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar {
+ width: 0.1rem;
+ }
+ }
+}
+
+.lightMode {
+ .traces-funnel-details {
+ &__steps-config {
+ border-color: var(--bg-vanilla-300);
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.tsx b/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.tsx
index 96260dbb0e..f993c22240 100644
--- a/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.tsx
+++ b/frontend/src/pages/TracesFunnelDetails/TracesFunnelDetails.tsx
@@ -1,13 +1,42 @@
+import './TracesFunnelDetails.styles.scss';
+
+import { Typography } from 'antd';
+import Spinner from 'components/Spinner';
+import { NotFoundContainer } from 'container/GridCardLayout/GridCard/FullView/styles';
import { useFunnelDetails } from 'hooks/TracesFunnels/useFunnels';
+import { FunnelProvider } from 'pages/TracesFunnels/FunnelContext';
import { useParams } from 'react-router-dom';
+import FunnelConfiguration from './components/FunnelConfiguration/FunnelConfiguration';
+import FunnelResults from './components/FunnelResults/FunnelResults';
+
function TracesFunnelDetails(): JSX.Element {
const { funnelId } = useParams<{ funnelId: string }>();
- const { data } = useFunnelDetails({ funnelId });
+ const { data, isLoading, isError } = useFunnelDetails({ funnelId });
+
+ if (isLoading || !data?.payload) {
+ return
;
+ }
+
+ if (isError) {
+ return (
+
+ Error loading funnel details
+
+ );
+ }
+
return (
-
- TracesFunnelDetails, {JSON.stringify(data)}
-
+
+
+
);
}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.styles.scss
new file mode 100644
index 0000000000..dbf7365ac2
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.styles.scss
@@ -0,0 +1,135 @@
+.funnel-step-modal {
+ .ant-modal-content {
+ background: var(--bg-ink-400);
+ .ant-modal-header {
+ background: var(--bg-ink-400);
+ .ant-modal-title {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ line-height: 20px;
+ }
+ }
+ .ant-modal-body {
+ padding-bottom: 20px;
+ }
+ }
+
+ &__ok-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--bg-robin-500);
+ border: none;
+
+ &[disabled] {
+ background: var(--bg-slate-400);
+ opacity: 1;
+ }
+
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+
+ &__cancel-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--bg-vanilla-400);
+
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+}
+
+.funnel-step-modal-content {
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+
+ &__field {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__label {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ }
+
+ &__input {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+
+ &::placeholder {
+ color: var(--bg-vanilla-400);
+ opacity: 0.6;
+ }
+
+ &.ant-input-textarea {
+ .ant-input {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+
+ &::placeholder {
+ color: var(--bg-vanilla-400);
+ opacity: 0.6;
+ }
+ }
+ }
+ }
+}
+
+// Light mode styles
+.lightMode {
+ .funnel-step-modal {
+ .ant-modal-content {
+ .ant-modal-header {
+ background: var(--bg-vanilla-100);
+ .ant-modal-title {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ &__cancel-btn {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .funnel-step-modal-content {
+ &__label {
+ color: var(--bg-ink-400);
+ }
+
+ &__input {
+ background: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+
+ &::placeholder {
+ color: var(--bg-ink-100);
+ }
+
+ &.ant-input-textarea {
+ .ant-input {
+ background: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+
+ &::placeholder {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx
new file mode 100644
index 0000000000..8f0ab20df1
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelDescriptionModal.tsx
@@ -0,0 +1,109 @@
+import './AddFunnelDescriptionModal.styles.scss';
+
+import { Input } from 'antd';
+import SignozModal from 'components/SignozModal/SignozModal';
+import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
+import { useSaveFunnelDescription } from 'hooks/TracesFunnels/useFunnels';
+import { useNotifications } from 'hooks/useNotifications';
+import { Check, X } from 'lucide-react';
+import { useState } from 'react';
+import { useQueryClient } from 'react-query';
+
+interface AddFunnelDescriptionProps {
+ isOpen: boolean;
+ onClose: () => void;
+ funnelId: string;
+ funnelDescription: string;
+}
+
+function AddFunnelDescriptionModal({
+ isOpen,
+ onClose,
+ funnelId,
+ funnelDescription,
+}: AddFunnelDescriptionProps): JSX.Element {
+ const [description, setDescription] = useState
(funnelDescription);
+ const { notifications } = useNotifications();
+ const queryClient = useQueryClient();
+
+ const {
+ mutate: saveFunnelDescription,
+ isLoading,
+ } = useSaveFunnelDescription();
+
+ const handleCancel = (): void => {
+ setDescription('');
+ onClose();
+ };
+
+ const handleSave = (): void => {
+ saveFunnelDescription(
+ {
+ funnel_id: funnelId,
+ description,
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries([
+ REACT_QUERY_KEY.GET_FUNNEL_DETAILS,
+ funnelId,
+ ]);
+ notifications.success({
+ message: 'Success',
+ description: 'Funnel description saved successfully',
+ });
+ handleCancel();
+ },
+ onError: (error) => {
+ notifications.error({
+ message: 'Failed to save funnel description',
+ description: error.message,
+ });
+ },
+ },
+ );
+ };
+
+ return (
+ ,
+ type: 'primary',
+ className: 'funnel-step-modal__ok-btn',
+ onClick: handleSave,
+ loading: isLoading,
+ }}
+ cancelButtonProps={{
+ icon: ,
+ type: 'text',
+ className: 'funnel-step-modal__cancel-btn',
+ onClick: handleCancel,
+ disabled: isLoading,
+ }}
+ destroyOnClose
+ >
+
+
+ Description
+ setDescription(e.target.value)}
+ autoSize={{ minRows: 3, maxRows: 5 }}
+ disabled={isLoading}
+ />
+
+
+
+ );
+}
+
+export default AddFunnelDescriptionModal;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.styles.scss
new file mode 100644
index 0000000000..65438fe7d2
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.styles.scss
@@ -0,0 +1,138 @@
+.funnel-step-modal {
+ .ant-modal-content {
+ background: var(--bg-ink-400);
+ .ant-modal-header {
+ background: var(--bg-ink-400);
+ .ant-modal-title {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ line-height: 20px;
+ }
+ }
+ .ant-modal-body {
+ padding-bottom: 20px;
+ }
+ }
+
+ &__ok-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--bg-robin-500);
+ border: none;
+
+ &[disabled] {
+ background: var(--bg-slate-400);
+ opacity: 1;
+ }
+
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+
+ &__cancel-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--bg-vanilla-400);
+
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+}
+
+.funnel-step-modal-content {
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+
+ &__field {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__label {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ }
+
+ &__input {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+
+ &::placeholder {
+ color: var(--bg-vanilla-400);
+ opacity: 0.6;
+ }
+
+ &.ant-input-textarea {
+ .ant-input {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+
+ &::placeholder {
+ color: var(--bg-vanilla-400);
+ opacity: 0.6;
+ }
+ }
+ }
+ }
+}
+
+// Light mode styles
+.lightMode {
+ .funnel-step-modal {
+ .ant-modal-content {
+ .ant-modal-header {
+ .ant-modal-title {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ &__ok-btn {
+ background: var(--bg-robin-500) !important;
+ }
+
+ &__cancel-btn {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .funnel-step-modal-content {
+ &__label {
+ color: var(--bg-ink-400);
+ }
+
+ &__input {
+ background: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+
+ &::placeholder {
+ color: var(--bg-ink-100);
+ }
+
+ &.ant-input-textarea {
+ .ant-input {
+ background: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+
+ &::placeholder {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.tsx
new file mode 100644
index 0000000000..9ee93601d2
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/AddFunnelStepDetailsModal.tsx
@@ -0,0 +1,140 @@
+import './AddFunnelStepDetailsModal.styles.scss';
+
+import { Input } from 'antd';
+import SignozModal from 'components/SignozModal/SignozModal';
+import { useUpdateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
+import { useNotifications } from 'hooks/useNotifications';
+import { Check, X } from 'lucide-react';
+import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
+import { useEffect, useState } from 'react';
+
+import { FunnelStepPopoverProps } from './FunnelStepPopover';
+
+interface AddFunnelStepDetailsModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ stepData: FunnelStepPopoverProps['stepData'];
+}
+
+function AddFunnelStepDetailsModal({
+ isOpen,
+ onClose,
+ stepData,
+}: AddFunnelStepDetailsModalProps): JSX.Element {
+ const { funnelId, steps, setSteps } = useFunnelContext();
+
+ const [stepName, setStepName] = useState(stepData?.name || '');
+ const [description, setDescription] = useState(
+ stepData?.description || '',
+ );
+ const { notifications } = useNotifications();
+
+ const { mutate: updateFunnelStepDetails, isLoading } = useUpdateFunnelSteps(
+ funnelId,
+ notifications,
+ );
+
+ useEffect(() => {
+ if (isOpen) {
+ setStepName(stepData?.name || '');
+ setDescription(stepData?.description || '');
+ }
+ }, [isOpen, stepData]);
+
+ const handleCancel = (): void => {
+ setStepName('');
+ setDescription('');
+ onClose();
+ };
+
+ const handleSave = (): void => {
+ updateFunnelStepDetails(
+ {
+ funnel_id: funnelId,
+ steps: steps.map((step) => ({
+ ...step,
+ ...(step.step_order === stepData.step_order
+ ? {
+ name: stepName || '',
+ description: description || '',
+ }
+ : {}),
+ })),
+ timestamp: Date.now(),
+ },
+ {
+ onSuccess: (data) => {
+ if (data.payload?.steps) {
+ setSteps(data.payload.steps);
+ }
+ notifications.success({
+ message: 'Success',
+ description: 'Funnel step details updated successfully',
+ });
+ handleCancel();
+ },
+ onError: (error) => {
+ notifications.error({
+ message: 'Failed to update funnel step details',
+ description: error.message,
+ });
+ },
+ },
+ );
+ };
+
+ return (
+ ,
+ type: 'primary',
+ className: 'funnel-step-modal__ok-btn',
+ onClick: handleSave,
+ disabled: !stepName.trim(),
+ loading: isLoading,
+ }}
+ cancelButtonProps={{
+ icon: ,
+ type: 'text',
+ className: 'funnel-step-modal__cancel-btn',
+ onClick: handleCancel,
+ disabled: isLoading,
+ }}
+ destroyOnClose
+ >
+
+
+ Step name
+ setStepName(e.target.value)}
+ autoFocus
+ disabled={isLoading}
+ />
+
+
+ Description
+ setDescription(e.target.value)}
+ autoSize={{ minRows: 3, maxRows: 5 }}
+ disabled={isLoading}
+ />
+
+
+
+ );
+}
+
+export default AddFunnelStepDetailsModal;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/DeleteFunnelStep.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/DeleteFunnelStep.styles.scss
new file mode 100644
index 0000000000..e59996da6e
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/DeleteFunnelStep.styles.scss
@@ -0,0 +1,134 @@
+.funnel-step-modal {
+ .ant-modal-content {
+ background: var(--bg-ink-400);
+ .ant-modal-header {
+ background: var(--bg-ink-400);
+ .ant-modal-title {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ line-height: 20px;
+ }
+ }
+ .ant-modal-body {
+ padding-bottom: 20px;
+ }
+ }
+
+ &__ok-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--bg-robin-500);
+ border: none;
+
+ &[disabled] {
+ background: var(--bg-slate-400);
+ opacity: 1;
+ }
+
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+
+ &__cancel-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--bg-vanilla-400);
+
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+}
+
+.funnel-step-modal-content {
+ display: flex;
+ flex-direction: column;
+ gap: 28px;
+
+ &__field {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ &__label {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ }
+
+ &__input {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+
+ &::placeholder {
+ color: var(--bg-vanilla-400);
+ opacity: 0.6;
+ }
+
+ &.ant-input-textarea {
+ .ant-input {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ color: var(--bg-vanilla-400);
+
+ &::placeholder {
+ color: var(--bg-vanilla-400);
+ opacity: 0.6;
+ }
+ }
+ }
+ }
+}
+
+// Light mode styles
+.lightMode {
+ .funnel-step-modal {
+ .ant-modal-content {
+ .ant-modal-header {
+ .ant-modal-title {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ &__cancel-btn {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .funnel-step-modal-content {
+ &__label {
+ color: var(--bg-ink-400);
+ }
+
+ &__input {
+ background: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+
+ &::placeholder {
+ color: var(--bg-ink-100);
+ }
+
+ &.ant-input-textarea {
+ .ant-input {
+ background: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+
+ &::placeholder {
+ color: var(--bg-ink-100);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/DeleteFunnelStep.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/DeleteFunnelStep.tsx
new file mode 100644
index 0000000000..3892b83b13
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/DeleteFunnelStep.tsx
@@ -0,0 +1,53 @@
+import './DeleteFunnelStep.styles.scss';
+
+import SignozModal from 'components/SignozModal/SignozModal';
+import { Trash2, X } from 'lucide-react';
+
+interface DeleteFunnelStepProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onStepRemove: () => void;
+}
+
+function DeleteFunnelStep({
+ isOpen,
+ onClose,
+ onStepRemove,
+}: DeleteFunnelStepProps): JSX.Element {
+ const handleStepRemoval = (): void => {
+ onStepRemove();
+ onClose();
+ };
+
+ return (
+ ,
+ type: 'primary',
+ className: 'funnel-modal__ok-btn',
+ onClick: handleStepRemoval,
+ }}
+ cancelButtonProps={{
+ icon: ,
+ type: 'text',
+ className: 'funnel-modal__cancel-btn',
+ onClick: onClose,
+ }}
+ destroyOnClose
+ >
+
+ Deleting this step would stop further analytics using this step of the
+ funnel.
+
+
+ );
+}
+
+export default DeleteFunnelStep;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelBreadcrumb.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelBreadcrumb.styles.scss
new file mode 100644
index 0000000000..68c97eb2dc
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelBreadcrumb.styles.scss
@@ -0,0 +1,38 @@
+.funnel-breadcrumb {
+ height: 20px;
+ &__link {
+ display: flex;
+ align-items: center;
+ }
+ li:first-of-type {
+ .funnel-breadcrumb__title {
+ color: var(--bg-vanilla-400);
+ }
+ }
+ .ant-breadcrumb-separator {
+ color: var(--bg-vanilla-100);
+ }
+ & > ol {
+ gap: 6px;
+ }
+ &__title {
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px; /* 142.857% */
+ }
+}
+
+.lightMode {
+ .funnel-breadcrumb__title,
+ .ant-breadcrumb-separator {
+ color: var(--bg-ink-400);
+ }
+ li:first-of-type {
+ .funnel-breadcrumb__title {
+ color: var(--bg-ink-400);
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelBreadcrumb.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelBreadcrumb.tsx
new file mode 100644
index 0000000000..592fd278a0
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelBreadcrumb.tsx
@@ -0,0 +1,34 @@
+import './FunnelBreadcrumb.styles.scss';
+
+import { Breadcrumb } from 'antd';
+import ROUTES from 'constants/routes';
+import { Link } from 'react-router-dom';
+
+interface FunnelBreadcrumbProps {
+ funnelName: string;
+}
+
+function FunnelBreadcrumb({ funnelName }: FunnelBreadcrumbProps): JSX.Element {
+ const breadcrumbItems = [
+ {
+ title: (
+
+
+ All funnels
+
+
+ ),
+ },
+ {
+ title: {funnelName}
,
+ },
+ ];
+
+ return (
+
+
+
+ );
+}
+
+export default FunnelBreadcrumb;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration.styles.scss
new file mode 100644
index 0000000000..0c60f330e2
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration.styles.scss
@@ -0,0 +1,67 @@
+.funnel-configuration {
+ &__steps-wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
+ }
+
+ &__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--bg-slate-400);
+ &-right {
+ &,
+ & > div {
+ display: flex;
+ align-items: center;
+ }
+ gap: 12px;
+ .ant-divider-vertical {
+ margin: 0;
+ }
+ .funnel-configuration__rename-btn {
+ padding: 4px;
+ width: 24px;
+ height: 24px;
+ justify-content: center;
+ }
+ .copy-to-clipboard {
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px;
+ letter-spacing: -0.06px;
+ width: 90px;
+ }
+ }
+ }
+
+ &__description {
+ padding: 16px 16px 0 16px;
+ color: var(--bg-vanilla-400);
+ font-size: 12px;
+ line-height: 18px; /* 150% */
+ letter-spacing: -0.06px;
+ }
+
+ .funnel-item__action-icon {
+ opacity: 1;
+ }
+ &__steps {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px;
+ }
+}
+
+.lightMode {
+ .funnel-configuration {
+ &__header {
+ border-color: var(--bg-vanilla-300);
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration.tsx
new file mode 100644
index 0000000000..b8ad64e547
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration.tsx
@@ -0,0 +1,102 @@
+import './FunnelConfiguration.styles.scss';
+
+import { Button, Divider, Tooltip } from 'antd';
+import useFunnelConfiguration from 'hooks/TracesFunnels/useFunnelConfiguration';
+import { PencilLine } from 'lucide-react';
+import FunnelItemPopover from 'pages/TracesFunnels/components/FunnelsList/FunnelItemPopover';
+import CopyToClipboard from 'periscope/components/CopyToClipboard';
+import { memo, useState } from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+import { FunnelData } from 'types/api/traceFunnels';
+
+import AddFunnelDescriptionModal from './AddFunnelDescriptionModal';
+import FunnelBreadcrumb from './FunnelBreadcrumb';
+import StepsContent from './StepsContent';
+import StepsFooter from './StepsFooter';
+import StepsHeader from './StepsHeader';
+
+interface FunnelConfigurationProps {
+ funnel: FunnelData;
+ isTraceDetailsPage?: boolean;
+ span?: Span;
+}
+
+function FunnelConfiguration({
+ funnel,
+ isTraceDetailsPage,
+ span,
+}: FunnelConfigurationProps): JSX.Element {
+ const { isPopoverOpen, setIsPopoverOpen, steps } = useFunnelConfiguration({
+ funnel,
+ });
+ const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState(
+ false,
+ );
+
+ const handleDescriptionModalClose = (): void => {
+ setIsDescriptionModalOpen(false);
+ };
+
+ return (
+
+ {!isTraceDetailsPage && (
+ <>
+
+
+
+
+
+
+ }
+ onClick={(): void => setIsDescriptionModalOpen(true)}
+ aria-label="Edit Funnel Description"
+ />
+
+
+
+
+
+
+
+ {funnel?.description}
+
+ >
+ )}
+
+
+ {!isTraceDetailsPage && }
+
+
+ {!isTraceDetailsPage &&
}
+
+ {!isTraceDetailsPage && (
+
+ )}
+
+ );
+}
+
+FunnelConfiguration.defaultProps = {
+ isTraceDetailsPage: false,
+ span: undefined,
+};
+
+export default memo(FunnelConfiguration);
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.styles.scss
new file mode 100644
index 0000000000..345368c198
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.styles.scss
@@ -0,0 +1,225 @@
+.traces-funnel-where-filter {
+ .keyboard-shortcuts {
+ display: none !important;
+ }
+}
+
+.funnel-step {
+ background: var(--bg-ink-400);
+ color: var(--bg-vanilla-400);
+ border: 1px solid var(--bg-slate-500);
+ border-radius: 6px;
+ .step-popover {
+ opacity: 0;
+ width: 22px;
+ height: 22px;
+ padding: 4px;
+ background: var(--bg-ink-100);
+ border-radius: 2px;
+ position: absolute;
+ right: -11px;
+ top: -11px;
+ }
+ &:hover .step-popover {
+ opacity: 1;
+ }
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: start;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--bg-slate-500);
+ .funnel-step-details {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ &__title {
+ color: var(--bg-vanilla-400);
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+ }
+ &__description {
+ color: var(--bg-vanilla-400);
+ font-size: 12px;
+ line-height: 18px;
+ letter-spacing: -0.06px;
+ }
+ }
+ .funnel-step-actions {
+ &,
+ & > div {
+ display: flex;
+ align-items: center;
+ }
+ .ant-divider-vertical {
+ margin: 0 12px;
+ }
+ .funnel-item__action-btn {
+ border: none;
+ padding: 4px;
+ width: 24px;
+ height: 24px;
+ justify-content: center;
+ }
+ }
+ }
+ &__content {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ padding: 16px;
+ padding-left: 6px;
+ .ant-form-item {
+ margin: 0;
+ width: 100%;
+ }
+ .drag-icon {
+ cursor: grab;
+ }
+ .filters {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ .ant-select-selector {
+ background: var(--bg-ink-300);
+ border: 1px solid var(--bg-slate-500);
+ .ant-select-selection-placeholder {
+ font-size: 12px;
+ line-height: 16px;
+ }
+ }
+ &__service-and-span {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ .ant-select-selection-placeholder {
+ color: var(--bg-vanilla-400);
+ }
+ .ant-select {
+ width: 239px;
+ }
+ }
+ &__where-filter {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ .label {
+ color: var(--bg-vanilla-400);
+ font-size: 14px;
+ line-height: 20px;
+ font-weight: 400;
+ }
+ .query-builder-search-v2 {
+ width: 100%;
+ }
+ }
+ }
+ .ant-steps.ant-steps-vertical > .ant-steps-item .ant-steps-item-description {
+ padding-bottom: 16px;
+ }
+ }
+ &__footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-top: 1px solid var(--bg-slate-500);
+
+ .error {
+ display: flex;
+ align-items: center;
+ padding: 10.5px 12px 10.5px 16px;
+ gap: 20px;
+ border-right: 1px solid var(--bg-slate-500);
+ width: 50%;
+ }
+ .error__label,
+ .latency-pointer__label {
+ color: var(--bg-vanilla-400);
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+ }
+ .latency-pointer {
+ padding: 10.5px 16px 10.5px 12px;
+ width: 55%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .ant-space {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ &-item {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+ &:last-child {
+ height: 14px;
+ }
+ }
+ }
+ }
+ }
+}
+
+.lightMode {
+ .funnel-step {
+ background: var(--bg-vanilla-100);
+ color: var(--bg-ink-400);
+ border-color: var(--bg-vanilla-300);
+
+ .step-popover {
+ background: var(--bg-vanilla-100);
+ }
+
+ &__header {
+ border-color: var(--bg-vanilla-300);
+ .funnel-step-details {
+ &__title {
+ color: var(--bg-ink-400);
+ }
+ &__description {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+
+ &__content {
+ .filters {
+ .ant-select-selector {
+ background: var(--bg-vanilla-100);
+ border-color: var(--bg-vanilla-300);
+ }
+ &__service-and-span {
+ .ant-select-selection-placeholder {
+ color: var(--bg-ink-400);
+ }
+ }
+ &__where-filter {
+ .label {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+ }
+
+ &__footer {
+ &,
+ .error {
+ border-color: var(--bg-vanilla-300);
+ }
+ .error__label,
+ .latency-pointer__label {
+ color: var(--bg-ink-400);
+ }
+ .latency-pointer {
+ .ant-space-item {
+ color: var(--bg-ink-400);
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.tsx
new file mode 100644
index 0000000000..ef0c121206
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStep.tsx
@@ -0,0 +1,207 @@
+import './FunnelStep.styles.scss';
+
+import { Button, Divider, Dropdown, Form, Space, Switch, Tooltip } from 'antd';
+import { MenuProps } from 'antd/lib';
+import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
+import { QueryParams } from 'constants/query';
+import { initialQueriesMap } from 'constants/queryBuilder';
+import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
+import { ChevronDown, GripVertical, HardHat, PencilLine } from 'lucide-react';
+import { LatencyPointers } from 'pages/TracesFunnelDetails/constants';
+import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
+import { useMemo, useState } from 'react';
+import { FunnelStepData } from 'types/api/traceFunnels';
+import { DataSource } from 'types/common/queryBuilder';
+
+import FunnelStepPopover from './FunnelStepPopover';
+
+interface FunnelStepProps {
+ stepData: FunnelStepData;
+ index: number;
+ stepsCount: number;
+}
+
+function FunnelStep({
+ stepData,
+ index,
+ stepsCount,
+}: FunnelStepProps): JSX.Element {
+ const {
+ handleStepChange: onStepChange,
+ handleStepRemoval: onStepRemove,
+ } = useFunnelContext();
+ const [form] = Form.useForm();
+ const currentQuery = initialQueriesMap[DataSource.TRACES];
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const [isAddDetailsModalOpen, setIsAddDetailsModalOpen] = useState(
+ false,
+ );
+
+ const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
+ (option) => ({
+ key: option.value,
+ label: option.key,
+ style:
+ option.value === stepData.latency_pointer
+ ? { backgroundColor: 'var(--bg-slate-100)' }
+ : {},
+ }),
+ );
+
+ const updatedCurrentQuery = useMemo(
+ () => ({
+ ...currentQuery,
+ builder: {
+ ...currentQuery.builder,
+ queryData: [
+ {
+ ...currentQuery.builder.queryData[0],
+ dataSource: DataSource.TRACES,
+ filters: stepData.filters ?? {
+ op: 'AND',
+ items: [],
+ },
+ },
+ ],
+ },
+ }),
+ [stepData.filters, currentQuery],
+ );
+
+ const query = updatedCurrentQuery?.builder?.queryData[0] || null;
+
+ return (
+
+ );
+}
+
+export default FunnelStep;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStepPopover.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStepPopover.tsx
new file mode 100644
index 0000000000..7ddbd17569
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelStepPopover.tsx
@@ -0,0 +1,136 @@
+import { Button, Popover, Tooltip } from 'antd';
+import cx from 'classnames';
+import { Ellipsis, PencilLine, Trash2 } from 'lucide-react';
+import { useState } from 'react';
+import { FunnelStepData } from 'types/api/traceFunnels';
+
+import AddFunnelStepDetailsModal from './AddFunnelStepDetailsModal';
+import DeleteFunnelStep from './DeleteFunnelStep';
+
+export interface FunnelStepPopoverProps {
+ isPopoverOpen: boolean;
+ setIsPopoverOpen: (isOpen: boolean) => void;
+ className?: string;
+ stepData: {
+ step_order: FunnelStepData['step_order'];
+ name?: FunnelStepData['name'];
+ description?: FunnelStepData['description'];
+ };
+ stepsCount: number;
+ onStepRemove: () => void;
+ isAddDetailsModalOpen: boolean;
+ setIsAddDetailsModalOpen: (isOpen: boolean) => void;
+}
+
+interface FunnelStepActionsProps {
+ setIsPopoverOpen: (isOpen: boolean) => void;
+ setIsAddDetailsModalOpen: (isOpen: boolean) => void;
+ setIsDeleteModalOpen: (isOpen: boolean) => void;
+ stepsCount: number;
+}
+
+function FunnelStepActions({
+ setIsPopoverOpen,
+ setIsAddDetailsModalOpen,
+ setIsDeleteModalOpen,
+ stepsCount,
+}: FunnelStepActionsProps): JSX.Element {
+ return (
+
+ }
+ onClick={(): void => {
+ setIsPopoverOpen(false);
+ setIsAddDetailsModalOpen(true);
+ }}
+ >
+ Add details
+
+
+
+ }
+ disabled={stepsCount <= 2}
+ onClick={(): void => {
+ if (stepsCount > 2) {
+ setIsPopoverOpen(false);
+ setIsDeleteModalOpen(true);
+ }
+ }}
+ >
+ Delete
+
+
+
+ );
+}
+
+function FunnelStepPopover({
+ isPopoverOpen,
+ setIsPopoverOpen,
+ stepData,
+ className,
+ onStepRemove,
+ stepsCount,
+ isAddDetailsModalOpen,
+ setIsAddDetailsModalOpen,
+}: FunnelStepPopoverProps): JSX.Element {
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const preventDefault = (e: React.MouseEvent | React.KeyboardEvent): void => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ return (
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events
+
+
+ }
+ placement="bottomRight"
+ arrow={false}
+ destroyTooltipOnHide
+ >
+
+
+
+
setIsDeleteModalOpen(false)}
+ onStepRemove={onStepRemove}
+ />
+
+ setIsAddDetailsModalOpen(false)}
+ stepData={stepData}
+ />
+
+ );
+}
+
+FunnelStepPopover.defaultProps = {
+ className: '',
+};
+
+export default FunnelStepPopover;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/InterStepConfig.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/InterStepConfig.styles.scss
new file mode 100644
index 0000000000..fec99440c0
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/InterStepConfig.styles.scss
@@ -0,0 +1,57 @@
+.inter-step-config {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ .ant-form-item {
+ margin-bottom: 0;
+ }
+ &::before {
+ content: '';
+ position: absolute;
+ left: 4px;
+ bottom: 16px;
+ transform: translateY(-50%);
+ width: 12px;
+ height: 12px;
+ background-color: var(--bg-slate-400);
+ border-radius: 50%;
+ z-index: 1;
+ }
+ &__label {
+ color: var(--Vanilla-400, #c0c1c3);
+ font-size: 14px;
+ line-height: 20px;
+ letter-spacing: -0.07px;
+ flex-shrink: 0;
+ }
+ &__divider {
+ width: 100%;
+ .ant-divider {
+ margin: 0;
+ border-color: var(--bg-slate-400);
+ }
+ }
+ &__latency-options {
+ flex-shrink: 0;
+ }
+}
+
+.lightMode {
+ .inter-step-config {
+ background-color: var(--bg-vanilla-200);
+ color: var(--bg-ink-400);
+ &::before {
+ background-color: var(--bg-vanilla-400);
+ }
+
+ &__label {
+ color: var(--bg-ink-300);
+ }
+
+ &__divider {
+ .ant-divider {
+ border-color: var(--bg-vanilla-400);
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/InterStepConfig.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/InterStepConfig.tsx
new file mode 100644
index 0000000000..9ff0a50697
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/InterStepConfig.tsx
@@ -0,0 +1,43 @@
+import './InterStepConfig.styles.scss';
+
+import { Divider } from 'antd';
+import SignozRadioGroup from 'components/SignozRadioGroup/SignozRadioGroup';
+import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
+import { FunnelStepData, LatencyOptions } from 'types/api/traceFunnels';
+
+function InterStepConfig({
+ index,
+ step,
+}: {
+ index: number;
+ step: FunnelStepData;
+}): JSX.Element {
+ const { handleStepChange: onStepChange } = useFunnelContext();
+ const options = Object.entries(LatencyOptions).map(([key, value]) => ({
+ label: key,
+ value,
+ }));
+
+ return (
+
+
Latency type
+
+
+
+ onStepChange(index, {
+ ...step,
+ latency_type: e.target.value,
+ })
+ }
+ />
+
+
+ );
+}
+
+export default InterStepConfig;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsContent.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsContent.styles.scss
new file mode 100644
index 0000000000..819a1f04de
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsContent.styles.scss
@@ -0,0 +1,156 @@
+.steps-content {
+ height: calc(
+ 100vh - 253px
+ ); // 64px (footer) + 12 (steps gap) + 32 (steps header) + 16 (steps padding) + 50 (breadcrumb) + 34 (description) + 45 (steps footer) = 219px
+ overflow-y: auto;
+ .ant-btn {
+ box-shadow: none;
+ &-icon {
+ margin-inline-end: 0 !important;
+ }
+ }
+ &__description {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ .funnel-step-wrapper {
+ display: flex;
+ gap: 16px;
+
+ &__replace-button {
+ display: flex;
+ height: 28px;
+ padding: 5px 12px;
+ justify-content: center;
+ align-items: center;
+ gap: 6px;
+ border-radius: 3px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-ink-300);
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 18px;
+ &:disabled {
+ background-color: rgba(209, 209, 209, 0.074);
+ color: #5f5f5f;
+ }
+ }
+ }
+ }
+
+ &__add-btn {
+ border-radius: 2px;
+ border: 1px solid var(--bg-ink-200);
+ background: var(--bg-ink-200);
+ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
+ padding: 6px 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 2px;
+ }
+
+ .ant-steps-item.steps-content__add-step {
+ .ant-steps-item-icon {
+ margin-left: 4px;
+ margin-right: 20px;
+ width: 12px;
+ height: 12px;
+ }
+ .ant-steps-icon {
+ display: none;
+ }
+ }
+
+ .ant-steps-item-process .ant-steps-item-icon,
+ .ant-steps-item-icon {
+ // margin-left: 6px;
+ height: 20px;
+ width: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background-color: var(--bg-slate-400) !important;
+
+ & > .ant-steps-icon {
+ font-size: 13px;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.065px;
+ color: var(--bg-vanilla-400);
+ }
+ }
+
+ .ant-steps.ant-steps-vertical
+ > .ant-steps-item
+ > .ant-steps-item-container
+ > .ant-steps-item-tail {
+ inset-inline-start: 9px;
+ }
+ .ant-steps-item-tail {
+ padding: 20px 0 0 !important;
+
+ &::after {
+ background-color: var(--bg-slate-400) !important;
+ }
+ }
+
+ .latency-step-marker {
+ &::before {
+ content: '';
+ position: absolute;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 12px;
+ height: 12px;
+ background-color: var(--bg-ink-400);
+ border-radius: 50%;
+ z-index: 1;
+ }
+ }
+}
+
+// Light mode styles
+.lightMode {
+ .funnel-step-wrapper__replace-button {
+ background: var(--bg-vanilla-300);
+ color: var(--bg-ink-400);
+ border: none;
+ }
+ .steps-content {
+ &__add-btn {
+ background: var(--bg-vanilla-300);
+ border: none;
+ color: var(--bg-ink-400);
+
+ &:hover {
+ background: var(--bg-vanilla-400);
+ }
+ }
+
+ .ant-steps-item-icon {
+ background-color: var(--bg-vanilla-400) !important;
+
+ .ant-steps-icon {
+ color: var(--bg-ink-400);
+ }
+ }
+
+ .ant-steps-item-tail::after {
+ background-color: var(--bg-vanilla-400) !important;
+ }
+
+ .inter-step-config::before {
+ background-color: var(--bg-vanilla-400);
+ }
+
+ .latency-step-marker::before {
+ background-color: var(--bg-vanilla-400);
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsContent.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsContent.tsx
new file mode 100644
index 0000000000..5895a998da
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsContent.tsx
@@ -0,0 +1,107 @@
+import './StepsContent.styles.scss';
+
+import { Button, Steps } from 'antd';
+import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
+import { PlusIcon, Undo2 } from 'lucide-react';
+import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
+import { memo, useCallback } from 'react';
+import { Span } from 'types/api/trace/getTraceV2';
+
+import FunnelStep from './FunnelStep';
+import InterStepConfig from './InterStepConfig';
+
+const { Step } = Steps;
+
+function StepsContent({
+ isTraceDetailsPage,
+ span,
+}: {
+ isTraceDetailsPage?: boolean;
+ span?: Span;
+}): JSX.Element {
+ const { steps, handleAddStep, handleReplaceStep } = useFunnelContext();
+
+ const handleAddForNewStep = useCallback(() => {
+ if (!span) return;
+
+ const stepWasAdded = handleAddStep();
+ if (stepWasAdded) {
+ handleReplaceStep(steps.length, span.serviceName, span.name);
+ }
+ }, [span, handleAddStep, handleReplaceStep, steps.length]);
+
+ return (
+
+
+
+ {steps.map((step, index) => (
+
+
+
+ {isTraceDetailsPage && span && (
+ }
+ disabled={
+ step.service_name === span.serviceName &&
+ step.span_name === span.name
+ }
+ onClick={(): void =>
+ handleReplaceStep(index, span.serviceName, span.name)
+ }
+ >
+ Replace
+
+ )}
+
+ {/* Display InterStepConfig only between steps */}
+ {index < steps.length - 1 && (
+
+ )}
+
+ }
+ />
+ ))}
+ {/* For now we are only supporting 3 steps */}
+ {steps.length < 3 && (
+ }
+ >
+ Add Funnel Step
+
+ ) : (
+ }
+ >
+ Add for new Step
+
+ )
+ }
+ />
+ )}
+
+
+
+ );
+}
+
+StepsContent.defaultProps = {
+ isTraceDetailsPage: false,
+ span: undefined,
+};
+
+export default memo(StepsContent);
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsFooter.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsFooter.styles.scss
new file mode 100644
index 0000000000..0191085a91
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsFooter.styles.scss
@@ -0,0 +1,74 @@
+.steps-footer {
+ border-top: 1px solid var(--bg-slate-500);
+ background: var(--bg-ink-500);
+ padding: 16px;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &__left {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ font-size: 14px;
+ line-height: 18px; /* 128.571% */
+ letter-spacing: -0.07px;
+ }
+ &__valid-traces {
+ &--none {
+ color: var(--text-amber-500);
+ }
+ }
+ &__right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ &__button {
+ border: none;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ .ant-btn-icon {
+ margin-inline-end: 0 !important;
+ }
+ &--save {
+ background-color: var(--bg-slate-400);
+ }
+ &--run {
+ background-color: var(--bg-robin-500);
+ }
+ }
+}
+
+.lightMode {
+ .steps-footer {
+ border-top: 1px solid var(--bg-vanilla-300);
+ background: var(--bg-vanilla-200);
+
+ &__left {
+ color: var(--bg-ink-400);
+ }
+
+ &__valid-traces {
+ &--none {
+ color: var(--text-amber-600);
+ }
+ }
+
+ &__button {
+ &--save {
+ background: var(--bg-vanilla-300);
+ }
+ &--run {
+ background-color: var(--bg-robin-400);
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsFooter.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsFooter.tsx
new file mode 100644
index 0000000000..30b6e53e47
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelConfiguration/StepsFooter.tsx
@@ -0,0 +1,73 @@
+import './StepsFooter.styles.scss';
+
+import { Button, Skeleton } from 'antd';
+import { Cone, Play } from 'lucide-react';
+import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
+
+interface StepsFooterProps {
+ stepsCount: number;
+}
+
+function ValidTracesCount(): JSX.Element {
+ const {
+ hasAllEmptyStepFields,
+ isValidateStepsLoading,
+ hasIncompleteStepFields,
+ validTracesCount,
+ } = useFunnelContext();
+ if (isValidateStepsLoading) {
+ return
+
+
+
+
+ {Array.from({ length: totalSteps }, (_, index) => {
+ const prevTotalSpans =
+ index > 0
+ ? successSteps[index - 1] + errorSteps[index - 1]
+ : successSteps[0] + errorSteps[0];
+ return renderLegendItem(
+ index + 1,
+ successSteps[index],
+ errorSteps[index],
+ prevTotalSpans,
+ {
+ onTotalHover: () => handleLegendHover(index, 'total'),
+ onErrorHover: () => handleLegendHover(index, 'error'),
+ onLegendLeave: handleLegendLeave,
+ },
+ );
+ })}
+
+
+
+ );
+}
+
+export default FunnelGraph;
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable.styles.scss b/frontend/src/pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable.styles.scss
new file mode 100644
index 0000000000..62fbe6fc20
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable.styles.scss
@@ -0,0 +1,122 @@
+.funnel-metrics {
+ background: var(--bg-ink-500);
+ border-radius: 6px;
+ box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
+ border: 1px solid var(--bg-slate-500);
+ &--loading-state,
+ &--empty-state {
+ padding: 16px;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--bg-slate-500);
+ }
+
+ &__title {
+ color: var(--bg-vanilla-400);
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 22px;
+ letter-spacing: 0.48px;
+ text-transform: uppercase;
+ }
+
+ &__subtitle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &-label {
+ color: var(--bg-vanilla-400);
+ font-family: Inter;
+ font-size: 14px;
+ line-height: 22px; /* 157.143% */
+ letter-spacing: -0.07px;
+ }
+
+ &-value {
+ color: var(--bg-vanilla-100);
+ font-family: 'Geist Mono';
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 22px; /* 157.143% */
+ letter-spacing: -0.07px;
+ }
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ }
+
+ &__item {
+ display: flex;
+ flex-direction: column;
+ gap: 26px;
+ padding: 14px 16px;
+ &:not(:last-child) {
+ border-right: 1px solid var(--bg-slate-500);
+ }
+ &-title {
+ color: var(--bg-vanilla-100);
+ font-size: 14px;
+ line-height: 22px; /* 157.143% */
+ letter-spacing: -0.07px;
+ }
+
+ &-value,
+ &-unit {
+ color: var(--bg-vanilla-400);
+ font-family: 'Geist Mono';
+ font-size: 14px;
+ line-height: 22px; /* 157.143% */
+ letter-spacing: -0.07px;
+ }
+ }
+}
+
+.lightMode {
+ .funnel-metrics {
+ background: var(--bg-vanilla-100);
+ border: 1px solid var(--bg-vanilla-300);
+ box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.05);
+
+ &__header {
+ border-bottom: 1px solid var(--bg-vanilla-300);
+ }
+
+ &__title {
+ color: var(--text-ink-300);
+ }
+
+ &__subtitle {
+ &-label {
+ color: var(--text-ink-300);
+ }
+
+ &-value {
+ color: var(--text-ink-500);
+ }
+ }
+
+ &__item {
+ &:not(:last-child) {
+ border-right: 1px solid var(--bg-vanilla-300);
+ }
+
+ &-title {
+ color: var(--text-ink-500);
+ }
+
+ &-value,
+ &-unit {
+ color: var(--text-ink-300);
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable.tsx b/frontend/src/pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable.tsx
new file mode 100644
index 0000000000..427835fa6e
--- /dev/null
+++ b/frontend/src/pages/TracesFunnelDetails/components/FunnelResults/FunnelMetricsTable.tsx
@@ -0,0 +1,104 @@
+import './FunnelMetricsTable.styles.scss';
+
+import { Empty } from 'antd';
+import Spinner from 'components/Spinner';
+
+export interface MetricItem {
+ title: string;
+ value: string | number;
+}
+
+interface FunnelMetricsTableProps {
+ title: string;
+ subtitle?: {
+ label: string;
+ value: string | number;
+ };
+ data: MetricItem[];
+ isLoading?: boolean;
+ isError?: boolean;
+ emptyState?: JSX.Element;
+}
+
+function FunnelMetricsContentRenderer({
+ data,
+ isLoading,
+ isError,
+ emptyState,
+}: {
+ data: MetricItem[];
+ isLoading?: boolean;
+ isError?: boolean;
+ emptyState?: JSX.Element;
+}): JSX.Element {
+ if (isLoading)
+ return (
+