diff --git a/DebugProbe.AspNetCore/Resources/css/debugprobe.css b/DebugProbe.AspNetCore/Resources/css/debugprobe.css index 92db731..b777aed 100644 --- a/DebugProbe.AspNetCore/Resources/css/debugprobe.css +++ b/DebugProbe.AspNetCore/Resources/css/debugprobe.css @@ -178,6 +178,16 @@ pre { text-align: left; } +.json-error { + font-size: 12px; + padding: 4px 8px; + margin-bottom: 4px; + border-radius: 6px; + background: rgba(231, 76, 60, 0.12); + color: #ff8a8a; + border: 1px solid rgba(231, 76, 60, 0.25); +} + /* ========================= Diff ========================= */ @@ -203,6 +213,24 @@ pre { border-left-color: #f1c40f; } +.diff-line-invalid { + background: rgba(231, 76, 60, 0.18); + border-left: 3px solid #e74c3c; +} + +.diff-badge { + min-width: 22px; + height: 22px; + border-radius: 999px; + background: #e74c3c; + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: bold; +} + /* ========================= Buttons ========================= */ @@ -302,15 +330,11 @@ pre { } .accordion-header { - /*min-height: 56px;*/ display: flex; justify-content: space-between; align-items: center; - padding: 14px 10px; cursor: pointer; - user-select: none; - background: #fafafa; - border-bottom: 1px solid #eee; + padding: 18px; } .accordion-header:hover { @@ -323,12 +347,9 @@ pre { } .accordion-meta { - font-size: 22px; - color: #777; - font-weight: 300; - width: 24px; - text-align: center; - flex-shrink: 0; + display: flex; + align-items: center; + gap: 10px; } .accordion-body { diff --git a/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js b/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js index f0d1d0e..4b74461 100644 --- a/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js +++ b/DebugProbe.AspNetCore/Resources/js/debugprobe-compare.js @@ -31,29 +31,53 @@ function setCompareResult(html) { } function renderCompare(result) { + + const environmentRows = [ + { field: 'Environment', local: result.environment?.local, remote: result.environment?.remote }, + { field: 'Culture', local: result.culture?.local, remote: result.culture?.remote } + ]; + var environmentRowsChangedCount = getChangedCount(environmentRows); + + const overviewRows = [ + { field: 'Method', local: result.method?.local, remote: result.method?.remote }, + { field: 'Path', local: result.path?.local, remote: result.path?.remote }, + { field: 'Status', local: result.status?.local, remote: result.status?.remote }, + { field: 'Request Time', local: result.requestTime?.local, remote: result.requestTime?.remote } + ]; + var overviewRowsChangedCount = getChangedCount(overviewRows); + + const localRequestBodyJson = result.requestBody?.local || '' + const remoteRequestBodyJson = result.requestBody?.remote || '' + const requestComparison = compareJsonBodies(localRequestBodyJson, remoteRequestBodyJson); + + const localResponseBodyJson = result.responseBody?.local || '' + const remoteResponseBodyJson = result.responseBody?.remote || '' + const responseComparison = compareJsonBodies(localResponseBodyJson, remoteResponseBodyJson); + return [ renderAccordionSection( 'Environment', - renderSectionRows([ - { field: 'Environment', local: result.environment?.local, remote: result.environment?.remote }, - { field: 'Culture', local: result.culture?.local, remote: result.culture?.remote } - ]) + renderSectionRows(environmentRows), + environmentRowsChangedCount > 0 ? true : false, + environmentRowsChangedCount ), - renderAccordionSection( 'Overview', - renderSectionRows([ - { field: 'Method', local: result.method?.local, remote: result.method?.remote }, - { field: 'Path', local: result.path?.local, remote: result.path?.remote }, - { field: 'Status', local: result.status?.local, remote: result.status?.remote }, - { field: 'Request Time', local: result.requestTime?.local, remote: result.requestTime?.remote } - ]), - true // expanded by default + renderSectionRows(overviewRows), + overviewRowsChangedCount > 0 ? true : false, + overviewRowsChangedCount ), - - renderAccordionSection('Request', renderSideBySideJson(result.requestBody)), - - renderAccordionSection('Response', renderSideBySideJson(result.responseBody) + renderAccordionSection( + 'Request', + renderSideBySideJson(requestComparison, localRequestBodyJson, remoteRequestBodyJson), + requestComparison.changes > 0 ? true : false, + requestComparison.changes + ), + renderAccordionSection( + 'Response', + renderSideBySideJson(responseComparison, localResponseBodyJson, remoteResponseBodyJson), + responseComparison.changes > 0 ? true : false, + responseComparison.changes ) ].join(''); } @@ -82,31 +106,34 @@ function renderSectionRows(rows) { `; } -function renderSideBySideJson(data) { - const localJson = data?.local || ''; - const remoteJson = data?.remote || ''; - const comparison = compareJsonBodies(localJson, remoteJson); +function renderSideBySideJson(comparison, localJson, remoteJson) { return `
Local + ${comparison.localError ? `
${comparison.localError}
` : '' } ${renderAlignedJson(comparison.local, localJson)}
+
Remote + ${comparison.remoteError ? `
${comparison.remoteError}
` : '' } ${renderAlignedJson(comparison.remote, remoteJson)}
`; } -function renderAccordionSection(title, content, expanded = false) { +function renderAccordionSection(title, content, expanded = false, changes = 0) { return `
${escapeHtml(title)}
-
- ${expanded ? '-' : '+'} + ${changes > 0 ? `${changes}` : '' } + + + ${expanded ? '-' : '+'} +
@@ -119,20 +146,43 @@ function renderAccordionSection(title, content, expanded = false) { function toggleAccordion(header) { const body = header.nextElementSibling; - const meta = header.querySelector('.accordion-meta'); + const toggle = header.querySelector('.accordion-toggle'); body.classList.toggle('open'); - meta.textContent = + toggle.textContent = body.classList.contains('open') ? '-' : '+'; } +function getChangedCount(rows) { + return rows.filter(x => x.local !== x.remote).length; +} + function compareJsonBodies(localJson, remoteJson) { const local = parseJson(localJson); const remote = parseJson(remoteJson); - if (!local.ok && !remote.ok) { - return emptyComparison(); + if (!local.ok || !remote.ok) { + return { + local: (local.raw || '(empty)') + .split('\n') + .map(x => ({ + text: x, + state: !local.ok ? 'invalid' : '' + })), + + remote: (remote.raw || '(empty)') + .split('\n') + .map(x => ({ + text: x, + state: !remote.ok ? 'invalid' : '' + })), + + localError: !local.ok ? 'Failed To Parse JSON' : null, + remoteError: !remote.ok ? 'Failed To Parse JSON' : null, + + changes: 1 + }; } const rows = createRows(); @@ -142,14 +192,23 @@ function compareJsonBodies(localJson, remoteJson) { } function parseJson(json) { - if (!json || json.trim() === '' || json === '{}') { - return { ok: false, value: null }; + if (!json || json.trim() === '') { + return { ok: false, value: null, raw: '' }; } try { - return { ok: true, value: JSON.parse(json) }; - } catch { - return { ok: false, value: null }; + return { + ok: true, + value: JSON.parse(json), + raw: json + }; + } catch (error) { + return { + ok: false, + value: null, + raw: json, + error: error.message + }; } } @@ -161,7 +220,7 @@ function emptyComparison() { } function createRows() { - return { local: [], remote: [] }; + return { local: [], remote: [], changes: 0 }; } function pushPair(rows, localText, remoteText, state = '') { @@ -170,6 +229,8 @@ function pushPair(rows, localText, remoteText, state = '') { } function pushPresence(rows, value, side, depth, trailingComma) { + rows.changes++; + const isLocal = side === 'local'; const lines = stringifyLines(value, depth, trailingComma); @@ -200,7 +261,13 @@ function appendValue(rows, localValue, remoteValue, hasLocal, hasRemote, depth, return; } - const state = jsonEquals(localValue, remoteValue) ? '' : 'changed'; + const changed = !jsonEquals(localValue, remoteValue); + + if (changed) { + rows.changes++; + } + + const state = changed ? 'changed' : ''; pushPair(rows, primitiveLine(localValue, depth, trailingComma), primitiveLine(remoteValue, depth, trailingComma), state); } @@ -246,7 +313,13 @@ function appendProperty(rows, key, localObject, remoteObject, depth, trailingCom return; } - const state = jsonEquals(localValue, remoteValue) ? '' : 'changed'; + const changed = !jsonEquals(localValue, remoteValue); + + if (changed) { + rows.changes++; + } + + const state = changed ? 'changed' : ''; pushPair(rows, propertyLine(key, localValue, depth, trailingComma), propertyLine(key, remoteValue, depth, trailingComma), state); } @@ -398,6 +471,8 @@ function pushNamedPresence(rows, key, value, side, depth, trailingComma) { } function appendChangedBlocks(rows, localValue, remoteValue, depth, trailingComma) { + rows.changes++; + const localLines = stringifyLines(localValue, depth, trailingComma); const remoteLines = stringifyLines(remoteValue, depth, trailingComma); const count = Math.max(localLines.length, remoteLines.length); @@ -409,6 +484,8 @@ function appendChangedBlocks(rows, localValue, remoteValue, depth, trailingComma } function appendChangedNamedBlocks(rows, key, localValue, remoteValue, depth, trailingComma) { + rows.changes++; + const localLines = stringifyLines(localValue, depth, trailingComma, `"${key}": `); const remoteLines = stringifyLines(remoteValue, depth, trailingComma, `"${key}": `); const count = Math.max(localLines.length, remoteLines.length);