mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-12 12:51:31 +08:00
feat(trace-details): frontend changes for trace details (#6905)
* feat(trace-details): frontend changes for trace details * feat(trace-detail): address review comments from elipsis * feat(trace0-detail): add the new drawer designs * feat(trace-detail): handle the selected span hover * feat(trace-detail): address theme colors and span selection * feat(trace-detail): fix some more css * feat(trace-detail): fix some more css * feat(trace-detail): add hoverred span and handled no data components for new drawer * feat(trace-detail): handle light mode designs * feat(trace-detail): remove the hover functionality in favor of performance * feat(trace-detail): span lines connectors * feat(trace-detail): span lines connectors * feat(trace-detail): handle the line matching for flamegraph and waterfall * feat(trace-waterfall): change the timeline color to make it less poky * feat(trace-waterfall): added where clause support in trace details page * feat(trace-waterfall): added where clause support in trace details page * feat(trace-detail): handle light mode designs * feat(trace-detail): handle light mode designs * feat(trace-detail): fix build issues * feat(trace-detail): handle loading error state for filters and flamegraph hovered state * feat(trace-detail): fix the hardcoded traceID * feat(trace-detail): remove unnecessaru use effects * feat(trace-detail): handled the flamegraph update with ID * feat(trace-detail): added timestamp bucketing and latency sampling * feat(trace-detail): extract the buckets and span limit in constants * feat(trace-detail): minor VQA comments * feat(trace-detail): remove unnecessaru use effects * feat(trace-detail): add go to related logs * feat(trace-detail): address review comments * feat(trace-detail): address review comments * feat(trace-detail): address review comments * feat(trace-detail): address review comments
This commit is contained in:
parent
8cb8beb63f
commit
08e1fd3ca5
@ -44,6 +44,7 @@
|
|||||||
"@sentry/webpack-plugin": "2.22.6",
|
"@sentry/webpack-plugin": "2.22.6",
|
||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
"@tanstack/react-table": "8.20.6",
|
"@tanstack/react-table": "8.20.6",
|
||||||
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@visx/group": "3.3.0",
|
"@visx/group": "3.3.0",
|
||||||
"@visx/shape": "3.5.0",
|
"@visx/shape": "3.5.0",
|
||||||
|
22
frontend/public/Icons/construction.svg
Normal file
22
frontend/public/Icons/construction.svg
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.02102 14.418C4.02102 14.418 3.82659 14.6658 3.32106 14.6658C2.81553 14.6658 2.62109 14.418 2.62109 14.418V5.74512H4.02102V14.418Z" fill="#9E9E9E"/>
|
||||||
|
<path d="M4.02102 14.4179C4.02102 14.4179 3.82659 14.6657 3.32106 14.6657C2.81553 14.6657 2.62109 14.4179 2.62109 14.4179V11.077C2.62109 11.077 2.76331 10.8059 3.28328 10.8059C3.80325 10.8059 4.02102 11.077 4.02102 11.077V14.4179Z" fill="#BDBDBD"/>
|
||||||
|
<path d="M13.3765 14.418C13.3765 14.418 13.1821 14.6658 12.6765 14.6658C12.171 14.6658 11.9766 14.418 11.9766 14.418V5.74512H13.3765V14.418Z" fill="#9E9E9E"/>
|
||||||
|
<path d="M13.3765 14.4179C13.3765 14.4179 13.1821 14.6657 12.6765 14.6657C12.171 14.6657 11.9766 14.4179 11.9766 14.4179V11.077C11.9766 11.077 12.1188 10.8059 12.6387 10.8059C13.1587 10.8059 13.3765 11.077 13.3765 11.077V14.4179Z" fill="#BDBDBD"/>
|
||||||
|
<path d="M14.3623 9.98379H1.63633C1.46856 9.98379 1.33301 9.84825 1.33301 9.68048V5.79624C1.33301 5.62847 1.46856 5.49292 1.63633 5.49292H14.3623C14.5301 5.49292 14.6656 5.62847 14.6656 5.79624V9.68048C14.6656 9.84825 14.5301 9.98379 14.3623 9.98379Z" fill="#9E9E9E"/>
|
||||||
|
<path d="M14.3623 5.71509H1.63633C1.46856 5.71509 1.33301 5.85064 1.33301 6.01841V9.90264C1.33301 10.0704 1.46856 10.206 1.63633 10.206H14.3623C14.5301 10.206 14.6656 10.0704 14.6656 9.90264V6.01841C14.6656 5.85064 14.5301 5.71509 14.3623 5.71509Z" fill="#FFD600"/>
|
||||||
|
<path d="M2.82515 5.71509L1.33301 7.20723V9.40712L5.02504 5.71509H2.82515Z" fill="#212121"/>
|
||||||
|
<path d="M7.01027 5.71509L2.52051 10.206H4.72039L9.21016 5.71509H7.01027Z" fill="#212121"/>
|
||||||
|
<path d="M11.1969 5.71509L6.70605 10.206H8.90594L13.3957 5.71509H11.1969Z" fill="#212121"/>
|
||||||
|
<path d="M14.6658 6.43188L10.8916 10.2061H13.0915L14.6658 8.63177V6.43188Z" fill="#212121"/>
|
||||||
|
<path d="M13.5322 5.9095H11.8223C11.6623 5.9095 11.5445 5.80506 11.5823 5.6984L11.7012 4.95288H13.6511L13.77 5.6984C13.81 5.80617 13.6922 5.9095 13.5322 5.9095Z" fill="#E2A610"/>
|
||||||
|
<path d="M12.6768 4.93514C13.4567 4.93514 14.0889 4.3029 14.0889 3.52299C14.0889 2.74308 13.4567 2.11084 12.6768 2.11084C11.8969 2.11084 11.2646 2.74308 11.2646 3.52299C11.2646 4.3029 11.8969 4.93514 12.6768 4.93514Z" fill="#FFCA28"/>
|
||||||
|
<path d="M12.6761 4.56629C13.2523 4.56629 13.7194 4.0992 13.7194 3.52301C13.7194 2.94683 13.2523 2.47974 12.6761 2.47974C12.0999 2.47974 11.6328 2.94683 11.6328 3.52301C11.6328 4.0992 12.0999 4.56629 12.6761 4.56629Z" fill="#FF5722"/>
|
||||||
|
<path d="M13.652 4.96417H11.7021C11.7021 4.96417 11.7088 4.65308 12.2666 4.65308C12.8243 4.65308 13.0932 4.65308 13.0932 4.65308C13.6832 4.65308 13.652 4.96417 13.652 4.96417Z" fill="#FFCA28"/>
|
||||||
|
<path d="M12.7998 3.02315L13.0665 2.67872C13.0787 2.66317 13.1031 2.67539 13.0987 2.69428L12.9954 3.11759C12.9709 3.21647 13.0465 3.31202 13.1487 3.3098L13.5842 3.30313C13.6042 3.30313 13.6098 3.3298 13.592 3.33869L13.1965 3.52201C13.1042 3.56534 13.0776 3.68311 13.142 3.762L13.4187 4.09865C13.4309 4.1142 13.4142 4.13531 13.3965 4.12642L13.0065 3.93199C12.9154 3.88644 12.8065 3.93866 12.7843 4.03865L12.6932 4.46529C12.6887 4.48529 12.6609 4.48529 12.6576 4.46529L12.5665 4.03865C12.5454 3.93866 12.4354 3.88644 12.3443 3.93199L11.9543 4.12642C11.9365 4.13531 11.9188 4.11309 11.9321 4.09865L12.2087 3.762C12.2732 3.68311 12.2465 3.56534 12.1543 3.52201L11.7588 3.33869C11.741 3.3298 11.7465 3.30313 11.7665 3.30313L12.2021 3.3098C12.3043 3.31091 12.3798 3.21647 12.3554 3.11759L12.2521 2.69428C12.2476 2.67539 12.2721 2.66317 12.2843 2.67872L12.5509 3.02315C12.6165 3.10314 12.7376 3.10314 12.7998 3.02315Z" fill="#FFD5CA"/>
|
||||||
|
<path d="M4.17575 5.9095H2.46584C2.30584 5.9095 2.18807 5.80506 2.22585 5.6984L2.34473 4.95288H4.29463L4.41351 5.6984C4.45351 5.80617 4.33574 5.9095 4.17575 5.9095Z" fill="#E2A610"/>
|
||||||
|
<path d="M3.3223 4.93514C4.10221 4.93514 4.73445 4.3029 4.73445 3.52299C4.73445 2.74308 4.10221 2.11084 3.3223 2.11084C2.5424 2.11084 1.91016 2.74308 1.91016 3.52299C1.91016 4.3029 2.5424 4.93514 3.3223 4.93514Z" fill="#FFCA28"/>
|
||||||
|
<path d="M3.3216 4.56629C3.89779 4.56629 4.36488 4.0992 4.36488 3.52301C4.36488 2.94683 3.89779 2.47974 3.3216 2.47974C2.74541 2.47974 2.27832 2.94683 2.27832 3.52301C2.27832 4.0992 2.74541 4.56629 3.3216 4.56629Z" fill="#FF5722"/>
|
||||||
|
<path d="M4.29658 4.96417H2.34668C2.34668 4.96417 2.35335 4.65308 2.91109 4.65308C3.46884 4.65308 3.73772 4.65308 3.73772 4.65308C4.32769 4.65308 4.29658 4.96417 4.29658 4.96417Z" fill="#FFCA28"/>
|
||||||
|
<path d="M3.44337 3.02315L3.71002 2.67872C3.72224 2.66317 3.74669 2.67539 3.74224 2.69428L3.63892 3.11759C3.61447 3.21647 3.69002 3.31202 3.79224 3.3098L4.22777 3.30313C4.24777 3.30313 4.25333 3.3298 4.23555 3.33869L3.84002 3.52201C3.7478 3.56534 3.72113 3.68311 3.78557 3.762L4.06223 4.09865C4.07445 4.1142 4.05778 4.13531 4.04001 4.12642L3.65003 3.93199C3.55892 3.88644 3.45004 3.93866 3.42782 4.03865L3.33671 4.46529C3.33227 4.48529 3.30449 4.48529 3.30116 4.46529L3.21005 4.03865C3.18894 3.93866 3.07894 3.88644 2.98784 3.93199L2.59786 4.12642C2.58008 4.13531 2.56231 4.11309 2.57564 4.09865L2.85229 3.762C2.91673 3.68311 2.89007 3.56534 2.79785 3.52201L2.40231 3.33869C2.38454 3.3298 2.39009 3.30313 2.41009 3.30313L2.84562 3.3098C2.94784 3.31091 3.02339 3.21647 2.99895 3.11759L2.89562 2.69428C2.89118 2.67539 2.91562 2.66317 2.92784 2.67872L3.19449 3.02315C3.26005 3.10314 3.38115 3.10314 3.44337 3.02315Z" fill="#FFD5CA"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 5.2 KiB |
1
frontend/public/Icons/no-data.svg
Normal file
1
frontend/public/Icons/no-data.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M17.947 6.906h-6.62V6.24h6.62v.666zM17.947 10.293h-6.62v-.667h6.62v.667z" fill="#616161"/><path d="M16.174 29.403h-2.553V3.442s.238-.553 1.278-.553 1.278.553 1.278.553v25.96h-.003z" fill="#9E9E9E"/><path d="M15.519 2.971v20.974H13.62v1.91c.322.001.613.04.876.097a1.3 1.3 0 011.024 1.269v2.182h.655V3.442c-.002 0-.14-.318-.657-.471z" fill="#757575"/><path d="M22.362 2.738a5.382 5.382 0 00-5.381 5.401 5.365 5.365 0 005.381 5.382 5.378 5.378 0 005.382-5.382c0-2.975-2.406-5.401-5.382-5.401z" fill="#2196F3"/><path d="M24.713 4.749c-.338-.085-1.011-.174-2.349-.174-1.338 0-2.01.087-2.349.174-.2.05-.869.328-.869 1.077v4.618c0 .253.205.46.46.46h.17v.51c0 .15.12.27.268.27h.545c.149 0 .269-.12.269-.27v-.51h3.008v.51c0 .15.12.27.27.27h.544c.148 0 .268-.12.268-.27v-.51h.174a.46.46 0 00.46-.46V5.826c0-.717-.67-1.029-.87-1.077zm-3.802.366c0-.113.09-.204.204-.204h2.494c.113 0 .204.09.204.204v.362a.204.204 0 01-.204.205h-2.494a.204.204 0 01-.204-.205v-.362zm.042 4.864a.139.139 0 01-.138.138h-.549a.469.469 0 01-.468-.469v-.246c0-.076.062-.138.137-.138h.55c.257 0 .468.209.468.469v.246zm3.973-.33a.469.469 0 01-.469.468h-.549a.138.138 0 01-.137-.138v-.246c0-.258.209-.47.468-.47h.55c.075 0 .137.063.137.139v.246zm.125-2.007c0 .297-.645.817-2.69.817-2.046 0-2.688-.482-2.688-.817V6.277c0-.089.089-.31.311-.31h4.791c.222 0 .276.224.276.31v1.365z" fill="#fff"/><path d="M18.86 24.652h-7.926a.506.506 0 01-.506-.507V14.99c0-.28.226-.506.506-.506h7.924c.28 0 .507.226.507.506v9.155c0 .28-.227.507-.504.507z" fill="#F5F5F5"/><path opacity=".8" d="M18.005 23.139h-6.216l-.018-7.235h6.218l.015 7.235z" fill="#82AEC0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M14.66 23.234v-7.33h.444v7.33h-.445z" fill="#F5F5F5"/><path d="M14.658 15.904h-2.886v.604h2.886v-.604z" fill="#616161"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.03 21.879h-6.263v-.223h6.264v.223zM18.03 20.412h-6.263v-.222h6.264v.222zM17.989 18.946H11.77v-.223h6.218v.223zM17.99 17.482h-6.223v-.223h6.224v.223z" fill="#F5F5F5"/><path d="M17.988 18.534h-2.886v.605h2.886v-.605zM14.672 19.999h-2.878v.604h2.878V20z" fill="#616161"/><path fill-rule="evenodd" clip-rule="evenodd" d="M10.935 14.817a.173.173 0 00-.174.173v9.155c0 .096.078.174.174.174h7.926a.173.173 0 00.171-.174V14.99a.173.173 0 00-.173-.173h-7.924zm-.84.173a.84.84 0 01.84-.84h7.924a.84.84 0 01.84.84v9.155a.84.84 0 01-.838.84h-7.926a.84.84 0 01-.84-.84V14.99z" fill="#9E9E9E"/><path d="M10.968 24.323c-.344.031-.344-.195-.344-.258V15.1c0-.25.204-.455.456-.455h7.693c.208 0 .304.127.262.384 0 0-.027-.164-.227-.164h-7.726a.237.237 0 00-.236.235v8.969c0 .209.122.255.122.255z" fill="#757575"/><path d="M12.268 4.933H4.695v6.577h7.573V4.933z" fill="#FFCA28"/><path d="M11.845 4.933c.233 0 .422.189.422.422v5.733a.422.422 0 01-.422.422H5.119a.422.422 0 01-.423-.422V5.355c0-.233.19-.422.423-.422h6.726zm0-.444H5.119a.868.868 0 00-.867.866v5.733c0 .478.389.867.867.867h6.726a.868.868 0 00.866-.867V5.355a.866.866 0 00-.866-.866z" fill="#9E9E9E"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.27 7.308H4.696v-.444h7.575v.444zM12.27 9.58H4.696v-.445h7.575v.444z" fill="#FFFDE7"/><path d="M7.066 5.664H5.162v.502h1.904v-.502zM6.598 10.27H5.162v.503h1.436v-.502zM11.648 10.27h-1.435v.503h1.435v-.502zM11.648 5.664h-1.435v.502h1.435v-.502zM10.301 7.968h-.826v.502h.826v-.502zM11.647 7.968h-.827v.502h.827v-.502zM7.802 7.968h-2.64v.502h2.64v-.502z" fill="#757575"/></svg>
|
After Width: | Height: | Size: 3.4 KiB |
@ -43,7 +43,10 @@ export const TraceFilter = Loadable(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const TraceDetail = Loadable(
|
export const TraceDetail = Loadable(
|
||||||
() => import(/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetail'),
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetailV2/index'
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UsageExplorerPage = Loadable(
|
export const UsageExplorerPage = Loadable(
|
||||||
|
27
frontend/src/api/trace/getTraceFlamegraph.tsx
Normal file
27
frontend/src/api/trace/getTraceFlamegraph.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
GetTraceFlamegraphPayloadProps,
|
||||||
|
GetTraceFlamegraphSuccessResponse,
|
||||||
|
} from 'types/api/trace/getTraceFlamegraph';
|
||||||
|
|
||||||
|
const getTraceFlamegraph = async (
|
||||||
|
props: GetTraceFlamegraphPayloadProps,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<GetTraceFlamegraphSuccessResponse> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.post<GetTraceFlamegraphSuccessResponse>(
|
||||||
|
`/traces/flamegraph/${props.traceId}`,
|
||||||
|
omit(props, 'traceId'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'Success',
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getTraceFlamegraph;
|
35
frontend/src/api/trace/getTraceV2.tsx
Normal file
35
frontend/src/api/trace/getTraceV2.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
GetTraceV2PayloadProps,
|
||||||
|
GetTraceV2SuccessResponse,
|
||||||
|
} from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
const getTraceV2 = async (
|
||||||
|
props: GetTraceV2PayloadProps,
|
||||||
|
): Promise<SuccessResponse<GetTraceV2SuccessResponse> | ErrorResponse> => {
|
||||||
|
let uncollapsedSpans = [...props.uncollapsedSpans];
|
||||||
|
if (!props.isSelectedSpanIDUnCollapsed) {
|
||||||
|
uncollapsedSpans = uncollapsedSpans.filter(
|
||||||
|
(node) => node !== props.selectedSpanId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const postData: GetTraceV2PayloadProps = {
|
||||||
|
...props,
|
||||||
|
uncollapsedSpans,
|
||||||
|
};
|
||||||
|
const response = await axios.post<GetTraceV2SuccessResponse>(
|
||||||
|
`/traces/waterfall/${props.traceId}`,
|
||||||
|
omit(postData, 'traceId'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'Success',
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getTraceV2;
|
25
frontend/src/assets/TraceDetail/Flamegraph.tsx
Normal file
25
frontend/src/assets/TraceDetail/Flamegraph.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
|
||||||
|
function FlamegraphImg(): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 3c1 3 2.5 3.5 3.5 4.5A5 5 0 0113 11a5 5 0 11-10 0c0-.3 0-.6.1-.9a2 2 0 103.3-2C4 5.5 7 3 8 3zM21 4h-8M20 14.5h-3M20 9.5h-3M21 20H4"
|
||||||
|
stroke={isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_500}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlamegraphImg;
|
134
frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss
Normal file
134
frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
.details-drawer {
|
||||||
|
.ant-drawer-wrapper-body {
|
||||||
|
border-left: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
.ant-drawer-header {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
|
.ant-drawer-header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ant-drawer-close {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
padding-right: 16px;
|
||||||
|
border-right: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-title {
|
||||||
|
padding-left: 16px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-drawer-tabs {
|
||||||
|
margin-top: 32px;
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 114px;
|
||||||
|
height: 32px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 7px 20px;
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn:hover {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab + .ant-tabs-tab {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-ink-bar {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.details-drawer {
|
||||||
|
.ant-drawer-wrapper-body {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
.ant-drawer-header {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-drawer-header-title {
|
||||||
|
.ant-drawer-close {
|
||||||
|
border-right: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-drawer-body {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-drawer-tabs {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab + .ant-tabs-tab {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
frontend/src/components/DetailsDrawer/DetailsDrawer.tsx
Normal file
57
frontend/src/components/DetailsDrawer/DetailsDrawer.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import './DetailsDrawer.styles.scss';
|
||||||
|
|
||||||
|
import { Drawer, Tabs, TabsProps } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
|
interface IDetailsDrawerProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
title: string;
|
||||||
|
descriptiveContent: JSX.Element;
|
||||||
|
defaultActiveKey: string;
|
||||||
|
items: TabsProps['items'];
|
||||||
|
detailsDrawerClassName?: string;
|
||||||
|
tabBarExtraContent?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailsDrawer(props: IDetailsDrawerProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
title,
|
||||||
|
descriptiveContent,
|
||||||
|
defaultActiveKey,
|
||||||
|
detailsDrawerClassName,
|
||||||
|
items,
|
||||||
|
tabBarExtraContent,
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
width="60%"
|
||||||
|
open={open}
|
||||||
|
afterOpenChange={setOpen}
|
||||||
|
mask={false}
|
||||||
|
title={title}
|
||||||
|
onClose={(): void => setOpen(false)}
|
||||||
|
className="details-drawer"
|
||||||
|
>
|
||||||
|
<div>{descriptiveContent}</div>
|
||||||
|
<Tabs
|
||||||
|
items={items}
|
||||||
|
addIcon
|
||||||
|
defaultActiveKey={defaultActiveKey}
|
||||||
|
animated
|
||||||
|
className={cx('details-drawer-tabs', detailsDrawerClassName)}
|
||||||
|
tabBarExtraContent={tabBarExtraContent}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DetailsDrawer.defaultProps = {
|
||||||
|
detailsDrawerClassName: '',
|
||||||
|
tabBarExtraContent: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailsDrawer;
|
@ -10,7 +10,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityText: 'INFO',
|
severityText: 'INFO',
|
||||||
severityNumber: 2,
|
severityNumber: 2,
|
||||||
@ -34,7 +34,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityText: 'INFO',
|
severityText: 'INFO',
|
||||||
severityNumber: 2,
|
severityNumber: 2,
|
||||||
@ -57,7 +57,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityText: 'INFO',
|
severityText: 'INFO',
|
||||||
severityNumber: 2,
|
severityNumber: 2,
|
||||||
@ -80,7 +80,7 @@ describe('getLogIndicatorType', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityNumber: 2,
|
severityNumber: 2,
|
||||||
body: 'Sample log',
|
body: 'Sample log',
|
||||||
@ -107,7 +107,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityNumber: 2,
|
severityNumber: 2,
|
||||||
severity_number: 2,
|
severity_number: 2,
|
||||||
@ -129,7 +129,7 @@ describe('getLogIndicatorTypeForTable', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityNumber: 0,
|
severityNumber: 0,
|
||||||
severity_number: 0,
|
severity_number: 0,
|
||||||
@ -165,7 +165,7 @@ describe('logIndicatorBySeverityNumber', () => {
|
|||||||
timestamp: 1646115296,
|
timestamp: 1646115296,
|
||||||
id: '123456',
|
id: '123456',
|
||||||
traceId: '987654',
|
traceId: '987654',
|
||||||
spanId: '54321',
|
spanID: '54321',
|
||||||
traceFlags: 0,
|
traceFlags: 0,
|
||||||
severityText: sevText,
|
severityText: sevText,
|
||||||
severityNumber: sevNum,
|
severityNumber: sevNum,
|
||||||
|
@ -7,28 +7,53 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import React, { useMemo } from 'react';
|
import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import React, {
|
||||||
|
Dispatch,
|
||||||
|
MutableRefObject,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
// here we are manually rendering the table body so that we can memoize the same for performant re-renders
|
// here we are manually rendering the table body so that we can memoize the same for performant re-renders
|
||||||
function TableBody<T>({ table }: { table: Table<T> }): JSX.Element {
|
function TableBody<T>({
|
||||||
|
table,
|
||||||
|
virtualizer,
|
||||||
|
}: {
|
||||||
|
table: Table<T>;
|
||||||
|
virtualizer: Virtualizer<HTMLDivElement, Element>;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { rows } = table.getRowModel();
|
||||||
return (
|
return (
|
||||||
<div className="div-tbody">
|
<div className="div-tbody">
|
||||||
{table.getRowModel().rows.map((row) => (
|
{virtualizer.getVirtualItems().map((virtualRow, index) => {
|
||||||
<div key={row.id} className="div-tr">
|
const row = rows[virtualRow.index];
|
||||||
{row.getVisibleCells().map((cell) => (
|
return (
|
||||||
<div
|
<div
|
||||||
key={cell.id}
|
key={virtualRow.index}
|
||||||
className="div-td"
|
className="div-tr"
|
||||||
// we are manually setting the column width here based on the calculated column vars
|
style={{
|
||||||
style={{
|
height: `${virtualRow.size}px`,
|
||||||
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
|
transform: `translateY(${virtualRow.start - index * virtualRow.size}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cell.renderValue<any>()}
|
{row.getVisibleCells().map((cell) => (
|
||||||
</div>
|
<div
|
||||||
))}
|
key={cell.id}
|
||||||
</div>
|
className="div-td"
|
||||||
))}
|
// we are manually setting the column width here based on the calculated column vars
|
||||||
|
style={{
|
||||||
|
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -40,17 +65,32 @@ const MemoizedTableBody = React.memo(
|
|||||||
) as typeof TableBody;
|
) as typeof TableBody;
|
||||||
|
|
||||||
interface ITableConfig {
|
interface ITableConfig {
|
||||||
defaultColumnMinSize: number;
|
defaultColumnMinSize?: number;
|
||||||
defaultColumnMaxSize: number;
|
defaultColumnMaxSize?: number;
|
||||||
|
handleVirtualizerInstanceChanged?: (
|
||||||
|
instance: Virtualizer<HTMLDivElement, Element>,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
interface ITableV3Props<T> {
|
interface ITableV3Props<T> {
|
||||||
columns: ColumnDef<T, any>[];
|
columns: ColumnDef<T, any>[];
|
||||||
data: T[];
|
data: T[];
|
||||||
config: ITableConfig;
|
config: ITableConfig;
|
||||||
|
customClassName?: string;
|
||||||
|
setColumnWidths: Dispatch<SetStateAction<number>>;
|
||||||
|
virtualiserRef?: MutableRefObject<
|
||||||
|
Virtualizer<HTMLDivElement, Element> | undefined
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
|
export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
|
||||||
const { data, columns, config } = props;
|
const {
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
config,
|
||||||
|
customClassName = '',
|
||||||
|
virtualiserRef,
|
||||||
|
setColumnWidths,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
@ -61,11 +101,26 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
|
|||||||
},
|
},
|
||||||
columnResizeMode: 'onChange',
|
columnResizeMode: 'onChange',
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
debugTable: true,
|
// turn on debug flags to get debug logs from these instances
|
||||||
debugHeaders: true,
|
debugAll: false,
|
||||||
debugColumns: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tableRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const { rows } = table.getRowModel();
|
||||||
|
const virtualizer = useVirtualizer({
|
||||||
|
count: rows.length,
|
||||||
|
getScrollElement: () => tableRef.current,
|
||||||
|
estimateSize: () => 54,
|
||||||
|
overscan: 20,
|
||||||
|
onChange: config.handleVirtualizerInstanceChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (virtualiserRef) {
|
||||||
|
virtualiserRef.current = virtualizer;
|
||||||
|
}
|
||||||
|
}, [virtualiserRef, virtualizer]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instead of calling `column.getSize()` on every render for every header
|
* Instead of calling `column.getSize()` on every render for every header
|
||||||
* and especially every data cell (very expensive),
|
* and especially every data cell (very expensive),
|
||||||
@ -84,14 +139,21 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
|
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const headers = table.getFlatHeaders();
|
||||||
|
setColumnWidths(headers[0].getSize());
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2">
|
<div className={cx('p-2', customClassName)} ref={tableRef}>
|
||||||
{/* Here in the <table> equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */}
|
{/* Here in the <table> equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */}
|
||||||
<div
|
<div
|
||||||
className="div-table"
|
className="div-table"
|
||||||
style={{
|
style={{
|
||||||
...columnSizeVars, // Define column sizes on the <table> element
|
...columnSizeVars, // Define column sizes on the <table> element
|
||||||
width: table.getTotalSize(),
|
width: table.getTotalSize(),
|
||||||
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="div-thead">
|
<div className="div-thead">
|
||||||
@ -113,6 +175,9 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
|
|||||||
onDoubleClick: (): void => header.column.resetSize(),
|
onDoubleClick: (): void => header.column.resetSize(),
|
||||||
onMouseDown: header.getResizeHandler(),
|
onMouseDown: header.getResizeHandler(),
|
||||||
onTouchStart: header.getResizeHandler(),
|
onTouchStart: header.getResizeHandler(),
|
||||||
|
style: {
|
||||||
|
display: !header.column.getCanResize() ? 'none' : '',
|
||||||
|
},
|
||||||
className: `resizer ${
|
className: `resizer ${
|
||||||
header.column.getIsResizing() ? 'isResizing' : ''
|
header.column.getIsResizing() ? 'isResizing' : ''
|
||||||
}`,
|
}`,
|
||||||
@ -125,11 +190,16 @@ export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
{/* When resizing any column we will render this special memoized version of our table body */}
|
{/* When resizing any column we will render this special memoized version of our table body */}
|
||||||
{table.getState().columnSizingInfo.isResizingColumn ? (
|
{table.getState().columnSizingInfo.isResizingColumn ? (
|
||||||
<MemoizedTableBody table={table} />
|
<MemoizedTableBody table={table} virtualizer={virtualizer} />
|
||||||
) : (
|
) : (
|
||||||
<TableBody table={table} />
|
<TableBody table={table} virtualizer={virtualizer} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TableV3.defaultProps = {
|
||||||
|
customClassName: '',
|
||||||
|
virtualiserRef: null,
|
||||||
|
};
|
||||||
|
@ -42,6 +42,8 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
|
|||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const strokeColor = isDarkMode ? ' rgb(192,193,195,0.8)' : 'black';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref as never} className="timeline-v2-container">
|
<div ref={ref as never} className="timeline-v2-container">
|
||||||
<svg
|
<svg
|
||||||
@ -55,7 +57,7 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
|
|||||||
y1={timelineHeight}
|
y1={timelineHeight}
|
||||||
x2={width}
|
x2={width}
|
||||||
y2={timelineHeight}
|
y2={timelineHeight}
|
||||||
stroke={isDarkMode ? 'white' : 'black'}
|
stroke={strokeColor}
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
/>
|
/>
|
||||||
{intervals &&
|
{intervals &&
|
||||||
@ -70,14 +72,14 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element {
|
|||||||
<text
|
<text
|
||||||
x={index === intervals.length - 1 ? -10 : 0}
|
x={index === intervals.length - 1 ? -10 : 0}
|
||||||
y={2 * Math.floor(timelineHeight / 4)}
|
y={2 * Math.floor(timelineHeight / 4)}
|
||||||
fill={isDarkMode ? 'white' : 'black'}
|
fill={strokeColor}
|
||||||
>
|
>
|
||||||
{interval.label}
|
{interval.label}
|
||||||
</text>
|
</text>
|
||||||
<line
|
<line
|
||||||
y1={3 * Math.floor(timelineHeight / 4)}
|
y1={3 * Math.floor(timelineHeight / 4)}
|
||||||
y2={timelineHeight + 0.5}
|
y2={timelineHeight + 0.5}
|
||||||
stroke={isDarkMode ? 'white' : 'black'}
|
stroke={strokeColor}
|
||||||
strokeWidth="1"
|
strokeWidth="1"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
@ -21,6 +21,8 @@ export const REACT_QUERY_KEY = {
|
|||||||
GET_HOST_LIST: 'GET_HOST_LIST',
|
GET_HOST_LIST: 'GET_HOST_LIST',
|
||||||
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
||||||
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
|
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
|
||||||
|
GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL',
|
||||||
|
GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH',
|
||||||
GET_POD_LIST: 'GET_POD_LIST',
|
GET_POD_LIST: 'GET_POD_LIST',
|
||||||
GET_NODE_LIST: 'GET_NODE_LIST',
|
GET_NODE_LIST: 'GET_NODE_LIST',
|
||||||
GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST',
|
GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST',
|
||||||
|
@ -1,4 +1,38 @@
|
|||||||
const themeColors = {
|
const themeColors = {
|
||||||
|
traceDetailColors: {
|
||||||
|
robin: '#3F5ECC',
|
||||||
|
dodgerBlue: '#2F80ED',
|
||||||
|
mediumOrchid: '#BB6BD9',
|
||||||
|
seaBuckthorn: '#F2994A',
|
||||||
|
turquoiseBlue: '#56CCF2',
|
||||||
|
festivalOrange: '#F2C94C',
|
||||||
|
silver: '#BDBDBD',
|
||||||
|
outrageousOrange: '#FF6633',
|
||||||
|
roseBud: '#FFB399',
|
||||||
|
canary: '#FFFF99',
|
||||||
|
deepSkyBlue: '#00B3E6',
|
||||||
|
goldTips: '#E6B333',
|
||||||
|
royalBlue: '#3366E6',
|
||||||
|
avocado: '#999966',
|
||||||
|
mintGreen: '#99FF99',
|
||||||
|
lima: '#80B300',
|
||||||
|
olive: '#809900',
|
||||||
|
beautyBush: '#E6B3B3',
|
||||||
|
danube: '#6680B3',
|
||||||
|
oliveDrab: '#66991A',
|
||||||
|
lavenderRose: '#FF99E6',
|
||||||
|
electricLime: '#CCFF1A',
|
||||||
|
turquoise: '#33FFCC',
|
||||||
|
gladeGreen: '#66994D',
|
||||||
|
hemlock: '#66664D',
|
||||||
|
vidaLoca: '#4D8000',
|
||||||
|
mediumAquamarine: '#66CDAA',
|
||||||
|
lavender: '#E6E6FA',
|
||||||
|
thistle: '#D8BFD8',
|
||||||
|
yellow: '#FFFF00',
|
||||||
|
purple: '#800080',
|
||||||
|
cyan: '#00FFFF',
|
||||||
|
},
|
||||||
chartcolors: {
|
chartcolors: {
|
||||||
robin: '#3F5ECC',
|
robin: '#3F5ECC',
|
||||||
dodgerBlue: '#2F80ED',
|
dodgerBlue: '#2F80ED',
|
||||||
|
@ -430,7 +430,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
? 0
|
? 0
|
||||||
: '0 1rem',
|
: '0 1rem',
|
||||||
|
|
||||||
...(isTraceDetailsView() ? { marginRight: 0 } : {}),
|
...(isTraceDetailsView() ? { margin: 0 } : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||||
|
@ -186,7 +186,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => {
|
|||||||
id: logData.id,
|
id: logData.id,
|
||||||
severityNumber: logData.severityNumber,
|
severityNumber: logData.severityNumber,
|
||||||
severityText: logData.severityText,
|
severityText: logData.severityText,
|
||||||
spanId: logData.spanId,
|
spanID: logData.spanID,
|
||||||
timestamp: logData.timestamp,
|
timestamp: logData.timestamp,
|
||||||
traceFlags: logData.traceFlags,
|
traceFlags: logData.traceFlags,
|
||||||
traceId: logData.traceId,
|
traceId: logData.traceId,
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
.flamegraph {
|
||||||
|
display: flex;
|
||||||
|
height: 30vh;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.flamegraph-chart {
|
||||||
|
padding: 15px;
|
||||||
|
|
||||||
|
.loading-skeleton {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flamegraph-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 16px 20px;
|
||||||
|
|
||||||
|
.exec-time-service {
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 80%;
|
||||||
|
|
||||||
|
.service-text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.square-box {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-service {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100px;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.service-progress-indicator {
|
||||||
|
width: fit-content;
|
||||||
|
margin-inline-end: 0px !important;
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
|
||||||
|
.ant-progress-inner {
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.percent-value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
text-align: right;
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
font-variant-numeric: lining-nums tabular-nums slashed-zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.flamegraph {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.flamegraph-stats {
|
||||||
|
border-right: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.exec-time-service {
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
background: var(--bg-vanilla-400);
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
.value-row {
|
||||||
|
.service-name {
|
||||||
|
.service-text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-service {
|
||||||
|
.percent-value {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,177 @@
|
|||||||
|
import './PaginatedTraceFlamegraph.styles.scss';
|
||||||
|
|
||||||
|
import { Progress, Skeleton, Tooltip, Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import useGetTraceFlamegraph from 'hooks/trace/useGetTraceFlamegraph';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { TraceDetailFlamegraphURLProps } from 'types/api/trace/getTraceFlamegraph';
|
||||||
|
import { Span } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
import { TraceFlamegraphStates } from './constants';
|
||||||
|
import Error from './TraceFlamegraphStates/Error/Error';
|
||||||
|
import NoData from './TraceFlamegraphStates/NoData/NoData';
|
||||||
|
import Success from './TraceFlamegraphStates/Success/Success';
|
||||||
|
|
||||||
|
interface ITraceFlamegraphProps {
|
||||||
|
serviceExecTime: Record<string, number>;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
traceFlamegraphStatsWidth: number;
|
||||||
|
selectedSpan: Span | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
serviceExecTime,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
traceFlamegraphStatsWidth,
|
||||||
|
selectedSpan,
|
||||||
|
} = props;
|
||||||
|
const { id: traceId } = useParams<TraceDetailFlamegraphURLProps>();
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState<string>(
|
||||||
|
urlQuery.get('spanId') || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFirstSpanAtFetchLevel(urlQuery.get('spanId') || '');
|
||||||
|
}, [urlQuery]);
|
||||||
|
|
||||||
|
const { data, isFetching, error } = useGetTraceFlamegraph({
|
||||||
|
traceId,
|
||||||
|
selectedSpanId: firstSpanAtFetchLevel,
|
||||||
|
});
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
// get the current state of trace flamegraph based on the API lifecycle
|
||||||
|
const traceFlamegraphState = useMemo(() => {
|
||||||
|
if (isFetching) {
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
data.payload &&
|
||||||
|
data.payload.spans &&
|
||||||
|
data.payload.spans.length > 0
|
||||||
|
) {
|
||||||
|
return TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT;
|
||||||
|
}
|
||||||
|
return TraceFlamegraphStates.LOADING;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return TraceFlamegraphStates.ERROR;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
data.payload &&
|
||||||
|
data.payload.spans &&
|
||||||
|
data.payload.spans.length === 0
|
||||||
|
) {
|
||||||
|
return TraceFlamegraphStates.NO_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TraceFlamegraphStates.SUCCESS;
|
||||||
|
}, [error, isFetching, data]);
|
||||||
|
|
||||||
|
// capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ]
|
||||||
|
const spans = useMemo(() => data?.payload?.spans || [], [
|
||||||
|
data?.payload?.spans,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// get the content based on the current state of the trace waterfall
|
||||||
|
const getContent = useMemo(() => {
|
||||||
|
switch (traceFlamegraphState) {
|
||||||
|
case TraceFlamegraphStates.LOADING:
|
||||||
|
return (
|
||||||
|
<div className="loading-skeleton">
|
||||||
|
<Skeleton active paragraph={{ rows: 3 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case TraceFlamegraphStates.ERROR:
|
||||||
|
return <Error error={error as AxiosError} />;
|
||||||
|
case TraceFlamegraphStates.NO_DATA:
|
||||||
|
return <NoData id={traceId} />;
|
||||||
|
case TraceFlamegraphStates.SUCCESS:
|
||||||
|
case TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT:
|
||||||
|
return (
|
||||||
|
<Success
|
||||||
|
spans={spans}
|
||||||
|
firstSpanAtFetchLevel={firstSpanAtFetchLevel}
|
||||||
|
setFirstSpanAtFetchLevel={setFirstSpanAtFetchLevel}
|
||||||
|
traceMetadata={{
|
||||||
|
startTime: data?.payload?.startTimestampMillis || 0,
|
||||||
|
endTime: data?.payload?.endTimestampMillis || 0,
|
||||||
|
}}
|
||||||
|
selectedSpan={selectedSpan}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <Spinner tip="Fetching the trace!" />;
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
data?.payload?.endTimestampMillis,
|
||||||
|
data?.payload?.startTimestampMillis,
|
||||||
|
error,
|
||||||
|
firstSpanAtFetchLevel,
|
||||||
|
selectedSpan,
|
||||||
|
spans,
|
||||||
|
traceFlamegraphState,
|
||||||
|
traceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flamegraph">
|
||||||
|
<div
|
||||||
|
className="flamegraph-stats"
|
||||||
|
style={{ width: `${traceFlamegraphStatsWidth + 22}px` }}
|
||||||
|
>
|
||||||
|
<div className="exec-time-service">% exec time</div>
|
||||||
|
<div className="stats">
|
||||||
|
{Object.keys(serviceExecTime).map((service) => {
|
||||||
|
const spread = endTime - startTime;
|
||||||
|
const value = (serviceExecTime[service] * 100) / spread;
|
||||||
|
const color = generateColor(
|
||||||
|
service,
|
||||||
|
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div key={service} className="value-row">
|
||||||
|
<section className="service-name">
|
||||||
|
<div className="square-box" style={{ backgroundColor: color }} />
|
||||||
|
<Tooltip title={service}>
|
||||||
|
<Typography.Text className="service-text" ellipsis>
|
||||||
|
{service}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
<section className="progress-service">
|
||||||
|
<Progress
|
||||||
|
percent={parseFloat(value.toFixed(2))}
|
||||||
|
className="service-progress-indicator"
|
||||||
|
showInfo={false}
|
||||||
|
/>
|
||||||
|
<Typography.Text className="percent-value">
|
||||||
|
{parseFloat(value.toFixed(2))}%
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flamegraph-chart"
|
||||||
|
style={{ width: `calc(100% - ${traceFlamegraphStatsWidth + 22}px)` }}
|
||||||
|
>
|
||||||
|
{getContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraceFlamegraph;
|
@ -0,0 +1,31 @@
|
|||||||
|
.error-flamegraph {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 15vh;
|
||||||
|
|
||||||
|
.error-flamegraph-img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.error-flamegraph {
|
||||||
|
.no-data-text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
import './Error.styles.scss';
|
||||||
|
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
interface IErrorProps {
|
||||||
|
error: AxiosError;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Error(props: IErrorProps): JSX.Element {
|
||||||
|
const { error } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="error-flamegraph">
|
||||||
|
<img
|
||||||
|
src="/Icons/no-data.svg"
|
||||||
|
alt="error-flamegraph"
|
||||||
|
className="error-flamegraph-img"
|
||||||
|
/>
|
||||||
|
<Tooltip title={error?.message}>
|
||||||
|
<Typography.Text className="no-data-text">
|
||||||
|
{error?.message || 'Something went wrong!'}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Error;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
interface INoDataProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoData(props: INoDataProps): JSX.Element {
|
||||||
|
const { id } = props;
|
||||||
|
return <Typography.Text>No Trace found with the id: {id} </Typography.Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoData;
|
@ -0,0 +1,28 @@
|
|||||||
|
.trace-flamegraph {
|
||||||
|
height: 90%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.trace-flamegraph-virtuoso {
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
.flamegraph-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 18px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
|
||||||
|
.span-item {
|
||||||
|
position: absolute;
|
||||||
|
height: 12px;
|
||||||
|
background-color: yellow;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './Success.styles.scss';
|
||||||
|
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import Color from 'color';
|
||||||
|
import TimelineV2 from 'components/TimelineV2/TimelineV2';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
|
||||||
|
import { Span } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
interface ITraceMetadata {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISuccessProps {
|
||||||
|
spans: FlamegraphSpan[][];
|
||||||
|
firstSpanAtFetchLevel: string;
|
||||||
|
setFirstSpanAtFetchLevel: Dispatch<SetStateAction<string>>;
|
||||||
|
traceMetadata: ITraceMetadata;
|
||||||
|
selectedSpan: Span | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Success(props: ISuccessProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
spans,
|
||||||
|
setFirstSpanAtFetchLevel,
|
||||||
|
traceMetadata,
|
||||||
|
firstSpanAtFetchLevel,
|
||||||
|
selectedSpan,
|
||||||
|
} = props;
|
||||||
|
const { search } = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||||
|
const [hoveredSpanId, setHoveredSpanId] = useState<string>('');
|
||||||
|
const renderSpanLevel = useCallback(
|
||||||
|
(_: number, spans: FlamegraphSpan[]): JSX.Element => (
|
||||||
|
<div className="flamegraph-row">
|
||||||
|
{spans.map((span) => {
|
||||||
|
const spread = traceMetadata.endTime - traceMetadata.startTime;
|
||||||
|
const leftOffset =
|
||||||
|
((span.timestamp - traceMetadata.startTime) * 100) / spread;
|
||||||
|
let width = ((span.durationNano / 1e6) * 100) / spread;
|
||||||
|
if (width > 100) {
|
||||||
|
width = 100;
|
||||||
|
}
|
||||||
|
const toolTipText = `${span.name}`;
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
|
||||||
|
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
|
||||||
|
|
||||||
|
const selectedSpanColor = isDarkMode
|
||||||
|
? Color(color).lighten(0.7)
|
||||||
|
: Color(color).darken(0.7);
|
||||||
|
|
||||||
|
if (span.hasError) {
|
||||||
|
color = `var(--bg-cherry-500)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={toolTipText} key={span.spanId}>
|
||||||
|
<div
|
||||||
|
className="span-item"
|
||||||
|
style={{
|
||||||
|
left: `${leftOffset}%`,
|
||||||
|
width: `${width}%`,
|
||||||
|
backgroundColor:
|
||||||
|
selectedSpan?.spanId === span.spanId || hoveredSpanId === span.spanId
|
||||||
|
? `${selectedSpanColor}`
|
||||||
|
: color,
|
||||||
|
}}
|
||||||
|
onMouseEnter={(): void => setHoveredSpanId(span.spanId)}
|
||||||
|
onMouseLeave={(): void => setHoveredSpanId('')}
|
||||||
|
onClick={(event): void => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
searchParams.set('spanId', span.spanId);
|
||||||
|
history.replace({ search: searchParams.toString() });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[traceMetadata.endTime, traceMetadata.startTime, selectedSpan, hoveredSpanId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRangeChanged = useCallback(
|
||||||
|
(range: ListRange) => {
|
||||||
|
// if there are less than 50 levels on any load that means a single API call is sufficient
|
||||||
|
if (spans.length < 50) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startIndex, endIndex } = range;
|
||||||
|
if (startIndex === 0 && spans[0][0].level !== 0) {
|
||||||
|
setFirstSpanAtFetchLevel(spans[0][0].spanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex === spans.length - 1) {
|
||||||
|
setFirstSpanAtFetchLevel(spans[spans.length - 1][0].spanId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setFirstSpanAtFetchLevel, spans],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const index = spans.findIndex(
|
||||||
|
(span) => span[0].spanId === firstSpanAtFetchLevel,
|
||||||
|
);
|
||||||
|
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index,
|
||||||
|
behavior: 'auto',
|
||||||
|
});
|
||||||
|
}, [firstSpanAtFetchLevel, spans]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="trace-flamegraph">
|
||||||
|
<Virtuoso
|
||||||
|
ref={virtuosoRef}
|
||||||
|
className="trace-flamegraph-virtuoso"
|
||||||
|
data={spans}
|
||||||
|
itemContent={renderSpanLevel}
|
||||||
|
rangeChanged={handleRangeChanged}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TimelineV2
|
||||||
|
startTimestamp={traceMetadata.startTime}
|
||||||
|
endTimestamp={traceMetadata.endTime}
|
||||||
|
timelineHeight={22}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Success;
|
@ -0,0 +1,7 @@
|
|||||||
|
export enum TraceFlamegraphStates {
|
||||||
|
LOADING = 'LOADING',
|
||||||
|
SUCCESS = 'SUCCESS',
|
||||||
|
NO_DATA = 'NO_DATA',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT',
|
||||||
|
}
|
@ -65,7 +65,7 @@ export default function QueryBuilderSearchDropdown(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{menu}
|
{menu}
|
||||||
{!searchValue && tags.length === 0 && (
|
{!searchValue && tags.length === 0 && exampleQueries.length > 0 && (
|
||||||
<div className="example-queries">
|
<div className="example-queries">
|
||||||
<div className="heading"> Example Queries </div>
|
<div className="heading"> Example Queries </div>
|
||||||
<div className="query-container">
|
<div className="query-container">
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
.show-all-filters {
|
.show-all-filters {
|
||||||
.content {
|
.content {
|
||||||
.rc-virtual-list-holder {
|
.rc-virtual-list-holder {
|
||||||
height: 100px;
|
height: 115px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,7 @@ function QueryBuilderSearchV2(
|
|||||||
|
|
||||||
const { data, isFetching } = useGetAggregateKeys(
|
const { data, isFetching } = useGetAggregateKeys(
|
||||||
{
|
{
|
||||||
searchText: searchValue,
|
searchText: searchValue?.split(' ')[0],
|
||||||
dataSource: query.dataSource,
|
dataSource: query.dataSource,
|
||||||
aggregateOperator: query.aggregateOperator,
|
aggregateOperator: query.aggregateOperator,
|
||||||
aggregateAttribute: query.aggregateAttribute.key,
|
aggregateAttribute: query.aggregateAttribute.key,
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
.attributes-corner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
height: 400px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
margin: 12px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
.item-key {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
.item-value {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-top {
|
||||||
|
border-top: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.attributes-corner {
|
||||||
|
.attributes-container {
|
||||||
|
.item {
|
||||||
|
.item-key {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.item-value {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-top {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import './Attributes.styles.scss';
|
||||||
|
|
||||||
|
import { Input, Tooltip, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { flattenObject } from 'container/LogDetailedView/utils';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Span } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
import NoData from '../NoData/NoData';
|
||||||
|
|
||||||
|
interface IAttributesProps {
|
||||||
|
span: Span;
|
||||||
|
isSearchVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Attributes(props: IAttributesProps): JSX.Element {
|
||||||
|
const { span, isSearchVisible } = props;
|
||||||
|
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||||
|
|
||||||
|
const flattenSpanData: Record<string, string> = useMemo(
|
||||||
|
() => (span.tagMap ? flattenObject(span.tagMap) : {}),
|
||||||
|
[span],
|
||||||
|
);
|
||||||
|
|
||||||
|
const datasource = Object.keys(flattenSpanData)
|
||||||
|
.filter((attribute) =>
|
||||||
|
attribute.toLowerCase().includes(fieldSearchInput.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((key) => ({ field: key, value: flattenSpanData[key] }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="attributes-corner">
|
||||||
|
{isSearchVisible &&
|
||||||
|
(datasource.length > 0 || fieldSearchInput.length > 0) && (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
placeholder="Search for attribute..."
|
||||||
|
className="search-input"
|
||||||
|
value={fieldSearchInput}
|
||||||
|
onChange={(e): void => setFieldSearchInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{datasource.length === 0 && fieldSearchInput.length === 0 && (
|
||||||
|
<NoData name="attributes" />
|
||||||
|
)}
|
||||||
|
<section
|
||||||
|
className={cx('attributes-container', isSearchVisible ? 'border-top' : '')}
|
||||||
|
>
|
||||||
|
{datasource.map((item) => (
|
||||||
|
<div className="item" key={`${item.field} + ${item.value}`}>
|
||||||
|
<Typography.Text className="item-key" ellipsis>
|
||||||
|
{item.field}
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Tooltip title={item.value}>
|
||||||
|
<Typography.Text className="item-value" ellipsis>
|
||||||
|
{item.value}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Attributes;
|
@ -0,0 +1,188 @@
|
|||||||
|
.events-table {
|
||||||
|
.no-events {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
.events-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.event {
|
||||||
|
.ant-collapse {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.ant-collapse-content {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-item {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-content-box {
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
.ant-collapse-header {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 6px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.ant-collapse-expand-icon {
|
||||||
|
padding-inline-start: 0px;
|
||||||
|
padding-inline-end: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.diamond {
|
||||||
|
fill: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.event-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.attribute-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.attribute-key {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.timestamp-text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-value {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
width: fit-content;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
.attribute-value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.events-table {
|
||||||
|
.events-container {
|
||||||
|
.event {
|
||||||
|
.ant-collapse-content-box {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
.ant-collapse-header {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.event-details {
|
||||||
|
.attribute-container {
|
||||||
|
.attribute-key {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-container {
|
||||||
|
.timestamp-text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-value {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
.attribute-value {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
118
frontend/src/container/SpanDetailsDrawer/Events/Events.tsx
Normal file
118
frontend/src/container/SpanDetailsDrawer/Events/Events.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import './Events.styles.scss';
|
||||||
|
|
||||||
|
import { Collapse, Input, Tooltip, Typography } from 'antd';
|
||||||
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
|
import { Diamond } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { Event, Span } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
import NoData from '../NoData/NoData';
|
||||||
|
|
||||||
|
interface IEventsTableProps {
|
||||||
|
span: Span;
|
||||||
|
startTime: number;
|
||||||
|
isSearchVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventsTable(props: IEventsTableProps): JSX.Element {
|
||||||
|
const { span, startTime, isSearchVisible } = props;
|
||||||
|
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||||
|
const events: Event[] = useMemo(() => {
|
||||||
|
const tempEvents = [];
|
||||||
|
for (let i = 0; i < span.event?.length; i++) {
|
||||||
|
const parsedEvent = JSON.parse(span.event[i]);
|
||||||
|
tempEvents.push(parsedEvent);
|
||||||
|
}
|
||||||
|
return tempEvents;
|
||||||
|
}, [span.event]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="events-table">
|
||||||
|
{events.length === 0 && (
|
||||||
|
<div className="no-events">
|
||||||
|
<NoData name="events" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="events-container">
|
||||||
|
{isSearchVisible && (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
placeholder="Search for events..."
|
||||||
|
className="search-input"
|
||||||
|
value={fieldSearchInput}
|
||||||
|
onChange={(e): void => setFieldSearchInput(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{events
|
||||||
|
.filter((eve) =>
|
||||||
|
eve.name.toLowerCase().includes(fieldSearchInput.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((event) => (
|
||||||
|
<div
|
||||||
|
className="event"
|
||||||
|
key={`${event.name} ${JSON.stringify(event.attributeMap)}`}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
size="small"
|
||||||
|
defaultActiveKey="1"
|
||||||
|
expandIconPosition="right"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: (
|
||||||
|
<div className="collapse-title">
|
||||||
|
<Diamond size={14} className="diamond" />
|
||||||
|
<Typography.Text className="collapse-title-name">
|
||||||
|
{event.name}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
children: (
|
||||||
|
<div className="event-details">
|
||||||
|
<div className="attribute-container" key="timeUnixNano">
|
||||||
|
<Typography.Text className="attribute-key">
|
||||||
|
Start Time
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="timestamp-container">
|
||||||
|
<Typography.Text className="attribute-value">
|
||||||
|
{getYAxisFormattedValue(
|
||||||
|
`${event.timeUnixNano / 1e6 - startTime}`,
|
||||||
|
'ms',
|
||||||
|
)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text className="timestamp-text">
|
||||||
|
after the start
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{event.attributeMap &&
|
||||||
|
Object.keys(event.attributeMap).map((attributeKey) => (
|
||||||
|
<div className="attribute-container" key={attributeKey}>
|
||||||
|
<Tooltip title={attributeKey}>
|
||||||
|
<Typography.Text className="attribute-key" ellipsis>
|
||||||
|
{attributeKey}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="wrapper">
|
||||||
|
<Tooltip title={event.attributeMap[attributeKey]}>
|
||||||
|
<Typography.Text className="attribute-value" ellipsis>
|
||||||
|
{event.attributeMap[attributeKey]}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventsTable;
|
@ -0,0 +1,28 @@
|
|||||||
|
.no-data {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.no-data-img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data-text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.no-data {
|
||||||
|
.no-data-text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx
Normal file
22
frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import './NoData.styles.scss';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
interface INoDataProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoData(props: INoDataProps): JSX.Element {
|
||||||
|
const { name } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="no-data">
|
||||||
|
<img src="/Icons/no-data.svg" alt="no-data" className="no-data-img" />
|
||||||
|
<Typography.Text className="no-data-text">
|
||||||
|
No {name} found for selected span
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoData;
|
@ -0,0 +1,263 @@
|
|||||||
|
.span-details-drawer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 330px;
|
||||||
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 48px;
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.attribute-key {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
|
||||||
|
.attribute-value {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
height: 4px;
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 0px;
|
||||||
|
border: none;
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
|
||||||
|
.service-value {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-logs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5px 12px;
|
||||||
|
margin: 10px 12px;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-events {
|
||||||
|
.details-drawer-tabs {
|
||||||
|
.ant-tabs-extra-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
width: 33px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-tab-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.attributes-tab-btn:hover {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-tab-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-tab-btn:hover {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-details-drawer-docked {
|
||||||
|
width: 48px;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.span-details-drawer {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
.item {
|
||||||
|
.attribute-key {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.attribute-value {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.service {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.value-wrapper {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.service-value {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-logs {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-events {
|
||||||
|
.details-drawer-tabs {
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav-wrap {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-ink-bar {
|
||||||
|
background: #4e74f8 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
209
frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx
Normal file
209
frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import './SpanDetailsDrawer.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Tabs, TabsProps, Tooltip, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { getTraceToLogsQuery } from 'container/TraceDetail/SelectedSpanDetails/config';
|
||||||
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import { Anvil, Bookmark, PanelRight, Search } from 'lucide-react';
|
||||||
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
import { Span } from 'types/api/trace/getTraceV2';
|
||||||
|
import { formatEpochTimestamp } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
import Attributes from './Attributes/Attributes';
|
||||||
|
import Events from './Events/Events';
|
||||||
|
|
||||||
|
const FIVE_MINUTES_IN_MS = 5 * 60 * 1000;
|
||||||
|
interface ISpanDetailsDrawerProps {
|
||||||
|
isSpanDetailsDocked: boolean;
|
||||||
|
setIsSpanDetailsDocked: Dispatch<SetStateAction<boolean>>;
|
||||||
|
selectedSpan: Span | undefined;
|
||||||
|
traceID: string;
|
||||||
|
traceStartTime: number;
|
||||||
|
traceEndTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
isSpanDetailsDocked,
|
||||||
|
setIsSpanDetailsDocked,
|
||||||
|
selectedSpan,
|
||||||
|
traceStartTime,
|
||||||
|
traceID,
|
||||||
|
traceEndTime,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isSearchVisible, setIsSearchVisible] = useState<boolean>(false);
|
||||||
|
const color = generateColor(
|
||||||
|
selectedSpan?.serviceName || '',
|
||||||
|
themeColors.traceDetailColors,
|
||||||
|
);
|
||||||
|
|
||||||
|
function getItems(span: Span, startTime: number): TabsProps['items'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<Bookmark size="14" />}
|
||||||
|
className="attributes-tab-btn"
|
||||||
|
>
|
||||||
|
Attributes
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
key: 'attributes',
|
||||||
|
children: <Attributes span={span} isSearchVisible={isSearchVisible} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Button type="text" icon={<Anvil size="14" />} className="events-tab-btn">
|
||||||
|
Events
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
key: 'events',
|
||||||
|
children: (
|
||||||
|
<Events
|
||||||
|
span={span}
|
||||||
|
startTime={startTime}
|
||||||
|
isSearchVisible={isSearchVisible}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const onLogsHandler = (): void => {
|
||||||
|
const query = getTraceToLogsQuery(traceID, traceStartTime, traceEndTime);
|
||||||
|
|
||||||
|
history.push(
|
||||||
|
`${ROUTES.LOGS_EXPLORER}?${createQueryParams({
|
||||||
|
[QueryParams.compositeQuery]: JSON.stringify(query),
|
||||||
|
// we subtract 5 minutes from the start time to handle the cases when the trace duration is in nanoseconds
|
||||||
|
[QueryParams.startTime]: traceStartTime - FIVE_MINUTES_IN_MS,
|
||||||
|
// we add 5 minutes to the end time for nano second duration traces
|
||||||
|
[QueryParams.endTime]: traceEndTime + FIVE_MINUTES_IN_MS,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'span-details-drawer',
|
||||||
|
isSpanDetailsDocked ? 'span-details-drawer-docked' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<section className="header">
|
||||||
|
{!isSpanDetailsDocked && (
|
||||||
|
<div className="heading">
|
||||||
|
<div className="dot" style={{ background: color }} />
|
||||||
|
<Typography.Text className="text">Span Details</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PanelRight
|
||||||
|
size={14}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(): void => setIsSpanDetailsDocked((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{selectedSpan && !isSpanDetailsDocked && (
|
||||||
|
<>
|
||||||
|
<section className="description">
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">span name</Typography.Text>
|
||||||
|
<Tooltip title={selectedSpan.name}>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Typography.Text className="attribute-value" ellipsis>
|
||||||
|
{selectedSpan.name}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">span id</Typography.Text>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Typography.Text className="attribute-value">
|
||||||
|
{selectedSpan.spanId}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">start time</Typography.Text>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Typography.Text className="attribute-value">
|
||||||
|
{formatEpochTimestamp(selectedSpan.timestamp)}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">duration</Typography.Text>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Typography.Text className="attribute-value">
|
||||||
|
{getYAxisFormattedValue(`${selectedSpan.durationNano}`, 'ns')}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">service</Typography.Text>
|
||||||
|
<div className="service">
|
||||||
|
<div className="dot" style={{ backgroundColor: color }} />
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Tooltip title={selectedSpan.serviceName}>
|
||||||
|
<Typography.Text className="service-value" ellipsis>
|
||||||
|
{selectedSpan.serviceName}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">span kind</Typography.Text>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Typography.Text className="attribute-value">
|
||||||
|
{selectedSpan.spanKind}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<Typography.Text className="attribute-key">
|
||||||
|
status code string
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="value-wrapper">
|
||||||
|
<Typography.Text className="attribute-value">
|
||||||
|
{selectedSpan.statusCodeString}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Button onClick={onLogsHandler} className="related-logs">
|
||||||
|
Go to related logs
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<section className="attributes-events">
|
||||||
|
<Tabs
|
||||||
|
items={getItems(selectedSpan, traceStartTime)}
|
||||||
|
addIcon
|
||||||
|
defaultActiveKey="attributes"
|
||||||
|
className="details-drawer-tabs"
|
||||||
|
tabBarExtraContent={
|
||||||
|
<Search
|
||||||
|
size={14}
|
||||||
|
className="search-icon"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(): void => setIsSearchVisible((prev) => !prev)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpanDetailsDrawer;
|
262
frontend/src/container/TraceMetadata/TraceMetadata.styles.scss
Normal file
262
frontend/src/container/TraceMetadata/TraceMetadata.styles.scss
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
.trace-metadata {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0px 16px 0px 16px;
|
||||||
|
|
||||||
|
.metadata-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.first-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.previous-btn {
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-300);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-name {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-left: 6px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-300);
|
||||||
|
border-radius: 4px 0px 0px 4px;
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
|
||||||
|
.drafting {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-id {
|
||||||
|
color: #fff;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-id-value {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
border: 1px solid var(--bg-slate-300);
|
||||||
|
border-left: unset;
|
||||||
|
border-radius: 0px 4px 4px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.service-entry-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-span-name {
|
||||||
|
display: flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-duration {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-time-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.datapoints-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
background: #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-point {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
text-align: center;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||||
|
slashed-zero;
|
||||||
|
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 20px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 28px; /* 140% */
|
||||||
|
letter-spacing: -0.1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.trace-metadata {
|
||||||
|
.metadata-info {
|
||||||
|
.first-row {
|
||||||
|
.previous-btn {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-name {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
.drafting {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-id {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-id-value {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-row {
|
||||||
|
.service-entry-info {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.root-span-name {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-duration {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-time-info {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.datapoints-info {
|
||||||
|
.separator {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-point {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
frontend/src/container/TraceMetadata/TraceMetadata.tsx
Normal file
100
frontend/src/container/TraceMetadata/TraceMetadata.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import './TraceMetadata.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Tooltip, Typography } from 'antd';
|
||||||
|
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
BetweenHorizonalStart,
|
||||||
|
CalendarClock,
|
||||||
|
DraftingCompass,
|
||||||
|
Timer,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { formatEpochTimestamp } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
export interface ITraceMetadataProps {
|
||||||
|
traceID: string;
|
||||||
|
rootServiceName: string;
|
||||||
|
rootSpanName: string;
|
||||||
|
startTime: number;
|
||||||
|
duration: number;
|
||||||
|
totalSpans: number;
|
||||||
|
totalErrorSpans: number;
|
||||||
|
notFound: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TraceMetadata(props: ITraceMetadataProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
traceID,
|
||||||
|
rootServiceName,
|
||||||
|
rootSpanName,
|
||||||
|
startTime,
|
||||||
|
duration,
|
||||||
|
totalErrorSpans,
|
||||||
|
totalSpans,
|
||||||
|
notFound,
|
||||||
|
} = props;
|
||||||
|
return (
|
||||||
|
<div className="trace-metadata">
|
||||||
|
<section className="metadata-info">
|
||||||
|
<div className="first-row">
|
||||||
|
<Button className="previous-btn">
|
||||||
|
<ArrowLeft
|
||||||
|
size={14}
|
||||||
|
onClick={(): void => history.push(ROUTES.TRACES_EXPLORER)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<div className="trace-name">
|
||||||
|
<DraftingCompass size={14} className="drafting" />
|
||||||
|
<Typography.Text className="trace-id">Trace ID</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="trace-id-value">{traceID}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
{!notFound && (
|
||||||
|
<div className="second-row">
|
||||||
|
<div className="service-entry-info">
|
||||||
|
<BetweenHorizonalStart size={14} />
|
||||||
|
<Typography.Text className="text">{rootServiceName}</Typography.Text>
|
||||||
|
—
|
||||||
|
<Typography.Text className="text root-span-name">
|
||||||
|
{rootSpanName}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="trace-duration">
|
||||||
|
<Tooltip title="Duration of trace">
|
||||||
|
<Timer size={14} />
|
||||||
|
</Tooltip>
|
||||||
|
<Typography.Text className="text">
|
||||||
|
{getYAxisFormattedValue(`${duration}`, 'ms')}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="start-time-info">
|
||||||
|
<Tooltip title="Start timestamp">
|
||||||
|
<CalendarClock size={14} />
|
||||||
|
</Tooltip>
|
||||||
|
<Typography.Text className="text">
|
||||||
|
{formatEpochTimestamp(startTime * 1000)}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
{!notFound && (
|
||||||
|
<section className="datapoints-info">
|
||||||
|
<div className="data-point">
|
||||||
|
<Typography.Text className="text">Total Spans</Typography.Text>
|
||||||
|
<Typography.Text className="value">{totalSpans}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="separator" />
|
||||||
|
<div className="data-point">
|
||||||
|
<Typography.Text className="text">Error Spans</Typography.Text>
|
||||||
|
<Typography.Text className="value">{totalErrorSpans}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraceMetadata;
|
@ -0,0 +1,9 @@
|
|||||||
|
.trace-waterfall {
|
||||||
|
height: calc(70vh - 236px);
|
||||||
|
|
||||||
|
.loading-skeleton {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
136
frontend/src/container/TraceWaterfall/TraceWaterfall.tsx
Normal file
136
frontend/src/container/TraceWaterfall/TraceWaterfall.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import './TraceWaterfall.styles.scss';
|
||||||
|
|
||||||
|
import { Skeleton } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { GetTraceV2SuccessResponse, Span } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
import { TraceWaterfallStates } from './constants';
|
||||||
|
import Error from './TraceWaterfallStates/Error/Error';
|
||||||
|
import NoData from './TraceWaterfallStates/NoData/NoData';
|
||||||
|
import Success from './TraceWaterfallStates/Success/Success';
|
||||||
|
|
||||||
|
export interface IInterestedSpan {
|
||||||
|
spanId: string;
|
||||||
|
isUncollapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITraceWaterfallProps {
|
||||||
|
traceId: string;
|
||||||
|
uncollapsedNodes: string[];
|
||||||
|
traceData:
|
||||||
|
| SuccessResponse<GetTraceV2SuccessResponse, unknown>
|
||||||
|
| ErrorResponse
|
||||||
|
| undefined;
|
||||||
|
isFetchingTraceData: boolean;
|
||||||
|
errorFetchingTraceData: unknown;
|
||||||
|
interestedSpanId: IInterestedSpan;
|
||||||
|
setInterestedSpanId: Dispatch<SetStateAction<IInterestedSpan>>;
|
||||||
|
setTraceFlamegraphStatsWidth: Dispatch<SetStateAction<number>>;
|
||||||
|
selectedSpan: Span | undefined;
|
||||||
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
traceData,
|
||||||
|
isFetchingTraceData,
|
||||||
|
errorFetchingTraceData,
|
||||||
|
interestedSpanId,
|
||||||
|
traceId,
|
||||||
|
uncollapsedNodes,
|
||||||
|
setInterestedSpanId,
|
||||||
|
setTraceFlamegraphStatsWidth,
|
||||||
|
setSelectedSpan,
|
||||||
|
selectedSpan,
|
||||||
|
} = props;
|
||||||
|
// get the current state of trace waterfall based on the API lifecycle
|
||||||
|
const traceWaterfallState = useMemo(() => {
|
||||||
|
if (isFetchingTraceData) {
|
||||||
|
if (
|
||||||
|
traceData &&
|
||||||
|
traceData.payload &&
|
||||||
|
traceData.payload.spans &&
|
||||||
|
traceData.payload.spans.length > 0
|
||||||
|
) {
|
||||||
|
return TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT;
|
||||||
|
}
|
||||||
|
return TraceWaterfallStates.LOADING;
|
||||||
|
}
|
||||||
|
if (errorFetchingTraceData) {
|
||||||
|
return TraceWaterfallStates.ERROR;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
traceData &&
|
||||||
|
traceData.payload &&
|
||||||
|
traceData.payload.spans &&
|
||||||
|
traceData.payload.spans.length === 0
|
||||||
|
) {
|
||||||
|
return TraceWaterfallStates.NO_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TraceWaterfallStates.SUCCESS;
|
||||||
|
}, [errorFetchingTraceData, isFetchingTraceData, traceData]);
|
||||||
|
|
||||||
|
// capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ]
|
||||||
|
const spans = useMemo(() => traceData?.payload?.spans || [], [
|
||||||
|
traceData?.payload?.spans,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// get the content based on the current state of the trace waterfall
|
||||||
|
const getContent = useMemo(() => {
|
||||||
|
switch (traceWaterfallState) {
|
||||||
|
case TraceWaterfallStates.LOADING:
|
||||||
|
return (
|
||||||
|
<div className="loading-skeleton">
|
||||||
|
<Skeleton active paragraph={{ rows: 6 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case TraceWaterfallStates.ERROR:
|
||||||
|
return <Error error={errorFetchingTraceData as AxiosError} />;
|
||||||
|
case TraceWaterfallStates.NO_DATA:
|
||||||
|
return <NoData id={traceId} />;
|
||||||
|
case TraceWaterfallStates.SUCCESS:
|
||||||
|
case TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT:
|
||||||
|
return (
|
||||||
|
<Success
|
||||||
|
spans={spans}
|
||||||
|
traceMetadata={{
|
||||||
|
traceId,
|
||||||
|
startTime: traceData?.payload?.startTimestampMillis || 0,
|
||||||
|
endTime: traceData?.payload?.endTimestampMillis || 0,
|
||||||
|
hasMissingSpans: traceData?.payload?.hasMissingSpans || false,
|
||||||
|
}}
|
||||||
|
interestedSpanId={interestedSpanId || ''}
|
||||||
|
uncollapsedNodes={uncollapsedNodes}
|
||||||
|
setInterestedSpanId={setInterestedSpanId}
|
||||||
|
setTraceFlamegraphStatsWidth={setTraceFlamegraphStatsWidth}
|
||||||
|
selectedSpan={selectedSpan}
|
||||||
|
setSelectedSpan={setSelectedSpan}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <Spinner tip="Fetching the trace!" />;
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
errorFetchingTraceData,
|
||||||
|
interestedSpanId,
|
||||||
|
selectedSpan,
|
||||||
|
setInterestedSpanId,
|
||||||
|
setSelectedSpan,
|
||||||
|
setTraceFlamegraphStatsWidth,
|
||||||
|
spans,
|
||||||
|
traceData?.payload?.endTimestampMillis,
|
||||||
|
traceData?.payload?.hasMissingSpans,
|
||||||
|
traceData?.payload?.startTimestampMillis,
|
||||||
|
traceId,
|
||||||
|
traceWaterfallState,
|
||||||
|
uncollapsedNodes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return <div className="trace-waterfall">{getContent}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraceWaterfall;
|
@ -0,0 +1,30 @@
|
|||||||
|
.error-waterfall {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 20px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-cherry-500);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import './Error.styles.scss';
|
||||||
|
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
interface IErrorProps {
|
||||||
|
error: AxiosError;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Error(props: IErrorProps): JSX.Element {
|
||||||
|
const { error } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="error-waterfall">
|
||||||
|
<Typography.Text className="text">Something went wrong!</Typography.Text>
|
||||||
|
<Tooltip title={error?.message}>
|
||||||
|
<Typography.Text className="value" ellipsis>
|
||||||
|
{error?.message}
|
||||||
|
</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Error;
|
@ -0,0 +1,12 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
interface INoDataProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NoData(props: INoDataProps): JSX.Element {
|
||||||
|
const { id } = props;
|
||||||
|
return <Typography.Text>No Trace found with the id: {id} </Typography.Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoData;
|
@ -0,0 +1,60 @@
|
|||||||
|
.filter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px 0px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.query-builder-search-v2 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pre-next-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.filter-row {
|
||||||
|
.pre-next-toggle {
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,180 @@
|
|||||||
|
import './Filters.styles.scss';
|
||||||
|
|
||||||
|
import { InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Spin, Tooltip, Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { BASE_FILTER_QUERY } from './constants';
|
||||||
|
|
||||||
|
function prepareQuery(filters: TagFilter, traceID: string): Query {
|
||||||
|
return {
|
||||||
|
...initialQueriesMap.traces,
|
||||||
|
builder: {
|
||||||
|
...initialQueriesMap.traces.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueriesMap.traces.builder.queryData[0],
|
||||||
|
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: 'asc' }],
|
||||||
|
filters: {
|
||||||
|
...filters,
|
||||||
|
items: [
|
||||||
|
...filters.items,
|
||||||
|
{
|
||||||
|
id: '5ab8e1cf',
|
||||||
|
key: {
|
||||||
|
key: 'trace_id',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: '',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'trace_id--string----true',
|
||||||
|
},
|
||||||
|
op: '=',
|
||||||
|
value: traceID,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function Filters({
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
traceID,
|
||||||
|
}: {
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
traceID: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [filters, setFilters] = useState<TagFilter>(BASE_FILTER_QUERY.filters);
|
||||||
|
const [noData, setNoData] = useState<boolean>(false);
|
||||||
|
const [filteredSpanIds, setFilteredSpanIds] = useState<string[]>([]);
|
||||||
|
const handleFilterChange = (value: TagFilter): void => {
|
||||||
|
setFilters(value);
|
||||||
|
};
|
||||||
|
const [currentSearchedIndex, setCurrentSearchedIndex] = useState<number>(0);
|
||||||
|
const { search } = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handlePrevNext = useCallback(
|
||||||
|
(index: number, spanId?: string): void => {
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
if (spanId) {
|
||||||
|
searchParams.set('spanId', spanId);
|
||||||
|
} else {
|
||||||
|
searchParams.set('spanId', filteredSpanIds[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replace({ search: searchParams.toString() });
|
||||||
|
},
|
||||||
|
[filteredSpanIds, history, search],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isFetching, error } = useGetQueryRange(
|
||||||
|
{
|
||||||
|
query: prepareQuery(filters, traceID),
|
||||||
|
graphType: PANEL_TYPES.LIST,
|
||||||
|
selectedTime: 'GLOBAL_TIME',
|
||||||
|
start: startTime,
|
||||||
|
end: endTime,
|
||||||
|
params: {
|
||||||
|
dataSource: 'traces',
|
||||||
|
},
|
||||||
|
tableParams: {
|
||||||
|
pagination: {
|
||||||
|
offset: 0,
|
||||||
|
limit: 200,
|
||||||
|
},
|
||||||
|
selectColumns: [
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
dataType: 'string',
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'name--string--tag--true',
|
||||||
|
isIndexed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DEFAULT_ENTITY_VERSION,
|
||||||
|
{
|
||||||
|
queryKey: [filters],
|
||||||
|
enabled: filters.items.length > 0,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data?.payload.data.newResult.data.result[0].list) {
|
||||||
|
const spanIds = data?.payload.data.newResult.data.result[0].list.map(
|
||||||
|
(val) => val.data.spanID,
|
||||||
|
);
|
||||||
|
setFilteredSpanIds(spanIds);
|
||||||
|
handlePrevNext(0, spanIds[0]);
|
||||||
|
setNoData(false);
|
||||||
|
} else {
|
||||||
|
setNoData(true);
|
||||||
|
setFilteredSpanIds([]);
|
||||||
|
setCurrentSearchedIndex(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="filter-row">
|
||||||
|
<QueryBuilderSearchV2
|
||||||
|
query={BASE_FILTER_QUERY}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
{filteredSpanIds.length > 0 && (
|
||||||
|
<div className="pre-next-toggle">
|
||||||
|
<Typography.Text>
|
||||||
|
{currentSearchedIndex + 1} / {filteredSpanIds.length}
|
||||||
|
</Typography.Text>
|
||||||
|
<Button
|
||||||
|
icon={<ChevronUp size={14} />}
|
||||||
|
disabled={currentSearchedIndex === 0}
|
||||||
|
type="text"
|
||||||
|
onClick={(): void => {
|
||||||
|
handlePrevNext(currentSearchedIndex - 1);
|
||||||
|
setCurrentSearchedIndex((prev) => prev - 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<ChevronDown size={14} />}
|
||||||
|
type="text"
|
||||||
|
disabled={currentSearchedIndex === filteredSpanIds.length - 1}
|
||||||
|
onClick={(): void => {
|
||||||
|
handlePrevNext(currentSearchedIndex + 1);
|
||||||
|
setCurrentSearchedIndex((prev) => prev + 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isFetching && <Spin indicator={<LoadingOutlined spin />} size="small" />}
|
||||||
|
{error && (
|
||||||
|
<Tooltip title={(error as AxiosError)?.message || 'Something went wrong'}>
|
||||||
|
<InfoCircleOutlined size={14} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{noData && (
|
||||||
|
<Typography.Text className="no-results">No results found</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Filters;
|
@ -0,0 +1,40 @@
|
|||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export const BASE_FILTER_QUERY: IBuilderQuery = {
|
||||||
|
queryName: 'A',
|
||||||
|
dataSource: DataSource.TRACES,
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: {
|
||||||
|
id: '------false',
|
||||||
|
dataType: DataTypes.EMPTY,
|
||||||
|
key: '',
|
||||||
|
isColumn: false,
|
||||||
|
type: '',
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
functions: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
stepInterval: 60,
|
||||||
|
having: [],
|
||||||
|
limit: 200,
|
||||||
|
orderBy: [
|
||||||
|
{
|
||||||
|
columnName: 'timestamp',
|
||||||
|
order: 'desc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
groupBy: [],
|
||||||
|
legend: '',
|
||||||
|
reduceTo: 'avg',
|
||||||
|
offset: 0,
|
||||||
|
selectColumns: [],
|
||||||
|
};
|
@ -0,0 +1,396 @@
|
|||||||
|
.success-content {
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.missing-spans {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 44px;
|
||||||
|
margin: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(69, 104, 220, 0.1);
|
||||||
|
|
||||||
|
.left-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--bg-robin-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-robin-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--bg-robin-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-info:hover {
|
||||||
|
background-color: unset;
|
||||||
|
color: var(--bg-robin-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.waterfall-table {
|
||||||
|
height: calc(70vh - 236px);
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 0px 20px 20px 20px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default table overrides css for table v3
|
||||||
|
.div-table {
|
||||||
|
width: 100% !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background-color: var(--bg-ink-500) !important;
|
||||||
|
|
||||||
|
.div-tr {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-tr {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
height: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-tr:hover {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(171, 189, 255, 0.06) !important;
|
||||||
|
|
||||||
|
.span-overview {
|
||||||
|
background: unset !important;
|
||||||
|
|
||||||
|
.span-overview-content {
|
||||||
|
background: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-th,
|
||||||
|
.div-td {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-th {
|
||||||
|
padding: 2px 4px;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-td {
|
||||||
|
display: flex;
|
||||||
|
height: 54px;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.span-overview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.connector-lines {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-overview-content {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #0b0c0e;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.first-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 20px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.span-det {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.collapse-uncollapse-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 4px;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
box-shadow: none;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
.children-count {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Space Mono';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-name {
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: 0.28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-code-container {
|
||||||
|
display: flex;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
.status-code {
|
||||||
|
display: flex;
|
||||||
|
height: 20px;
|
||||||
|
padding: 3px;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
border: 1px solid var(--bg-robin-500);
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
border: 1px solid var(--bg-cherry-500);
|
||||||
|
background: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 18px;
|
||||||
|
width: 100%;
|
||||||
|
.service-name {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-duration {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 54px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.span-line {
|
||||||
|
position: absolute;
|
||||||
|
height: 12px;
|
||||||
|
top: 35%;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-line-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 65%;
|
||||||
|
font-variant-numeric: lining-nums tabular-nums stacked-fractions
|
||||||
|
slashed-zero;
|
||||||
|
font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interested-span {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(171, 189, 255, 0.06) !important;
|
||||||
|
|
||||||
|
.span-overview-content {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-td + .div-td {
|
||||||
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-th + .div-th {
|
||||||
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-tr .div-th:nth-child(2) {
|
||||||
|
width: calc(100% - var(--header-span-name-size) * 1px) !important;
|
||||||
|
}
|
||||||
|
.div-tr .div-td:nth-child(2) {
|
||||||
|
width: calc(100% - var(--header-span-name-size) * 1px) !important;
|
||||||
|
}
|
||||||
|
.resizer {
|
||||||
|
width: 10px !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: calc(70vh - 236px);
|
||||||
|
right: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: rgba(35, 196, 248, 0.2);
|
||||||
|
cursor: col-resize;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer.isResizing {
|
||||||
|
background: rgba(35, 196, 248, 0.2);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.resizer {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:hover > .resizer {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-spans-waterfall-table {
|
||||||
|
height: calc(70vh - 312px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-dets {
|
||||||
|
.related-logs {
|
||||||
|
display: flex;
|
||||||
|
width: 160px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--Slate-400, #1d212d);
|
||||||
|
background: var(--Slate-500, #161922);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.success-content {
|
||||||
|
.waterfall-table {
|
||||||
|
.div-td {
|
||||||
|
.span-overview {
|
||||||
|
.span-overview-content {
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
.first-row {
|
||||||
|
.collapse-uncollapse-button {
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
background: var(--bg-vanilla-400);
|
||||||
|
|
||||||
|
.children-count {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-name {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-row {
|
||||||
|
.service-name {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interested-span {
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-td + .div-td {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-th + .div-th {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-thead {
|
||||||
|
background-color: var(--bg-vanilla-200) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.span-dets {
|
||||||
|
.related-logs {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,388 @@
|
|||||||
|
/* 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 { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
ArrowUpRight,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Leaf,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} 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,
|
||||||
|
selectedSpan,
|
||||||
|
}: {
|
||||||
|
span: Span;
|
||||||
|
isSpanCollapsed: boolean;
|
||||||
|
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
|
||||||
|
selectedSpan: Span | undefined;
|
||||||
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
||||||
|
}): 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>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
|
||||||
|
|
||||||
|
if (span.hasError) {
|
||||||
|
color = `var(--bg-cherry-500)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'span-duration',
|
||||||
|
selectedSpan?.spanId === span.spanId ? 'interested-span' : '',
|
||||||
|
)}
|
||||||
|
onClick={(): void => {
|
||||||
|
setSelectedSpan(span);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="span-line"
|
||||||
|
style={{
|
||||||
|
left: `${leftOffset}%`,
|
||||||
|
width: `${width}%`,
|
||||||
|
backgroundColor: color,
|
||||||
|
marginLeft: '15px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
|
||||||
|
<Typography.Text
|
||||||
|
className="span-line-text"
|
||||||
|
ellipsis
|
||||||
|
style={{ left: `${leftOffset}%`, color, marginLeft: '15px' }}
|
||||||
|
>{`${toFixed(time, 2)} ${timeUnitName}`}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// table config
|
||||||
|
const columnDefHelper = createColumnHelper<Span>();
|
||||||
|
|
||||||
|
function getWaterfallColumns({
|
||||||
|
handleCollapseUncollapse,
|
||||||
|
uncollapsedNodes,
|
||||||
|
traceMetadata,
|
||||||
|
selectedSpan,
|
||||||
|
setSelectedSpan,
|
||||||
|
}: {
|
||||||
|
handleCollapseUncollapse: (id: string, collapse: boolean) => void;
|
||||||
|
uncollapsedNodes: string[];
|
||||||
|
traceMetadata: ITraceMetadata;
|
||||||
|
selectedSpan: Span | undefined;
|
||||||
|
setSelectedSpan: Dispatch<SetStateAction<Span | undefined>>;
|
||||||
|
}): 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}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
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 columns = useMemo(
|
||||||
|
() =>
|
||||||
|
getWaterfallColumns({
|
||||||
|
handleCollapseUncollapse,
|
||||||
|
uncollapsedNodes,
|
||||||
|
traceMetadata,
|
||||||
|
selectedSpan,
|
||||||
|
setSelectedSpan,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
handleCollapseUncollapse,
|
||||||
|
uncollapsedNodes,
|
||||||
|
traceMetadata,
|
||||||
|
selectedSpan,
|
||||||
|
setSelectedSpan,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Success;
|
7
frontend/src/container/TraceWaterfall/constants.ts
Normal file
7
frontend/src/container/TraceWaterfall/constants.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export enum TraceWaterfallStates {
|
||||||
|
LOADING = 'LOADING',
|
||||||
|
SUCCESS = 'SUCCESS',
|
||||||
|
NO_DATA = 'NO_DATA',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT',
|
||||||
|
}
|
31
frontend/src/hooks/trace/useGetTraceFlamegraph.tsx
Normal file
31
frontend/src/hooks/trace/useGetTraceFlamegraph.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import getTraceFlamegraph from 'api/trace/getTraceFlamegraph';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useQuery, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
GetTraceFlamegraphPayloadProps,
|
||||||
|
GetTraceFlamegraphSuccessResponse,
|
||||||
|
} from 'types/api/trace/getTraceFlamegraph';
|
||||||
|
|
||||||
|
const useGetTraceFlamegraph = (
|
||||||
|
props: GetTraceFlamegraphPayloadProps,
|
||||||
|
): UseLicense =>
|
||||||
|
useQuery({
|
||||||
|
queryFn: () => getTraceFlamegraph(props),
|
||||||
|
// if any of the props changes then we need to trigger an API call as the older data will be obsolete
|
||||||
|
queryKey: [
|
||||||
|
REACT_QUERY_KEY.GET_TRACE_V2_FLAMEGRAPH,
|
||||||
|
props.traceId,
|
||||||
|
props.selectedSpanId,
|
||||||
|
],
|
||||||
|
enabled: !!props.traceId,
|
||||||
|
keepPreviousData: true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
type UseLicense = UseQueryResult<
|
||||||
|
SuccessResponse<GetTraceFlamegraphSuccessResponse> | ErrorResponse,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default useGetTraceFlamegraph;
|
30
frontend/src/hooks/trace/useGetTraceV2.tsx
Normal file
30
frontend/src/hooks/trace/useGetTraceV2.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import getTraceV2 from 'api/trace/getTraceV2';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useQuery, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
GetTraceV2PayloadProps,
|
||||||
|
GetTraceV2SuccessResponse,
|
||||||
|
} from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
const useGetTraceV2 = (props: GetTraceV2PayloadProps): UseLicense =>
|
||||||
|
useQuery({
|
||||||
|
queryFn: () => getTraceV2(props),
|
||||||
|
// if any of the props changes then we need to trigger an API call as the older data will be obsolete
|
||||||
|
queryKey: [
|
||||||
|
REACT_QUERY_KEY.GET_TRACE_V2_WATERFALL,
|
||||||
|
props.traceId,
|
||||||
|
props.selectedSpanId,
|
||||||
|
props.isSelectedSpanIDUnCollapsed,
|
||||||
|
],
|
||||||
|
enabled: !!props.traceId,
|
||||||
|
keepPreviousData: true,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
type UseLicense = UseQueryResult<
|
||||||
|
SuccessResponse<GetTraceV2SuccessResponse> | ErrorResponse,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default useGetTraceV2;
|
45
frontend/src/pages/TraceDetail/TraceDetail.styles.scss
Normal file
45
frontend/src/pages/TraceDetail/TraceDetail.styles.scss
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.old-trace-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.top-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
padding: 5px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.new-cta-btn {
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 6px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
|
||||||
|
/* Bifrost (Ancient)/Content/sm */
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.ant-btn-icon {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.old-trace-container {
|
||||||
|
.top-header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.new-cta-btn {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
import { Typography } from 'antd';
|
import './TraceDetail.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Typography } from 'antd';
|
||||||
import getTraceItem from 'api/trace/getTraceItem';
|
import getTraceItem from 'api/trace/getTraceItem';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TraceDetailContainer from 'container/TraceDetail';
|
import TraceDetailContainer from 'container/TraceDetail';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { useMemo } from 'react';
|
import { Undo } from 'lucide-react';
|
||||||
|
import TraceDetailsPage from 'pages/TraceDetailV2';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem';
|
import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem';
|
||||||
@ -13,6 +17,7 @@ import { noEventMessage } from './constants';
|
|||||||
|
|
||||||
function TraceDetail(): JSX.Element {
|
function TraceDetail(): JSX.Element {
|
||||||
const { id } = useParams<TraceDetailProps>();
|
const { id } = useParams<TraceDetailProps>();
|
||||||
|
const [showNewTraceDetails, setShowNewTraceDetails] = useState<boolean>(false);
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
const { spanId, levelUp, levelDown } = useMemo(
|
const { spanId, levelUp, levelDown } = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -31,6 +36,10 @@ function TraceDetail(): JSX.Element {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (showNewTraceDetails) {
|
||||||
|
return <TraceDetailsPage />;
|
||||||
|
}
|
||||||
|
|
||||||
if (traceDetailResponse?.error || error || isError) {
|
if (traceDetailResponse?.error || error || isError) {
|
||||||
return (
|
return (
|
||||||
<Typography>
|
<Typography>
|
||||||
@ -47,7 +56,21 @@ function TraceDetail(): JSX.Element {
|
|||||||
return <NotFound text={noEventMessage} />;
|
return <NotFound text={noEventMessage} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <TraceDetailContainer response={traceDetailResponse.payload} />;
|
return (
|
||||||
|
<div className="old-trace-container">
|
||||||
|
<div className="top-header">
|
||||||
|
<Button
|
||||||
|
onClick={(): void => setShowNewTraceDetails(true)}
|
||||||
|
icon={<Undo size={14} />}
|
||||||
|
type="text"
|
||||||
|
className="new-cta-btn"
|
||||||
|
>
|
||||||
|
New Trace Detail
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<TraceDetailContainer response={traceDetailResponse.payload} />;
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TraceDetail;
|
export default TraceDetail;
|
||||||
|
167
frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss
Normal file
167
frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
.not-found-trace {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 60vh;
|
||||||
|
width: 500px;
|
||||||
|
gap: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.not-found-img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-text-1 {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
.not-found-text-2 {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reasons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.reason-1 {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(171, 189, 255, 0.04);
|
||||||
|
|
||||||
|
.construction-img {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reason-2 {
|
||||||
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(171, 189, 255, 0.04);
|
||||||
|
|
||||||
|
.broom-img {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.none-of-above {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
width: 160px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.ant-btn-icon {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.not-found-trace {
|
||||||
|
.description {
|
||||||
|
.not-found-text-1 {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
.not-found-text-2 {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reasons {
|
||||||
|
.reason-1 {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.reason-2 {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.none-of-above {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btns {
|
||||||
|
.action-btn {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
frontend/src/pages/TraceDetailV2/NoData/NoData.tsx
Normal file
65
frontend/src/pages/TraceDetailV2/NoData/NoData.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import './NoData.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import { LifeBuoy, RefreshCw } from 'lucide-react';
|
||||||
|
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||||
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
|
function NoData(): JSX.Element {
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
return (
|
||||||
|
<div className="not-found-trace">
|
||||||
|
<section className="description">
|
||||||
|
<img src="/Icons/no-data.svg" alt="no-data" className="not-found-img" />
|
||||||
|
<Typography.Text className="not-found-text-1">
|
||||||
|
Uh-oh! We cannot show the selected trace.
|
||||||
|
<span className="not-found-text-2">
|
||||||
|
This can happen in either of the two scenraios -
|
||||||
|
</span>
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
<section className="reasons">
|
||||||
|
<div className="reason-1">
|
||||||
|
<img
|
||||||
|
src="/Icons/construction.svg"
|
||||||
|
alt="no-data"
|
||||||
|
className="construction-img"
|
||||||
|
/>
|
||||||
|
<Typography.Text className="text">
|
||||||
|
The trace data has not been rendered on your SigNoz server yet. You can
|
||||||
|
wait for a bit and refresh this page if this is the case.
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="reason-2">
|
||||||
|
<img src="/Icons/broom.svg" alt="no-data" className="broom-img" />
|
||||||
|
<Typography.Text className="text">
|
||||||
|
The trace has been deleted as the data has crossed it’s retention period.
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="none-of-above">
|
||||||
|
<Typography.Text className="text">
|
||||||
|
If you feel the issue is none of the above, please contact support.
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="action-btns">
|
||||||
|
<Button
|
||||||
|
className="action-btn"
|
||||||
|
icon={<RefreshCw size={14} />}
|
||||||
|
onClick={(): void => window.location.reload()}
|
||||||
|
>
|
||||||
|
Refresh this page
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="action-btn"
|
||||||
|
icon={<LifeBuoy size={14} />}
|
||||||
|
onClick={(): void => handleContactSupport(isCloudUserVal)}
|
||||||
|
>
|
||||||
|
Contact Support
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoData;
|
207
frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss
Normal file
207
frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
.traces-module-container {
|
||||||
|
.trace-module {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
.tab-item {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav-list {
|
||||||
|
transform: translate(15px, 0px) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.old-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
|
||||||
|
/* Bifrost (Ancient)/Content/sm */
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.ant-btn-icon {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-layout {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 44px);
|
||||||
|
|
||||||
|
.trace-left-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
padding-top: 16px;
|
||||||
|
|
||||||
|
.flamegraph-waterfall-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 31px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
padding: 5px 20px;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.ant-btn-icon {
|
||||||
|
margin-inline-end: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-list-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 31px;
|
||||||
|
padding: 5px 20px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.ant-btn-icon {
|
||||||
|
margin-inline-end: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-visualisation-tabs {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
border-radius: 2px 0px 0px 0px;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
border-radius: 2px 2px 0px 0px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
height: 31px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab + .ant-tabs-tab {
|
||||||
|
margin: 0px;
|
||||||
|
border-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-ink-bar {
|
||||||
|
height: 1px !important;
|
||||||
|
background: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav-list {
|
||||||
|
transform: translate(15px, 0px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.traces-module-container {
|
||||||
|
.trace-module {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
.tab-item {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
.tab-item {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.old-switch {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-layout {
|
||||||
|
.flamegraph-waterfall-toggle {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-list-toggle {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-visualisation-tabs {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
color: var(--bg-ink-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-ink-bar {
|
||||||
|
height: 1px !important;
|
||||||
|
background: var(--bg-vanilla-200) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
160
frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
Normal file
160
frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import './TraceDetailV2.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Tabs } from 'antd';
|
||||||
|
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
|
||||||
|
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
|
||||||
|
import SpanDetailsDrawer from 'container/SpanDetailsDrawer/SpanDetailsDrawer';
|
||||||
|
import TraceMetadata from 'container/TraceMetadata/TraceMetadata';
|
||||||
|
import TraceWaterfall, {
|
||||||
|
IInterestedSpan,
|
||||||
|
} from 'container/TraceWaterfall/TraceWaterfall';
|
||||||
|
import useGetTraceV2 from 'hooks/trace/useGetTraceV2';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { Span, TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
import NoData from './NoData/NoData';
|
||||||
|
|
||||||
|
function TraceDetailsV2(): JSX.Element {
|
||||||
|
const { id: traceId } = useParams<TraceDetailV2URLProps>();
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const [interestedSpanId, setInterestedSpanId] = useState<IInterestedSpan>(
|
||||||
|
() => ({
|
||||||
|
spanId: urlQuery.get('spanId') || '',
|
||||||
|
isUncollapsed: urlQuery.get('spanId') !== '',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
traceFlamegraphStatsWidth,
|
||||||
|
setTraceFlamegraphStatsWidth,
|
||||||
|
] = useState<number>(450);
|
||||||
|
const [isSpanDetailsDocked, setIsSpanDetailsDocked] = useState<boolean>(false);
|
||||||
|
const [selectedSpan, setSelectedSpan] = useState<Span>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInterestedSpanId({
|
||||||
|
spanId: urlQuery.get('spanId') || '',
|
||||||
|
isUncollapsed: urlQuery.get('spanId') !== '',
|
||||||
|
});
|
||||||
|
}, [urlQuery]);
|
||||||
|
|
||||||
|
const [uncollapsedNodes, setUncollapsedNodes] = useState<string[]>([]);
|
||||||
|
const {
|
||||||
|
data: traceData,
|
||||||
|
isFetching: isFetchingTraceData,
|
||||||
|
error: errorFetchingTraceData,
|
||||||
|
} = useGetTraceV2({
|
||||||
|
traceId,
|
||||||
|
uncollapsedSpans: uncollapsedNodes,
|
||||||
|
selectedSpanId: interestedSpanId.spanId,
|
||||||
|
isSelectedSpanIDUnCollapsed: interestedSpanId.isUncollapsed,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (traceData && traceData.payload && traceData.payload.uncollapsedSpans) {
|
||||||
|
setUncollapsedNodes(traceData.payload.uncollapsedSpans);
|
||||||
|
}
|
||||||
|
}, [traceData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedSpan) {
|
||||||
|
setIsSpanDetailsDocked(false);
|
||||||
|
}
|
||||||
|
}, [selectedSpan]);
|
||||||
|
|
||||||
|
const noData = useMemo(
|
||||||
|
() =>
|
||||||
|
!isFetchingTraceData &&
|
||||||
|
!errorFetchingTraceData &&
|
||||||
|
defaultTo(traceData?.payload?.spans.length, 0) === 0,
|
||||||
|
[
|
||||||
|
errorFetchingTraceData,
|
||||||
|
isFetchingTraceData,
|
||||||
|
traceData?.payload?.spans.length,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (noData) {
|
||||||
|
setIsSpanDetailsDocked(true);
|
||||||
|
}
|
||||||
|
}, [noData]);
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<FlamegraphImg />}
|
||||||
|
className="flamegraph-waterfall-toggle"
|
||||||
|
>
|
||||||
|
Flamegraph
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
key: 'flamegraph',
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
<TraceFlamegraph
|
||||||
|
serviceExecTime={traceData?.payload?.serviceNameToTotalDurationMap || {}}
|
||||||
|
startTime={traceData?.payload?.startTimestampMillis || 0}
|
||||||
|
endTime={traceData?.payload?.endTimestampMillis || 0}
|
||||||
|
traceFlamegraphStatsWidth={traceFlamegraphStatsWidth}
|
||||||
|
selectedSpan={selectedSpan}
|
||||||
|
/>
|
||||||
|
<TraceWaterfall
|
||||||
|
traceData={traceData}
|
||||||
|
isFetchingTraceData={isFetchingTraceData}
|
||||||
|
errorFetchingTraceData={errorFetchingTraceData}
|
||||||
|
traceId={traceId}
|
||||||
|
interestedSpanId={interestedSpanId}
|
||||||
|
setInterestedSpanId={setInterestedSpanId}
|
||||||
|
uncollapsedNodes={uncollapsedNodes}
|
||||||
|
setTraceFlamegraphStatsWidth={setTraceFlamegraphStatsWidth}
|
||||||
|
selectedSpan={selectedSpan}
|
||||||
|
setSelectedSpan={setSelectedSpan}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="trace-layout">
|
||||||
|
<div
|
||||||
|
className="trace-left-content"
|
||||||
|
style={{ width: `calc(100% - ${isSpanDetailsDocked ? 48 : 330}px)` }}
|
||||||
|
>
|
||||||
|
<TraceMetadata
|
||||||
|
traceID={traceId}
|
||||||
|
duration={
|
||||||
|
(traceData?.payload?.endTimestampMillis || 0) -
|
||||||
|
(traceData?.payload?.startTimestampMillis || 0)
|
||||||
|
}
|
||||||
|
startTime={(traceData?.payload?.startTimestampMillis || 0) / 1e3}
|
||||||
|
rootServiceName={traceData?.payload?.rootServiceName || ''}
|
||||||
|
rootSpanName={traceData?.payload?.rootServiceEntryPoint || ''}
|
||||||
|
totalErrorSpans={traceData?.payload?.totalErrorSpansCount || 0}
|
||||||
|
totalSpans={traceData?.payload?.totalSpansCount || 0}
|
||||||
|
notFound={noData}
|
||||||
|
/>
|
||||||
|
{!noData ? (
|
||||||
|
<Tabs items={items} animated className="trace-visualisation-tabs" />
|
||||||
|
) : (
|
||||||
|
<NoData />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SpanDetailsDrawer
|
||||||
|
isSpanDetailsDocked={isSpanDetailsDocked}
|
||||||
|
setIsSpanDetailsDocked={setIsSpanDetailsDocked}
|
||||||
|
selectedSpan={selectedSpan}
|
||||||
|
traceID={traceId}
|
||||||
|
traceStartTime={traceData?.payload?.startTimestampMillis || 0}
|
||||||
|
traceEndTime={traceData?.payload?.endTimestampMillis || 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraceDetailsV2;
|
83
frontend/src/pages/TraceDetailV2/index.tsx
Normal file
83
frontend/src/pages/TraceDetailV2/index.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import './TraceDetailV2.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Tabs } from 'antd';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { Compass, TowerControl, Undo } from 'lucide-react';
|
||||||
|
import TraceDetail from 'pages/TraceDetail';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import TraceDetailsV2 from './TraceDetailV2';
|
||||||
|
|
||||||
|
interface INewTraceDetailProps {
|
||||||
|
items: {
|
||||||
|
label: JSX.Element;
|
||||||
|
key: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}[];
|
||||||
|
handleOldTraceDetails: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewTraceDetail(props: INewTraceDetailProps): JSX.Element {
|
||||||
|
const { items, handleOldTraceDetails } = props;
|
||||||
|
return (
|
||||||
|
<div className="traces-module-container">
|
||||||
|
<Tabs
|
||||||
|
items={items}
|
||||||
|
animated
|
||||||
|
className="trace-module"
|
||||||
|
onTabClick={(activeKey): void => {
|
||||||
|
if (activeKey === 'saved-views') {
|
||||||
|
history.push(ROUTES.TRACES_SAVE_VIEWS);
|
||||||
|
}
|
||||||
|
if (activeKey === 'trace-details') {
|
||||||
|
history.push(ROUTES.TRACES_EXPLORER);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
tabBarExtraContent={
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
onClick={handleOldTraceDetails}
|
||||||
|
className="old-switch"
|
||||||
|
icon={<Undo size={14} />}
|
||||||
|
>
|
||||||
|
Old Trace Details
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TraceDetailsPage(): JSX.Element {
|
||||||
|
const [showOldTraceDetails, setShowOldTraceDetails] = useState<boolean>(false);
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<Compass size={16} /> Explorer
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: 'trace-details',
|
||||||
|
children: <TraceDetailsV2 />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<div className="tab-item">
|
||||||
|
<TowerControl size={16} /> Views
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
key: 'saved-views',
|
||||||
|
children: <div />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const handleOldTraceDetails = useCallback(() => {
|
||||||
|
setShowOldTraceDetails(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return showOldTraceDetails ? (
|
||||||
|
<TraceDetail />
|
||||||
|
) : (
|
||||||
|
<NewTraceDetail items={items} handleOldTraceDetails={handleOldTraceDetails} />
|
||||||
|
);
|
||||||
|
}
|
@ -3,7 +3,7 @@ export interface ILog {
|
|||||||
timestamp: number | string;
|
timestamp: number | string;
|
||||||
id: string;
|
id: string;
|
||||||
traceId: string;
|
traceId: string;
|
||||||
spanId: string;
|
spanID: string;
|
||||||
traceFlags: number;
|
traceFlags: number;
|
||||||
severityText: string;
|
severityText: string;
|
||||||
severityNumber: number;
|
severityNumber: number;
|
||||||
|
26
frontend/src/types/api/trace/getTraceFlamegraph.ts
Normal file
26
frontend/src/types/api/trace/getTraceFlamegraph.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export interface TraceDetailFlamegraphURLProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetTraceFlamegraphPayloadProps {
|
||||||
|
traceId: string;
|
||||||
|
selectedSpanId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlamegraphSpan {
|
||||||
|
timestamp: number;
|
||||||
|
durationNano: number;
|
||||||
|
spanId: string;
|
||||||
|
parentSpanId: string;
|
||||||
|
traceId: string;
|
||||||
|
hasError: boolean;
|
||||||
|
serviceName: string;
|
||||||
|
name: string;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetTraceFlamegraphSuccessResponse {
|
||||||
|
spans: FlamegraphSpan[][];
|
||||||
|
startTimestampMillis: number;
|
||||||
|
endTimestampMillis: number;
|
||||||
|
}
|
52
frontend/src/types/api/trace/getTraceV2.ts
Normal file
52
frontend/src/types/api/trace/getTraceV2.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export interface TraceDetailV2URLProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetTraceV2PayloadProps {
|
||||||
|
traceId: string;
|
||||||
|
selectedSpanId: string;
|
||||||
|
uncollapsedSpans: string[];
|
||||||
|
isSelectedSpanIDUnCollapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Event {
|
||||||
|
name: string;
|
||||||
|
timeUnixNano: number;
|
||||||
|
attributeMap: Record<string, string>;
|
||||||
|
}
|
||||||
|
export interface Span {
|
||||||
|
timestamp: number;
|
||||||
|
durationNano: number;
|
||||||
|
spanId: string;
|
||||||
|
rootSpanId: string;
|
||||||
|
parentSpanId: string;
|
||||||
|
traceId: string;
|
||||||
|
hasError: boolean;
|
||||||
|
kind: number;
|
||||||
|
serviceName: string;
|
||||||
|
name: string;
|
||||||
|
references: any;
|
||||||
|
tagMap: Record<string, string>;
|
||||||
|
event: string[];
|
||||||
|
rootName: string;
|
||||||
|
statusMessage: string;
|
||||||
|
statusCodeString: string;
|
||||||
|
spanKind: string;
|
||||||
|
hasChildren: boolean;
|
||||||
|
hasSibling: boolean;
|
||||||
|
subTreeNodeCount: number;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetTraceV2SuccessResponse {
|
||||||
|
spans: Span[];
|
||||||
|
hasMissingSpans: boolean;
|
||||||
|
uncollapsedSpans: string[];
|
||||||
|
startTimestampMillis: number;
|
||||||
|
endTimestampMillis: number;
|
||||||
|
totalSpansCount: number;
|
||||||
|
totalErrorSpansCount: number;
|
||||||
|
rootServiceName: string;
|
||||||
|
rootServiceEntryPoint: string;
|
||||||
|
serviceNameToTotalDurationMap: Record<string, number>;
|
||||||
|
}
|
@ -3690,11 +3690,23 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@tanstack/table-core" "8.20.5"
|
"@tanstack/table-core" "8.20.5"
|
||||||
|
|
||||||
|
"@tanstack/react-virtual@3.11.2":
|
||||||
|
version "3.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322"
|
||||||
|
integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==
|
||||||
|
dependencies:
|
||||||
|
"@tanstack/virtual-core" "3.11.2"
|
||||||
|
|
||||||
"@tanstack/table-core@8.20.5":
|
"@tanstack/table-core@8.20.5":
|
||||||
version "8.20.5"
|
version "8.20.5"
|
||||||
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
|
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d"
|
||||||
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
|
integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==
|
||||||
|
|
||||||
|
"@tanstack/virtual-core@3.11.2":
|
||||||
|
version "3.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212"
|
||||||
|
integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==
|
||||||
|
|
||||||
"@testing-library/dom@^8.5.0":
|
"@testing-library/dom@^8.5.0":
|
||||||
version "8.20.0"
|
version "8.20.0"
|
||||||
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
|
resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz"
|
||||||
|
@ -1811,7 +1811,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, trace
|
|||||||
}
|
}
|
||||||
|
|
||||||
processingPostCache := time.Now()
|
processingPostCache := time.Now()
|
||||||
selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans)
|
selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, startTime, endTime)
|
||||||
zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
|
zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID))
|
||||||
|
|
||||||
trace.Spans = selectedSpansForRequest
|
trace.Spans = selectedSpansForRequest
|
||||||
|
@ -8,6 +8,8 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50
|
SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50
|
||||||
|
SPAN_LIMIT_PER_LEVEL int = 100
|
||||||
|
TIMESTAMP_SAMPLING_BUCKET_COUNT int = 50
|
||||||
)
|
)
|
||||||
|
|
||||||
func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool {
|
func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool {
|
||||||
@ -39,9 +41,11 @@ func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId st
|
|||||||
var selectedSpanLevel int = 0
|
var selectedSpanLevel int = 0
|
||||||
|
|
||||||
for index, _spans := range spans {
|
for index, _spans := range spans {
|
||||||
if len(_spans) > 0 && _spans[0].SpanID == selectedSpanId {
|
for _, span := range _spans {
|
||||||
selectedSpanLevel = index
|
if span.SpanID == selectedSpanId {
|
||||||
break
|
selectedSpanLevel = index
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +92,64 @@ func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToS
|
|||||||
return selectedSpans
|
return selectedSpans
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan) [][]*model.FlamegraphSpan {
|
func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selectedSpanID string, isSelectedSpanIDPresent bool, startTime uint64, endTime uint64) []*model.FlamegraphSpan {
|
||||||
|
var sampledSpans []*model.FlamegraphSpan
|
||||||
|
// sort the spans by latency for latency filtering
|
||||||
|
sort.Slice(spans, func(i, j int) bool {
|
||||||
|
return spans[i].DurationNano > spans[j].DurationNano
|
||||||
|
})
|
||||||
|
|
||||||
|
// pick the top 5 latency spans
|
||||||
|
for idx := range 5 {
|
||||||
|
sampledSpans = append(sampledSpans, spans[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
// always add the selectedSpan
|
||||||
|
if isSelectedSpanIDPresent {
|
||||||
|
idx := -1
|
||||||
|
for _idx, span := range spans {
|
||||||
|
if span.SpanID == selectedSpanID {
|
||||||
|
idx = _idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx != -1 {
|
||||||
|
sampledSpans = append(sampledSpans, spans[idx])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT)
|
||||||
|
if bucketSize == 0 {
|
||||||
|
bucketSize = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketedSpans := make([][]*model.FlamegraphSpan, 50)
|
||||||
|
|
||||||
|
for _, span := range spans {
|
||||||
|
if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime {
|
||||||
|
bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize)
|
||||||
|
if bucketIndex >= 0 && bucketIndex < 50 {
|
||||||
|
bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range bucketedSpans {
|
||||||
|
if len(bucketedSpans[i]) > 2 {
|
||||||
|
// Keep only the first 2 spans
|
||||||
|
bucketedSpans[i] = bucketedSpans[i][:2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten the bucketed spans into a single slice
|
||||||
|
for _, bucket := range bucketedSpans {
|
||||||
|
sampledSpans = append(sampledSpans, bucket...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sampledSpans
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan, startTime uint64, endTime uint64) [][]*model.FlamegraphSpan {
|
||||||
|
var selectedSpansForRequest [][]*model.FlamegraphSpan
|
||||||
var selectedIndex = 0
|
var selectedIndex = 0
|
||||||
|
|
||||||
if selectedSpanID != "" {
|
if selectedSpanID != "" {
|
||||||
@ -112,5 +173,14 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan
|
|||||||
lowerLimit = 0
|
lowerLimit = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedSpans[lowerLimit:upperLimit]
|
for i := lowerLimit; i < upperLimit; i++ {
|
||||||
|
if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL {
|
||||||
|
_spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime)
|
||||||
|
selectedSpansForRequest = append(selectedSpansForRequest, _spans)
|
||||||
|
} else {
|
||||||
|
selectedSpansForRequest = append(selectedSpansForRequest, selectedSpans[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedSpansForRequest
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user