mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-06 05:42:02 +00:00
Compare commits
3 Commits
feat/trace
...
feat/disab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3195411c7 | ||
|
|
745e4e7c0a | ||
|
|
3e6d105e81 |
@@ -1,4 +0,0 @@
|
||||
.timeline-v3-container {
|
||||
// flex: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import {
|
||||
getIntervals,
|
||||
getMinimumIntervalsBasedOnWidth,
|
||||
Interval,
|
||||
} from './utils';
|
||||
|
||||
import './TimelineV3.styles.scss';
|
||||
|
||||
interface ITimelineV3Props {
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
timelineHeight: number;
|
||||
offsetTimestamp: number;
|
||||
}
|
||||
|
||||
function TimelineV3(props: ITimelineV3Props): JSX.Element {
|
||||
const {
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
timelineHeight,
|
||||
offsetTimestamp,
|
||||
} = props;
|
||||
const [intervals, setIntervals] = useState<Interval[]>([]);
|
||||
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
const spread = endTimestamp - startTimestamp;
|
||||
if (spread < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
|
||||
const intervalisedSpread = (spread / minIntervals) * 1.0;
|
||||
const intervals = getIntervals(intervalisedSpread, spread, offsetTimestamp);
|
||||
|
||||
setIntervals(intervals);
|
||||
}, [startTimestamp, endTimestamp, width, offsetTimestamp]);
|
||||
|
||||
if (endTimestamp < startTimestamp) {
|
||||
console.error(
|
||||
'endTimestamp cannot be less than startTimestamp',
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
);
|
||||
return <div />;
|
||||
}
|
||||
|
||||
const strokeColor = isDarkMode ? ' rgb(192,193,195,0.8)' : 'black';
|
||||
|
||||
return (
|
||||
<div ref={ref as never} className="timeline-v3-container">
|
||||
<svg
|
||||
width={width}
|
||||
height={timelineHeight}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
overflow="visible"
|
||||
>
|
||||
<line
|
||||
x1="0"
|
||||
y1={timelineHeight}
|
||||
x2={width}
|
||||
y2={timelineHeight}
|
||||
stroke={strokeColor}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
{intervals &&
|
||||
intervals.length > 0 &&
|
||||
intervals.map((interval, index) => (
|
||||
<g
|
||||
transform={`translate(${(interval.percentage * width) / 100},0)`}
|
||||
key={`${interval.percentage + interval.label + index}`}
|
||||
textAnchor="middle"
|
||||
fontSize="0.6rem"
|
||||
>
|
||||
<text
|
||||
x={index === intervals.length - 1 ? -10 : 0}
|
||||
y={2 * Math.floor(timelineHeight / 4)}
|
||||
fill={strokeColor}
|
||||
>
|
||||
{interval.label}
|
||||
</text>
|
||||
<line
|
||||
y1={3 * Math.floor(timelineHeight / 4)}
|
||||
y2={timelineHeight + 0.5}
|
||||
stroke={strokeColor}
|
||||
strokeWidth="1"
|
||||
/>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimelineV3;
|
||||
@@ -1,78 +0,0 @@
|
||||
import {
|
||||
getMinimumIntervalsBasedOnWidth,
|
||||
IIntervalUnit,
|
||||
Interval,
|
||||
INTERVAL_UNITS,
|
||||
resolveTimeFromInterval,
|
||||
} from 'components/TimelineV2/utils';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
|
||||
export type { Interval };
|
||||
|
||||
export { getMinimumIntervalsBasedOnWidth };
|
||||
|
||||
/**
|
||||
* Computes timeline intervals with offset-aware labels.
|
||||
* Labels reflect absolute time from trace start (offsetTimestamp + elapsed),
|
||||
* so when zoomed into a window, the first tick shows e.g. "50ms" not "0ms".
|
||||
*/
|
||||
export function getIntervals(
|
||||
intervalSpread: number,
|
||||
baseSpread: number,
|
||||
offsetTimestamp: number,
|
||||
): Interval[] {
|
||||
const integerPartString = intervalSpread.toString().split('.')[0];
|
||||
const integerPartLength = integerPartString.length;
|
||||
|
||||
const intervalSpreadNormalized =
|
||||
intervalSpread < 1.0
|
||||
? intervalSpread
|
||||
: Math.floor(Number(integerPartString) / 10 ** (integerPartLength - 1)) *
|
||||
10 ** (integerPartLength - 1);
|
||||
|
||||
let intervalUnit: IIntervalUnit = INTERVAL_UNITS[0];
|
||||
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {
|
||||
const standardInterval = INTERVAL_UNITS[idx];
|
||||
if (intervalSpread * standardInterval.multiplier >= 1) {
|
||||
intervalUnit = INTERVAL_UNITS[idx];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const intervals: Interval[] = [
|
||||
{
|
||||
label: `${toFixed(
|
||||
resolveTimeFromInterval(offsetTimestamp, intervalUnit),
|
||||
2,
|
||||
)}${intervalUnit.name}`,
|
||||
percentage: 0,
|
||||
},
|
||||
];
|
||||
|
||||
let tempBaseSpread = baseSpread;
|
||||
let elapsedIntervals = 0;
|
||||
|
||||
while (tempBaseSpread && intervals.length < 20) {
|
||||
let intervalTime: number;
|
||||
|
||||
if (tempBaseSpread <= 1.5 * intervalSpreadNormalized) {
|
||||
intervalTime = elapsedIntervals + tempBaseSpread;
|
||||
tempBaseSpread = 0;
|
||||
} else {
|
||||
intervalTime = elapsedIntervals + intervalSpreadNormalized;
|
||||
tempBaseSpread -= intervalSpreadNormalized;
|
||||
}
|
||||
|
||||
elapsedIntervals = intervalTime;
|
||||
const labelTime = offsetTimestamp + intervalTime;
|
||||
|
||||
intervals.push({
|
||||
label: `${toFixed(resolveTimeFromInterval(labelTime, intervalUnit), 2)}${
|
||||
intervalUnit.name
|
||||
}`,
|
||||
percentage: (intervalTime / baseSpread) * 100,
|
||||
});
|
||||
}
|
||||
|
||||
return intervals;
|
||||
}
|
||||
@@ -1,76 +1,37 @@
|
||||
const themeColors = {
|
||||
traceDetailColors: {
|
||||
c01: '#4C6FFF',
|
||||
c02: '#FF6B57',
|
||||
c03: '#2ECC71',
|
||||
c04: '#F5B72E',
|
||||
c05: '#9B59B6',
|
||||
c06: '#00C2FF',
|
||||
c07: '#FF8A00',
|
||||
c08: '#1ABC9C',
|
||||
|
||||
c09: '#FF4D6D',
|
||||
c10: '#7C3AED',
|
||||
c11: '#22C55E',
|
||||
c12: '#0EA5E9',
|
||||
c13: '#E11D48',
|
||||
c14: '#F97316',
|
||||
c15: '#14B8A6',
|
||||
c16: '#84CC16',
|
||||
|
||||
c17: '#A855F7',
|
||||
c18: '#3B82F6',
|
||||
c19: '#F43F5E',
|
||||
c20: '#10B981',
|
||||
c21: '#6366F1',
|
||||
c22: '#F59E0B',
|
||||
c23: '#06B6D4',
|
||||
c24: '#D946EF',
|
||||
|
||||
c25: '#4ADE80',
|
||||
c26: '#FB7185',
|
||||
c27: '#38BDF8',
|
||||
c28: '#C084FC',
|
||||
c29: '#34D399',
|
||||
c30: '#FBBF24',
|
||||
c31: '#60A5FA',
|
||||
c32: '#F87171',
|
||||
|
||||
c33: '#2DD4BF',
|
||||
c34: '#A3E635',
|
||||
c35: '#818CF8',
|
||||
c36: '#F472B6',
|
||||
c37: '#22D3EE',
|
||||
c38: '#BE123C',
|
||||
c39: '#65A30D',
|
||||
c40: '#9333EA',
|
||||
|
||||
c41: '#0284C7',
|
||||
c42: '#DC2626',
|
||||
c43: '#059669',
|
||||
c44: '#B45309',
|
||||
c45: '#7DD3FC',
|
||||
c46: '#A78BFA',
|
||||
c47: '#4D7C0F',
|
||||
c48: '#DB2777',
|
||||
|
||||
c49: '#0F766E',
|
||||
c50: '#2563EB',
|
||||
c51: '#FF3D00',
|
||||
c52: '#00E676',
|
||||
c53: '#FFC400',
|
||||
c54: '#00E5FF',
|
||||
c55: '#D633FF',
|
||||
c56: '#9BE22D',
|
||||
|
||||
c57: '#FF5A5F',
|
||||
c58: '#00B3E6',
|
||||
c59: '#FFB300',
|
||||
c60: '#3FA600',
|
||||
c61: '#FF4DD2',
|
||||
c62: '#1D4ED8',
|
||||
c63: '#2F80ED',
|
||||
c64: '#17C3B2',
|
||||
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: {
|
||||
// Blues (3)
|
||||
|
||||
@@ -451,6 +451,9 @@ function K8sClustersList({
|
||||
|
||||
const handleRowClick = (record: K8sClustersRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.clusterNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedClusterName(record.clusterUID);
|
||||
setSearchParams({
|
||||
@@ -515,9 +518,13 @@ function K8sClustersList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedClusterName(record.clusterUID);
|
||||
if (record.clusterNameRaw) {
|
||||
setselectedClusterName(record.clusterUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.clusterNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -707,7 +714,10 @@ function K8sClustersList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.clusterNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sClustersRowData {
|
||||
key: string;
|
||||
clusterUID: string;
|
||||
clusterNameRaw: string;
|
||||
clusterName: React.ReactNode;
|
||||
cpu: React.ReactNode;
|
||||
memory: React.ReactNode;
|
||||
@@ -175,6 +176,7 @@ export const formatDataForTable = (
|
||||
data.map((cluster, index) => ({
|
||||
key: index.toString(),
|
||||
clusterUID: cluster.meta.k8s_cluster_name,
|
||||
clusterNameRaw: cluster.meta.k8s_cluster_name || '',
|
||||
clusterName: (
|
||||
<Tooltip title={cluster.meta.k8s_cluster_name}>
|
||||
{cluster.meta.k8s_cluster_name}
|
||||
|
||||
@@ -457,6 +457,9 @@ function K8sDaemonSetsList({
|
||||
|
||||
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.daemonsetNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
setSearchParams({
|
||||
@@ -521,9 +524,13 @@ function K8sDaemonSetsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
if (record.daemonsetNameRaw) {
|
||||
setSelectedDaemonSetUID(record.daemonsetUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.daemonsetNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -715,7 +722,10 @@ function K8sDaemonSetsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.daemonsetNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -82,6 +82,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sDaemonSetsRowData {
|
||||
key: string;
|
||||
daemonsetUID: string;
|
||||
daemonsetNameRaw: string;
|
||||
daemonsetName: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
@@ -276,6 +277,7 @@ export const formatDataForTable = (
|
||||
data.map((daemonSet, index) => ({
|
||||
key: index.toString(),
|
||||
daemonsetUID: daemonSet.daemonSetName,
|
||||
daemonsetNameRaw: daemonSet.meta.k8s_daemonset_name || '',
|
||||
daemonsetName: (
|
||||
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
|
||||
{daemonSet.meta.k8s_daemonset_name || ''}
|
||||
|
||||
@@ -463,6 +463,9 @@ function K8sDeploymentsList({
|
||||
|
||||
const handleRowClick = (record: K8sDeploymentsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.deploymentNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
setSearchParams({
|
||||
@@ -527,9 +530,13 @@ function K8sDeploymentsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
if (record.deploymentNameRaw) {
|
||||
setselectedDeploymentUID(record.deploymentUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.deploymentNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -722,7 +729,10 @@ function K8sDeploymentsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.deploymentNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -81,6 +81,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sDeploymentsRowData {
|
||||
key: string;
|
||||
deploymentUID: string;
|
||||
deploymentNameRaw: string;
|
||||
deploymentName: React.ReactNode;
|
||||
available_pods: React.ReactNode;
|
||||
desired_pods: React.ReactNode;
|
||||
@@ -267,6 +268,7 @@ export const formatDataForTable = (
|
||||
data.map((deployment, index) => ({
|
||||
key: index.toString(),
|
||||
deploymentUID: deployment.meta.k8s_deployment_name,
|
||||
deploymentNameRaw: deployment.meta.k8s_deployment_name || '',
|
||||
deploymentName: (
|
||||
<Tooltip title={deployment.meta.k8s_deployment_name}>
|
||||
{deployment.meta.k8s_deployment_name}
|
||||
|
||||
@@ -337,6 +337,11 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled-row {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.k8s-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
|
||||
@@ -428,6 +428,9 @@ function K8sJobsList({
|
||||
|
||||
const handleRowClick = (record: K8sJobsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.jobNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedJobUID(record.jobUID);
|
||||
setSearchParams({
|
||||
@@ -492,9 +495,11 @@ function K8sJobsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedJobUID(record.jobUID);
|
||||
if (record.jobNameRaw) {
|
||||
setselectedJobUID(record.jobUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.jobNameRaw ? 'expanded-clickable-row' : 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -684,7 +689,10 @@ function K8sJobsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.jobNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -94,6 +94,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sJobsRowData {
|
||||
key: string;
|
||||
jobUID: string;
|
||||
jobNameRaw: string;
|
||||
jobName: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
successful_pods: React.ReactNode;
|
||||
@@ -303,6 +304,7 @@ export const formatDataForTable = (
|
||||
data.map((job, index) => ({
|
||||
key: index.toString(),
|
||||
jobUID: job.jobName,
|
||||
jobNameRaw: job.meta.k8s_job_name || '',
|
||||
jobName: (
|
||||
<Tooltip title={job.meta.k8s_job_name}>
|
||||
{job.meta.k8s_job_name || ''}
|
||||
|
||||
@@ -459,6 +459,9 @@ function K8sNamespacesList({
|
||||
|
||||
const handleRowClick = (record: K8sNamespacesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.namespaceNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
setSearchParams({
|
||||
@@ -523,9 +526,13 @@ function K8sNamespacesList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
if (record.namespaceNameRaw) {
|
||||
setselectedNamespaceUID(record.namespaceUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.namespaceNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -716,7 +723,10 @@ function K8sNamespacesList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.namespaceNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sNamespacesRowData {
|
||||
key: string;
|
||||
namespaceUID: string;
|
||||
namespaceNameRaw: string;
|
||||
namespaceName: string;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
@@ -161,6 +162,7 @@ export const formatDataForTable = (
|
||||
data.map((namespace, index) => ({
|
||||
key: index.toString(),
|
||||
namespaceUID: namespace.namespaceName,
|
||||
namespaceNameRaw: namespace.namespaceName || '',
|
||||
namespaceName: namespace.namespaceName,
|
||||
clusterName: namespace.meta.k8s_cluster_name,
|
||||
cpu: (
|
||||
|
||||
@@ -438,6 +438,9 @@ function K8sNodesList({
|
||||
|
||||
const handleRowClick = (record: K8sNodesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.nodeNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
setSearchParams({
|
||||
@@ -503,9 +506,13 @@ function K8sNodesList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
if (record.nodeNameRaw) {
|
||||
setSelectedNodeUID(record.nodeUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.nodeNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -695,7 +702,10 @@ function K8sNodesList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.nodeNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -53,6 +53,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sNodesRowData {
|
||||
key: string;
|
||||
nodeUID: string;
|
||||
nodeNameRaw: string;
|
||||
nodeName: React.ReactNode;
|
||||
clusterName: string;
|
||||
cpu: React.ReactNode;
|
||||
@@ -193,6 +194,7 @@ export const formatDataForTable = (
|
||||
data.map((node, index) => ({
|
||||
key: `${node.nodeUID}-${index}`,
|
||||
nodeUID: node.nodeUID || '',
|
||||
nodeNameRaw: node.meta.k8s_node_name || '',
|
||||
nodeName: (
|
||||
<Tooltip title={node.meta.k8s_node_name}>
|
||||
{node.meta.k8s_node_name || ''}
|
||||
|
||||
@@ -496,6 +496,9 @@ function K8sPodsList({
|
||||
|
||||
const handleRowClick = (record: K8sPodsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.podNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedPodUID(record.podUID);
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
@@ -616,9 +619,11 @@ function K8sPodsList({
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setSelectedPodUID(record.podUID);
|
||||
if (record.podNameRaw) {
|
||||
setSelectedPodUID(record.podUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.podNameRaw ? 'expanded-clickable-row' : 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -753,7 +758,10 @@ function K8sPodsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.podNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -460,6 +460,9 @@ function K8sStatefulSetsList({
|
||||
|
||||
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.statefulsetNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
setSearchParams({
|
||||
@@ -524,9 +527,13 @@ function K8sStatefulSetsList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
if (record.statefulsetNameRaw) {
|
||||
setselectedStatefulSetUID(record.statefulsetUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.statefulsetNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -718,7 +725,10 @@ function K8sStatefulSetsList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.statefulsetNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -82,6 +82,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sStatefulSetsRowData {
|
||||
key: string;
|
||||
statefulsetUID: string;
|
||||
statefulsetNameRaw: string;
|
||||
statefulsetName: React.ReactNode;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
@@ -276,6 +277,7 @@ export const formatDataForTable = (
|
||||
data.map((statefulSet, index) => ({
|
||||
key: index.toString(),
|
||||
statefulsetUID: statefulSet.statefulSetName,
|
||||
statefulsetNameRaw: statefulSet.meta.k8s_statefulset_name || '',
|
||||
statefulsetName: (
|
||||
<Tooltip title={statefulSet.meta.k8s_statefulset_name}>
|
||||
{statefulSet.meta.k8s_statefulset_name || ''}
|
||||
|
||||
@@ -390,6 +390,9 @@ function K8sVolumesList({
|
||||
|
||||
const handleRowClick = (record: K8sVolumesRowData): void => {
|
||||
if (groupBy.length === 0) {
|
||||
if (!record.volumeNameRaw) {
|
||||
return;
|
||||
}
|
||||
setSelectedRowData(null);
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
setSearchParams({
|
||||
@@ -454,9 +457,13 @@ function K8sVolumesList({
|
||||
showHeader={false}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => {
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
if (record.volumeNameRaw) {
|
||||
setselectedVolumeUID(record.volumeUID);
|
||||
}
|
||||
},
|
||||
className: 'expanded-clickable-row',
|
||||
className: record.volumeNameRaw
|
||||
? 'expanded-clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -641,7 +648,10 @@ function K8sVolumesList({
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
className: 'clickable-row',
|
||||
className:
|
||||
groupBy.length > 0 || record.volumeNameRaw
|
||||
? 'clickable-row'
|
||||
: 'disabled-row',
|
||||
})}
|
||||
expandable={{
|
||||
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const defaultAddedColumns: IEntityColumn[] = [
|
||||
export interface K8sVolumesRowData {
|
||||
key: string;
|
||||
volumeUID: string;
|
||||
volumeNameRaw: string;
|
||||
pvcName: React.ReactNode;
|
||||
namespaceName: React.ReactNode;
|
||||
capacity: React.ReactNode;
|
||||
@@ -186,6 +187,7 @@ export const formatDataForTable = (
|
||||
data.map((volume, index) => ({
|
||||
key: index.toString(),
|
||||
volumeUID: volume.persistentVolumeClaimName,
|
||||
volumeNameRaw: volume.persistentVolumeClaimName || '',
|
||||
pvcName: (
|
||||
<Tooltip title={volume.persistentVolumeClaimName}>
|
||||
{volume.persistentVolumeClaimName || ''}
|
||||
|
||||
@@ -105,6 +105,7 @@ export const defaultAvailableColumns = [
|
||||
export interface K8sPodsRowData {
|
||||
key: string;
|
||||
podName: React.ReactNode;
|
||||
podNameRaw: string;
|
||||
podUID: string;
|
||||
cpu_request: React.ReactNode;
|
||||
cpu_limit: React.ReactNode;
|
||||
@@ -347,6 +348,7 @@ export const formatDataForTable = (
|
||||
{pod.meta.k8s_pod_name || ''}
|
||||
</Tooltip>
|
||||
),
|
||||
podNameRaw: pod.meta.k8s_pod_name || '',
|
||||
podUID: pod.podUID || '',
|
||||
cpu_request: (
|
||||
<ValidateColumnValueWrapper
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
.trace-details-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 16px;
|
||||
|
||||
.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: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
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: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
border-left: unset;
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.trace-details-header {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: move to new css module name system
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import './TraceDetailsHeader.styles.scss';
|
||||
|
||||
function TraceDetailsHeader(): JSX.Element {
|
||||
const { id: traceID } = useParams<TraceDetailV2URLProps>();
|
||||
|
||||
const handlePreviousBtnClick = useCallback((): void => {
|
||||
const isSpaNavigate =
|
||||
document.referrer &&
|
||||
new URL(document.referrer).origin === window.location.origin;
|
||||
if (isSpaNavigate) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="trace-details-header">
|
||||
<Button className="previous-btn" onClick={handlePreviousBtnClick}>
|
||||
<ArrowLeft size={14} />
|
||||
</Button>
|
||||
<div className="trace-name">
|
||||
<Typography.Text className="trace-id">Trace ID</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text className="trace-id-value">{traceID}</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceDetailsHeader;
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import TimelineV3 from 'components/TimelineV3/TimelineV3';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import { DEFAULT_ROW_HEIGHT } from './constants';
|
||||
import { useCanvasSetup } from './hooks/useCanvasSetup';
|
||||
import { useFlamegraphDrag } from './hooks/useFlamegraphDrag';
|
||||
import { useFlamegraphDraw } from './hooks/useFlamegraphDraw';
|
||||
import { useFlamegraphHover } from './hooks/useFlamegraphHover';
|
||||
import { useFlamegraphZoom } from './hooks/useFlamegraphZoom';
|
||||
import { FlamegraphCanvasProps, SpanRect } from './types';
|
||||
import { formatDuration } from './utils';
|
||||
|
||||
function FlamegraphCanvas(props: FlamegraphCanvasProps): JSX.Element {
|
||||
const { spans, traceMetadata, firstSpanAtFetchLevel, onSpanClick } = props;
|
||||
|
||||
const isDarkMode = useIsDarkMode(); //TODO: see if can be removed or use a new hook
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const spanRectsRef = useRef<SpanRect[]>([]);
|
||||
|
||||
const [viewStartTs, setViewStartTs] = useState<number>(
|
||||
traceMetadata.startTime,
|
||||
);
|
||||
const [viewEndTs, setViewEndTs] = useState<number>(traceMetadata.endTime);
|
||||
const [scrollTop, setScrollTop] = useState<number>(0);
|
||||
const [rowHeight, setRowHeight] = useState<number>(DEFAULT_ROW_HEIGHT);
|
||||
|
||||
// Mutable refs for zoom and drag hooks to read during rAF / mouse callbacks
|
||||
const viewStartRef = useRef(viewStartTs);
|
||||
const viewEndRef = useRef(viewEndTs);
|
||||
const rowHeightRef = useRef(rowHeight);
|
||||
const scrollTopRef = useRef(scrollTop);
|
||||
|
||||
useEffect(() => {
|
||||
viewStartRef.current = viewStartTs;
|
||||
}, [viewStartTs]);
|
||||
|
||||
useEffect(() => {
|
||||
viewEndRef.current = viewEndTs;
|
||||
}, [viewEndTs]);
|
||||
|
||||
useEffect(() => {
|
||||
rowHeightRef.current = rowHeight;
|
||||
}, [rowHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollTopRef.current = scrollTop;
|
||||
}, [scrollTop]);
|
||||
|
||||
useEffect(() => {
|
||||
//TODO: see if this can be removed as once loaded the view start and end ts will not change
|
||||
setViewStartTs(traceMetadata.startTime);
|
||||
setViewEndTs(traceMetadata.endTime);
|
||||
viewStartRef.current = traceMetadata.startTime;
|
||||
viewEndRef.current = traceMetadata.endTime;
|
||||
}, [traceMetadata.startTime, traceMetadata.endTime]);
|
||||
|
||||
const totalHeight = spans.length * rowHeight;
|
||||
|
||||
const { isOverFlamegraphRef } = useFlamegraphZoom({
|
||||
canvasRef,
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
rowHeightRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
setRowHeight,
|
||||
});
|
||||
|
||||
const {
|
||||
handleMouseDown,
|
||||
handleMouseMove: handleDragMouseMove,
|
||||
handleMouseUp,
|
||||
handleDragMouseLeave,
|
||||
suppressClickRef,
|
||||
isDraggingRef,
|
||||
} = useFlamegraphDrag({
|
||||
canvasRef,
|
||||
containerRef,
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
scrollTopRef,
|
||||
setScrollTop,
|
||||
totalHeight,
|
||||
});
|
||||
|
||||
const {
|
||||
hoveredSpanId,
|
||||
handleHoverMouseMove,
|
||||
handleHoverMouseLeave,
|
||||
handleClick,
|
||||
tooltipContent,
|
||||
} = useFlamegraphHover({
|
||||
canvasRef,
|
||||
spanRectsRef,
|
||||
traceMetadata,
|
||||
viewStartTs,
|
||||
viewEndTs,
|
||||
isDraggingRef,
|
||||
suppressClickRef,
|
||||
onSpanClick,
|
||||
isDarkMode,
|
||||
});
|
||||
|
||||
const { drawFlamegraph } = useFlamegraphDraw({
|
||||
canvasRef,
|
||||
containerRef,
|
||||
spans,
|
||||
viewStartTs,
|
||||
viewEndTs,
|
||||
scrollTop,
|
||||
rowHeight,
|
||||
selectedSpanId: firstSpanAtFetchLevel || undefined,
|
||||
hoveredSpanId: hoveredSpanId ?? '',
|
||||
isDarkMode,
|
||||
spanRectsRef,
|
||||
});
|
||||
|
||||
useCanvasSetup(canvasRef, containerRef, drawFlamegraph);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
handleDragMouseMove(e);
|
||||
handleHoverMouseMove(e);
|
||||
},
|
||||
[handleDragMouseMove, handleHoverMouseMove],
|
||||
);
|
||||
|
||||
const handleMouseLeave = useCallback((): void => {
|
||||
isOverFlamegraphRef.current = false;
|
||||
handleDragMouseLeave();
|
||||
handleHoverMouseLeave();
|
||||
}, [isOverFlamegraphRef, handleDragMouseLeave, handleHoverMouseLeave]);
|
||||
|
||||
// todo: move to a separate component/utils file
|
||||
const tooltipElement = tooltipContent
|
||||
? createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: Math.min(tooltipContent.clientX + 15, window.innerWidth - 220),
|
||||
top: Math.min(tooltipContent.clientY + 15, window.innerHeight - 100),
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'rgba(30, 30, 30, 0.95)',
|
||||
color: '#fff',
|
||||
padding: '8px 12px',
|
||||
borderRadius: 4,
|
||||
fontSize: 12,
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
marginBottom: 4,
|
||||
color: tooltipContent.spanColor,
|
||||
}}
|
||||
>
|
||||
{tooltipContent.spanName}
|
||||
</div>
|
||||
<div>Status: {tooltipContent.status}</div>
|
||||
<div>Start: {tooltipContent.startMs.toFixed(2)} ms</div>
|
||||
<div>Duration: {formatDuration(tooltipContent.durationMs * 1e6)}</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
{tooltipElement}
|
||||
<TimelineV3
|
||||
startTimestamp={viewStartTs}
|
||||
endTimestamp={viewEndTs}
|
||||
offsetTimestamp={viewStartTs - traceMetadata.startTime}
|
||||
timelineHeight={22}
|
||||
/>
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(): void => {
|
||||
isOverFlamegraphRef.current = true;
|
||||
}}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
cursor: 'grab',
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FlamegraphCanvas;
|
||||
@@ -1,118 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useLocation, useParams } from 'react-router-dom';
|
||||
import useGetTraceFlamegraph from 'hooks/trace/useGetTraceFlamegraph';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { TraceDetailFlamegraphURLProps } from 'types/api/trace/getTraceFlamegraph';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import FlamegraphCanvas from './FlamegraphCanvas';
|
||||
|
||||
//TODO: analyse if this is needed or not and move to separate file if needed else delete this enum.
|
||||
enum TraceFlamegraphState {
|
||||
LOADING = 'LOADING',
|
||||
SUCCESS = 'SUCCESS',
|
||||
NO_DATA = 'NO_DATA',
|
||||
ERROR = 'ERROR',
|
||||
FETCHING_WITH_OLD_DATA = 'FETCHING_WITH_OLD_DATA',
|
||||
}
|
||||
|
||||
interface TraceFlamegraphProps {
|
||||
serviceExecTime: Record<string, number>;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
selectedSpan: Span | undefined;
|
||||
}
|
||||
|
||||
function TraceFlamegraph(props: TraceFlamegraphProps): JSX.Element {
|
||||
const { selectedSpan } = props;
|
||||
const { id: traceId } = useParams<TraceDetailFlamegraphURLProps>();
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
const { search } = useLocation();
|
||||
const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState<string>(
|
||||
urlQuery.get('spanId') || '',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setFirstSpanAtFetchLevel(urlQuery.get('spanId') || '');
|
||||
}, [urlQuery]);
|
||||
|
||||
const handleSpanClick = useCallback(
|
||||
(spanId: string): void => {
|
||||
setFirstSpanAtFetchLevel(spanId);
|
||||
const searchParams = new URLSearchParams(search);
|
||||
//tood: use from query params constants
|
||||
if (searchParams.get('spanId') !== spanId) {
|
||||
searchParams.set('spanId', spanId);
|
||||
history.replace({ search: searchParams.toString() });
|
||||
}
|
||||
},
|
||||
[history, search],
|
||||
);
|
||||
|
||||
const { data, isFetching, error } = useGetTraceFlamegraph({
|
||||
traceId,
|
||||
selectedSpanId: firstSpanAtFetchLevel,
|
||||
});
|
||||
|
||||
const flamegraphState = useMemo(() => {
|
||||
if (isFetching) {
|
||||
if (data?.payload?.spans && data.payload.spans.length > 0) {
|
||||
return TraceFlamegraphState.FETCHING_WITH_OLD_DATA;
|
||||
}
|
||||
return TraceFlamegraphState.LOADING;
|
||||
}
|
||||
if (error) {
|
||||
return TraceFlamegraphState.ERROR;
|
||||
}
|
||||
if (data?.payload?.spans && data.payload.spans.length === 0) {
|
||||
return TraceFlamegraphState.NO_DATA;
|
||||
}
|
||||
return TraceFlamegraphState.SUCCESS;
|
||||
}, [error, isFetching, data]);
|
||||
|
||||
const spans = useMemo(() => data?.payload?.spans || [], [
|
||||
data?.payload?.spans,
|
||||
]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
switch (flamegraphState) {
|
||||
case TraceFlamegraphState.LOADING:
|
||||
return <div>Loading...</div>;
|
||||
case TraceFlamegraphState.ERROR:
|
||||
return <div>Error loading flamegraph</div>;
|
||||
case TraceFlamegraphState.NO_DATA:
|
||||
return <div>No data found for trace {traceId}</div>;
|
||||
case TraceFlamegraphState.SUCCESS:
|
||||
case TraceFlamegraphState.FETCHING_WITH_OLD_DATA:
|
||||
return (
|
||||
<FlamegraphCanvas
|
||||
spans={spans}
|
||||
firstSpanAtFetchLevel={firstSpanAtFetchLevel}
|
||||
setFirstSpanAtFetchLevel={setFirstSpanAtFetchLevel}
|
||||
onSpanClick={handleSpanClick}
|
||||
traceMetadata={{
|
||||
startTime: data?.payload?.startTimestampMillis || 0,
|
||||
endTime: data?.payload?.endTimestampMillis || 0,
|
||||
}}
|
||||
selectedSpan={selectedSpan}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div>Fetching the trace...</div>;
|
||||
}
|
||||
}, [
|
||||
data?.payload?.endTimestampMillis,
|
||||
data?.payload?.startTimestampMillis,
|
||||
firstSpanAtFetchLevel,
|
||||
flamegraphState,
|
||||
selectedSpan,
|
||||
spans,
|
||||
traceId,
|
||||
handleSpanClick,
|
||||
]);
|
||||
|
||||
return <>{content}</>;
|
||||
}
|
||||
|
||||
export default TraceFlamegraph;
|
||||
@@ -1,36 +0,0 @@
|
||||
export const ROW_HEIGHT = 24;
|
||||
export const SPAN_BAR_HEIGHT = 22;
|
||||
export const SPAN_BAR_Y_OFFSET = Math.floor((ROW_HEIGHT - SPAN_BAR_HEIGHT) / 2);
|
||||
export const EVENT_DOT_SIZE = 6;
|
||||
|
||||
// Span bar sizing relative to row height (used by getFlamegraphRowMetrics)
|
||||
export const SPAN_BAR_HEIGHT_RATIO = SPAN_BAR_HEIGHT / ROW_HEIGHT;
|
||||
export const MIN_SPAN_BAR_HEIGHT = 8;
|
||||
export const MAX_SPAN_BAR_HEIGHT = SPAN_BAR_HEIGHT;
|
||||
|
||||
// Event dot sizing relative to span bar height
|
||||
export const EVENT_DOT_SIZE_RATIO = EVENT_DOT_SIZE / SPAN_BAR_HEIGHT;
|
||||
export const MIN_EVENT_DOT_SIZE = 4;
|
||||
export const MAX_EVENT_DOT_SIZE = EVENT_DOT_SIZE;
|
||||
|
||||
export const LABEL_FONT = '11px Inter, sans-serif';
|
||||
export const LABEL_PADDING_X = 8;
|
||||
export const MIN_WIDTH_FOR_NAME = 30;
|
||||
export const MIN_WIDTH_FOR_NAME_AND_DURATION = 80;
|
||||
|
||||
// Dynamic row height (vertical zoom) -- disabled for now (MIN === MAX)
|
||||
export const MIN_ROW_HEIGHT = 24;
|
||||
export const MAX_ROW_HEIGHT = 24;
|
||||
export const DEFAULT_ROW_HEIGHT = MIN_ROW_HEIGHT;
|
||||
|
||||
// Zoom intensity -- how fast zoom reacts to wheel/pinch delta
|
||||
export const PINCH_ZOOM_INTENSITY_H = 0.01;
|
||||
export const SCROLL_ZOOM_INTENSITY_H = 0.0015;
|
||||
export const PINCH_ZOOM_INTENSITY_V = 0.008;
|
||||
export const SCROLL_ZOOM_INTENSITY_V = 0.001;
|
||||
|
||||
// Minimum visible time span in ms (prevents zooming to sub-pixel)
|
||||
export const MIN_VISIBLE_SPAN_MS = 5;
|
||||
|
||||
// Selection and hover style (derived from span color per mock design)
|
||||
export const DASHED_BORDER_LINE_DASH = [4, 2];
|
||||
@@ -1,55 +0,0 @@
|
||||
import { RefObject, useCallback, useEffect } from 'react';
|
||||
|
||||
export function useCanvasSetup(
|
||||
canvasRef: RefObject<HTMLCanvasElement>,
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
onDraw: () => void,
|
||||
): void {
|
||||
const updateCanvasSize = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = container.getBoundingClientRect();
|
||||
const viewportHeight = container.clientHeight;
|
||||
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${viewportHeight}px`;
|
||||
|
||||
const newWidth = Math.floor(rect.width * dpr);
|
||||
const newHeight = Math.floor(viewportHeight * dpr);
|
||||
|
||||
if (canvas.width !== newWidth || canvas.height !== newHeight) {
|
||||
canvas.width = newWidth;
|
||||
canvas.height = newHeight;
|
||||
onDraw();
|
||||
}
|
||||
}, [canvasRef, containerRef, onDraw]);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return (): void => {};
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateCanvasSize);
|
||||
resizeObserver.observe(container);
|
||||
updateCanvasSize();
|
||||
|
||||
// when dpr changes, update the canvas size
|
||||
const dprQuery = window.matchMedia('(resolution: 1dppx)');
|
||||
dprQuery.addEventListener('change', updateCanvasSize);
|
||||
|
||||
return (): void => {
|
||||
resizeObserver.disconnect();
|
||||
dprQuery.removeEventListener('change', updateCanvasSize);
|
||||
};
|
||||
}, [containerRef, updateCanvasSize]);
|
||||
|
||||
useEffect(() => {
|
||||
onDraw();
|
||||
}, [onDraw]);
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
MutableRefObject,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { ITraceMetadata } from '../types';
|
||||
import { clamp } from '../utils';
|
||||
|
||||
interface UseFlamegraphDragArgs {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
traceMetadata: ITraceMetadata;
|
||||
viewStartRef: MutableRefObject<number>;
|
||||
viewEndRef: MutableRefObject<number>;
|
||||
setViewStartTs: Dispatch<SetStateAction<number>>;
|
||||
setViewEndTs: Dispatch<SetStateAction<number>>;
|
||||
scrollTopRef: MutableRefObject<number>;
|
||||
setScrollTop: Dispatch<SetStateAction<number>>;
|
||||
totalHeight: number;
|
||||
}
|
||||
|
||||
interface UseFlamegraphDragResult {
|
||||
handleMouseDown: (e: ReactMouseEvent) => void;
|
||||
handleMouseMove: (e: ReactMouseEvent) => void;
|
||||
handleMouseUp: () => void;
|
||||
handleDragMouseLeave: () => void;
|
||||
suppressClickRef: MutableRefObject<boolean>;
|
||||
isDraggingRef: MutableRefObject<boolean>;
|
||||
}
|
||||
|
||||
const DRAG_THRESHOLD = 5;
|
||||
|
||||
export function useFlamegraphDrag(
|
||||
args: UseFlamegraphDragArgs,
|
||||
): UseFlamegraphDragResult {
|
||||
const {
|
||||
canvasRef,
|
||||
containerRef,
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
scrollTopRef,
|
||||
setScrollTop,
|
||||
totalHeight,
|
||||
} = args;
|
||||
|
||||
const isDraggingRef = useRef(false);
|
||||
const dragStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const dragDistanceRef = useRef(0);
|
||||
const suppressClickRef = useRef(false);
|
||||
|
||||
const clampScrollTop = useCallback(
|
||||
(next: number): number => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return 0;
|
||||
}
|
||||
const viewportHeight = container.clientHeight;
|
||||
const maxScroll = Math.max(0, totalHeight - viewportHeight);
|
||||
return clamp(next, 0, maxScroll);
|
||||
},
|
||||
[containerRef, totalHeight],
|
||||
);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(event: ReactMouseEvent): void => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
isDraggingRef.current = true;
|
||||
dragStartRef.current = { x: event.clientX, y: event.clientY };
|
||||
dragDistanceRef.current = 0;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
canvas.style.cursor = 'grabbing';
|
||||
}
|
||||
},
|
||||
[canvasRef],
|
||||
);
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(event: ReactMouseEvent): void => {
|
||||
if (!isDraggingRef.current || !dragStartRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const deltaX = event.clientX - dragStartRef.current.x;
|
||||
const deltaY = event.clientY - dragStartRef.current.y;
|
||||
|
||||
dragDistanceRef.current = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
|
||||
// --- Horizontal pan ---
|
||||
const timeSpan = viewEndRef.current - viewStartRef.current;
|
||||
const deltaTime = (deltaX / rect.width) * timeSpan;
|
||||
|
||||
const newStart = viewStartRef.current - deltaTime;
|
||||
const clampedStart = clamp(
|
||||
newStart,
|
||||
traceMetadata.startTime,
|
||||
traceMetadata.endTime - timeSpan,
|
||||
);
|
||||
const clampedEnd = clampedStart + timeSpan;
|
||||
|
||||
viewStartRef.current = clampedStart;
|
||||
viewEndRef.current = clampedEnd;
|
||||
setViewStartTs(clampedStart);
|
||||
setViewEndTs(clampedEnd);
|
||||
|
||||
// --- Vertical scroll pan ---
|
||||
const nextScrollTop = clampScrollTop(scrollTopRef.current - deltaY);
|
||||
scrollTopRef.current = nextScrollTop;
|
||||
setScrollTop(nextScrollTop);
|
||||
|
||||
dragStartRef.current = { x: event.clientX, y: event.clientY };
|
||||
},
|
||||
[
|
||||
canvasRef,
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
scrollTopRef,
|
||||
setScrollTop,
|
||||
clampScrollTop,
|
||||
],
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback((): void => {
|
||||
const wasDrag = dragDistanceRef.current > DRAG_THRESHOLD;
|
||||
suppressClickRef.current = wasDrag;
|
||||
|
||||
isDraggingRef.current = false;
|
||||
dragStartRef.current = null;
|
||||
dragDistanceRef.current = 0;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
canvas.style.cursor = 'grab';
|
||||
}
|
||||
}, [canvasRef]);
|
||||
|
||||
const handleDragMouseLeave = useCallback((): void => {
|
||||
isDraggingRef.current = false;
|
||||
dragStartRef.current = null;
|
||||
dragDistanceRef.current = 0;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
canvas.style.cursor = 'grab';
|
||||
}
|
||||
}, [canvasRef]);
|
||||
|
||||
return {
|
||||
handleMouseDown,
|
||||
handleMouseMove,
|
||||
handleMouseUp,
|
||||
handleDragMouseLeave,
|
||||
suppressClickRef,
|
||||
isDraggingRef,
|
||||
};
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import React, { RefObject, useCallback, useRef } from 'react';
|
||||
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
|
||||
|
||||
import { SpanRect } from '../types';
|
||||
import {
|
||||
clamp,
|
||||
drawSpanBar,
|
||||
FlamegraphRowMetrics,
|
||||
getFlamegraphRowMetrics,
|
||||
getSpanColor,
|
||||
} from '../utils';
|
||||
|
||||
interface UseFlamegraphDrawArgs {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
spans: FlamegraphSpan[][];
|
||||
viewStartTs: number;
|
||||
viewEndTs: number;
|
||||
scrollTop: number;
|
||||
rowHeight: number;
|
||||
selectedSpanId: string | undefined;
|
||||
hoveredSpanId: string;
|
||||
isDarkMode: boolean;
|
||||
spanRectsRef?: React.MutableRefObject<SpanRect[]>;
|
||||
}
|
||||
|
||||
interface UseFlamegraphDrawResult {
|
||||
drawFlamegraph: () => void;
|
||||
spanRectsRef: RefObject<SpanRect[]>;
|
||||
}
|
||||
|
||||
const OVERSCAN_ROWS = 4;
|
||||
|
||||
interface DrawLevelArgs {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
levelSpans: FlamegraphSpan[];
|
||||
levelIndex: number;
|
||||
y: number;
|
||||
viewStartTs: number;
|
||||
timeSpan: number;
|
||||
cssWidth: number;
|
||||
selectedSpanId: string | undefined;
|
||||
hoveredSpanId: string;
|
||||
isDarkMode: boolean;
|
||||
spanRectsArray: SpanRect[];
|
||||
metrics: FlamegraphRowMetrics;
|
||||
}
|
||||
|
||||
function drawLevel(args: DrawLevelArgs): void {
|
||||
const {
|
||||
ctx,
|
||||
levelSpans,
|
||||
levelIndex,
|
||||
y,
|
||||
viewStartTs,
|
||||
timeSpan,
|
||||
cssWidth,
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
isDarkMode,
|
||||
spanRectsArray,
|
||||
metrics,
|
||||
} = args;
|
||||
|
||||
const viewEndTs = viewStartTs + timeSpan;
|
||||
|
||||
for (let i = 0; i < levelSpans.length; i++) {
|
||||
const span = levelSpans[i];
|
||||
const spanStartMs = span.timestamp;
|
||||
const spanEndMs = span.timestamp + span.durationNano / 1e6;
|
||||
|
||||
// Time culling -- skip spans entirely outside the visible time window
|
||||
if (spanEndMs < viewStartTs || spanStartMs > viewEndTs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const leftOffset = ((spanStartMs - viewStartTs) / timeSpan) * cssWidth;
|
||||
const rightEdge = ((spanEndMs - viewStartTs) / timeSpan) * cssWidth;
|
||||
let width = rightEdge - leftOffset;
|
||||
|
||||
// Clamp to visible x-range
|
||||
if (leftOffset < 0) {
|
||||
width += leftOffset;
|
||||
if (width <= 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (rightEdge > cssWidth) {
|
||||
width = cssWidth - Math.max(0, leftOffset);
|
||||
if (width <= 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Minimum 1px width so tiny spans remain visible
|
||||
width = clamp(width, 1, Infinity);
|
||||
|
||||
const color = getSpanColor({ span, isDarkMode });
|
||||
|
||||
drawSpanBar({
|
||||
ctx,
|
||||
span,
|
||||
x: Math.max(0, leftOffset),
|
||||
y,
|
||||
width,
|
||||
levelIndex,
|
||||
spanRectsArray,
|
||||
color,
|
||||
isDarkMode,
|
||||
metrics,
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function useFlamegraphDraw(
|
||||
args: UseFlamegraphDrawArgs,
|
||||
): UseFlamegraphDrawResult {
|
||||
const {
|
||||
canvasRef,
|
||||
containerRef,
|
||||
spans,
|
||||
viewStartTs,
|
||||
viewEndTs,
|
||||
scrollTop,
|
||||
rowHeight,
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
isDarkMode,
|
||||
spanRectsRef: spanRectsRefProp,
|
||||
} = args;
|
||||
|
||||
const spanRectsRefInternal = useRef<SpanRect[]>([]);
|
||||
const spanRectsRef = spanRectsRefProp ?? spanRectsRefInternal;
|
||||
|
||||
const drawFlamegraph = useCallback(() => {
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
const timeSpan = viewEndTs - viewStartTs;
|
||||
if (timeSpan <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cssWidth = canvas.width / dpr;
|
||||
const metrics = getFlamegraphRowMetrics(rowHeight);
|
||||
|
||||
// ---- Vertical clipping window ----
|
||||
const viewportHeight = container.clientHeight;
|
||||
|
||||
const firstLevel = Math.max(
|
||||
0,
|
||||
Math.floor(scrollTop / metrics.ROW_HEIGHT) - OVERSCAN_ROWS,
|
||||
);
|
||||
const visibleLevelCount =
|
||||
Math.ceil(viewportHeight / metrics.ROW_HEIGHT) + 2 * OVERSCAN_ROWS;
|
||||
const lastLevel = Math.min(spans.length - 1, firstLevel + visibleLevelCount);
|
||||
|
||||
ctx.clearRect(0, 0, cssWidth, viewportHeight);
|
||||
|
||||
const spanRectsArray: SpanRect[] = [];
|
||||
|
||||
// ---- Draw only visible levels ----
|
||||
for (let levelIndex = firstLevel; levelIndex <= lastLevel; levelIndex++) {
|
||||
const levelSpans = spans[levelIndex];
|
||||
if (!levelSpans) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawLevel({
|
||||
ctx,
|
||||
levelSpans,
|
||||
levelIndex,
|
||||
y: levelIndex * metrics.ROW_HEIGHT - scrollTop,
|
||||
viewStartTs,
|
||||
timeSpan,
|
||||
cssWidth,
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
isDarkMode,
|
||||
spanRectsArray,
|
||||
metrics,
|
||||
});
|
||||
}
|
||||
|
||||
spanRectsRef.current = spanRectsArray;
|
||||
}, [
|
||||
canvasRef,
|
||||
containerRef,
|
||||
spanRectsRef,
|
||||
spans,
|
||||
viewStartTs,
|
||||
viewEndTs,
|
||||
scrollTop,
|
||||
rowHeight,
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
isDarkMode,
|
||||
]);
|
||||
|
||||
// TODO: spanRectsRef is a flat array — hover scans all visible rects O(N).
|
||||
// Upgrade to per-level buckets: spanRects[levelIndex] = [...] so hover can
|
||||
// compute level from mouseY / ROW_HEIGHT and scan only that row.
|
||||
// Further: binary search within a level by x (spans are sorted by start time)
|
||||
// to reduce hover cost from O(N) to O(log N).
|
||||
return { drawFlamegraph, spanRectsRef };
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MouseEvent as ReactMouseEvent,
|
||||
MutableRefObject,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
|
||||
|
||||
import { SpanRect } from '../types';
|
||||
import { ITraceMetadata } from '../types';
|
||||
import { getSpanColor } from '../utils';
|
||||
|
||||
function getCanvasPointer(
|
||||
canvas: HTMLCanvasElement,
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
): { cssX: number; cssY: number } | null {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const cssWidth = canvas.width / dpr;
|
||||
const cssHeight = canvas.height / dpr;
|
||||
const cssX = (clientX - rect.left) * (cssWidth / rect.width);
|
||||
const cssY = (clientY - rect.top) * (cssHeight / rect.height);
|
||||
return { cssX, cssY };
|
||||
}
|
||||
|
||||
function findSpanAtPosition(
|
||||
cssX: number,
|
||||
cssY: number,
|
||||
spanRects: SpanRect[],
|
||||
): FlamegraphSpan | null {
|
||||
for (let i = spanRects.length - 1; i >= 0; i--) {
|
||||
const r = spanRects[i];
|
||||
if (
|
||||
cssX >= r.x &&
|
||||
cssX <= r.x + r.width &&
|
||||
cssY >= r.y &&
|
||||
cssY <= r.y + r.height
|
||||
) {
|
||||
return r.span;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface TooltipContent {
|
||||
spanName: string;
|
||||
status: 'ok' | 'warning' | 'error';
|
||||
startMs: number;
|
||||
durationMs: number;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
spanColor: string;
|
||||
}
|
||||
|
||||
interface UseFlamegraphHoverArgs {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
spanRectsRef: MutableRefObject<SpanRect[]>;
|
||||
traceMetadata: ITraceMetadata;
|
||||
viewStartTs: number;
|
||||
viewEndTs: number;
|
||||
isDraggingRef: MutableRefObject<boolean>;
|
||||
suppressClickRef: MutableRefObject<boolean>;
|
||||
onSpanClick: (spanId: string) => void;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
interface UseFlamegraphHoverResult {
|
||||
hoveredSpanId: string | null;
|
||||
setHoveredSpanId: Dispatch<SetStateAction<string | null>>;
|
||||
handleHoverMouseMove: (e: ReactMouseEvent) => void;
|
||||
handleHoverMouseLeave: () => void;
|
||||
handleClick: (e: ReactMouseEvent) => void;
|
||||
tooltipContent: TooltipContent | null;
|
||||
}
|
||||
|
||||
export function useFlamegraphHover(
|
||||
args: UseFlamegraphHoverArgs,
|
||||
): UseFlamegraphHoverResult {
|
||||
const {
|
||||
canvasRef,
|
||||
spanRectsRef,
|
||||
traceMetadata,
|
||||
viewStartTs,
|
||||
viewEndTs,
|
||||
isDraggingRef,
|
||||
suppressClickRef,
|
||||
onSpanClick,
|
||||
isDarkMode,
|
||||
} = args;
|
||||
|
||||
const [hoveredSpanId, setHoveredSpanId] = useState<string | null>(null);
|
||||
const [tooltipContent, setTooltipContent] = useState<TooltipContent | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const isZoomed =
|
||||
viewStartTs !== traceMetadata.startTime ||
|
||||
viewEndTs !== traceMetadata.endTime;
|
||||
|
||||
const updateCursor = useCallback(
|
||||
(canvas: HTMLCanvasElement, span: FlamegraphSpan | null): void => {
|
||||
if (span) {
|
||||
canvas.style.cursor = 'pointer';
|
||||
} else if (isZoomed) {
|
||||
canvas.style.cursor = 'grab';
|
||||
} else {
|
||||
canvas.style.cursor = 'default';
|
||||
}
|
||||
},
|
||||
[isZoomed],
|
||||
);
|
||||
|
||||
const handleHoverMouseMove = useCallback(
|
||||
(e: ReactMouseEvent): void => {
|
||||
if (isDraggingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointer = getCanvasPointer(canvas, e.clientX, e.clientY);
|
||||
if (!pointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const span = findSpanAtPosition(
|
||||
pointer.cssX,
|
||||
pointer.cssY,
|
||||
spanRectsRef.current,
|
||||
);
|
||||
|
||||
if (span) {
|
||||
setHoveredSpanId(span.spanId);
|
||||
setTooltipContent({
|
||||
spanName: span.name || 'unknown',
|
||||
status: span.hasError ? 'error' : 'ok',
|
||||
startMs: span.timestamp - traceMetadata.startTime,
|
||||
durationMs: span.durationNano / 1e6,
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
spanColor: getSpanColor({ span, isDarkMode }),
|
||||
});
|
||||
updateCursor(canvas, span);
|
||||
} else {
|
||||
setHoveredSpanId(null);
|
||||
setTooltipContent(null);
|
||||
updateCursor(canvas, null);
|
||||
}
|
||||
},
|
||||
[
|
||||
canvasRef,
|
||||
spanRectsRef,
|
||||
traceMetadata.startTime,
|
||||
isDraggingRef,
|
||||
updateCursor,
|
||||
isDarkMode,
|
||||
],
|
||||
);
|
||||
|
||||
const handleHoverMouseLeave = useCallback((): void => {
|
||||
setHoveredSpanId(null);
|
||||
setTooltipContent(null);
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (canvas) {
|
||||
updateCursor(canvas, null);
|
||||
}
|
||||
}, [canvasRef, updateCursor]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: ReactMouseEvent): void => {
|
||||
if (suppressClickRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointer = getCanvasPointer(canvas, e.clientX, e.clientY);
|
||||
if (!pointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const span = findSpanAtPosition(
|
||||
pointer.cssX,
|
||||
pointer.cssY,
|
||||
spanRectsRef.current,
|
||||
);
|
||||
|
||||
if (span) {
|
||||
onSpanClick(span.spanId);
|
||||
}
|
||||
},
|
||||
[canvasRef, spanRectsRef, suppressClickRef, onSpanClick],
|
||||
);
|
||||
|
||||
return {
|
||||
hoveredSpanId,
|
||||
setHoveredSpanId,
|
||||
handleHoverMouseMove,
|
||||
handleHoverMouseLeave,
|
||||
handleClick,
|
||||
tooltipContent,
|
||||
};
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
import {
|
||||
Dispatch,
|
||||
MutableRefObject,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
DEFAULT_ROW_HEIGHT,
|
||||
MAX_ROW_HEIGHT,
|
||||
MIN_ROW_HEIGHT,
|
||||
MIN_VISIBLE_SPAN_MS,
|
||||
PINCH_ZOOM_INTENSITY_H,
|
||||
PINCH_ZOOM_INTENSITY_V,
|
||||
SCROLL_ZOOM_INTENSITY_H,
|
||||
SCROLL_ZOOM_INTENSITY_V,
|
||||
} from '../constants';
|
||||
import { ITraceMetadata } from '../types';
|
||||
import { clamp } from '../utils';
|
||||
|
||||
interface UseFlamegraphZoomArgs {
|
||||
canvasRef: RefObject<HTMLCanvasElement>;
|
||||
traceMetadata: ITraceMetadata;
|
||||
viewStartRef: MutableRefObject<number>;
|
||||
viewEndRef: MutableRefObject<number>;
|
||||
rowHeightRef: MutableRefObject<number>;
|
||||
setViewStartTs: Dispatch<SetStateAction<number>>;
|
||||
setViewEndTs: Dispatch<SetStateAction<number>>;
|
||||
setRowHeight: Dispatch<SetStateAction<number>>;
|
||||
}
|
||||
|
||||
interface UseFlamegraphZoomResult {
|
||||
handleResetZoom: () => void;
|
||||
isOverFlamegraphRef: MutableRefObject<boolean>;
|
||||
}
|
||||
|
||||
function getCanvasPointer(
|
||||
canvasRef: RefObject<HTMLCanvasElement>,
|
||||
clientX: number,
|
||||
): { cssX: number; cssWidth: number } | null {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const cssWidth = canvas.width / dpr;
|
||||
const cssX = (clientX - rect.left) * (cssWidth / rect.width);
|
||||
|
||||
return { cssX, cssWidth };
|
||||
}
|
||||
|
||||
export function useFlamegraphZoom(
|
||||
args: UseFlamegraphZoomArgs,
|
||||
): UseFlamegraphZoomResult {
|
||||
const {
|
||||
canvasRef,
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
rowHeightRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
setRowHeight,
|
||||
} = args;
|
||||
|
||||
const isOverFlamegraphRef = useRef(false);
|
||||
const wheelDeltaRef = useRef(0);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const lastCursorXRef = useRef(0);
|
||||
const lastCssWidthRef = useRef(1);
|
||||
const lastIsPinchRef = useRef(false);
|
||||
const lastWheelClientXRef = useRef<number | null>(null);
|
||||
|
||||
// Prevent browser zoom when pinching over the flamegraph
|
||||
useEffect(() => {
|
||||
const onWheel = (e: WheelEvent): void => {
|
||||
if (isOverFlamegraphRef.current && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('wheel', onWheel, { passive: false, capture: true });
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener('wheel', onWheel, {
|
||||
capture: true,
|
||||
} as EventListenerOptions);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const applyWheelZoom = useCallback(() => {
|
||||
rafRef.current = null;
|
||||
|
||||
const cssWidth = lastCssWidthRef.current || 1;
|
||||
const cursorX = lastCursorXRef.current;
|
||||
const fullSpanMs = traceMetadata.endTime - traceMetadata.startTime;
|
||||
|
||||
const oldStart = viewStartRef.current;
|
||||
const oldEnd = viewEndRef.current;
|
||||
const oldSpan = oldEnd - oldStart;
|
||||
|
||||
const deltaY = wheelDeltaRef.current;
|
||||
wheelDeltaRef.current = 0;
|
||||
if (deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomH = lastIsPinchRef.current
|
||||
? PINCH_ZOOM_INTENSITY_H
|
||||
: SCROLL_ZOOM_INTENSITY_H;
|
||||
const zoomV = lastIsPinchRef.current
|
||||
? PINCH_ZOOM_INTENSITY_V
|
||||
: SCROLL_ZOOM_INTENSITY_V;
|
||||
|
||||
const factorH = Math.exp(deltaY * zoomH);
|
||||
const factorV = Math.exp(deltaY * zoomV);
|
||||
|
||||
// --- Horizontal zoom ---
|
||||
const desiredSpan = oldSpan * factorH;
|
||||
const minSpanMs = Math.max(
|
||||
MIN_VISIBLE_SPAN_MS,
|
||||
oldSpan / Math.max(cssWidth, 1),
|
||||
);
|
||||
const clampedSpan = clamp(desiredSpan, minSpanMs, fullSpanMs);
|
||||
|
||||
const cursorRatio = clamp(cursorX / cssWidth, 0, 1);
|
||||
const anchorTs = oldStart + cursorRatio * oldSpan;
|
||||
|
||||
let nextStart = anchorTs - cursorRatio * clampedSpan;
|
||||
nextStart = clamp(
|
||||
nextStart,
|
||||
traceMetadata.startTime,
|
||||
traceMetadata.endTime - clampedSpan,
|
||||
);
|
||||
const nextEnd = nextStart + clampedSpan;
|
||||
|
||||
// --- Vertical zoom (row height) ---
|
||||
const desiredRow = rowHeightRef.current * (1 / factorV);
|
||||
const nextRow = clamp(desiredRow, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT);
|
||||
|
||||
// Write refs immediately so rapid wheel events read fresh values
|
||||
viewStartRef.current = nextStart;
|
||||
viewEndRef.current = nextEnd;
|
||||
rowHeightRef.current = nextRow;
|
||||
|
||||
setViewStartTs(nextStart);
|
||||
setViewEndTs(nextEnd);
|
||||
setRowHeight(nextRow);
|
||||
}, [
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
rowHeightRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
setRowHeight,
|
||||
]);
|
||||
|
||||
// Native wheel listener on the canvas (passive: false for reliable preventDefault)
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) {
|
||||
return (): void => {};
|
||||
}
|
||||
|
||||
const onWheel = (e: WheelEvent): void => {
|
||||
e.preventDefault();
|
||||
|
||||
const pointer = getCanvasPointer(canvasRef, e.clientX);
|
||||
if (!pointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Flush accumulated delta if cursor moved significantly
|
||||
if (lastWheelClientXRef.current !== null) {
|
||||
const moved = Math.abs(e.clientX - lastWheelClientXRef.current);
|
||||
if (moved > 6) {
|
||||
wheelDeltaRef.current = 0;
|
||||
}
|
||||
}
|
||||
lastWheelClientXRef.current = e.clientX;
|
||||
|
||||
lastIsPinchRef.current = e.ctrlKey;
|
||||
lastCssWidthRef.current = pointer.cssWidth;
|
||||
lastCursorXRef.current = pointer.cssX;
|
||||
wheelDeltaRef.current += e.deltaY;
|
||||
|
||||
if (rafRef.current == null) {
|
||||
rafRef.current = requestAnimationFrame(applyWheelZoom);
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener('wheel', onWheel, { passive: false });
|
||||
|
||||
return (): void => {
|
||||
canvas.removeEventListener('wheel', onWheel);
|
||||
};
|
||||
}, [canvasRef, applyWheelZoom]);
|
||||
|
||||
const handleResetZoom = useCallback(() => {
|
||||
viewStartRef.current = traceMetadata.startTime;
|
||||
viewEndRef.current = traceMetadata.endTime;
|
||||
rowHeightRef.current = DEFAULT_ROW_HEIGHT;
|
||||
|
||||
setViewStartTs(traceMetadata.startTime);
|
||||
setViewEndTs(traceMetadata.endTime);
|
||||
setRowHeight(DEFAULT_ROW_HEIGHT);
|
||||
}, [
|
||||
traceMetadata,
|
||||
viewStartRef,
|
||||
viewEndRef,
|
||||
rowHeightRef,
|
||||
setViewStartTs,
|
||||
setViewEndTs,
|
||||
setRowHeight,
|
||||
]);
|
||||
|
||||
return { handleResetZoom, isOverFlamegraphRef };
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
export interface ITraceMetadata {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export interface FlamegraphCanvasProps {
|
||||
spans: FlamegraphSpan[][];
|
||||
firstSpanAtFetchLevel: string;
|
||||
setFirstSpanAtFetchLevel: Dispatch<SetStateAction<string>>;
|
||||
onSpanClick: (spanId: string) => void;
|
||||
traceMetadata: ITraceMetadata;
|
||||
selectedSpan: Span | undefined;
|
||||
}
|
||||
|
||||
export interface SpanRect {
|
||||
span: FlamegraphSpan;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
level: number;
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph';
|
||||
|
||||
import {
|
||||
DASHED_BORDER_LINE_DASH,
|
||||
EVENT_DOT_SIZE_RATIO,
|
||||
LABEL_FONT,
|
||||
LABEL_PADDING_X,
|
||||
MAX_EVENT_DOT_SIZE,
|
||||
MAX_SPAN_BAR_HEIGHT,
|
||||
MIN_EVENT_DOT_SIZE,
|
||||
MIN_SPAN_BAR_HEIGHT,
|
||||
MIN_WIDTH_FOR_NAME,
|
||||
MIN_WIDTH_FOR_NAME_AND_DURATION,
|
||||
SPAN_BAR_HEIGHT_RATIO,
|
||||
} from './constants';
|
||||
import { SpanRect } from './types';
|
||||
|
||||
export function clamp(v: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
|
||||
export interface FlamegraphRowMetrics {
|
||||
ROW_HEIGHT: number;
|
||||
SPAN_BAR_HEIGHT: number;
|
||||
SPAN_BAR_Y_OFFSET: number;
|
||||
EVENT_DOT_SIZE: number;
|
||||
}
|
||||
|
||||
export function getFlamegraphRowMetrics(
|
||||
rowHeight: number,
|
||||
): FlamegraphRowMetrics {
|
||||
const spanBarHeight = clamp(
|
||||
Math.round(rowHeight * SPAN_BAR_HEIGHT_RATIO),
|
||||
MIN_SPAN_BAR_HEIGHT,
|
||||
MAX_SPAN_BAR_HEIGHT,
|
||||
);
|
||||
const spanBarYOffset = Math.floor((rowHeight - spanBarHeight) / 2);
|
||||
const eventDotSize = clamp(
|
||||
Math.round(spanBarHeight * EVENT_DOT_SIZE_RATIO),
|
||||
MIN_EVENT_DOT_SIZE,
|
||||
MAX_EVENT_DOT_SIZE,
|
||||
);
|
||||
|
||||
return {
|
||||
ROW_HEIGHT: rowHeight,
|
||||
SPAN_BAR_HEIGHT: spanBarHeight,
|
||||
SPAN_BAR_Y_OFFSET: spanBarYOffset,
|
||||
EVENT_DOT_SIZE: eventDotSize,
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSpanColorArgs {
|
||||
span: FlamegraphSpan;
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
|
||||
export function getSpanColor(args: GetSpanColorArgs): string {
|
||||
const { span, isDarkMode } = args;
|
||||
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
|
||||
|
||||
if (span.hasError) {
|
||||
color = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
interface DrawEventDotArgs {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
x: number;
|
||||
y: number;
|
||||
isError: boolean;
|
||||
isDarkMode: boolean;
|
||||
eventDotSize: number;
|
||||
}
|
||||
|
||||
export function drawEventDot(args: DrawEventDotArgs): void {
|
||||
const { ctx, x, y, isError, isDarkMode, eventDotSize } = args;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate(Math.PI / 4);
|
||||
|
||||
if (isError) {
|
||||
ctx.fillStyle = isDarkMode ? 'rgb(239, 68, 68)' : 'rgb(220, 38, 38)';
|
||||
ctx.strokeStyle = isDarkMode ? 'rgb(185, 28, 28)' : 'rgb(153, 27, 27)';
|
||||
} else {
|
||||
ctx.fillStyle = isDarkMode ? 'rgb(14, 165, 233)' : 'rgb(6, 182, 212)';
|
||||
ctx.strokeStyle = isDarkMode ? 'rgb(2, 132, 199)' : 'rgb(8, 145, 178)';
|
||||
}
|
||||
|
||||
ctx.lineWidth = 1;
|
||||
const half = eventDotSize / 2;
|
||||
ctx.fillRect(-half, -half, eventDotSize, eventDotSize);
|
||||
ctx.strokeRect(-half, -half, eventDotSize, eventDotSize);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
interface DrawSpanBarArgs {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
span: FlamegraphSpan;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
levelIndex: number;
|
||||
spanRectsArray: SpanRect[];
|
||||
color: string;
|
||||
isDarkMode: boolean;
|
||||
metrics: FlamegraphRowMetrics;
|
||||
selectedSpanId?: string | null;
|
||||
hoveredSpanId?: string | null;
|
||||
}
|
||||
|
||||
export function drawSpanBar(args: DrawSpanBarArgs): void {
|
||||
const {
|
||||
ctx,
|
||||
span,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
levelIndex,
|
||||
spanRectsArray,
|
||||
color,
|
||||
isDarkMode,
|
||||
metrics,
|
||||
selectedSpanId,
|
||||
hoveredSpanId,
|
||||
} = args;
|
||||
|
||||
const spanY = y + metrics.SPAN_BAR_Y_OFFSET;
|
||||
const isSelectedOrHovered =
|
||||
selectedSpanId === span.spanId || hoveredSpanId === span.spanId;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, spanY, width, metrics.SPAN_BAR_HEIGHT, 2);
|
||||
|
||||
if (isSelectedOrHovered) {
|
||||
// Transparent background, dashed border in same color as span
|
||||
ctx.setLineDash(DASHED_BORDER_LINE_DASH);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
} else {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
spanRectsArray.push({
|
||||
span,
|
||||
x,
|
||||
y: spanY,
|
||||
width,
|
||||
height: metrics.SPAN_BAR_HEIGHT,
|
||||
level: levelIndex,
|
||||
});
|
||||
|
||||
span.event?.forEach((event) => {
|
||||
const spanDurationMs = span.durationNano / 1e6;
|
||||
if (spanDurationMs <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTimeMs = event.timeUnixNano / 1e6;
|
||||
const eventOffsetPercent =
|
||||
((eventTimeMs - span.timestamp) / spanDurationMs) * 100;
|
||||
const clampedOffset = clamp(eventOffsetPercent, 1, 99);
|
||||
const eventX = x + (clampedOffset / 100) * width;
|
||||
const eventY = spanY + metrics.SPAN_BAR_HEIGHT / 2;
|
||||
|
||||
drawEventDot({
|
||||
ctx,
|
||||
x: eventX,
|
||||
y: eventY,
|
||||
isError: event.isError,
|
||||
isDarkMode,
|
||||
eventDotSize: metrics.EVENT_DOT_SIZE,
|
||||
});
|
||||
});
|
||||
|
||||
drawSpanLabel({
|
||||
ctx,
|
||||
span,
|
||||
x,
|
||||
y: spanY,
|
||||
width,
|
||||
color,
|
||||
isSelectedOrHovered,
|
||||
isDarkMode,
|
||||
spanBarHeight: metrics.SPAN_BAR_HEIGHT,
|
||||
});
|
||||
}
|
||||
|
||||
export function formatDuration(durationNano: number): string {
|
||||
const durationMs = durationNano / 1e6;
|
||||
const { time, timeUnitName } = convertTimeToRelevantUnit(durationMs);
|
||||
return `${parseFloat(time.toFixed(2))}${timeUnitName}`;
|
||||
}
|
||||
|
||||
interface DrawSpanLabelArgs {
|
||||
ctx: CanvasRenderingContext2D;
|
||||
span: FlamegraphSpan;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
color: string;
|
||||
isSelectedOrHovered: boolean;
|
||||
isDarkMode: boolean;
|
||||
spanBarHeight: number;
|
||||
}
|
||||
|
||||
function drawSpanLabel(args: DrawSpanLabelArgs): void {
|
||||
const {
|
||||
ctx,
|
||||
span,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
color,
|
||||
isSelectedOrHovered,
|
||||
isDarkMode,
|
||||
spanBarHeight,
|
||||
} = args;
|
||||
|
||||
if (width < MIN_WIDTH_FOR_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = span.name;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Clip text to span bar bounds
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, width, spanBarHeight);
|
||||
ctx.clip();
|
||||
|
||||
ctx.font = LABEL_FONT;
|
||||
ctx.fillStyle = isSelectedOrHovered
|
||||
? color
|
||||
: isDarkMode
|
||||
? 'rgba(0, 0, 0, 0.9)'
|
||||
: 'rgba(255, 255, 255, 0.9)';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
const textY = y + spanBarHeight / 2;
|
||||
const leftX = x + LABEL_PADDING_X;
|
||||
const rightX = x + width - LABEL_PADDING_X;
|
||||
const availableWidth = width - LABEL_PADDING_X * 2;
|
||||
|
||||
if (width >= MIN_WIDTH_FOR_NAME_AND_DURATION) {
|
||||
const duration = formatDuration(span.durationNano);
|
||||
const durationWidth = ctx.measureText(duration).width;
|
||||
const minGap = 6;
|
||||
const nameSpace = availableWidth - durationWidth - minGap;
|
||||
|
||||
// Duration right-aligned
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(duration, rightX, textY);
|
||||
|
||||
// Name left-aligned, truncated to fit remaining space
|
||||
if (nameSpace > 20) {
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(truncateText(ctx, name, nameSpace), leftX, textY);
|
||||
}
|
||||
} else {
|
||||
// Name only, truncated to fit
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(truncateText(ctx, name, availableWidth), leftX, textY);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function truncateText(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
maxWidth: number,
|
||||
): string {
|
||||
const ellipsis = '...';
|
||||
const ellipsisWidth = ctx.measureText(ellipsis).width;
|
||||
|
||||
if (ctx.measureText(text).width <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let lo = 0;
|
||||
let hi = text.length;
|
||||
while (lo < hi) {
|
||||
const mid = Math.ceil((lo + hi) / 2);
|
||||
if (ctx.measureText(text.slice(0, mid)).width + ellipsisWidth <= maxWidth) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return lo > 0 ? `${text.slice(0, lo)}${ellipsis}` : ellipsis;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@signozhq/resizable';
|
||||
import useGetTraceV2 from 'hooks/trace/useGetTraceV2';
|
||||
import { Span, TraceDetailV2URLProps } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import TraceDetailsHeader from './TraceDetailsHeader/TraceDetailsHeader';
|
||||
import TraceFlamegraph from './TraceFlamegraph/TraceFlamegraph';
|
||||
|
||||
function TraceDetailsV3(): JSX.Element {
|
||||
const { id: traceId } = useParams<TraceDetailV2URLProps>();
|
||||
const [selectedSpan, _setSelectedSpan] = useState<Span>();
|
||||
const [uncollapsedNodes] = useState<string[]>([]);
|
||||
|
||||
const { data: traceData } = useGetTraceV2({
|
||||
traceId,
|
||||
uncollapsedSpans: uncollapsedNodes,
|
||||
selectedSpanId: '',
|
||||
isSelectedSpanIDUnCollapsed: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: 'calc(100vh - 90px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<TraceDetailsHeader />
|
||||
<ResizablePanelGroup
|
||||
direction="vertical"
|
||||
autoSaveId="trace-details-v3-layout"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<ResizablePanel defaultSize={40} minSize={20} maxSize={80}>
|
||||
<TraceFlamegraph
|
||||
serviceExecTime={traceData?.payload?.serviceNameToTotalDurationMap || {}}
|
||||
startTime={traceData?.payload?.startTimestampMillis || 0}
|
||||
endTime={traceData?.payload?.endTimestampMillis || 0}
|
||||
selectedSpan={selectedSpan}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={60} minSize={20}>
|
||||
<div />
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraceDetailsV3;
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -25,7 +24,7 @@ func NewModule(sqlstore sqlstore.SQLStore) savedview.Module {
|
||||
}
|
||||
|
||||
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) {
|
||||
var views []savedviewtypes.SavedView
|
||||
var views []types.SavedView
|
||||
var err error
|
||||
if len(category) == 0 {
|
||||
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx)
|
||||
@@ -77,7 +76,7 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
|
||||
createBy := claims.Email
|
||||
updatedBy := claims.Email
|
||||
|
||||
dbView := savedviewtypes.SavedView{
|
||||
dbView := types.SavedView{
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: createdAt,
|
||||
UpdatedAt: updatedAt,
|
||||
@@ -106,7 +105,7 @@ func (module *module) CreateView(ctx context.Context, orgID string, view v3.Save
|
||||
}
|
||||
|
||||
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error) {
|
||||
var view savedviewtypes.SavedView
|
||||
var view types.SavedView
|
||||
err := module.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, uuid.StringValue()).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved view")
|
||||
@@ -147,7 +146,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
|
||||
updatedBy := claims.Email
|
||||
|
||||
_, err = module.sqlstore.BunDB().NewUpdate().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model(&types.SavedView{}).
|
||||
Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?",
|
||||
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData).
|
||||
Where("id = ?", uuid.StringValue()).
|
||||
@@ -161,7 +160,7 @@ func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.
|
||||
|
||||
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
|
||||
_, err := module.sqlstore.BunDB().NewDelete().
|
||||
Model(&savedviewtypes.SavedView{}).
|
||||
Model(&types.SavedView{}).
|
||||
Where("id = ?", uuid.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
@@ -172,7 +171,7 @@ func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.
|
||||
}
|
||||
|
||||
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
savedViews := []*savedviewtypes.SavedView{}
|
||||
savedViews := []*types.SavedView{}
|
||||
|
||||
err := module.
|
||||
sqlstore.
|
||||
@@ -185,5 +184,5 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return savedviewtypes.NewStatsFromSavedViews(savedViews), nil
|
||||
return types.NewStatsFromSavedViews(savedViews), nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
root "github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -463,7 +462,7 @@ func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email.String())) {
|
||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
||||
return
|
||||
}
|
||||
@@ -508,7 +507,7 @@ func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email.String())) {
|
||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/roletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/dustin/go-humanize"
|
||||
@@ -280,7 +279,7 @@ func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id stri
|
||||
return errors.WithAdditionalf(err, "cannot delete root user")
|
||||
}
|
||||
|
||||
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(user.Email.String())) {
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(user.Email.String())) {
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "integration user cannot be deleted")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,15 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type cloudProviderAccountsRepository interface {
|
||||
listConnected(ctx context.Context, orgId string, provider string) ([]integrationtypes.CloudIntegration, *model.ApiError)
|
||||
listConnected(ctx context.Context, orgId string, provider string) ([]types.CloudIntegration, *model.ApiError)
|
||||
|
||||
get(ctx context.Context, orgId string, provider string, id string) (*integrationtypes.CloudIntegration, *model.ApiError)
|
||||
get(ctx context.Context, orgId string, provider string, id string) (*types.CloudIntegration, *model.ApiError)
|
||||
|
||||
getConnectedCloudAccount(ctx context.Context, orgId string, provider string, accountID string) (*integrationtypes.CloudIntegration, *model.ApiError)
|
||||
getConnectedCloudAccount(ctx context.Context, orgId string, provider string, accountID string) (*types.CloudIntegration, *model.ApiError)
|
||||
|
||||
// Insert an account or update it by (cloudProvider, id)
|
||||
// for specified non-empty fields
|
||||
@@ -28,11 +27,11 @@ type cloudProviderAccountsRepository interface {
|
||||
orgId string,
|
||||
provider string,
|
||||
id *string,
|
||||
config *integrationtypes.AccountConfig,
|
||||
config *types.AccountConfig,
|
||||
accountId *string,
|
||||
agentReport *integrationtypes.AgentReport,
|
||||
agentReport *types.AgentReport,
|
||||
removedAt *time.Time,
|
||||
) (*integrationtypes.CloudIntegration, *model.ApiError)
|
||||
) (*types.CloudIntegration, *model.ApiError)
|
||||
}
|
||||
|
||||
func newCloudProviderAccountsRepository(store sqlstore.SQLStore) (
|
||||
@@ -49,8 +48,8 @@ type cloudProviderAccountsSQLRepository struct {
|
||||
|
||||
func (r *cloudProviderAccountsSQLRepository) listConnected(
|
||||
ctx context.Context, orgId string, cloudProvider string,
|
||||
) ([]integrationtypes.CloudIntegration, *model.ApiError) {
|
||||
accounts := []integrationtypes.CloudIntegration{}
|
||||
) ([]types.CloudIntegration, *model.ApiError) {
|
||||
accounts := []types.CloudIntegration{}
|
||||
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&accounts).
|
||||
@@ -73,8 +72,8 @@ func (r *cloudProviderAccountsSQLRepository) listConnected(
|
||||
|
||||
func (r *cloudProviderAccountsSQLRepository) get(
|
||||
ctx context.Context, orgId string, provider string, id string,
|
||||
) (*integrationtypes.CloudIntegration, *model.ApiError) {
|
||||
var result integrationtypes.CloudIntegration
|
||||
) (*types.CloudIntegration, *model.ApiError) {
|
||||
var result types.CloudIntegration
|
||||
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&result).
|
||||
@@ -98,8 +97,8 @@ func (r *cloudProviderAccountsSQLRepository) get(
|
||||
|
||||
func (r *cloudProviderAccountsSQLRepository) getConnectedCloudAccount(
|
||||
ctx context.Context, orgId string, provider string, accountId string,
|
||||
) (*integrationtypes.CloudIntegration, *model.ApiError) {
|
||||
var result integrationtypes.CloudIntegration
|
||||
) (*types.CloudIntegration, *model.ApiError) {
|
||||
var result types.CloudIntegration
|
||||
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&result).
|
||||
@@ -128,11 +127,11 @@ func (r *cloudProviderAccountsSQLRepository) upsert(
|
||||
orgId string,
|
||||
provider string,
|
||||
id *string,
|
||||
config *integrationtypes.AccountConfig,
|
||||
config *types.AccountConfig,
|
||||
accountId *string,
|
||||
agentReport *integrationtypes.AgentReport,
|
||||
agentReport *types.AgentReport,
|
||||
removedAt *time.Time,
|
||||
) (*integrationtypes.CloudIntegration, *model.ApiError) {
|
||||
) (*types.CloudIntegration, *model.ApiError) {
|
||||
// Insert
|
||||
if id == nil {
|
||||
temp := valuer.GenerateUUID().StringValue()
|
||||
@@ -182,7 +181,7 @@ func (r *cloudProviderAccountsSQLRepository) upsert(
|
||||
)
|
||||
}
|
||||
|
||||
integration := integrationtypes.CloudIntegration{
|
||||
integration := types.CloudIntegration{
|
||||
OrgID: orgId,
|
||||
Provider: provider,
|
||||
Identifiable: types.Identifiable{ID: valuer.MustNewUUID(*id)},
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
@@ -53,7 +52,7 @@ func NewController(sqlStore sqlstore.SQLStore) (*Controller, error) {
|
||||
}
|
||||
|
||||
type ConnectedAccountsListResponse struct {
|
||||
Accounts []integrationtypes.Account `json:"accounts"`
|
||||
Accounts []types.Account `json:"accounts"`
|
||||
}
|
||||
|
||||
func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cloudProvider string) (
|
||||
@@ -68,7 +67,7 @@ func (c *Controller) ListConnectedAccounts(ctx context.Context, orgId string, cl
|
||||
return nil, model.WrapApiError(apiErr, "couldn't list cloud accounts")
|
||||
}
|
||||
|
||||
connectedAccounts := []integrationtypes.Account{}
|
||||
connectedAccounts := []types.Account{}
|
||||
for _, a := range accountRecords {
|
||||
connectedAccounts = append(connectedAccounts, a.Account())
|
||||
}
|
||||
@@ -82,7 +81,7 @@ type GenerateConnectionUrlRequest struct {
|
||||
// Optional. To be specified for updates.
|
||||
AccountId *string `json:"account_id,omitempty"`
|
||||
|
||||
AccountConfig integrationtypes.AccountConfig `json:"account_config"`
|
||||
AccountConfig types.AccountConfig `json:"account_config"`
|
||||
|
||||
AgentConfig SigNozAgentConfig `json:"agent_config"`
|
||||
}
|
||||
@@ -150,9 +149,9 @@ func (c *Controller) GenerateConnectionUrl(ctx context.Context, orgId string, cl
|
||||
}
|
||||
|
||||
type AccountStatusResponse struct {
|
||||
Id string `json:"id"`
|
||||
CloudAccountId *string `json:"cloud_account_id,omitempty"`
|
||||
Status integrationtypes.AccountStatus `json:"status"`
|
||||
Id string `json:"id"`
|
||||
CloudAccountId *string `json:"cloud_account_id,omitempty"`
|
||||
Status types.AccountStatus `json:"status"`
|
||||
}
|
||||
|
||||
func (c *Controller) GetAccountStatus(ctx context.Context, orgId string, cloudProvider string, accountId string) (
|
||||
@@ -218,7 +217,7 @@ func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProv
|
||||
))
|
||||
}
|
||||
|
||||
agentReport := integrationtypes.AgentReport{
|
||||
agentReport := types.AgentReport{
|
||||
TimestampMillis: time.Now().UnixMilli(),
|
||||
Data: req.Data,
|
||||
}
|
||||
@@ -287,10 +286,10 @@ func (c *Controller) CheckInAsAgent(ctx context.Context, orgId string, cloudProv
|
||||
}
|
||||
|
||||
type UpdateAccountConfigRequest struct {
|
||||
Config integrationtypes.AccountConfig `json:"config"`
|
||||
Config types.AccountConfig `json:"config"`
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*integrationtypes.Account, *model.ApiError) {
|
||||
func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, cloudProvider string, accountId string, req UpdateAccountConfigRequest) (*types.Account, *model.ApiError) {
|
||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
@@ -307,7 +306,7 @@ func (c *Controller) UpdateAccountConfig(ctx context.Context, orgId string, clou
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*integrationtypes.CloudIntegration, *model.ApiError) {
|
||||
func (c *Controller) DisconnectAccount(ctx context.Context, orgId string, cloudProvider string, accountId string) (*types.CloudIntegration, *model.ApiError) {
|
||||
if apiErr := validateCloudProviderName(cloudProvider); apiErr != nil {
|
||||
return nil, apiErr
|
||||
}
|
||||
@@ -347,7 +346,7 @@ func (c *Controller) ListServices(
|
||||
return nil, model.WrapApiError(apiErr, "couldn't list cloud services")
|
||||
}
|
||||
|
||||
svcConfigs := map[string]*integrationtypes.CloudServiceConfig{}
|
||||
svcConfigs := map[string]*types.CloudServiceConfig{}
|
||||
if cloudAccountId != nil {
|
||||
activeAccount, apiErr := c.accountsRepo.getConnectedCloudAccount(
|
||||
ctx, orgID, cloudProvider, *cloudAccountId,
|
||||
@@ -442,8 +441,8 @@ func (c *Controller) GetServiceDetails(
|
||||
}
|
||||
|
||||
type UpdateServiceConfigRequest struct {
|
||||
CloudAccountId string `json:"cloud_account_id"`
|
||||
Config integrationtypes.CloudServiceConfig `json:"config"`
|
||||
CloudAccountId string `json:"cloud_account_id"`
|
||||
Config types.CloudServiceConfig `json:"config"`
|
||||
}
|
||||
|
||||
func (u *UpdateServiceConfigRequest) Validate(def *services.Definition) error {
|
||||
@@ -461,8 +460,8 @@ func (u *UpdateServiceConfigRequest) Validate(def *services.Definition) error {
|
||||
}
|
||||
|
||||
type UpdateServiceConfigResponse struct {
|
||||
Id string `json:"id"`
|
||||
Config integrationtypes.CloudServiceConfig `json:"config"`
|
||||
Id string `json:"id"`
|
||||
Config types.CloudServiceConfig `json:"config"`
|
||||
}
|
||||
|
||||
func (c *Controller) UpdateServiceConfig(
|
||||
|
||||
@@ -3,20 +3,20 @@ package cloudintegrations
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type ServiceSummary struct {
|
||||
services.Metadata
|
||||
|
||||
Config *integrationtypes.CloudServiceConfig `json:"config"`
|
||||
Config *types.CloudServiceConfig `json:"config"`
|
||||
}
|
||||
|
||||
type ServiceDetails struct {
|
||||
services.Definition
|
||||
|
||||
Config *integrationtypes.CloudServiceConfig `json:"config"`
|
||||
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
|
||||
Config *types.CloudServiceConfig `json:"config"`
|
||||
ConnectionStatus *ServiceConnectionStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type AccountStatus struct {
|
||||
@@ -61,7 +61,7 @@ func NewCompiledCollectionStrategy(provider string) (*CompiledCollectionStrategy
|
||||
|
||||
// Helper for accumulating strategies for enabled services.
|
||||
func AddServiceStrategy(serviceType string, cs *CompiledCollectionStrategy,
|
||||
definitionStrat *services.CollectionStrategy, config *integrationtypes.CloudServiceConfig) error {
|
||||
definitionStrat *services.CollectionStrategy, config *types.CloudServiceConfig) error {
|
||||
if definitionStrat.Provider != cs.Provider {
|
||||
return errors.NewInternalf(CodeMismatchCloudProvider, "can't add %s service strategy to compiled strategy for %s",
|
||||
definitionStrat.Provider, cs.Provider)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -19,7 +18,7 @@ type ServiceConfigDatabase interface {
|
||||
orgID string,
|
||||
cloudAccountId string,
|
||||
serviceType string,
|
||||
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
|
||||
) (*types.CloudServiceConfig, *model.ApiError)
|
||||
|
||||
upsert(
|
||||
ctx context.Context,
|
||||
@@ -27,15 +26,15 @@ type ServiceConfigDatabase interface {
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
serviceId string,
|
||||
config integrationtypes.CloudServiceConfig,
|
||||
) (*integrationtypes.CloudServiceConfig, *model.ApiError)
|
||||
config types.CloudServiceConfig,
|
||||
) (*types.CloudServiceConfig, *model.ApiError)
|
||||
|
||||
getAllForAccount(
|
||||
ctx context.Context,
|
||||
orgID string,
|
||||
cloudAccountId string,
|
||||
) (
|
||||
configsBySvcId map[string]*integrationtypes.CloudServiceConfig,
|
||||
configsBySvcId map[string]*types.CloudServiceConfig,
|
||||
apiErr *model.ApiError,
|
||||
)
|
||||
}
|
||||
@@ -57,9 +56,9 @@ func (r *serviceConfigSQLRepository) get(
|
||||
orgID string,
|
||||
cloudAccountId string,
|
||||
serviceType string,
|
||||
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
|
||||
) (*types.CloudServiceConfig, *model.ApiError) {
|
||||
|
||||
var result integrationtypes.CloudIntegrationService
|
||||
var result types.CloudIntegrationService
|
||||
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&result).
|
||||
@@ -90,14 +89,14 @@ func (r *serviceConfigSQLRepository) upsert(
|
||||
cloudProvider string,
|
||||
cloudAccountId string,
|
||||
serviceId string,
|
||||
config integrationtypes.CloudServiceConfig,
|
||||
) (*integrationtypes.CloudServiceConfig, *model.ApiError) {
|
||||
config types.CloudServiceConfig,
|
||||
) (*types.CloudServiceConfig, *model.ApiError) {
|
||||
|
||||
// get cloud integration id from account id
|
||||
// if the account is not connected, we don't need to upsert the config
|
||||
var cloudIntegrationId string
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model((*integrationtypes.CloudIntegration)(nil)).
|
||||
Model((*types.CloudIntegration)(nil)).
|
||||
Column("id").
|
||||
Where("provider = ?", cloudProvider).
|
||||
Where("account_id = ?", cloudAccountId).
|
||||
@@ -112,7 +111,7 @@ func (r *serviceConfigSQLRepository) upsert(
|
||||
))
|
||||
}
|
||||
|
||||
serviceConfig := integrationtypes.CloudIntegrationService{
|
||||
serviceConfig := types.CloudIntegrationService{
|
||||
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
@@ -140,8 +139,8 @@ func (r *serviceConfigSQLRepository) getAllForAccount(
|
||||
ctx context.Context,
|
||||
orgID string,
|
||||
cloudAccountId string,
|
||||
) (map[string]*integrationtypes.CloudServiceConfig, *model.ApiError) {
|
||||
serviceConfigs := []integrationtypes.CloudIntegrationService{}
|
||||
) (map[string]*types.CloudServiceConfig, *model.ApiError) {
|
||||
serviceConfigs := []types.CloudIntegrationService{}
|
||||
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&serviceConfigs).
|
||||
@@ -155,7 +154,7 @@ func (r *serviceConfigSQLRepository) getAllForAccount(
|
||||
))
|
||||
}
|
||||
|
||||
result := map[string]*integrationtypes.CloudServiceConfig{}
|
||||
result := map[string]*types.CloudServiceConfig{}
|
||||
|
||||
for _, r := range serviceConfigs {
|
||||
result[r.Type] = &r.Config
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/pipelinetypes"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -108,7 +107,7 @@ type IntegrationsListItem struct {
|
||||
|
||||
type Integration struct {
|
||||
IntegrationDetails
|
||||
Installation *integrationtypes.InstalledIntegration `json:"installation"`
|
||||
Installation *types.InstalledIntegration `json:"installation"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
@@ -224,7 +223,7 @@ func (m *Manager) InstallIntegration(
|
||||
ctx context.Context,
|
||||
orgId string,
|
||||
integrationId string,
|
||||
config integrationtypes.InstalledIntegrationConfig,
|
||||
config types.InstalledIntegrationConfig,
|
||||
) (*IntegrationsListItem, *model.ApiError) {
|
||||
integrationDetails, apiErr := m.getIntegrationDetails(ctx, integrationId)
|
||||
if apiErr != nil {
|
||||
@@ -430,7 +429,7 @@ func (m *Manager) getInstalledIntegration(
|
||||
ctx context.Context,
|
||||
orgId string,
|
||||
integrationId string,
|
||||
) (*integrationtypes.InstalledIntegration, *model.ApiError) {
|
||||
) (*types.InstalledIntegration, *model.ApiError) {
|
||||
iis, apiErr := m.installedIntegrationsRepo.get(
|
||||
ctx, orgId, []string{integrationId},
|
||||
)
|
||||
@@ -458,7 +457,7 @@ func (m *Manager) getInstalledIntegrations(
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
installedTypes := utils.MapSlice(installations, func(i integrationtypes.InstalledIntegration) string {
|
||||
installedTypes := utils.MapSlice(installations, func(i types.InstalledIntegration) string {
|
||||
return i.Type
|
||||
})
|
||||
integrationDetails, apiErr := m.availableIntegrationsRepo.get(ctx, installedTypes)
|
||||
|
||||
@@ -4,22 +4,22 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
)
|
||||
|
||||
type InstalledIntegrationsRepo interface {
|
||||
list(ctx context.Context, orgId string) ([]integrationtypes.InstalledIntegration, *model.ApiError)
|
||||
list(ctx context.Context, orgId string) ([]types.InstalledIntegration, *model.ApiError)
|
||||
|
||||
get(
|
||||
ctx context.Context, orgId string, integrationTypes []string,
|
||||
) (map[string]integrationtypes.InstalledIntegration, *model.ApiError)
|
||||
) (map[string]types.InstalledIntegration, *model.ApiError)
|
||||
|
||||
upsert(
|
||||
ctx context.Context,
|
||||
orgId string,
|
||||
integrationType string,
|
||||
config integrationtypes.InstalledIntegrationConfig,
|
||||
) (*integrationtypes.InstalledIntegration, *model.ApiError)
|
||||
config types.InstalledIntegrationConfig,
|
||||
) (*types.InstalledIntegration, *model.ApiError)
|
||||
|
||||
delete(ctx context.Context, orgId string, integrationType string) *model.ApiError
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
@@ -27,8 +26,8 @@ func NewInstalledIntegrationsSqliteRepo(store sqlstore.SQLStore) (
|
||||
func (r *InstalledIntegrationsSqliteRepo) list(
|
||||
ctx context.Context,
|
||||
orgId string,
|
||||
) ([]integrationtypes.InstalledIntegration, *model.ApiError) {
|
||||
integrations := []integrationtypes.InstalledIntegration{}
|
||||
) ([]types.InstalledIntegration, *model.ApiError) {
|
||||
integrations := []types.InstalledIntegration{}
|
||||
|
||||
err := r.store.BunDB().NewSelect().
|
||||
Model(&integrations).
|
||||
@@ -45,8 +44,8 @@ func (r *InstalledIntegrationsSqliteRepo) list(
|
||||
|
||||
func (r *InstalledIntegrationsSqliteRepo) get(
|
||||
ctx context.Context, orgId string, integrationTypes []string,
|
||||
) (map[string]integrationtypes.InstalledIntegration, *model.ApiError) {
|
||||
integrations := []integrationtypes.InstalledIntegration{}
|
||||
) (map[string]types.InstalledIntegration, *model.ApiError) {
|
||||
integrations := []types.InstalledIntegration{}
|
||||
|
||||
typeValues := []interface{}{}
|
||||
for _, integrationType := range integrationTypes {
|
||||
@@ -63,7 +62,7 @@ func (r *InstalledIntegrationsSqliteRepo) get(
|
||||
))
|
||||
}
|
||||
|
||||
result := map[string]integrationtypes.InstalledIntegration{}
|
||||
result := map[string]types.InstalledIntegration{}
|
||||
for _, ii := range integrations {
|
||||
result[ii.Type] = ii
|
||||
}
|
||||
@@ -75,10 +74,10 @@ func (r *InstalledIntegrationsSqliteRepo) upsert(
|
||||
ctx context.Context,
|
||||
orgId string,
|
||||
integrationType string,
|
||||
config integrationtypes.InstalledIntegrationConfig,
|
||||
) (*integrationtypes.InstalledIntegration, *model.ApiError) {
|
||||
config types.InstalledIntegrationConfig,
|
||||
) (*types.InstalledIntegration, *model.ApiError) {
|
||||
|
||||
integration := integrationtypes.InstalledIntegration{
|
||||
integration := types.InstalledIntegration{
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
@@ -115,7 +114,7 @@ func (r *InstalledIntegrationsSqliteRepo) delete(
|
||||
ctx context.Context, orgId string, integrationType string,
|
||||
) *model.ApiError {
|
||||
_, dbErr := r.store.BunDB().NewDelete().
|
||||
Model(&integrationtypes.InstalledIntegration{}).
|
||||
Model(&types.InstalledIntegration{}).
|
||||
Where("type = ?", integrationType).
|
||||
Where("org_id = ?", orgId).
|
||||
Exec(ctx)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package integrationtypes
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
@@ -27,17 +26,17 @@ var AllIntegrationUserEmails = []IntegrationUserEmail{
|
||||
type InstalledIntegration struct {
|
||||
bun.BaseModel `bun:"table:installed_integration"`
|
||||
|
||||
types.Identifiable
|
||||
Identifiable
|
||||
Type string `json:"type" bun:"type,type:text,unique:org_id_type"`
|
||||
Config InstalledIntegrationConfig `json:"config" bun:"config,type:text"`
|
||||
InstalledAt time.Time `json:"installed_at" bun:"installed_at,default:current_timestamp"`
|
||||
OrgID string `json:"org_id" bun:"org_id,type:text,unique:org_id_type,references:organizations(id),on_delete:cascade"`
|
||||
}
|
||||
|
||||
type InstalledIntegrationConfig map[string]any
|
||||
type InstalledIntegrationConfig map[string]interface{}
|
||||
|
||||
// For serializing from db
|
||||
func (c *InstalledIntegrationConfig) Scan(src any) error {
|
||||
func (c *InstalledIntegrationConfig) Scan(src interface{}) error {
|
||||
var data []byte
|
||||
switch v := src.(type) {
|
||||
case []byte:
|
||||
@@ -68,8 +67,8 @@ func (c *InstalledIntegrationConfig) Value() (driver.Value, error) {
|
||||
type CloudIntegration struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
Provider string `json:"provider" bun:"provider,type:text,unique:provider_id"`
|
||||
Config *AccountConfig `json:"config" bun:"config,type:text"`
|
||||
AccountID *string `json:"account_id" bun:"account_id,type:text"`
|
||||
@@ -195,8 +194,8 @@ func (r *AgentReport) Value() (driver.Value, error) {
|
||||
type CloudIntegrationService struct {
|
||||
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
Type string `bun:"type,type:text,notnull,unique:cloud_integration_id_type"`
|
||||
Config CloudServiceConfig `bun:"config,type:text"`
|
||||
CloudIntegrationID string `bun:"cloud_integration_id,type:text,notnull,unique:cloud_integration_id_type,references:cloud_integrations(id),on_delete:cascade"`
|
||||
@@ -1,18 +1,17 @@
|
||||
package savedviewtypes
|
||||
package types
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type SavedView struct {
|
||||
bun.BaseModel `bun:"table:saved_views"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
Identifiable
|
||||
TimeAuditable
|
||||
UserAuditable
|
||||
OrgID string `json:"orgId" bun:"org_id,notnull"`
|
||||
Name string `json:"name" bun:"name,type:text,notnull"`
|
||||
Category string `json:"category" bun:"category,type:text,notnull"`
|
||||
Reference in New Issue
Block a user