Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 0 additions & 33 deletions src/Routes/Tables/Jobs/Trace.js

This file was deleted.

206 changes: 206 additions & 0 deletions src/Routes/Tables/Jobs/Trace/ModernTraceViewer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import React, { useState, useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import TraceHeader from './TraceHeader';
import SearchBox from './SearchBox';
import TraceTimeline from './TraceTimeline';
import TraceTimelineMinimap from './TraceTimelineMinimap';
import SpanRow from './SpanRow';
import { getCurrentTheme, getSystemColors } from './traceConstants';

const ViewerContainer = styled.div`
position: relative;
background: ${props => {
const colors = getSystemColors(props.$isDark);
return colors.background;
}};
border-radius: 8px;
overflow: hidden;
border: 1px solid
${props => {
const colors = getSystemColors(props.$isDark);
return colors.border;
}};
`;

const SpanListContainer = styled.div`
background: ${props => {
const colors = getSystemColors(props.$isDark);
return props.$isDark ? '#1f2937' : colors.background;
}};
max-height: 600px;
overflow-y: auto;
border-bottom: 1px solid
${props => {
const colors = getSystemColors(props.$isDark);
return colors.border;
}};

/* Custom scrollbar */
&::-webkit-scrollbar {
width: 10px;
}

&::-webkit-scrollbar-track {
background: ${props => (props.$isDark ? '#1f2937' : '#f1f1f1')};
}

&::-webkit-scrollbar-thumb {
background: ${props => (props.$isDark ? '#3d5a7e' : '#888')};
border-radius: 5px;
}

&::-webkit-scrollbar-thumb:hover {
background: ${props => (props.$isDark ? '#4a6a92' : '#555')};
}
`;

const ModernTraceViewer = ({ data }) => {
const [expandedSpans, setExpandedSpans] = useState(new Set());
const [collapsedChildren, setCollapsedChildren] = useState(new Set());
const [searchTerm, setSearchTerm] = useState('');
const [selectedTimeRange, setSelectedTimeRange] = useState(null);
const [isDark, setIsDark] = useState(getCurrentTheme() === 'DARK');

// Listen for theme changes
useEffect(() => {
const checkTheme = () => {
setIsDark(getCurrentTheme() === 'DARK');
};

const interval = setInterval(checkTheme, 500);
window.addEventListener('storage', checkTheme);

return () => {
clearInterval(interval);
window.removeEventListener('storage', checkTheme);
};
}, []);

const toggleSpanDetails = spanId => {
const newExpanded = new Set(expandedSpans);
if (newExpanded.has(spanId)) {
newExpanded.delete(spanId);
} else {
newExpanded.add(spanId);
}
setExpandedSpans(newExpanded);
};

const toggleChildrenVisibility = spanId => {
const newCollapsed = new Set(collapsedChildren);
if (newCollapsed.has(spanId)) {
newCollapsed.delete(spanId);
} else {
newCollapsed.add(spanId);
}
setCollapsedChildren(newCollapsed);
};

const spanHierarchy = useMemo(() => {
if (!data || !data.spans) {
return [];
}

const { spans } = data;

let filteredSpans = spans;
if (selectedTimeRange) {
filteredSpans = spans.filter(span => {
const spanStart = span.relativeStartTime;
const spanEnd = span.relativeStartTime + span.duration;
return (
spanEnd >= selectedTimeRange.startTime &&
spanStart <= selectedTimeRange.endTime
);
});
}

const hierarchy = [];
const processedSpans = new Set();
const spanById = new Map(filteredSpans.map(span => [span.spanID, span]));

const addSpanAndChildren = (span, depth = 0) => {
if (processedSpans.has(span.spanID)) return;

processedSpans.add(span.spanID);

const children = filteredSpans.filter(
s =>
s.references &&
s.references.some(
ref => ref.refType === 'CHILD_OF' && ref.spanID === span.spanID
)
);

hierarchy.push({
...span,
depth,
hasChildren: children.length > 0,
});

if (!collapsedChildren.has(span.spanID)) {
children.forEach(child => addSpanAndChildren(child, depth + 1));
}
};

const rootSpans = filteredSpans.filter(span => {
if (!span.references || span.references.length === 0) return true;
const hasParentInSpanSet = span.references.some(
ref => ref.refType === 'CHILD_OF' && spanById.has(ref.spanID)
);
return !hasParentInSpanSet;
});

rootSpans.sort((a, b) => a.startTime - b.startTime);
rootSpans.forEach(span => addSpanAndChildren(span));

return hierarchy;
}, [data, collapsedChildren, selectedTimeRange]);

const handleTimeRangeSelection = range => {
setSelectedTimeRange(range);
};

return (
<ViewerContainer $isDark={isDark}>
<TraceHeader traceData={data} />
<SearchBox searchTerm={searchTerm} onSearchChange={setSearchTerm} />
<TraceTimelineMinimap
traceData={data}
processes={data.processes}
onSelectionChange={handleTimeRangeSelection}
/>
<TraceTimeline traceData={data} />
<SpanListContainer $isDark={isDark}>
{spanHierarchy.map(span => (
<SpanRow
key={span.spanID}
span={span}
totalDuration={data.duration}
traceStartTime={data.startTime}
isExpanded={expandedSpans.has(span.spanID)}
onToggle={toggleSpanDetails}
hasChildren={span.hasChildren}
depth={span.depth}
processes={data.processes}
searchTerm={searchTerm}
isChildrenVisible={!collapsedChildren.has(span.spanID)}
onToggleChildren={toggleChildrenVisibility}
/>
))}
</SpanListContainer>
</ViewerContainer>
);
};

ModernTraceViewer.propTypes = {
data: PropTypes.shape({
spans: PropTypes.array.isRequired,
processes: PropTypes.object.isRequired,
duration: PropTypes.number.isRequired,
startTime: PropTypes.number.isRequired,
}).isRequired,
};

export default React.memo(ModernTraceViewer);
130 changes: 130 additions & 0 deletions src/Routes/Tables/Jobs/Trace/SearchBox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Input, Button, Tooltip } from 'antd';
import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
import { getCurrentTheme, getSystemColors } from './traceConstants';

const SearchContainer = styled.div`
position: absolute;
top: 20px;
right: 16px;
display: flex;
align-items: center;
gap: 8px;
z-index: 100;
`;

const StyledInput = styled(Input)`
width: 250px;
border-radius: 6px;
border: 2px solid
${props => {
const colors = getSystemColors(props.$isDark);
return colors.blue;
}};
background-color: ${props => {
const colors = getSystemColors(props.$isDark);
return props.$isDark ? '#1e3a52' : colors.background;
}};
color: ${props => {
const colors = getSystemColors(props.$isDark);
return props.$isDark ? '#ffffff' : colors.text;
}};
box-shadow: ${props =>
props.$isDark ? '0 2px 6px rgba(64, 169, 255, 0.15)' : 'none'};

&:hover {
border-color: ${props => {
const colors = getSystemColors(props.$isDark);
return colors.blueLight;
}};
box-shadow: ${props =>
props.$isDark ? '0 2px 8px rgba(64, 169, 255, 0.25)' : 'none'};
}

&:focus,
&.ant-input-affix-wrapper-focused {
border-color: ${props => {
const colors = getSystemColors(props.$isDark);
return colors.blue;
}};
box-shadow: 0 0 0 2px
${props => {
const colors = getSystemColors(props.$isDark);
return colors.blue;
}}33;
}

input {
background-color: transparent !important;
color: ${props => {
const colors = getSystemColors(props.$isDark);
return props.$isDark ? '#ffffff' : colors.text;
}} !important;
}

input::placeholder {
color: ${props => (props.$isDark ? '#8c8c8c' : '#999999')} !important;
}

.anticon {
color: ${props => {
const colors = getSystemColors(props.$isDark);
return colors.blue;
}};
}
`;

const SearchBox = ({ searchTerm, onSearchChange }) => {
const [isDark, setIsDark] = useState(getCurrentTheme() === 'DARK');

useEffect(() => {
const checkTheme = () => {
setIsDark(getCurrentTheme() === 'DARK');
};

const interval = setInterval(checkTheme, 500);
window.addEventListener('storage', checkTheme);

return () => {
clearInterval(interval);
window.removeEventListener('storage', checkTheme);
};
}, []);

const clearSearch = () => {
onSearchChange('');
};

return (
<SearchContainer>
<StyledInput
placeholder="Search spans..."
value={searchTerm}
onChange={e => onSearchChange(e.target.value)}
prefix={<SearchOutlined />}
size="middle"
$isDark={isDark}
/>
{searchTerm && (
<Tooltip title="Clear search">
<Button
type="primary"
danger
size="small"
icon={<CloseOutlined />}
onClick={clearSearch}
/>
</Tooltip>
)}
</SearchContainer>
);
};

SearchBox.propTypes = {
searchTerm: PropTypes.string.isRequired,
onSearchChange: PropTypes.func.isRequired,
};

export default React.memo(SearchBox);
Loading