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
6 changes: 6 additions & 0 deletions .changeset/cyan-toes-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/web-shared": patch
"@workflow/web": patch
---

Refine span viewer panel UI: reduced font sizes and spacing, added connecting lines in detail cards, improved attribute layout with bordered containers. Improve status badge with colored indicators and optional duration, add overlay mode to copyable text, simplify stream detail back navigation.
91 changes: 77 additions & 14 deletions packages/web-shared/src/sidebar/attribute-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DetailCard } from './detail-card';
const JsonBlock = (value: unknown) => {
return (
<pre
className="text-copy-12 overflow-x-auto rounded-md border p-4"
className="text-[11px] overflow-x-auto rounded-md border p-3"
style={{
borderColor: 'var(--ds-gray-300)',
backgroundColor: 'var(--ds-gray-100)',
Expand Down Expand Up @@ -167,13 +167,13 @@ const attributeToDisplayFn: Record<
{error.code && (
<div>
<span
className="text-copy-12 font-medium"
className="text-[11px] font-medium"
style={{ color: 'var(--ds-gray-700)' }}
>
Error Code:{' '}
</span>
<code
className="text-copy-12"
className="text-[11px]"
style={{ color: 'var(--ds-gray-1000)' }}
>
{error.code}
Expand All @@ -182,7 +182,7 @@ const attributeToDisplayFn: Record<
)}
{/* Show stack if available, otherwise just the message */}
<pre
className="text-copy-12 overflow-x-auto rounded-md border p-4"
className="text-[11px] overflow-x-auto rounded-md border p-3"
style={{
borderColor: 'var(--ds-gray-300)',
backgroundColor: 'var(--ds-gray-100)',
Expand All @@ -201,7 +201,7 @@ const attributeToDisplayFn: Record<
return (
<DetailCard summary="Error">
<pre
className="text-copy-12 overflow-x-auto rounded-md border p-4"
className="text-[11px] overflow-x-auto rounded-md border p-3"
style={{
borderColor: 'var(--ds-gray-300)',
backgroundColor: 'var(--ds-gray-100)',
Expand Down Expand Up @@ -231,10 +231,12 @@ export const AttributeBlock = ({
attribute,
value,
isLoading,
inline = false,
}: {
attribute: string;
value: unknown;
isLoading?: boolean;
inline?: boolean;
}) => {
const displayFn =
attributeToDisplayFn[attribute as keyof typeof attributeToDisplayFn];
Expand All @@ -245,6 +247,23 @@ export const AttributeBlock = ({
if (!displayValue) {
return null;
}

if (inline) {
return (
<div className="flex items-center gap-1.5">
<span
className="text-[11px] font-medium"
style={{ color: 'var(--ds-gray-500)' }}
>
{attribute}
</span>
<span className="text-[11px]" style={{ color: 'var(--ds-gray-1000)' }}>
{displayValue}
</span>
</div>
);
}

return (
<div className="relative">
{typeof isLoading === 'boolean' && isLoading && (
Expand All @@ -256,10 +275,15 @@ export const AttributeBlock = ({
</div>
)}
<div key={attribute} className="flex flex-col gap-0 my-2">
<span className="font-medium" style={{ color: 'var(--ds-gray-500)' }}>
<span
className="text-xs font-medium"
style={{ color: 'var(--ds-gray-500)' }}
>
{attribute}
</span>
<span style={{ color: 'var(--ds-gray-1000)' }}>{displayValue}</span>
<span className="text-xs" style={{ color: 'var(--ds-gray-1000)' }}>
{displayValue}
</span>
</div>
</div>
);
Expand All @@ -282,15 +306,54 @@ export const AttributePanel = ({
.filter((key) => resolvableAttributes.includes(key))
.sort(sortByAttributeOrder);

// Filter out attributes that return null
const visibleBasicAttributes = basicAttributes.filter((attribute) => {
const displayFn =
attributeToDisplayFn[attribute as keyof typeof attributeToDisplayFn];
if (!displayFn) return false;
const displayValue = displayFn(
displayData[attribute as keyof typeof displayData]
);
return displayValue !== null;
});

return (
<div>
{basicAttributes.map((attribute) => (
<AttributeBlock
key={attribute}
attribute={attribute}
value={displayData[attribute as keyof typeof displayData]}
/>
))}
{/* Basic attributes in a vertical layout with border */}
{visibleBasicAttributes.length > 0 && (
<div
className="flex flex-col divide-y rounded-lg border mb-3 overflow-hidden"
style={{
borderColor: 'var(--ds-gray-300)',
backgroundColor: 'var(--ds-gray-100)',
}}
>
{visibleBasicAttributes.map((attribute) => (
<div
key={attribute}
className="flex items-center justify-between px-3 py-1.5"
style={{
borderColor: 'var(--ds-gray-300)',
}}
>
<span
className="text-[11px] font-medium"
style={{ color: 'var(--ds-gray-500)' }}
>
{attribute}
</span>
<span
className="text-[11px] font-mono"
style={{ color: 'var(--ds-gray-1000)' }}
>
{attributeToDisplayFn[
attribute as keyof typeof attributeToDisplayFn
]?.(displayData[attribute as keyof typeof displayData])}
</span>
</div>
))}
</div>
)}
{error ? (
<Alert variant="destructive" className="my-4">
<AlertCircle className="h-4 w-4" />
Expand Down
22 changes: 20 additions & 2 deletions packages/web-shared/src/sidebar/detail-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function DetailCard({
return (
<details className="group">
<summary
className="cursor-pointer rounded-md border px-3 py-2 text-copy-14 hover:brightness-95"
className="cursor-pointer rounded-md border px-2.5 py-1.5 text-xs hover:brightness-95"
style={{
borderColor: 'var(--ds-gray-300)',
backgroundColor: 'var(--ds-gray-100)',
Expand All @@ -19,7 +19,25 @@ export function DetailCard({
>
{summary}
</summary>
<div className="p-2">{children}</div>
{/* Expanded content with connecting line */}
<div className="relative pl-6 mt-3">
{/* Curved connecting line - vertical part from summary */}
<div
className="absolute left-3 -top-3 w-px h-3"
style={{ backgroundColor: 'var(--ds-gray-400)' }}
/>
{/* Curved corner */}
<div
className="absolute left-3 top-0 w-3 h-3 border-l border-b rounded-bl-lg"
style={{ borderColor: 'var(--ds-gray-400)' }}
/>
{/* Horizontal part to content */}
<div
className="absolute left-6 top-3 w-0 h-px -translate-y-px"
style={{ backgroundColor: 'var(--ds-gray-400)' }}
/>
<div>{children}</div>
</div>
</details>
);
}
55 changes: 41 additions & 14 deletions packages/web-shared/src/sidebar/events-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,52 @@ export function EventsList({
</>
}
>
<div className="mt-2 px-4">
{/* Bordered container with separator */}
<div
className="flex flex-col divide-y rounded-md border overflow-hidden"
style={{
borderColor: 'var(--ds-gray-300)',
backgroundColor: 'var(--ds-gray-100)',
}}
>
{Object.entries(event.attributes)
.filter(([key]) => key !== 'eventData')
.map(([key, value]) => (
<AttributeBlock key={key} attribute={key} value={value} />
<div
key={key}
className="flex items-center justify-between px-2.5 py-1.5"
style={{ borderColor: 'var(--ds-gray-300)' }}
>
<span
className="text-[11px] font-medium"
style={{ color: 'var(--ds-gray-500)' }}
>
{key}
</span>
<span
className="text-[11px] font-mono"
style={{ color: 'var(--ds-gray-1000)' }}
>
{String(value)}
</span>
Comment on lines +119 to +124
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event attributes are displayed using String(value) instead of the AttributeBlock component, bypassing custom attribute formatters that were used in the previous implementation.

View Details
📝 Patch Details
diff --git a/packages/web-shared/src/sidebar/attribute-panel.tsx b/packages/web-shared/src/sidebar/attribute-panel.tsx
index aba8ef8..601b05c 100644
--- a/packages/web-shared/src/sidebar/attribute-panel.tsx
+++ b/packages/web-shared/src/sidebar/attribute-panel.tsx
@@ -219,6 +219,18 @@ const attributeToDisplayFn: Record<
   },
 };
 
+export const getAttributeDisplayValue = (
+  attribute: string,
+  value: unknown
+): null | string | ReactNode => {
+  const displayFn =
+    attributeToDisplayFn[attribute as keyof typeof attributeToDisplayFn];
+  if (!displayFn) {
+    return String(value);
+  }
+  return displayFn(value);
+};
+
 const resolvableAttributes = [
   'input',
   'output',
diff --git a/packages/web-shared/src/sidebar/events-list.tsx b/packages/web-shared/src/sidebar/events-list.tsx
index 02136c0..e291298 100644
--- a/packages/web-shared/src/sidebar/events-list.tsx
+++ b/packages/web-shared/src/sidebar/events-list.tsx
@@ -10,7 +10,7 @@ import {
 import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert';
 import type { SpanEvent } from '../trace-viewer/types';
 import { convertEventsToSpanEvents } from '../workflow-traces/trace-span-construction';
-import { AttributeBlock } from './attribute-panel';
+import { AttributeBlock, getAttributeDisplayValue } from './attribute-panel';
 import { DetailCard } from './detail-card';
 
 export function EventsList({
@@ -104,26 +104,33 @@ export function EventsList({
               >
                 {Object.entries(event.attributes)
                   .filter(([key]) => key !== 'eventData')
-                  .map(([key, value]) => (
-                    <div
-                      key={key}
-                      className="flex items-center justify-between px-2.5 py-1.5"
-                      style={{ borderColor: 'var(--ds-gray-300)' }}
-                    >
-                      <span
-                        className="text-[11px] font-medium"
-                        style={{ color: 'var(--ds-gray-500)' }}
+                  .map(([key, value]) => {
+                    const displayValue = getAttributeDisplayValue(key, value);
+                    // Skip attributes that should not be displayed (returns null)
+                    if (displayValue === null) {
+                      return null;
+                    }
+                    return (
+                      <div
+                        key={key}
+                        className="flex items-center justify-between px-2.5 py-1.5"
+                        style={{ borderColor: 'var(--ds-gray-300)' }}
                       >
-                        {key}
-                      </span>
-                      <span
-                        className="text-[11px] font-mono"
-                        style={{ color: 'var(--ds-gray-1000)' }}
-                      >
-                        {String(value)}
-                      </span>
-                    </div>
-                  ))}
+                        <span
+                          className="text-[11px] font-medium"
+                          style={{ color: 'var(--ds-gray-500)' }}
+                        >
+                          {key}
+                        </span>
+                        <span
+                          className="text-[11px] font-mono"
+                          style={{ color: 'var(--ds-gray-1000)' }}
+                        >
+                          {displayValue}
+                        </span>
+                      </div>
+                    );
+                  })}
               </div>
               {/* Event data section */}
               {eventError && (

Analysis

Event attributes bypass custom formatters in events-list.tsx

What fails: Event attributes in packages/web-shared/src/sidebar/events-list.tsx lines 119-124 display raw String(value) instead of applying custom attribute formatters, causing date attributes to display as raw ISO strings instead of formatted dates, and hiding attributes like ownerId/projectId to display incorrectly.

How to reproduce:

  1. Display a workflow event with a date attribute (e.g., createdAt: "2025-11-28T17:47:00.000Z")
  2. Observe the Events panel in the sidebar
  3. Expected: Date should display as formatted (e.g., "11/28/2025, 5:47:00 PM")
  4. Actual (before fix): Displays raw ISO string "2025-11-28T17:47:00.000Z"

Result:

  • Date attributes (createdAt, startedAt, updatedAt, completedAt, retryAfter, resumeAt) displayed as raw ISO strings instead of localized dates
  • Hidden attributes (ownerId, projectId, environment, executionContext) displayed as values instead of being hidden
  • Loses formatting applied by attributeToDisplayFn lookup table in AttributeBlock component

Expected: Per the attribute formatting logic, the attributeToDisplayFn provides custom formatters for event attributes to ensure consistent display across the UI. Date attributes should use toLocaleString() for user-friendly formatting, and certain sensitive attributes should be hidden.

Fix implemented:

  1. Exported getAttributeDisplayValue() utility from attribute-panel.tsx that applies the attribute formatters
  2. Updated events-list.tsx to import and use this utility for formatting event attribute values
  3. This restores the custom formatting while maintaining the inline display layout

</div>
))}
<div className="relative">
{eventError && <div>Error loading event data</div>}
{!eventError &&
!eventsLoading &&
event.attributes.eventData && (
<AttributeBlock
isLoading={eventsLoading}
attribute="eventData"
value={event.attributes.eventData}
/>
)}
</div>
</div>
{/* Event data section */}
{eventError && (
<div className="text-xs text-red-500 mt-2">
Error loading event data
</div>
)}
{!eventError && !eventsLoading && event.attributes.eventData && (
<div className="mt-2">
<AttributeBlock
isLoading={eventsLoading}
attribute="eventData"
value={event.attributes.eventData}
/>
</div>
)}
</DetailCard>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,17 +262,17 @@ export function SpanDetailPanel({
{attached && !isMobile ? <PanelResizer /> : null}
<div className={styles.spanDetailPanelTop}>
<div className={styles.spanDetailPanelTopInfo}>
<div className={styles.spanDetailPanelTruncatedHeading}>
{/* Name/ID first */}
<span className={styles.spanDetailPanelName} title={span.name}>
{span.name}
</span>
{/* Right side: duration badge, separator, close */}
<div className={styles.spanDetailPanelCorner}>
{selected.isInstrumentationHint ? null : (
<span className={styles.spanDetailPanelDuration}>
{formatDuration(selected.duration)}
</span>
)}
<span className={styles.spanDetailPanelName} title={span.name}>
{span.name}
</span>
</div>
<div className={styles.spanDetailPanelCorner}>
<div className={styles.spanDetailPanelCloseVerticalRule} />
<button
aria-label="Close Span Details"
Expand All @@ -284,7 +284,7 @@ export function SpanDetailPanel({
}
type="button"
>
<IconCross color="gray-700" size={24} />
<IconCross color="gray-700" size={20} />
</button>
</div>
</div>
Expand Down
Loading
Loading