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

View file

@ -94,7 +94,6 @@ const Log: React.FC<LogProps> = ({
const operationTimestamp = latestLog?.timestamp; const operationTimestamp = latestLog?.timestamp;
// Calculate consistent indentation per level (24px per level) // Calculate consistent indentation per level (24px per level)
const indentPx = depth * 24;
const hasIndent = depth > 0; const hasIndent = depth > 0;
// Calculate log entry indentation to align with operation name // Calculate log entry indentation to align with operation name
@ -108,12 +107,23 @@ const Log: React.FC<LogProps> = ({
// Indented: 28px - 12px = 16px // Indented: 28px - 12px = 16px
const logIndentPx = hasIndent ? 16 : 28; 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 ( return (
<div <div
key={operationId} key={operationId}
className={`${styles.operationNode} ${hasIndent ? styles.operationNodeIndented : ''}`} className={`${styles.operationNode} ${hasIndent ? styles.operationNodeIndented : ''}`}
style={{ style={{
marginLeft: hasIndent ? `${indentPx}px` : '0', marginLeft: `${headerIndentPx}px`,
paddingLeft: hasIndent ? '12px' : '0', paddingLeft: hasIndent ? '12px' : '0',
position: 'relative' position: 'relative'
}} }}
@ -122,53 +132,55 @@ const Log: React.FC<LogProps> = ({
{/* Operation content */} {/* Operation content */}
<div className={styles.operationContent}> <div className={styles.operationContent}>
<div className={styles.operationHeader}> <div className={styles.operationHeader}>
{hasContentToExpand && ( <div className={styles.operationHeaderRow}>
<button {hasContentToExpand && (
className={styles.expandButton} <button
onClick={() => onToggleOperationExpanded?.(operationId)} className={styles.expandButton}
aria-label={operation.expanded ? 'Collapse' : 'Expand'} onClick={() => onToggleOperationExpanded?.(operationId)}
> aria-label={operation.expanded ? 'Collapse' : 'Expand'}
<span className={`${styles.collapseIcon} ${operation.expanded ? '' : styles.collapsed}`}> >
<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> </span>
</button> )}
)}
{!hasContentToExpand && <span className={styles.expandButtonSpacer} />}
<span className={styles.operationName}>{operationName}</span> {operationTimestamp && (
<span className={styles.operationTimestamp}>
{formatLogTimestamp(operationTimestamp)}
</span>
)}
{/* Latest status message tag (updates with each poll) */} <span className={getStatusBadgeClass(operationStatus)}>
{latestMessage && ( {operationStatus}
<span className={styles.statusMessageTag}>
{latestMessage}
</span> </span>
)}
{operationTimestamp && ( {progressPercentage > 0 && (
<span className={styles.operationTimestamp}> <span className={styles.progressPercentage}>
{formatLogTimestamp(operationTimestamp)} {Math.round(progressPercentage)}%
</span> </span>
)} )}
</div>
<span className={getStatusBadgeClass(operationStatus)}>
{operationStatus}
</span>
{progressPercentage > 0 && ( {progressPercentage > 0 && (
<span className={styles.progressPercentage}> <div className={styles.progressBarContainer}>
{Math.round(progressPercentage)}% <div
</span> className={`${styles.progressBar} ${progressPercentage >= 100 ? styles.progressCompleted : ''}`}
style={{ width: `${progressPercentage}%` }}
/>
</div>
)} )}
</div> </div>
{progressPercentage > 0 && (
<div className={styles.progressBarContainer}>
<div
className={`${styles.progressBar} ${progressPercentage >= 100 ? styles.progressCompleted : ''}`}
style={{ width: `${progressPercentage}%` }}
/>
</div>
)}
</div> </div>
</div> </div>
@ -180,7 +192,10 @@ const Log: React.FC<LogProps> = ({
<div <div
className={styles.operationLogsContainer} className={styles.operationLogsContainer}
style={{ 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}> <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 // Log messages are status updates and should go in latestMessage, not operationName
let operationName = existingOperation?.operationName || null; let operationName = existingOperation?.operationName || null;
if (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" // This creates a stable, readable name like "Workflow Planning" from "workflow-planning"
const formattedName = operationId const formattedName = cleanedId
.split(/[-_]/) .split(/[-_\s]+/)
.filter(word => word.length > 0) // Remove empty strings
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' '); .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 // Update latest message (for status tag) - this updates with each poll
@ -120,7 +130,25 @@ export function useDashboardLogTree() {
rootOpsSet.add(opId); 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; return newTree;
}); });
@ -179,10 +207,24 @@ export function useDashboardLogTree() {
const getChildOperations = useCallback((parentId: string | null): string[] => { const getChildOperations = useCallback((parentId: string | null): string[] => {
const currentTree = treeRef.current; const currentTree = treeRef.current;
return Array.from(currentTree.operations.entries()) const childOps = Array.from(currentTree.operations.entries())
.filter(([_, op]) => op.parentId === parentId) .filter(([_, op]) => op.parentId === parentId)
.map(([opId]) => opId) .map(([opId, op]) => ({ opId, op }));
.sort(); // Alphabetical sort
// 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 { return {