fix: fixed styling of log messages

This commit is contained in:
Ida Dittrich 2026-01-02 11:51:01 +01:00
parent 641930b1d0
commit 401c0885a1
3 changed files with 157 additions and 71 deletions

View file

@ -12,7 +12,10 @@
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
overflow-x: hidden;
box-sizing: border-box;
max-width: 100%;
}
.emptyState {
@ -33,18 +36,25 @@
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
overflow-x: hidden;
}
.dashboardContainer {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
min-width: 0;
overflow-x: hidden;
box-sizing: border-box;
max-width: 100%;
}
.dashboardContainer > .operationNode {
width: 100%;
min-width: 0;
max-width: 100%;
}
.operationNode {
@ -52,7 +62,8 @@
flex-direction: column;
gap: 0;
position: relative;
margin-bottom: 2px;
min-width: 0;
box-sizing: border-box;
}
.operationNodeIndented {
@ -76,23 +87,21 @@
flex-direction: row !important;
align-items: flex-start;
min-height: 32px;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.operationContent {
flex: 1;
display: flex !important;
flex-direction: column !important;
gap: 4px;
min-width: 0;
width: 100%;
box-sizing: border-box;
}
.operationHeader {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 8px;
flex-direction: column !important;
padding: 6px 12px;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
@ -101,7 +110,19 @@
transition: background-color 0.2s ease, border-color 0.2s ease;
min-height: 32px;
flex: 1;
width: 100%;
min-width: 0;
overflow: hidden;
margin-top: 4px;
box-sizing: border-box;
}
.operationHeaderRow {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
gap: 4px;
min-width: 0;
box-sizing: border-box;
}
.expandButton {
@ -181,6 +202,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.statusMessageTag {
@ -246,7 +268,7 @@
background-color: var(--color-highlight-gray);
border-radius: 2px;
overflow: hidden;
margin-top: 4px;
width: 100%;
}
.progressBar {
@ -268,37 +290,41 @@
/* Log messages container */
.operationLogsContainer {
margin-top: 8px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
box-sizing: border-box;
}
.operationLogsList {
display: flex;
flex-direction: column;
gap: 6px;
padding-left: 0;
border-left: 1px solid var(--color-border);
margin-left: 12px;
margin-left: 30px; /* Align with header content: operationNode paddingLeft (12px) + header padding (12px) */
min-width: 0;
box-sizing: border-box;
}
.logEntry {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 8px;
background-color: var(--color-surface);
border-radius: var(--object-radius-small);
border: 1px solid var(--color-border);
min-width: 0;
box-sizing: border-box;
margin-top: 4px;
}
.logEntryHeader {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
flex-wrap: wrap;
min-width: 0;
box-sizing: border-box;
}
.logTimestamp {
@ -313,8 +339,11 @@
color: var(--color-text);
line-height: 1.4;
word-wrap: break-word;
word-break: break-word;
overflow-wrap: break-word;
flex: 1;
min-width: 0;
box-sizing: border-box;
}
.logProgress {

View file

@ -94,7 +94,6 @@ const Log: React.FC<LogProps> = ({
const operationTimestamp = latestLog?.timestamp;
// Calculate consistent indentation per level (24px per level)
const indentPx = depth * 24;
const hasIndent = depth > 0;
// Calculate log entry indentation to align with operation name
@ -107,13 +106,24 @@ const Log: React.FC<LogProps> = ({
// Root: 28px - 0 = 28px
// Indented: 28px - 12px = 16px
const logIndentPx = hasIndent ? 16 : 28;
// Calculate header indentation to match message indentation
// Headers are inside operationNode which has paddingLeft: 12px (for indented)
// Messages container has marginLeft: logIndentPx from operationNode, and list has margin-left: 12px
// So messages start at: logIndentPx + 12px from operationNode's left edge
// Headers start at: operationNode marginLeft + paddingLeft = headerIndentPx + 12px
// To align: headerIndentPx + 12px = logIndentPx + 12px, so headerIndentPx = logIndentPx
// But headers have their own padding (12px), so header content starts at headerIndentPx + 12px + 12px
// Messages start at logIndentPx + 12px, so we need headerIndentPx = logIndentPx - 12px to align content
// Actually, we want the header box to align with messages, so headerIndentPx should account for header padding
const headerIndentPx = logIndentPx; // Headers and messages both use logIndentPx, paddingLeft handles alignment
return (
<div
key={operationId}
className={`${styles.operationNode} ${hasIndent ? styles.operationNodeIndented : ''}`}
style={{
marginLeft: hasIndent ? `${indentPx}px` : '0',
marginLeft: `${headerIndentPx}px`,
paddingLeft: hasIndent ? '12px' : '0',
position: 'relative'
}}
@ -122,53 +132,55 @@ const Log: React.FC<LogProps> = ({
{/* Operation content */}
<div className={styles.operationContent}>
<div className={styles.operationHeader}>
{hasContentToExpand && (
<button
className={styles.expandButton}
onClick={() => onToggleOperationExpanded?.(operationId)}
aria-label={operation.expanded ? 'Collapse' : 'Expand'}
>
<span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}>
<div className={styles.operationHeaderRow}>
{hasContentToExpand && (
<button
className={styles.expandButton}
onClick={() => onToggleOperationExpanded?.(operationId)}
aria-label={operation.expanded ? 'Collapse' : 'Expand'}
>
<span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}>
</span>
</button>
)}
{!hasContentToExpand && <span className={styles.expandButtonSpacer} />}
<span className={styles.operationName}>{operationName}</span>
{/* Latest status message tag (updates with each poll) */}
{latestMessage && (
<span className={styles.statusMessageTag}>
{latestMessage}
</span>
</button>
)}
{!hasContentToExpand && <span className={styles.expandButtonSpacer} />}
<span className={styles.operationName}>{operationName}</span>
{/* Latest status message tag (updates with each poll) */}
{latestMessage && (
<span className={styles.statusMessageTag}>
{latestMessage}
)}
{operationTimestamp && (
<span className={styles.operationTimestamp}>
{formatLogTimestamp(operationTimestamp)}
</span>
)}
<span className={getStatusBadgeClass(operationStatus)}>
{operationStatus}
</span>
)}
{operationTimestamp && (
<span className={styles.operationTimestamp}>
{formatLogTimestamp(operationTimestamp)}
</span>
)}
<span className={getStatusBadgeClass(operationStatus)}>
{operationStatus}
</span>
{progressPercentage > 0 && (
<span className={styles.progressPercentage}>
{Math.round(progressPercentage)}%
</span>
)}
</div>
{progressPercentage > 0 && (
<span className={styles.progressPercentage}>
{Math.round(progressPercentage)}%
</span>
<div className={styles.progressBarContainer}>
<div
className={`${styles.progressBar} ${progressPercentage >= 100 ? styles.progressCompleted : ''}`}
style={{ width: `${progressPercentage}%` }}
/>
</div>
)}
</div>
{progressPercentage > 0 && (
<div className={styles.progressBarContainer}>
<div
className={`${styles.progressBar} ${progressPercentage >= 100 ? styles.progressCompleted : ''}`}
style={{ width: `${progressPercentage}%` }}
/>
</div>
)}
</div>
</div>
@ -180,7 +192,10 @@ const Log: React.FC<LogProps> = ({
<div
className={styles.operationLogsContainer}
style={{
marginLeft: `${logIndentPx}px` // Align with operation name consistently at all levels
// Messages should align with header content (not header box edge)
// Header content starts at: operationNode paddingLeft (12px) + header padding (12px) = 24px from operationNode left
// Messages should start at the same position: 0 marginLeft (since operationNode already has paddingLeft)
marginLeft: '0px'
}}
>
<div className={styles.operationLogsList}>

View file

@ -71,13 +71,23 @@ export function useDashboardLogTree() {
// Log messages are status updates and should go in latestMessage, not operationName
let operationName = existingOperation?.operationName || null;
if (operationName === null) {
// Format operationId by splitting on dashes/underscores and capitalizing
// Remove UUIDs and timestamps from operationId before formatting
// UUID pattern: 8-4-4-4-12 hex digits (e.g., "1e6d7b14-4f30-40e2-b7a6-748b63b6a7f5")
// Also remove standalone long hex strings that might be timestamps or IDs
let cleanedId = operationId
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '') // Remove UUIDs
.replace(/\b[0-9a-f]{32,}\b/gi, '') // Remove long hex strings (timestamps/IDs)
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
// Format by splitting on dashes/underscores and capitalizing
// This creates a stable, readable name like "Workflow Planning" from "workflow-planning"
const formattedName = operationId
.split(/[-_]/)
const formattedName = cleanedId
.split(/[-_\s]+/)
.filter(word => word.length > 0) // Remove empty strings
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
operationName = formattedName || operationId;
operationName = formattedName || operationId.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '').trim();
}
// Update latest message (for status tag) - this updates with each poll
@ -120,7 +130,25 @@ export function useDashboardLogTree() {
rootOpsSet.add(opId);
}
});
newTree.rootOperations = Array.from(rootOpsSet).sort(); // Alphabetical sort
// Sort by timestamp of earliest log entry (chronological order)
newTree.rootOperations = Array.from(rootOpsSet).sort((opIdA, opIdB) => {
const opA = newTree.operations.get(opIdA);
const opB = newTree.operations.get(opIdB);
if (!opA || !opB) return 0;
// Get earliest log timestamp for each operation
const logsA = Array.from(opA.logs.values());
const logsB = Array.from(opB.logs.values());
if (logsA.length === 0 && logsB.length === 0) return 0;
if (logsA.length === 0) return 1; // Put operations without logs at the end
if (logsB.length === 0) return -1;
const earliestA = Math.min(...logsA.map(log => log.timestamp || 0));
const earliestB = Math.min(...logsB.map(log => log.timestamp || 0));
return earliestA - earliestB; // Ascending order (oldest first)
});
return newTree;
});
@ -179,10 +207,24 @@ export function useDashboardLogTree() {
const getChildOperations = useCallback((parentId: string | null): string[] => {
const currentTree = treeRef.current;
return Array.from(currentTree.operations.entries())
const childOps = Array.from(currentTree.operations.entries())
.filter(([_, op]) => op.parentId === parentId)
.map(([opId]) => opId)
.sort(); // Alphabetical sort
.map(([opId, op]) => ({ opId, op }));
// Sort by timestamp of earliest log entry (chronological order)
return childOps.sort((a, b) => {
const logsA = Array.from(a.op.logs.values());
const logsB = Array.from(b.op.logs.values());
if (logsA.length === 0 && logsB.length === 0) return 0;
if (logsA.length === 0) return 1; // Put operations without logs at the end
if (logsB.length === 0) return -1;
const earliestA = Math.min(...logsA.map(log => log.timestamp || 0));
const earliestB = Math.min(...logsB.map(log => log.timestamp || 0));
return earliestA - earliestB; // Ascending order (oldest first)
}).map(({ opId }) => opId);
}, []);
return {