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;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue