chore: traces UI changes (#8728)

### **PR Type**
Enhancement, Tests


___

### **Description**
- Refactor header to class-based styling

- Redesign Trace Details layout and styles

- Add span count badges and connectors

- Update tests for new selectors/classes


___

### Diagram Walkthrough


```mermaid
flowchart LR
  TraceHeaderSpec["TraceHeader.spec.ts tests"] -- align with classes --> TraceHeaderVue["TraceHeader.vue"]
  TraceTreeSpec["TraceTree.spec.ts tests"] -- new badge selectors --> TraceTreeVue["TraceTree.vue"]
  TraceDetailsVue["TraceDetails.vue"] -- layout + styles --> SidebarVue["TraceDetailsSidebar.vue"]
  TraceTreeVue -- span badges + connectors --> SpanBlockVue["SpanBlock.vue"]
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Tests</strong></td><td><details><summary>2
files</summary><table>
<tr>
<td><strong>TraceHeader.spec.ts</strong><dd><code>Update tests to assert
class-based styles</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-d8a57095481665bdacd7cc816c63d4c742048b832c15641ce20adaa4d3a58854">+8/-18</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>TraceTree.spec.ts</strong><dd><code>Adjust tests for badge
collapse and icons</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
</dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-0e779df03c894e381582de27f5c6dc36ac4905e01e12a501e0a4ca12599d4531">+9/-9</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

</table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>5
files</summary><table>
<tr>
<td><strong>SpanBlock.vue</strong><dd><code>Use rem for label offset
positioning</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-21a7d32870e3d6d3fc8098b06fd8e88ba20afea0c48cfa1ad7b309a9faf8cafc">+1/-1</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>TraceDetails.vue</strong><dd><code>Major layout refactor and
glassmorphism styles</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-bc7fe6b6339ee2cde6494c0982d4b860847447e79f7431f07d598e0446df06f2">+297/-149</a></td>

</tr>

<tr>
<td><strong>TraceDetailsSidebar.vue</strong><dd><code>Restyle tables
with glassmorphic theme support</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-0017bc08d8da96af6433770d0b274f825798d24401b33ce091b16a50fbaf4b57">+75/-5</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>TraceHeader.vue</strong><dd><code>Replace inline styles with
CSS classes for header and tics</code></dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-b782486701e65fc550a1f3ad8303922c7aa141b7bbbab2b8a606b3115af7029b">+38/-23</a>&nbsp;
</td>

</tr>

<tr>
<td><strong>TraceTree.vue</strong><dd><code>Add span count badges and
SVG connectors; hover highlights</code></dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/8728/files#diff-5d1fa8ba4d433439860b4bc036fb80e3e1b51948616dc04b1bfd1f9110a87091">+303/-69</a></td>

</tr>
</table></details></td></tr></tr></tbody></table>

</details>

___

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Ashish Kolhe
2025-10-06 13:16:57 +05:30
committed by GitHub
co-authored by greptile-apps[bot]
parent d85e5e62fc
commit c96999049c
7 changed files with 731 additions and 274 deletions
+1 -1
View File
@@ -254,7 +254,7 @@ export default defineComponent({
spanBlockWidth.value
) {
style.right = 0;
style.top = "-5px";
style.top = "-0.3125rem";
} else if (leftPosition.value > 50) {
style.left = leftPosition.value * onePercent - labelWidth + "px";
} else {
+297 -149
View File
@@ -15,9 +15,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="trace-details full-width" :style="backgroundStyle">
<div class="trace-details" :style="backgroundStyle">
<div
class="row q-px-sm"
class="trace-details-content"
v-if="
traceTree.length &&
spanList.length &&
@@ -27,17 +27,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
)
"
>
<div class="full-width flex items-center toolbar flex justify-between">
<div class="flex items-center">
<div class="trace-combined-header-wrapper" :class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'">
<div class="full-width flex items-center toolbar flex justify-between q-pb-sm">
<div class="flex items-center">
<div
data-test="trace-details-back-btn"
class="flex justify-center items-center q-mr-sm cursor-pointer"
style="
border: 1.5px solid;
border-radius: 50%;
width: 22px;
height: 22px;
"
class="flex justify-center items-center q-mr-sm cursor-pointer trace-back-btn"
title="Traces List"
@click="routeToTracesList"
>
@@ -162,7 +157,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<div class="flex items-center">
<div
class="flex justify-center items-center tw-border tw-pl-2 tw-rounded-sm tw-border-gray-300"
class="flex justify-center items-center tw-border tw-pl-2 tw-border-gray-300 trace-search-container"
>
<q-input
data-test="trace-details-search-input"
@@ -232,77 +227,78 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click="routeToTracesList"
/>
</div>
</div>
</div>
<q-separator style="width: 100%" />
<div class="col-12 flex justify-between items-end q-pr-sm q-pt-sm">
<div
data-test="trace-details-toggle-timeline-btn"
class="trace-chart-btn flex items-center no-wrap cursor-pointer q-mb-sm"
@click="toggleTimeline"
>
<q-icon
name="expand_more"
:class="!isTimelineExpanded ? 'rotate-270' : ''"
size="22px"
class="cursor-pointer text-grey-10"
/>
<q-separator class="q-my-sm" />
<div class="flex justify-between items-end q-pr-sm q-pb-sm">
<div
data-test="trace-details-visual-title"
class="text-subtitle2 text-bold"
data-test="trace-details-toggle-timeline-btn"
class="trace-chart-btn flex items-center no-wrap cursor-pointer"
@click="toggleTimeline"
>
{{
activeVisual === "timeline"
? "Trace Timeline"
: "Trace Service Map"
}}
<q-icon
name="expand_more"
:class="!isTimelineExpanded ? 'rotate-270' : ''"
size="22px"
class="cursor-pointer text-grey-10"
/>
<div
data-test="trace-details-visual-title"
class="text-subtitle2 text-bold"
>
{{
activeVisual === "timeline"
? "Trace Timeline"
: "Trace Service Map"
}}
</div>
</div>
<div
v-if="isTimelineExpanded"
class="rounded-borders visual-selector-container"
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
>
<template v-for="visual in traceVisuals" :key="visual.value">
<q-btn
:data-test="`trace-details-visual-${visual.value}-btn`"
:color="visual.value === activeVisual ? 'primary' : ''"
:flat="visual.value === activeVisual ? false : true"
dense
no-caps
size="11px"
class="q-px-sm visual-selection-btn"
@click="activeVisual = visual.value"
>
<q-icon><component :is="visual.icon" /></q-icon>
{{ visual.label }}</q-btn
>
</template>
</div>
</div>
<div
v-if="isTimelineExpanded"
class="rounded-borders"
style="border: 1px solid #cacaca; padding: 2px"
v-show="isTimelineExpanded"
class="chart-container-inner q-px-sm q-pb-sm"
:key="isTimelineExpanded.toString()"
>
<template v-for="visual in traceVisuals" :key="visual.value">
<q-btn
:data-test="`trace-details-visual-${visual.value}-btn`"
:color="visual.value === activeVisual ? 'primary' : ''"
:flat="visual.value === activeVisual ? false : true"
dense
no-caps
size="11px"
class="q-px-sm visual-selection-btn"
@click="activeVisual = visual.value"
>
<q-icon><component :is="visual.icon" /></q-icon>
{{ visual.label }}</q-btn
>
</template>
<ChartRenderer
data-test="trace-details-timeline-chart"
v-if="activeVisual === 'timeline'"
class="trace-details-chart trace-chart-height"
id="trace_details_gantt_chart"
:data="ChartData"
@updated:chart="updateChart"
/>
<ChartRenderer
data-test="trace-details-service-map-chart"
v-else
:data="traceServiceMap"
class="trace-chart-height"
/>
</div>
</div>
<div
v-show="isTimelineExpanded"
class="col-12"
:key="isTimelineExpanded.toString()"
>
<ChartRenderer
data-test="trace-details-timeline-chart"
v-if="activeVisual === 'timeline'"
class="trace-details-chart"
id="trace_details_gantt_chart"
:data="ChartData"
@updated:chart="updateChart"
style="height: 200px"
/>
<ChartRenderer
data-test="trace-details-service-map-chart"
v-else
:data="traceServiceMap"
style="height: 200px"
/>
</div>
<q-separator style="width: 100%" class="q-mb-sm" />
<div style="display: flex; flex: 1; min-height: 0;">
<div
class="histogram-spans-container"
:class="[
@@ -311,68 +307,75 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
]"
ref="parentContainer"
>
<trace-header
data-test="trace-details-header"
:baseTracePosition="baseTracePosition"
:splitterWidth="leftWidth"
@resize-start="startResize"
/>
<div class="relative-position full-height">
<div
class="trace-tree-container"
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
data-test="trace-details-tree-container"
>
<div class="position-relative">
<div
class="trace-tree-wrapper"
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
>
<trace-header
data-test="trace-details-header"
:baseTracePosition="baseTracePosition"
:splitterWidth="leftWidth"
@resize-start="startResize"
/>
<div style="display: flex; flex: 1; min-height: 0;">
<div class="relative-position trace-content-scroll">
<div
:style="{
width: '1px',
left: `${leftWidth}px`,
backgroundColor:
store.state.theme === 'dark' ? '#3c3c3c' : '#ececec',
zIndex: 999,
top: '-28px',
height: `${parentHeight}px`,
cursor: 'col-resize',
}"
class="absolute resize"
@mousedown="startResize"
/>
<trace-tree
data-test="trace-details-tree"
:collapseMapping="collapseMapping"
:spans="spanPositionList"
class="trace-tree-container"
data-test="trace-details-tree-container"
>
<div class="position-relative">
<div
:style="{
width: '1px',
left: `${leftWidth}px`,
backgroundColor:
store.state.theme === 'dark' ? '#3c3c3c' : '#ececec',
zIndex: 999,
top: '-28px',
height: `${spanPositionList.length * spanDimensions.height + 28}px`,
cursor: 'col-resize',
}"
class="absolute resize"
@mousedown="startResize"
/>
<trace-tree
data-test="trace-details-tree"
:collapseMapping="collapseMapping"
:spans="spanPositionList"
:baseTracePosition="baseTracePosition"
:spanDimensions="spanDimensions"
:spanMap="spanMap"
:leftWidth="leftWidth"
ref="traceTreeRef"
:search-query="searchQuery"
:spanList="spanList"
@toggle-collapse="toggleSpanCollapse"
@select-span="updateSelectedSpan"
@update-current-index="handleIndexUpdate"
@search-result="handleSearchResult"
/>
</div>
</div>
</div>
<q-separator v-if="isSidebarOpen && (selectedSpanId || showTraceDetails)" vertical />
<div
v-if="isSidebarOpen && (selectedSpanId || showTraceDetails)"
class="histogram-sidebar-inner"
:class="isTimelineExpanded ? '' : 'full'"
>
<trace-details-sidebar
data-test="trace-details-sidebar"
:span="spanMap[selectedSpanId as string]"
:baseTracePosition="baseTracePosition"
:spanDimensions="spanDimensions"
:spanMap="spanMap"
:leftWidth="leftWidth"
ref="traceTreeRef"
:search-query="searchQuery"
:spanList="spanList"
@toggle-collapse="toggleSpanCollapse"
@select-span="updateSelectedSpan"
@update-current-index="handleIndexUpdate"
@search-result="handleSearchResult"
@view-logs="redirectToLogs"
@close="closeSidebar"
@open-trace="openTraceLink"
/>
</div>
</div>
</div>
</div>
<q-separator vertical />
<div
v-if="isSidebarOpen && (selectedSpanId || showTraceDetails)"
class="histogram-sidebar"
:class="isTimelineExpanded ? '' : 'full'"
>
<trace-details-sidebar
data-test="trace-details-sidebar"
:span="spanMap[selectedSpanId as string]"
:baseTracePosition="baseTracePosition"
:search-query="searchQuery"
@view-logs="redirectToLogs"
@close="closeSidebar"
@open-trace="openTraceLink"
/>
</div>
</div>
<div
@@ -1402,7 +1405,22 @@ $traceChartCollapseHeight: 42px;
height: $toolbarHeight;
}
.trace-details {
overflow: auto;
overflow: hidden;
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.trace-details-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 0 0.75rem;
box-sizing: border-box;
}
.histogram-container-full {
width: 100%;
@@ -1411,34 +1429,46 @@ $traceChartCollapseHeight: 42px;
width: calc(100% - $sidebarWidth - $separatorWidth);
}
.histogram-sidebar {
.histogram-sidebar-inner {
width: $sidebarWidth;
height: calc(
100vh - $toolbarHeight - $traceChartHeight - 44px - $appNavbarHeight
);
flex-shrink: 0;
overflow-y: auto;
overflow-x: hidden;
&.full {
height: calc(100vh - $toolbarHeight - 8px - 44px - $appNavbarHeight);
}
flex: 1;
min-height: 0;
}
.histogram-spans-container {
height: calc(
100vh - $toolbarHeight - $traceChartHeight - 44px - $appNavbarHeight
);
overflow-y: auto;
flex: 1;
min-height: 0;
position: relative;
overflow-x: hidden;
padding-bottom: 0.5rem;
}
&.full {
height: calc(100vh - $toolbarHeight - 8px - 44px - $appNavbarHeight);
}
.trace-tree-wrapper {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(0.625rem);
border-radius: 0.5rem;
border: 0.1875rem solid rgba(255, 255, 255, 0.3);
overflow: hidden;
height: calc(100% - 2.5rem);
box-sizing: border-box;
display: flex;
flex-direction: column;
margin-bottom: 2.5rem;
}
.trace-tree-wrapper.bg-white {
background: rgba(240, 240, 245, 0.8);
backdrop-filter: blur(0.625rem);
border: 0.1875rem solid rgba(100, 100, 120, 0.5);
}
.trace-tree-container {
overflow: auto;
padding-top: 0;
padding-bottom: 0;
margin-bottom: 0;
min-height: 100%;
}
.trace-chart-btn {
@@ -1475,6 +1505,20 @@ $traceChartCollapseHeight: 42px;
}
</style>
<style lang="scss">
// Prevent parent containers from adding scrollbars
body:has(.trace-details),
html:has(.trace-details) {
overflow: hidden !important;
}
.trace-content-scroll {
flex: 1 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
min-height: 0 !important;
scrollbar-gutter: stable !important;
}
.trace-details {
.q-splitter__before,
.q-splitter__after {
@@ -1488,11 +1532,30 @@ $traceChartCollapseHeight: 42px;
.trace-details-chart {
.rangeslider-slidebox {
fill: #7076be !important;
opacity: 0.3 !important;
}
.rangeslider-mask-max,
.rangeslider-mask-min {
fill: #d2d2d2 !important;
fill-opacity: 1 !important;
fill-opacity: 0.15 !important;
}
.rangeslider-grabber {
fill: #7076be !important;
stroke: #ffffff !important;
stroke-width: 2 !important;
opacity: 1 !important;
}
.rangeslider-grabber:hover {
fill: #5a5fa0 !important;
cursor: ew-resize !important;
}
// Enhance the line graph (trace duration) visibility
.trace {
stroke-width: 2 !important;
opacity: 0.8 !important;
}
.scatterlayer .trace {
opacity: 1 !important;
}
}
@@ -1502,6 +1565,44 @@ $traceChartCollapseHeight: 42px;
font-size: 15px;
}
}
.visual-selector-container {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(0.625rem);
border-radius: 0.5rem;
border: 0.0625rem solid rgba(255, 255, 255, 0.1);
padding: 0.125rem;
}
.visual-selector-container.bg-white {
background: rgba(240, 240, 245, 0.8);
border: 0.125rem solid rgba(100, 100, 120, 0.3);
}
.trace-combined-header-wrapper {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(0.625rem);
border-radius: 0.5rem;
border: 0.0625rem solid rgba(255, 255, 255, 0.1);
padding: 0.5rem;
margin-bottom: 0.5rem;
flex-shrink: 0;
}
.trace-combined-header-wrapper.bg-white {
background: rgba(240, 240, 245, 0.8);
border: 0.125rem solid rgba(100, 100, 120, 0.3);
}
.chart-container-inner {
min-height: 12.5rem;
overflow: hidden;
}
.trace-chart-height {
height: 12.5rem !important;
min-height: 12.5rem !important;
}
}
.no-select {
@@ -1521,6 +1622,8 @@ $traceChartCollapseHeight: 42px;
.trace-logs-selector {
.q-field {
border-radius: 0.5rem 0 0 0.5rem;
span {
display: inline-block;
width: 180px;
@@ -1529,20 +1632,37 @@ $traceChartCollapseHeight: 42px;
text-overflow: ellipsis;
text-align: left;
}
.q-field__control {
border-radius: 0.5rem 0 0 0.5rem;
}
.q-field__control:before,
.q-field__control:after {
border: none !important;
}
}
}
.log-stream-search-input {
.q-field .q-field__control {
padding: 0px 4px;
border-radius: 0.5rem;
}
.q-field .q-field__control:before,
.q-field .q-field__control:after {
border: none !important;
}
}
.traces-view-logs-btn {
height: 36px;
margin-left: -1px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-top-right-radius: 0.5rem !important;
border-bottom-right-radius: 0.5rem !important;
.q-btn__content {
span {
@@ -1554,6 +1674,34 @@ $traceChartCollapseHeight: 42px;
height: 34px;
}
.trace-search-container {
border-radius: 0.5rem;
}
.q-menu .q-item.q-item--active {
background-color: rgba(25, 118, 210, 0.2) !important;
font-weight: 600 !important;
}
.q-dark .q-menu .q-item.q-item--active {
background-color: rgba(144, 202, 249, 0.2) !important;
}
.q-menu .q-item.q-manual-focusable--focused {
background-color: rgba(25, 118, 210, 0.1) !important;
}
.q-dark .q-menu .q-item.q-manual-focusable--focused {
background-color: rgba(144, 202, 249, 0.1) !important;
}
.trace-back-btn {
border: 0.09375rem solid;
border-radius: 50%;
width: 1.375rem;
height: 1.375rem;
}
.custom-height .q-field__control,
.custom-height .q-field__append {
height: 100%; /* Ensures the input control fills the container height */
+75 -5
View File
@@ -827,19 +827,55 @@ export default defineComponent({
<style scoped lang="scss">
.span_details_tab-panels {
table {
border-collapse: collapse;
border-collapse: separate;
border-spacing: 0;
width: 100%;
/* Other styling properties */
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(0.625rem);
border-radius: 0.5rem;
border: 0.125rem solid rgba(255, 255, 255, 0.3);
overflow: hidden;
}
th,
td {
border: 1px solid #f0f0f0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.15);
text-align: left;
padding: 4px 8px !important;
padding: 8px 12px !important;
font-size: 13px;
/* Other styling properties */
}
th:last-child,
td:last-child {
border-right: none;
}
tr:last-child td {
border-bottom: none;
}
tbody tr:first-child td:first-child {
border-top-left-radius: 0.5rem;
}
tbody tr:first-child td:last-child {
border-top-right-radius: 0.5rem;
}
tbody tr:last-child td:first-child {
border-bottom-left-radius: 0.5rem;
}
tbody tr:last-child td:last-child {
border-bottom-right-radius: 0.5rem;
}
}
.span_details_tab-panels table.q-table {
background: rgba(240, 240, 245, 0.8);
backdrop-filter: blur(0.625rem);
border: 0.125rem solid rgba(100, 100, 120, 0.5);
}
.attr-text {
font-size: 12px;
@@ -1040,3 +1076,37 @@ export default defineComponent({
background-color: yellow; /* Adjust background color as desired */
}
</style>
<style lang="scss">
// Dark theme support for glassmorphic tables
.body--dark {
.span_details_tab-panels {
table {
background: rgba(255, 255, 255, 0.05);
border: 0.125rem solid rgba(255, 255, 255, 0.3);
}
th,
td {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.15);
}
}
}
// Light theme support for glassmorphic tables
.body--light {
.span_details_tab-panels {
table {
background: rgba(240, 240, 245, 0.8);
border: 0.125rem solid rgba(100, 100, 120, 0.5);
}
th,
td {
border-bottom: 1px solid rgba(100, 100, 120, 0.2);
border-right: 1px solid rgba(100, 100, 120, 0.3);
}
}
}
</style>
+8 -18
View File
@@ -124,14 +124,12 @@ describe("TraceHeader", () => {
it("should have correct default height", () => {
const headerContainer = wrapper.find('[data-test="trace-header"]');
expect(headerContainer.attributes("style")).toContain("height: 30px");
expect(headerContainer.classes()).toContain("trace-header-container");
});
it("should have sticky positioning", () => {
const headerContainer = wrapper.find('[data-test="trace-header"]');
expect(headerContainer.attributes("style")).toContain("position: sticky");
expect(headerContainer.attributes("style")).toContain("top: 0");
expect(headerContainer.attributes("style")).toContain("z-index: 1999");
expect(headerContainer.classes()).toContain("trace-header-container");
});
});
@@ -232,7 +230,7 @@ describe("TraceHeader", () => {
it("should have correct z-index for first tic", () => {
const firstTic = wrapper.find('[data-test="trace-header-tic-line-0"]');
expect(firstTic.attributes("style")).toContain("z-index: 5");
expect(firstTic.classes()).toContain("trace-tic-first");
});
it("should have correct z-index for other tics", () => {
@@ -240,7 +238,7 @@ describe("TraceHeader", () => {
.findAll('[data-test^="trace-header-tic-line-"]')
.slice(1);
otherTics.forEach((tic) => {
expect(tic.attributes("style")).toContain("z-index: 1");
expect(tic.classes()).toContain("trace-tic");
});
});
});
@@ -249,9 +247,7 @@ describe("TraceHeader", () => {
it("should apply light theme by default", () => {
const headerContainer = wrapper.find('[data-test="trace-header"]');
expect(headerContainer.classes()).toContain("bg-grey-2");
expect(headerContainer.attributes("style")).toContain(
"border: 1px solid rgb(236, 236, 236)",
);
expect(headerContainer.classes()).toContain("trace-header-container");
});
it("should apply dark theme when store theme is dark", async () => {
@@ -287,9 +283,7 @@ describe("TraceHeader", () => {
const headerContainer = darkWrapper.find('[data-test="trace-header"]');
expect(headerContainer.classes()).toContain("bg-grey-9");
expect(headerContainer.attributes("style")).toContain(
"border: 1px solid rgb(60, 60, 60)",
);
expect(headerContainer.classes()).toContain("trace-header-container");
darkWrapper.unmount();
});
@@ -329,9 +323,7 @@ describe("TraceHeader", () => {
'[data-test^="trace-header-tic-line-"]',
);
ticLines.forEach((tic) => {
expect(tic.attributes("style")).toContain(
"background-color: rgb(60, 60, 60)",
);
expect(tic.classes()).toContain("bg-dark-tic");
});
darkWrapper.unmount();
@@ -340,9 +332,7 @@ describe("TraceHeader", () => {
it("should apply light theme colors to tic lines", () => {
const ticLines = wrapper.findAll('[data-test^="trace-header-tic-line-"]');
ticLines.forEach((tic) => {
expect(tic.attributes("style")).toContain(
"background-color: rgb(202, 202, 202)",
);
expect(tic.classes()).toContain("trace-tic");
});
});
});
+38 -23
View File
@@ -16,20 +16,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div
:style="{
height: '30px',
border:
store.state.theme === 'dark'
? '1px solid #3c3c3c'
: '1px solid #ececec',
}"
style="top: 0; z-index: 1999; position: sticky"
class="flex justify-start items-center header-bg bg-grey-2"
class="flex justify-start items-center header-bg bg-grey-2 trace-header-container"
:class="store.state.theme === 'dark' ? 'bg-grey-9' : 'bg-grey-2'"
data-test="trace-header"
>
<div
class="tw-relative flex justify-start items-center no-wrap row q-px-sm"
class="tw-relative flex justify-start items-center no-wrap row q-px-sm trace-header-left"
:style="{
width: splitterWidth + 'px',
}"
@@ -39,7 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<q-avatar
color="primary"
text-color="white"
size="20px"
size="1.25rem"
icon="drag_indicator"
class="resize-btn"
@mousedown="handleMouseDown"
@@ -47,7 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</div>
<div
class="flex justify-start items-center no-wrap row relative-position"
class="flex justify-start items-center no-wrap row relative-position trace-header-right"
:style="{
width: `calc(100% - ${splitterWidth}px)`,
}"
@@ -82,15 +74,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div
v-for="(tick, index) in baseTracePosition['tics']"
class="trace-tic"
:class="{'trace-tic-first': index === 0, 'bg-dark-tic': store.state.theme === 'dark'}"
:key="tick.value + index"
:style="{
position: 'absolute',
left: tick.left,
top: '-3px',
width: '1px',
backgroundColor: store.state.theme === 'dark' ? '#3c3c3c' : '#cacaca',
zIndex: index === 0 ? '5' : '1',
height: '26px',
}"
:data-test="`trace-header-tic-line-${index}`"
></div>
@@ -138,25 +125,53 @@ $toolbarHeight: 50px;
$traceHeaderHeight: 30px;
$traceChartHeight: 250px;
.trace-header-container {
height: 1.875rem;
top: 0;
z-index: 1999;
position: sticky;
border-radius: 0.5rem 0.5rem 0 0;
border: 0.0625rem solid #ececec;
}
.trace-header-container.bg-grey-9 {
border-color: #3c3c3c;
}
.spans-container {
position: relative;
}
.collapse-btn {
width: 10px;
height: 10px;
width: 0.625rem;
height: 0.625rem;
}
.trace-tic {
height: calc(100vh - $toolbarHeight - 5px - $traceChartHeight);
position: absolute;
top: -0.1875rem;
width: 0.0625rem;
background-color: #cacaca;
z-index: 1;
height: 1.625rem;
}
.trace-tic.bg-dark-tic {
background-color: #3c3c3c;
}
.trace-tic-first {
z-index: 5;
}
.header-bg {
background-color: color-mix(in srgb, currentColor 5%, transparent);
}
.resize-btn {
position: absolute;
right: -10px;
top: -2px;
right: -0.625rem;
top: -0.125rem;
z-index: 10;
cursor: col-resize;
}
+9 -9
View File
@@ -272,14 +272,14 @@ describe("TraceTree", () => {
it("should render collapse button for spans with children", () => {
const collapseBtn = wrapper.find(
'[data-test="trace-tree-span-collapse-btn-d9603ec7f76eb499"]',
'[data-test="trace-tree-span-badge-collapse-btn-d9603ec7f76eb499"]',
);
expect(collapseBtn.exists()).toBe(true);
});
it("should not render collapse button for spans without children", () => {
const collapseBtn = wrapper.find(
'[data-test="trace-tree-span-collapse-btn-6702b0494b2b6e57"]',
'[data-test="trace-tree-span-badge-collapse-btn-6702b0494b2b6e57"]',
);
expect(collapseBtn.exists()).toBe(false);
});
@@ -316,7 +316,7 @@ describe("TraceTree", () => {
describe("Span collapse functionality", () => {
it("should emit toggleCollapse when collapse button is clicked", async () => {
const collapseBtn = wrapper.find(
'[data-test="trace-tree-span-collapse-btn-d9603ec7f76eb499"]',
'[data-test="trace-tree-span-badge-collapse-btn-d9603ec7f76eb499"]',
);
await collapseBtn.trigger("click");
@@ -331,10 +331,10 @@ describe("TraceTree", () => {
collapseMapping: { d9603ec7f76eb499: true },
});
const collapseIcon = wrapper.find(
'[data-test="trace-tree-span-collapse-btn-d9603ec7f76eb499"] .collapse-btn',
const collapseBtn = wrapper.find(
'[data-test="trace-tree-span-badge-collapse-btn-d9603ec7f76eb499"]',
);
expect(collapseIcon.attributes("style")).toContain("rotate: 0deg");
expect(collapseBtn.exists()).toBe(true);
});
it("should apply correct expand icon rotation", async () => {
@@ -342,10 +342,10 @@ describe("TraceTree", () => {
collapseMapping: { d9603ec7f76eb499: false },
});
const collapseIcon = wrapper.find(
'[data-test="trace-tree-span-collapse-btn-d9603ec7f76eb499"] .collapse-btn',
const collapseBtn = wrapper.find(
'[data-test="trace-tree-span-badge-collapse-btn-d9603ec7f76eb499"]',
);
expect(collapseIcon.attributes("style")).toContain("rotate: 270deg");
expect(collapseBtn.exists()).toBe(true);
});
});
+303 -69
View File
@@ -15,22 +15,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<template v-for="(span, index) in spans as any[]" :key="span.spanId">
<div
:style="{
position: 'relative',
width: '100%',
overflow: 'visible',
flexWrap: 'nowrap',
}"
class="flex"
:data-test="`trace-tree-span-container-${span.spanId}`"
>
<div v-bind="$attrs" ref="rootContainer">
<template v-for="(span, index) in spans as any[]" :key="span.spanId">
<div
:style="{
position: 'relative',
width: '100%',
flexWrap: 'nowrap',
}"
class="flex span-row"
:class="spanHoveredIndex === index ? 'span-row-highlight' : ''"
:data-test="`trace-tree-span-container-${span.spanId}`"
@mouseover="() => (spanHoveredIndex = index)"
@mouseout="() => (spanHoveredIndex = -1)"
>
<div :style="{ width: leftWidth + 'px' }">
<div
:style="{
height: '100%',
margin: `0 0px 0 ${
margin: `0 0 0 ${
span.hasChildSpans
? span.style.left
: parseInt(span.style.left) +
@@ -44,9 +47,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div
class="flex no-wrap q-pt-sm full-width relative-position operation-name-container"
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
:style="{ height: '30px' }"
@mouseover="() => (spanHoveredIndex = index)"
@mouseout="() => (spanHoveredIndex = -1)"
:data-test="`trace-tree-span-operation-name-container-${span.spanId}`"
>
<div
@@ -57,7 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<q-btn
class="q-mx-xs view-span-logs"
:class="store.state.theme === 'dark' ? 'bg-dark' : 'bg-white'"
size="10px"
size="0.625rem"
icon="search"
dense
no-caps
@@ -70,31 +70,30 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
<div
v-if="span.hasChildSpans"
class="span-count-box cursor-pointer"
:ref="el => setBadgeRef(span.spanId, el)"
:style="{
width: spanDimensions.collapseWidth + 'px',
height: spanDimensions.collapseHeight + 'px',
borderColor: span.style.color,
color: span.style.color,
}"
class="q-pt-xs flex justify-center items-center collapse-container cursor-pointer"
@click.stop="toggleSpanCollapse(span.spanId)"
:data-test="`trace-tree-span-collapse-btn-${span.spanId}`"
:data-test="`trace-tree-span-badge-collapse-btn-${span.spanId}`"
:title="`Click to ${collapseMapping[span.spanId] ? 'expand' : 'collapse'}`"
>
<q-icon
dense
round
flat
name="expand_more"
class="collapse-btn"
:style="{
rotate: collapseMapping[span.spanId] ? '0deg' : '270deg',
}"
/>
{{ getChildCount(span) }}
</div>
<div
class="ellipsis q-pl-xs cursor-pointer"
v-else
class="span-leaf-dot"
:ref="el => setBadgeRef(span.spanId, el)"
:style="{
paddingLeft: '4px',
borderLeft: `3px solid ${span.style.color}`,
backgroundColor: span.style.color,
}"
></div>
<div
class="ellipsis q-pl-xs cursor-pointer span-name-section"
@click="selectSpan(span.spanId)"
:data-test="`trace-tree-span-select-btn-${span.spanId}`"
>
@@ -130,45 +129,88 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
<div
class="span-background-wrapper"
:style="{
backgroundColor: span.style.backgroundColor,
height: `calc(100% - 30px)`,
borderLeft: `3px solid ${span.style.color}`,
marginLeft: span.hasChildSpans ? '14px' : '0',
height: `calc(100% - 1.875rem)`,
borderLeft: `0.1875rem solid ${span.style.color}`,
marginLeft: span.hasChildSpans ? '0.875rem' : '0',
width: '100%',
position: 'relative',
}"
:data-test="`trace-tree-span-background-${span.spanId}`"
></div>
>
</div>
</div>
</div>
<span-block
:span="span"
:depth="depth"
:baseTracePosition="baseTracePosition"
:styleObj="{
position: 'absolute',
top: span.style.top,
left: span.style.left,
height: '60px',
}"
:style="{
width: `calc(100% - ${leftWidth}px)`,
}"
:spanDimensions="spanDimensions"
:isCollapsed="collapseMapping[span.spanId]"
:spanData="spanMap[span.spanId]"
@toggle-collapse="toggleSpanCollapse"
@select-span="selectSpan"
@mouseover="() => (spanHoveredIndex = index)"
@mouseout="() => (spanHoveredIndex = -1)"
@view-logs="viewSpanLogs(span)"
/>
</div>
</template>
<div :style="{ width: `calc(100% - ${leftWidth}px)` }">
<span-block
:span="span"
:depth="depth"
:baseTracePosition="baseTracePosition"
:styleObj="{
position: 'absolute',
top: span.style.top,
left: span.style.left,
height: '3.75rem',
}"
:spanDimensions="spanDimensions"
:isCollapsed="collapseMapping[span.spanId]"
:spanData="spanMap[span.spanId]"
:class="spanHoveredIndex === index ? 'span-block-highlight' : ''"
@toggle-collapse="toggleSpanCollapse"
@select-span="selectSpan"
@view-logs="viewSpanLogs(span)"
/>
</div>
</div>
</template>
<!-- SVG overlay for connectors -->
<svg
v-if="Object.keys(connectorPaths).length > 0"
class="connector-overlay"
:style="{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: 1000,
overflow: 'visible',
}"
>
<template v-for="(connector, key) in connectorPaths" :key="key">
<!-- Vertical line -->
<line
:x1="connector.x1"
:y1="connector.y1"
:x2="connector.x1"
:y2="connector.y2"
:stroke="connector.color"
stroke-width="1.5"
opacity="0.6"
/>
<!-- Horizontal lines to children -->
<template v-for="(child, idx) in connector.children" :key="idx">
<line
:x1="connector.x1"
:y1="child.y"
:x2="child.x"
:y2="child.y"
:stroke="connector.color"
stroke-width="1.5"
opacity="0.6"
/>
</template>
</template>
</svg>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick, ref, watch, computed } from "vue";
import { defineComponent, nextTick, ref, watch, computed, onMounted } from "vue";
import { getImageURL } from "@/utils/zincutils";
import useTraces from "@/composables/useTraces";
import { useStore } from "vuex";
@@ -178,6 +220,7 @@ import { useRouter } from "vue-router";
export default defineComponent({
name: "TraceTree",
inheritAttrs: false,
props: {
spans: {
type: Array,
@@ -236,6 +279,16 @@ export default defineComponent({
const router = useRouter();
const badgeRefs = ref<Record<string, HTMLElement>>({});
const connectorPaths = ref<Record<string, any>>({});
const rootContainer = ref<HTMLElement | null>(null);
const setBadgeRef = (spanId: string, el: any) => {
if (el) {
badgeRefs.value[spanId] = el;
}
};
function toggleSpanCollapse(spanId: number | string) {
emit("toggleCollapse", spanId);
}
@@ -248,6 +301,89 @@ export default defineComponent({
const queryDetails = buildQueryDetails(span);
navigateToLogs(queryDetails);
};
const getChildCount = (span: any): number => {
if (!span.spans || !Array.isArray(span.spans)) return 0;
return span.spans.length;
};
const getDirectChildren = (span: any) => {
if (!span.spans || !Array.isArray(span.spans)) return [];
return span.spans;
};
const getChildrenHeight = (span: any): number => {
if (!span.spans || !Array.isArray(span.spans) || !props.collapseMapping[span.spanId]) {
return 0;
}
const countVisibleChildren = (children: any[]): number => {
let count = 0;
children.forEach((child: any) => {
count++;
if (child.spans && child.spans.length > 0 && props.collapseMapping[child.spanId]) {
count += countVisibleChildren(child.spans);
}
});
return count;
};
return countVisibleChildren(span.spans);
};
const calculateConnectors = () => {
if (!rootContainer.value) return;
const newConnectorPaths: Record<string, any> = {};
const containerRect = rootContainer.value.getBoundingClientRect();
if (props.spans && Array.isArray(props.spans)) {
props.spans.forEach((span: any) => {
if (span.hasChildSpans && props.collapseMapping[span.spanId]) {
const parentBadge = badgeRefs.value[span.spanId];
if (!parentBadge) return;
const parentRect = parentBadge.getBoundingClientRect();
const parentX = parentRect.left - containerRect.left + parentRect.width / 2;
const parentY = parentRect.bottom - containerRect.top;
const children = getDirectChildren(span);
const childPositions: any[] = [];
let lastChildY = parentY;
children.forEach((child: any) => {
const childBadge = badgeRefs.value[child.spanId];
if (childBadge) {
const childRect = childBadge.getBoundingClientRect();
const childX = childRect.left - containerRect.left;
const childY = childRect.top - containerRect.top + childRect.height / 2;
childPositions.push({
x: childX,
y: childY,
isLeaf: !child.hasChildSpans,
});
lastChildY = childY;
}
});
if (childPositions.length > 0) {
newConnectorPaths[span.spanId] = {
x1: parentX,
y1: parentY,
x2: parentX,
y2: lastChildY,
color: span.style.color,
children: childPositions,
};
}
}
});
}
connectorPaths.value = newConnectorPaths;
};
const searchResults = ref<any[]>([]);
const currentIndex = ref<number | null>(null);
const currentSelectedValue = computed(() => {
@@ -357,6 +493,29 @@ export default defineComponent({
emit("search-result", newValue.length);
});
// Watch for changes that require recalculating connectors
watch(
() => [props.spans, props.collapseMapping],
() => {
nextTick(() => {
// Use setTimeout to ensure all refs are set after DOM updates
setTimeout(() => {
calculateConnectors();
}, 0);
});
},
{ deep: true }
);
onMounted(() => {
nextTick(() => {
// Use setTimeout to ensure all refs are set after DOM updates
setTimeout(() => {
calculateConnectors();
}, 0);
});
});
return {
toggleSpanCollapse,
getImageURL,
@@ -374,6 +533,13 @@ export default defineComponent({
currentSelectedValue,
scrollToMatch,
findMatches,
getChildCount,
getDirectChildren,
getChildrenHeight,
setBadgeRef,
connectorPaths,
calculateConnectors,
rootContainer,
};
},
components: { SpanBlock },
@@ -382,20 +548,53 @@ export default defineComponent({
<style scoped lang="scss">
.view-logs-container {
top: 7px;
top: 0.25rem;
right: 0;
}
.spans-container {
position: relative;
}
.collapse-btn {
width: 14px;
height: auto;
opacity: 0.6;
.span-count-box {
min-width: 1.5rem;
height: 1.25rem;
padding: 0 0.25rem;
border-radius: 0.25rem;
border: 0.0625rem solid;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
margin-right: 0.25rem;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
.bg-dark .span-count-box:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.span-leaf-dot {
width: 0.375rem;
height: 0.375rem;
border-radius: 50%;
margin-right: 0.25rem;
flex-shrink: 0;
align-self: center;
}
.span-name-section {
padding-left: 0.25rem;
}
.operation-name-container {
height: 1.875rem;
overflow: visible;
.view-logs-container {
visibility: hidden;
}
@@ -405,6 +604,34 @@ export default defineComponent({
}
}
}
.span-row {
position: relative;
min-height: 1.875rem;
&.span-row-highlight {
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
height: 1.875rem;
background-color: rgba(0, 123, 255, 0.2);
pointer-events: none;
z-index: 999;
}
.operation-name-container {
background-color: transparent !important;
}
}
> * {
position: relative;
z-index: 2;
}
}
</style>
<style lang="scss">
.view-logs-btn-text {
@@ -414,7 +641,7 @@ export default defineComponent({
background-color: inherit;
.view-logs-btn-text {
visibility: hidden;
width: 0px;
width: 0;
transition: width 0.3s ease-in;
}
&:hover .view-logs-btn-text {
@@ -423,7 +650,7 @@ export default defineComponent({
}
}
.border-right {
border-right: 1px solid rgb(236, 236, 236);
border-right: 0.0625rem solid rgb(236, 236, 236);
}
.highlighted {
background-color: yellow;
@@ -434,4 +661,11 @@ export default defineComponent({
color: red;
font-weight: bold;
}
.span-block-highlight {
.bg-dark,
.bg-white {
background-color: transparent !important;
}
}
</style>