fix: fixed styling of log messages
This commit is contained in:
parent
641930b1d0
commit
401c0885a1
3 changed files with 157 additions and 71 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -107,13 +106,24 @@ const Log: React.FC<LogProps> = ({
|
||||||
// Root: 28px - 0 = 28px
|
// Root: 28px - 0 = 28px
|
||||||
// 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} />}
|
{operationTimestamp && (
|
||||||
|
<span className={styles.operationTimestamp}>
|
||||||
<span className={styles.operationName}>{operationName}</span>
|
{formatLogTimestamp(operationTimestamp)}
|
||||||
|
</span>
|
||||||
{/* Latest status message tag (updates with each poll) */}
|
)}
|
||||||
{latestMessage && (
|
|
||||||
<span className={styles.statusMessageTag}>
|
<span className={getStatusBadgeClass(operationStatus)}>
|
||||||
{latestMessage}
|
{operationStatus}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
|
{progressPercentage > 0 && (
|
||||||
{operationTimestamp && (
|
<span className={styles.progressPercentage}>
|
||||||
<span className={styles.operationTimestamp}>
|
{Math.round(progressPercentage)}%
|
||||||
{formatLogTimestamp(operationTimestamp)}
|
</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}>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue