mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-18 11:01:26 +08:00

* feat: funnels list page basic UI * feat: get funnels list data from mock API, and handle data, loading and empty states * feat: implement funnel rename * chore: move useFunnels to hooks/TracesFunnels * feat: create traces funnels details basic page + funnel -> details redirection * fix: properly display created at in funnels list item + preventDefault * chore: add tab bar to trace funnel details page * chore: traces funnel details page overall skeleton * chore: traces funnel details results skeleton * fix: hide step count for add button only * feat: funnel details page steps and configuration (#7424) * chore: add a new tab for traces funnels * feat: funnels list page basic UI * feat: get funnels list data from mock API, and handle data, loading and empty states * feat: implement funnel rename * refactor: overall improvements * feat: implement sorting in traces funnels list page * feat: add sort column key and order to url params * chore: move useFunnels to hooks/TracesFunnels * feat: implement traces funnels search and refactor search and sort by extracting to custom hooks * chore: overall improvements to rename trace funnel modal * chore: make the rename input auto-focusable * feat: handle create funnel modal * feat: delete funnel modal and functionality * fix: fix the layout shift in funnel item caused by getContainer={false} * chore: overall improvements and use live api in traces funnels * feat: create traces funnels details basic page + funnel -> details redirection * fix: funnels traces light mode UI * fix: properly display created at in funnels list item + preventDefault * refactor: extract FunnelItemPopover into a separate component * chore: hide funnel tab from traces explorer * chore: add check to display trace funnels tab only in dev environment * chore: improve funnels modals light mode * chore: overall improvements * fix: properly pass funnel details link * chore: address PR review changes * chore: add tab bar to trace funnel details page * feat: funnel step UI with service, span, and where filters * feat: build radio button component * refactor: use the SignozRadioButton in funnel results -> step transitions radio buttons * feat: inter step config (i.e. latency type) UI * chore: improve steps header styles by removing divider width * feat: funnel steps title, description, popover UI + pass data from API * chore: update FilterSelect component to conditionally add url params and accept on change * fix: fix funnel step where clause and update the state variables for filters * chore: add support for isMultiple and fix the type in FilterSelect * feat: centralize the steps state management in StepsContent * fix: move steps state up + pass steps count from state * feat: implement auto save for updating the steps whenever any step changes * feat: implement auto save for validating steps if service name or span names change * feat: impelement funnel step removal * feat: implement add details modal for funnel steps * fix: fix the overflowing time range picker * feat: funnel details empty state * feat: add support for saving funnel description * chore: overall improvements * fix: fix the light mode styles * fix: fix the failing build + broken search UI * refactor: remove the reference of useLocation from traceFunnel item in TraceModulePage constant * fix: fix the issue of update steps getting triggered on initial render if we have filters * fix: fix the edge case of stale state causing filters to be re-added after removing * feat: funnel details page results (#7451) * feat: funnel metrics table component * feat: funnel metrics and steps transition metrics components UI * feat: funnel table component * feat: slowest traces and traces with error components * fix: overall light theme fixes * fix: fix the warning * chore: add empty and loading states to FunnelMetricsTable * feat: get overall funnel metrics from the API * fix: fix the empty state of funnel metrics table * feat: get data for slowest traces and traces with errors * fix: link trace id to trace details page * fix: get data for funnel step transition metrics and refactor the existing data fetching logic * refactor: add funnel context + overall refactoring and optimizations * refactor: move steps states to funnel context + handle empty and run funnel disabled states * feat: handle run funnel * fix: improve empty state * chore: rename isValidateStepsMutationLoading -> isValidateStepsLoading * chore: improve query key * fix: display loading state if funnel results are fetching * refactor: move steps validation fetching and states to the context API * fix: display loading state in funnel results while steps validation is fetching * fix: call validate steps API only on changing the service name or span name of any step * refactor: move validateStepsQuery key out of useEffect and update the dependencies * chore: centralize hasIncompleteSteps and run validate only if steps have service and spans * fix: handle all empty fields state + overall improvements * fix: handle long where query tags * feat: build the funnel result graph component * feat: build the funnel result graph component * feat: handle loading, error, empty states in funnel graph * fix: don't display change percentage if % is 0 * refactor: overall improvements * feat: get funnel steps graph data from API + move logic to custom hook * fix: improve empty and error states * fix: handle funnel graph legends width using css * fix: redirect to trace funnels list page on clicking delete from funnel details * fix: update the query cache while updating steps * fix: implement debounced search for funnel list search * fix: refetch steps graph data query on clicking run funnel / sync button * fix: improve the step footer spacing * chore: add gap between divider to inter-step-config * fix: handle loading state while fetching * feat: add span to funnel flow (from trace details page) (#7477) * chore: display add to funnel icon on hovering any span in trace details page * chore: add className to funnel item actions popover * feat: add funnels tab to trace details v2 tab bar * feat: add span to funnel flow * chore: hide actions popover button from funnel item in span -> funnel flows * chore: improve the funnel details UI in add span to funnel modal * fix: display empty state + don't redirect to funnels list on delete success + overall improvements * chore: add null check * fix: display add to funnel button based on feature flag * fix: display funnels tab in trace details based on feature flag * fix: remove maxTagCount * feat: change ms to ns * chore: address review comments * chore: remove feature flag and display trace funnels only in dev envirnoment * fix: handle restoring steps if updating funnel steps fail * refactor: update the get and delete funnel endpoints to adjust to the BE changes (#7697) * refactor: address review comments * fix: handle nested funnel response structure to fix missing funnel_id… (#7740) * fix: handle nested funnel response structure to fix missing funnel_id in updates Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com> * chore: remove console.og Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com> * chore: revert explicitly passing funnelId to updateFunnelSteps --------- Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com> Co-authored-by: ahmadshaheer <ashaheerki@gmail.com> * chore: fix api endpoint Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com> * refactor: incorporate the recent funnel details API changes (#7760) * chore: trace funnels feedback changes (#7772) * chore: change the copy from x traces to valid traces found / not found * chore: add open funnel button in add span to funnel modal * feat: display buttons for adding step details and funnel description + copy to clipboard * feat: highlight funnel graph column based on selected (total / error span) from the legend items * chore: trace funnel changes (#7780) * refactor: handle funnels list search on frontend * refactor: use funnel steps update API for adding / updating step title and description * feat: allow selecting user's typed option in trace funnel service and span name dropdowns * chore: properly render the -> between steps in funnel results * fix: sync funnel step name with add details modal text fields --------- Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com> Co-authored-by: Yunus M <myounis.ar@live.com> Co-authored-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
465 lines
12 KiB
TypeScript
465 lines
12 KiB
TypeScript
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
|
import './Success.styles.scss';
|
|
|
|
import { ColumnDef, createColumnHelper } from '@tanstack/react-table';
|
|
import { Virtualizer } from '@tanstack/react-virtual';
|
|
import { Button, Tooltip, Typography } from 'antd';
|
|
import cx from 'classnames';
|
|
import { TableV3 } from 'components/TableV3/TableV3';
|
|
import { themeColors } from 'constants/theme';
|
|
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
|
import AddSpanToFunnelModal from 'container/TraceWaterfall/AddSpanToFunnelModal/AddSpanToFunnelModal';
|
|
import SpanLineActionButtons from 'container/TraceWaterfall/SpanLineActionButtons';
|
|
import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
|
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
|
import useUrlQuery from 'hooks/useUrlQuery';
|
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
|
import {
|
|
AlertCircle,
|
|
ArrowUpRight,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
Leaf,
|
|
} from 'lucide-react';
|
|
import {
|
|
Dispatch,
|
|
SetStateAction,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { Span } from 'types/api/trace/getTraceV2';
|
|
import { toFixed } from 'utils/toFixed';
|
|
|
|
import Filters from './Filters/Filters';
|
|
|
|
// css config
|
|
const CONNECTOR_WIDTH = 28;
|
|
const VERTICAL_CONNECTOR_WIDTH = 1;
|
|
|
|
interface ITraceMetadata {
|
|
traceId: string;
|
|
startTime: number;
|
|
endTime: number;
|
|
hasMissingSpans: boolean;
|
|
}
|
|
interface ISuccessProps {
|
|
spans: Span[];
|
|
traceMetadata: ITraceMetadata;
|
|
interestedSpanId: IInterestedSpan;
|
|
uncollapsedNodes: string[];
|
|
setInterestedSpanId: Dispatch<SetStateAction<IInterestedSpan>>;
|
|
setTraceFlamegraphStatsWidth: Dispatch<SetStateAction<number>>;
|
|
selectedSpan: Span | undefined;
|
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
|
}
|
|
|
|
function SpanOverview({
|
|
span,
|
|
isSpanCollapsed,
|
|
handleCollapseUncollapse,
|
|
setSelectedSpan,
|
|
handleAddSpanToFunnel,
|
|
selectedSpan,
|
|
}: {
|
|
span: Span;
|
|
isSpanCollapsed: boolean;
|
|
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
|
|
selectedSpan: Span | undefined;
|
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
|
|
|
handleAddSpanToFunnel: (span: Span) => void;
|
|
}): JSX.Element {
|
|
const isRootSpan = span.level === 0;
|
|
|
|
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
|
|
if (span.hasError) {
|
|
color = `var(--bg-cherry-500)`;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cx(
|
|
'span-overview',
|
|
selectedSpan?.spanId === span.spanId ? 'interested-span' : '',
|
|
)}
|
|
style={{
|
|
paddingLeft: `${
|
|
isRootSpan
|
|
? span.level * CONNECTOR_WIDTH
|
|
: (span.level - 1) * (CONNECTOR_WIDTH + VERTICAL_CONNECTOR_WIDTH)
|
|
}px`,
|
|
backgroundImage: `url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="28" height="54"><line x1="0" y1="0" x2="0" y2="54" stroke="rgb(29 33 45)" stroke-width="1" /></svg>')`,
|
|
backgroundRepeat: 'repeat',
|
|
backgroundSize: `${CONNECTOR_WIDTH + 1}px 54px`,
|
|
}}
|
|
onClick={(): void => {
|
|
setSelectedSpan(span);
|
|
}}
|
|
>
|
|
{!isRootSpan && (
|
|
<div className="connector-lines">
|
|
<div
|
|
style={{
|
|
width: `${CONNECTOR_WIDTH}px`,
|
|
height: '1px',
|
|
borderTop: '1px solid var(--bg-slate-400)',
|
|
display: 'flex',
|
|
flexShrink: 0,
|
|
position: 'relative',
|
|
top: '-10px',
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="span-overview-content">
|
|
<section className="first-row">
|
|
<div className="span-det">
|
|
{span.hasChildren ? (
|
|
<Button
|
|
onClick={(event): void => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
handleCollapseUncollapse(span.spanId, !isSpanCollapsed);
|
|
}}
|
|
className="collapse-uncollapse-button"
|
|
>
|
|
{isSpanCollapsed ? (
|
|
<ChevronRight size={14} />
|
|
) : (
|
|
<ChevronDown size={14} />
|
|
)}
|
|
<Typography.Text className="children-count">
|
|
{span.subTreeNodeCount}
|
|
</Typography.Text>
|
|
</Button>
|
|
) : (
|
|
<Button className="collapse-uncollapse-button">
|
|
<Leaf size={14} />
|
|
</Button>
|
|
)}
|
|
<Typography.Text className="span-name">{span.name}</Typography.Text>
|
|
</div>
|
|
</section>
|
|
<section className="second-row">
|
|
<div style={{ width: '2px', background: color, height: '100%' }} />
|
|
<Typography.Text className="service-name">
|
|
{span.serviceName}
|
|
</Typography.Text>
|
|
{!!span.serviceName &&
|
|
!!span.name &&
|
|
process.env.NODE_ENV === 'development' && (
|
|
<div className="add-funnel-button">
|
|
<span className="add-funnel-button__separator">·</span>
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
className="add-funnel-button__button"
|
|
onClick={(e): void => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleAddSpanToFunnel(span);
|
|
}}
|
|
icon={
|
|
<img
|
|
className="add-funnel-button__icon"
|
|
src="/Icons/funnel-add.svg"
|
|
alt="funnel-icon"
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function SpanDuration({
|
|
span,
|
|
traceMetadata,
|
|
setSelectedSpan,
|
|
selectedSpan,
|
|
}: {
|
|
span: Span;
|
|
traceMetadata: ITraceMetadata;
|
|
selectedSpan: Span | undefined;
|
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
|
}): JSX.Element {
|
|
const { time, timeUnitName } = convertTimeToRelevantUnit(
|
|
span.durationNano / 1e6,
|
|
);
|
|
|
|
const spread = traceMetadata.endTime - traceMetadata.startTime;
|
|
const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
|
|
const width = (span.durationNano * 1e2) / (spread * 1e6);
|
|
|
|
const urlQuery = useUrlQuery();
|
|
const { safeNavigate } = useSafeNavigate();
|
|
|
|
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
|
|
|
|
if (span.hasError) {
|
|
color = `var(--bg-cherry-500)`;
|
|
}
|
|
|
|
const [hasActionButtons, setHasActionButtons] = useState(false);
|
|
|
|
const handleMouseEnter = (): void => {
|
|
setHasActionButtons(true);
|
|
};
|
|
|
|
const handleMouseLeave = (): void => {
|
|
setHasActionButtons(false);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={cx(
|
|
'span-duration',
|
|
selectedSpan?.spanId === span.spanId ? 'interested-span' : '',
|
|
)}
|
|
onMouseEnter={handleMouseEnter}
|
|
onMouseLeave={handleMouseLeave}
|
|
onClick={(): void => {
|
|
setSelectedSpan(span);
|
|
if (span?.spanId) {
|
|
urlQuery.set('spanId', span?.spanId);
|
|
}
|
|
|
|
safeNavigate({ search: urlQuery.toString() });
|
|
}}
|
|
>
|
|
<div
|
|
className="span-line"
|
|
style={{
|
|
left: `${leftOffset}%`,
|
|
width: `${width}%`,
|
|
backgroundColor: color,
|
|
}}
|
|
/>
|
|
{hasActionButtons && <SpanLineActionButtons span={span} />}
|
|
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
|
|
<Typography.Text
|
|
className="span-line-text"
|
|
ellipsis
|
|
style={{ left: `${leftOffset}%`, color }}
|
|
>{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// table config
|
|
const columnDefHelper = createColumnHelper<Span>();
|
|
|
|
function getWaterfallColumns({
|
|
handleCollapseUncollapse,
|
|
uncollapsedNodes,
|
|
traceMetadata,
|
|
selectedSpan,
|
|
setSelectedSpan,
|
|
handleAddSpanToFunnel,
|
|
}: {
|
|
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
|
|
uncollapsedNodes: string[];
|
|
traceMetadata: ITraceMetadata;
|
|
selectedSpan: Span | undefined;
|
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
|
|
|
handleAddSpanToFunnel: (span: Span) => void;
|
|
}): ColumnDef<Span, any>[] {
|
|
const waterfallColumns: ColumnDef<Span, any>[] = [
|
|
columnDefHelper.display({
|
|
id: 'span-name',
|
|
header: '',
|
|
cell: (props): JSX.Element => (
|
|
<SpanOverview
|
|
span={props.row.original}
|
|
handleCollapseUncollapse={handleCollapseUncollapse}
|
|
isSpanCollapsed={!uncollapsedNodes.includes(props.row.original.spanId)}
|
|
selectedSpan={selectedSpan}
|
|
setSelectedSpan={setSelectedSpan}
|
|
handleAddSpanToFunnel={handleAddSpanToFunnel}
|
|
/>
|
|
),
|
|
size: 450,
|
|
}),
|
|
columnDefHelper.display({
|
|
id: 'span-duration',
|
|
header: () => <div />,
|
|
enableResizing: false,
|
|
cell: (props): JSX.Element => (
|
|
<SpanDuration
|
|
span={props.row.original}
|
|
traceMetadata={traceMetadata}
|
|
selectedSpan={selectedSpan}
|
|
setSelectedSpan={setSelectedSpan}
|
|
/>
|
|
),
|
|
}),
|
|
];
|
|
|
|
return waterfallColumns;
|
|
}
|
|
|
|
function Success(props: ISuccessProps): JSX.Element {
|
|
const {
|
|
spans,
|
|
traceMetadata,
|
|
interestedSpanId,
|
|
uncollapsedNodes,
|
|
setInterestedSpanId,
|
|
setTraceFlamegraphStatsWidth,
|
|
setSelectedSpan,
|
|
selectedSpan,
|
|
} = props;
|
|
const virtualizerRef = useRef<Virtualizer<HTMLDivElement, Element>>();
|
|
|
|
const handleCollapseUncollapse = useCallback(
|
|
(spanId: string, collapse: boolean) => {
|
|
setInterestedSpanId({ spanId, isUncollapsed: !collapse });
|
|
},
|
|
[setInterestedSpanId],
|
|
);
|
|
|
|
const handleVirtualizerInstanceChanged = (
|
|
instance: Virtualizer<HTMLDivElement, Element>,
|
|
): void => {
|
|
const { range } = instance;
|
|
// when there are less than 500 elements in the API call that means there is nothing to fetch on top and bottom so
|
|
// do not trigger the API call
|
|
if (spans.length < 500) return;
|
|
|
|
if (range?.startIndex === 0 && instance.isScrolling) {
|
|
// do not trigger for trace root as nothing to fetch above
|
|
if (spans[0].level !== 0) {
|
|
setInterestedSpanId({ spanId: spans[0].spanId, isUncollapsed: false });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (range?.endIndex === spans.length - 1 && instance.isScrolling) {
|
|
setInterestedSpanId({
|
|
spanId: spans[spans.length - 1].spanId,
|
|
isUncollapsed: false,
|
|
});
|
|
}
|
|
};
|
|
|
|
const [isAddSpanToFunnelModalOpen, setIsAddSpanToFunnelModalOpen] = useState(
|
|
false,
|
|
);
|
|
const [selectedSpanToAddToFunnel, setSelectedSpanToAddToFunnel] = useState<
|
|
Span | undefined
|
|
>(undefined);
|
|
const handleAddSpanToFunnel = useCallback((span: Span): void => {
|
|
setIsAddSpanToFunnelModalOpen(true);
|
|
setSelectedSpanToAddToFunnel(span);
|
|
}, []);
|
|
|
|
const columns = useMemo(
|
|
() =>
|
|
getWaterfallColumns({
|
|
handleCollapseUncollapse,
|
|
uncollapsedNodes,
|
|
traceMetadata,
|
|
selectedSpan,
|
|
setSelectedSpan,
|
|
handleAddSpanToFunnel,
|
|
}),
|
|
[
|
|
handleCollapseUncollapse,
|
|
uncollapsedNodes,
|
|
traceMetadata,
|
|
selectedSpan,
|
|
setSelectedSpan,
|
|
handleAddSpanToFunnel,
|
|
],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (interestedSpanId.spanId !== '' && virtualizerRef.current) {
|
|
const idx = spans.findIndex(
|
|
(span) => span.spanId === interestedSpanId.spanId,
|
|
);
|
|
if (idx !== -1) {
|
|
setTimeout(() => {
|
|
virtualizerRef.current?.scrollToIndex(idx, {
|
|
align: 'center',
|
|
behavior: 'auto',
|
|
});
|
|
}, 400);
|
|
|
|
setSelectedSpan(spans[idx]);
|
|
}
|
|
} else {
|
|
setSelectedSpan((prev) => {
|
|
if (!prev) {
|
|
return spans[0];
|
|
}
|
|
return prev;
|
|
});
|
|
}
|
|
}, [interestedSpanId, setSelectedSpan, spans]);
|
|
|
|
return (
|
|
<div className="success-content">
|
|
{traceMetadata.hasMissingSpans && (
|
|
<div className="missing-spans">
|
|
<section className="left-info">
|
|
<AlertCircle size={14} />
|
|
<Typography.Text className="text">
|
|
This trace has missing spans
|
|
</Typography.Text>
|
|
</section>
|
|
<Button
|
|
icon={<ArrowUpRight size={14} />}
|
|
className="right-info"
|
|
type="text"
|
|
onClick={(): WindowProxy | null =>
|
|
window.open(
|
|
'https://signoz.io/docs/userguide/traces/#missing-spans',
|
|
'_blank',
|
|
)
|
|
}
|
|
>
|
|
Learn More
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<Filters
|
|
startTime={traceMetadata.startTime / 1e3}
|
|
endTime={traceMetadata.endTime / 1e3}
|
|
traceID={traceMetadata.traceId}
|
|
/>
|
|
<TableV3
|
|
columns={columns}
|
|
data={spans}
|
|
config={{
|
|
handleVirtualizerInstanceChanged,
|
|
}}
|
|
customClassName={cx(
|
|
'waterfall-table',
|
|
traceMetadata.hasMissingSpans ? 'missing-spans-waterfall-table' : '',
|
|
)}
|
|
virtualiserRef={virtualizerRef}
|
|
setColumnWidths={setTraceFlamegraphStatsWidth}
|
|
/>
|
|
{selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
|
|
<AddSpanToFunnelModal
|
|
span={selectedSpanToAddToFunnel}
|
|
isOpen={isAddSpanToFunnelModalOpen}
|
|
onClose={(): void => setIsAddSpanToFunnelModalOpen(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default Success;
|