mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 12:53:17 +08:00
Redesign compare board for many sheets: sidebar nav + strip + table.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ab4cb6e27d
commit
33264d7b76
4 changed files with 565 additions and 471 deletions
|
|
@ -337,6 +337,9 @@ const en = {
|
||||||
matchedRows: "Matched",
|
matchedRows: "Matched",
|
||||||
totalRows: "Total rows",
|
totalRows: "Total rows",
|
||||||
sheetCount: "Sheets",
|
sheetCount: "Sheets",
|
||||||
|
sheetNavTitle: "Items",
|
||||||
|
sheetNavHint: "{{n}} items · click to switch",
|
||||||
|
sheetFailCount: "{{n}} with diffs",
|
||||||
topChangedFields: "Top changed fields",
|
topChangedFields: "Top changed fields",
|
||||||
exportTables: "Download tables",
|
exportTables: "Download tables",
|
||||||
exportOk: "Compare tables exported",
|
exportOk: "Compare tables exported",
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,9 @@ const zh = {
|
||||||
matchedRows: "对齐条目",
|
matchedRows: "对齐条目",
|
||||||
totalRows: "合计条目",
|
totalRows: "合计条目",
|
||||||
sheetCount: "监控表",
|
sheetCount: "监控表",
|
||||||
|
sheetNavTitle: "对比项",
|
||||||
|
sheetNavHint: "共 {{n}} 项 · 点击切换",
|
||||||
|
sheetFailCount: "{{n}} 项有差异",
|
||||||
topChangedFields: "变更字段 Top",
|
topChangedFields: "变更字段 Top",
|
||||||
exportTables: "下载表格",
|
exportTables: "下载表格",
|
||||||
exportOk: "已导出比对表格",
|
exportOk: "已导出比对表格",
|
||||||
|
|
|
||||||
|
|
@ -9957,10 +9957,14 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.bs-sheet-table {
|
.bs-sheet-table {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 280px;
|
min-height: 240px;
|
||||||
max-height: min(48vh, 520px);
|
max-height: min(48vh, 520px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bs-cmp-result-table.bs-sheet-table {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-sheet-tabs {
|
.bs-sheet-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -10044,15 +10048,14 @@ html.login-page--paused .login-page__flare {
|
||||||
--bs-cmp-muted: #94a3b8;
|
--bs-cmp-muted: #94a3b8;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: min(62vh, 720px);
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
color: var(--bs-cmp-ink);
|
color: var(--bs-cmp-ink);
|
||||||
background:
|
background:
|
||||||
radial-gradient(1200px 420px at 10% -10%, rgba(37, 99, 235, 0.12), transparent 55%),
|
radial-gradient(1200px 420px at 10% -10%, rgba(37, 99, 235, 0.1), transparent 55%),
|
||||||
radial-gradient(900px 360px at 90% 0%, rgba(16, 185, 129, 0.08), transparent 50%),
|
|
||||||
rgba(8, 13, 24, 0.35);
|
rgba(8, 13, 24, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10061,9 +10064,11 @@ html.login-page--paused .login-page__flare {
|
||||||
--bs-cmp-surface: rgba(15, 23, 42, 0.88);
|
--bs-cmp-surface: rgba(15, 23, 42, 0.88);
|
||||||
background: #0b1220;
|
background: #0b1220;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
padding: 16px 18px;
|
padding: 12px 14px;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
min-height: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-board__toolbar {
|
.bs-cmp-board__toolbar {
|
||||||
|
|
@ -10072,179 +10077,282 @@ html.login-page--paused .login-page__flare {
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-board__hero {
|
.bs-cmp-board__body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(160px, 220px) 1fr;
|
grid-template-columns: minmax(176px, 220px) minmax(0, 1fr);
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-hero-pass {
|
.bs-cmp-nav {
|
||||||
border-radius: 14px;
|
|
||||||
border: 1px solid var(--bs-cmp-border);
|
|
||||||
padding: 14px 16px;
|
|
||||||
background: var(--bs-cmp-surface);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
min-height: 0;
|
||||||
|
border: 1px solid var(--bs-cmp-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bs-cmp-surface);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-hero-pass.is-ok {
|
.bs-cmp-nav__head {
|
||||||
border-color: rgba(34, 197, 94, 0.45);
|
padding: 10px 12px 8px;
|
||||||
background: rgba(22, 101, 52, 0.18);
|
border-bottom: 1px solid var(--bs-cmp-border);
|
||||||
box-shadow: inset 0 0 0 1px rgba(34, 197, 94, 0.12);
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-hero-pass.is-warn {
|
.bs-cmp-nav__title {
|
||||||
border-color: rgba(245, 158, 11, 0.5);
|
|
||||||
background: rgba(120, 53, 15, 0.2);
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-hero-pass__label {
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--bs-cmp-muted);
|
font-weight: 700;
|
||||||
}
|
letter-spacing: 0.02em;
|
||||||
|
|
||||||
.bs-cmp-hero-pass__value {
|
|
||||||
font-size: 42px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.05;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-hero-pass.is-ok .bs-cmp-hero-pass__value {
|
.bs-cmp-nav__hint {
|
||||||
color: #86efac;
|
margin-top: 2px;
|
||||||
}
|
font-size: 11px;
|
||||||
|
|
||||||
.bs-cmp-hero-pass.is-warn .bs-cmp-hero-pass__value {
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-hero-pass__unit {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-left: 2px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-hero-pass__sub {
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--bs-cmp-muted);
|
color: var(--bs-cmp-muted);
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-hero-metrics {
|
.bs-cmp-nav__list {
|
||||||
display: grid;
|
flex: 1 1 auto;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
min-height: 0;
|
||||||
gap: 8px;
|
overflow: auto;
|
||||||
}
|
padding: 6px;
|
||||||
|
|
||||||
.bs-cmp-hero-metric {
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--bs-cmp-border);
|
|
||||||
background: var(--bs-cmp-surface);
|
|
||||||
padding: 10px 12px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-hero-metric__label {
|
.bs-cmp-nav__item {
|
||||||
font-size: 11px;
|
|
||||||
color: var(--bs-cmp-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-hero-metric__value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
line-height: 1.15;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-hero-metric__sub {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--bs-cmp-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-sheet-cards {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-sheet-card {
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 8px minmax(0, 1fr) auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border: 1px solid var(--bs-cmp-border);
|
border: 1px solid transparent;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
background: var(--bs-cmp-surface);
|
background: transparent;
|
||||||
color: var(--bs-cmp-ink);
|
color: var(--bs-cmp-ink);
|
||||||
padding: 10px 12px;
|
padding: 7px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card:hover {
|
.bs-cmp-nav__item:hover {
|
||||||
background: rgba(30, 41, 59, 0.85);
|
background: rgba(30, 41, 59, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card.is-active {
|
.bs-cmp-nav__item.is-active {
|
||||||
border-color: rgba(59, 130, 246, 0.65);
|
background: rgba(37, 99, 235, 0.18);
|
||||||
background: rgba(37, 99, 235, 0.16);
|
border-color: rgba(59, 130, 246, 0.45);
|
||||||
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card.has-diff {
|
.bs-cmp-nav__dot {
|
||||||
border-color: rgba(245, 158, 11, 0.45);
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #64748b;
|
||||||
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card.is-clean {
|
.bs-cmp-nav__item.is-clean .bs-cmp-nav__dot {
|
||||||
border-color: rgba(34, 197, 94, 0.35);
|
background: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card__title {
|
.bs-cmp-nav__item.has-diff .bs-cmp-nav__dot {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav__name {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
overflow: hidden;
|
||||||
word-break: break-all;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card__pass {
|
.bs-cmp-nav__meta {
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 800;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
line-height: 1.1;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-sheet-card.is-clean .bs-cmp-sheet-card__pass {
|
|
||||||
color: #86efac;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-sheet-card.has-diff .bs-cmp-sheet-card__pass {
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-sheet-card__stats {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
margin-top: 6px;
|
align-items: flex-end;
|
||||||
font-size: 11px;
|
gap: 1px;
|
||||||
|
font-size: 10px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--bs-cmp-muted);
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav__meta em {
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-sheet-card__meta {
|
.bs-cmp-nav__item.is-clean .bs-cmp-nav__meta em {
|
||||||
margin-top: 4px;
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav__item.has-diff .bs-cmp-nav__meta em {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 14px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--bs-cmp-border);
|
||||||
|
background: var(--bs-cmp-surface);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip.is-ok {
|
||||||
|
border-color: rgba(34, 197, 94, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip.is-warn {
|
||||||
|
border-color: rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__pass {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__pass-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--bs-cmp-muted);
|
color: var(--bs-cmp-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__pass-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip.is-ok .bs-cmp-strip__pass-value {
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip.is-warn .bs-cmp-strip__pass-value {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__pass-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--bs-cmp-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__stats b {
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kinds {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(15, 23, 42, 0.55);
|
||||||
|
color: #cbd5e1;
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind b {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind:hover {
|
||||||
|
background: rgba(30, 41, 59, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind.is-active {
|
||||||
|
border-color: rgba(59, 130, 246, 0.55);
|
||||||
|
background: rgba(37, 99, 235, 0.2);
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind--added b {
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind--removed b {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind--changed b {
|
||||||
|
color: #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-strip__kind--unchanged b {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-sheet-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-sheet-bar__title {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-sheet-bar__title strong {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-cmp-top-fields {
|
.bs-cmp-top-fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
@ -10252,20 +10360,24 @@ html.login-page--paused .login-page__flare {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bs-cmp-top-fields--inline {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-cmp-top-fields__label {
|
.bs-cmp-top-fields__label {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--bs-cmp-muted);
|
color: var(--bs-cmp-muted);
|
||||||
margin-right: 4px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-top-fields__chip {
|
.bs-cmp-top-fields__chip {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
padding: 3px 8px;
|
padding: 2px 7px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid rgba(245, 158, 11, 0.35);
|
border: 1px solid rgba(245, 158, 11, 0.35);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
background: rgba(120, 53, 15, 0.28);
|
background: rgba(120, 53, 15, 0.28);
|
||||||
color: #fde68a;
|
color: #fde68a;
|
||||||
}
|
}
|
||||||
|
|
@ -10281,71 +10393,12 @@ html.login-page--paused .login-page__flare {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-kpis {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi {
|
|
||||||
appearance: none;
|
|
||||||
border: 1px solid var(--bs-cmp-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--bs-cmp-surface);
|
|
||||||
padding: 10px 12px;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--bs-cmp-ink);
|
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi:hover {
|
|
||||||
background: rgba(30, 41, 59, 0.85);
|
|
||||||
border-color: rgba(59, 130, 246, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi.is-active {
|
|
||||||
border-color: rgba(59, 130, 246, 0.65);
|
|
||||||
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.25);
|
|
||||||
background: rgba(37, 99, 235, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi__label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--bs-cmp-muted);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi__value {
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
line-height: 1.1;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi--added .bs-cmp-kpi__value {
|
|
||||||
color: #86efac;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi--removed .bs-cmp-kpi__value {
|
|
||||||
color: #fca5a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi--changed .bs-cmp-kpi__value {
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-kpi--unchanged .bs-cmp-kpi__value {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bs-cmp-filter-bar {
|
.bs-cmp-filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bs-cmp-kind-pills {
|
.bs-cmp-kind-pills {
|
||||||
|
|
@ -10380,8 +10433,8 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.bs-cmp-result-table {
|
.bs-cmp-result-table {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 280px;
|
min-height: 240px;
|
||||||
max-height: min(42vh, 480px);
|
max-height: none;
|
||||||
border-color: var(--bs-cmp-border);
|
border-color: var(--bs-cmp-border);
|
||||||
background: rgba(8, 13, 24, 0.35);
|
background: rgba(8, 13, 24, 0.35);
|
||||||
}
|
}
|
||||||
|
|
@ -10389,10 +10442,37 @@ html.login-page--paused .login-page__flare {
|
||||||
.bs-cmp-board.is-fullscreen .bs-cmp-result-table,
|
.bs-cmp-board.is-fullscreen .bs-cmp-result-table,
|
||||||
.bs-cmp-board:fullscreen .bs-cmp-result-table {
|
.bs-cmp-board:fullscreen .bs-cmp-result-table {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
min-height: 40vh;
|
min-height: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.bs-cmp-board__body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav {
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav__list {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav__item {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 148px;
|
||||||
|
grid-template-columns: 8px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bs-cmp-nav__meta {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bs-cmp-diff-table {
|
.bs-cmp-diff-table {
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
|
|
|
||||||
|
|
@ -358,22 +358,33 @@ export function BizComparePage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const summary = runDetail?.summary || {};
|
const summary = runDetail?.summary || {};
|
||||||
const sheetCards = (summary.sheet_cards || []) as Array<{
|
const sheetCards = useMemo(() => {
|
||||||
metric_id: string;
|
const raw = (summary.sheet_cards || []) as Array<{
|
||||||
mode?: string;
|
metric_id: string;
|
||||||
added?: number;
|
mode?: string;
|
||||||
removed?: number;
|
added?: number;
|
||||||
changed?: number;
|
removed?: number;
|
||||||
unchanged?: number;
|
changed?: number;
|
||||||
before_count?: number;
|
unchanged?: number;
|
||||||
after_count?: number;
|
before_count?: number;
|
||||||
diff_count?: number;
|
after_count?: number;
|
||||||
pass_rate?: number;
|
diff_count?: number;
|
||||||
}>;
|
pass_rate?: number;
|
||||||
|
}>;
|
||||||
|
// Diffs first so ops can scan quickly when many sheets
|
||||||
|
return [...raw].sort((a, b) => {
|
||||||
|
const da = Number(a.diff_count || 0);
|
||||||
|
const db = Number(b.diff_count || 0);
|
||||||
|
if (da !== db) return db - da;
|
||||||
|
return String(a.metric_id).localeCompare(String(b.metric_id));
|
||||||
|
});
|
||||||
|
}, [summary.sheet_cards]);
|
||||||
|
const sheetFailCount = sheetCards.filter((c) => Number(c.diff_count || 0) > 0).length;
|
||||||
const topChangedFields = (summary.top_changed_fields || []) as Array<{
|
const topChangedFields = (summary.top_changed_fields || []) as Array<{
|
||||||
field: string;
|
field: string;
|
||||||
count: number;
|
count: number;
|
||||||
}>;
|
}>;
|
||||||
|
const activeSheetCard = sheetCards.find((c) => c.metric_id === resultSheetId) || sheetCards[0];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const syncFs = () => {
|
const syncFs = () => {
|
||||||
|
|
@ -1309,276 +1320,273 @@ export function BizComparePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{runDetail ? (
|
{runDetail ? (
|
||||||
<>
|
<div className="bs-cmp-board__body">
|
||||||
<div className="bs-cmp-board__hero">
|
<aside className="bs-cmp-nav" aria-label={t("bizCompare.sheetNavTitle")}>
|
||||||
<div
|
<div className="bs-cmp-nav__head">
|
||||||
className={`bs-cmp-hero-pass${summary.ok ? " is-ok" : " is-warn"}`}
|
<div className="bs-cmp-nav__title">{t("bizCompare.sheetNavTitle")}</div>
|
||||||
>
|
<div className="bs-cmp-nav__hint">
|
||||||
<div className="bs-cmp-hero-pass__label">{t("bizCompare.passRate")}</div>
|
{t("bizCompare.sheetNavHint", {
|
||||||
<div className="bs-cmp-hero-pass__value">
|
n: String(sheetCards.length || runSheets.length),
|
||||||
{Number(summary.pass_rate ?? 0).toFixed(1)}
|
})}
|
||||||
<span className="bs-cmp-hero-pass__unit">%</span>
|
{sheetFailCount > 0
|
||||||
</div>
|
? ` · ${t("bizCompare.sheetFailCount", { n: String(sheetFailCount) })}`
|
||||||
<div className="bs-cmp-hero-pass__sub">
|
: ""}
|
||||||
{summary.ok ? t("bizCompare.passOk") : t("bizCompare.passWarn")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bs-cmp-hero-metrics">
|
<div className="bs-cmp-nav__list" role="tablist">
|
||||||
<div className="bs-cmp-hero-metric">
|
{(sheetCards.length
|
||||||
<span className="bs-cmp-hero-metric__label">{t("bizCompare.diffCountLabel")}</span>
|
? sheetCards
|
||||||
<span className="bs-cmp-hero-metric__value">{summary.diff_count ?? 0}</span>
|
: runSheets.map((s) => ({
|
||||||
<span className="bs-cmp-hero-metric__sub">
|
metric_id: s.metric_id,
|
||||||
{t("bizCompare.diffRate")}: {Number(summary.diff_rate ?? 0).toFixed(1)}%
|
mode: s.mode,
|
||||||
</span>
|
added: s.summary?.added,
|
||||||
</div>
|
removed: s.summary?.removed,
|
||||||
<div className="bs-cmp-hero-metric">
|
changed: s.summary?.changed,
|
||||||
<span className="bs-cmp-hero-metric__label">{t("bizCompare.beforeCount")}</span>
|
unchanged: s.summary?.unchanged,
|
||||||
<span className="bs-cmp-hero-metric__value">{summary.before_count ?? 0}</span>
|
before_count: s.summary?.before_count,
|
||||||
<span className="bs-cmp-hero-metric__sub">
|
after_count: s.summary?.after_count,
|
||||||
{t("bizCompare.afterCount")}: {summary.after_count ?? 0}
|
diff_count:
|
||||||
</span>
|
Number(s.summary?.added || 0) +
|
||||||
</div>
|
Number(s.summary?.removed || 0) +
|
||||||
<div className="bs-cmp-hero-metric">
|
Number(s.summary?.changed || 0),
|
||||||
<span className="bs-cmp-hero-metric__label">{t("bizCompare.matchedRows")}</span>
|
pass_rate: s.summary?.pass_rate,
|
||||||
<span className="bs-cmp-hero-metric__value">{summary.matched_rows ?? 0}</span>
|
}))
|
||||||
<span className="bs-cmp-hero-metric__sub">
|
).map((c) => {
|
||||||
{t("bizCompare.totalRows")}: {summary.total_rows ?? 0}
|
const dirty = Number(c.diff_count || 0);
|
||||||
</span>
|
const active = resultSheetId === c.metric_id;
|
||||||
</div>
|
return (
|
||||||
<div className="bs-cmp-hero-metric">
|
<button
|
||||||
<span className="bs-cmp-hero-metric__label">{t("bizCompare.sheetCount")}</span>
|
key={c.metric_id}
|
||||||
<span className="bs-cmp-hero-metric__value">
|
type="button"
|
||||||
{summary.sheet_count ?? runSheets.length}
|
role="tab"
|
||||||
</span>
|
aria-selected={active}
|
||||||
<span className="bs-cmp-hero-metric__sub">
|
className={`bs-cmp-nav__item${active ? " is-active" : ""}${
|
||||||
{fmtTime(runDetail.created_at)}
|
dirty > 0 ? " has-diff" : " is-clean"
|
||||||
</span>
|
}`}
|
||||||
</div>
|
onClick={() => setResultSheetId(c.metric_id)}
|
||||||
|
title={c.metric_id}
|
||||||
|
>
|
||||||
|
<span className="bs-cmp-nav__dot" aria-hidden />
|
||||||
|
<span className="bs-cmp-nav__name">{c.metric_id}</span>
|
||||||
|
<span className="bs-cmp-nav__meta">
|
||||||
|
<em>{Number(c.pass_rate ?? 0).toFixed(0)}%</em>
|
||||||
|
<span>{dirty > 0 ? `Δ${dirty}` : "="}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
|
|
||||||
{sheetCards.length ? (
|
<div className="bs-cmp-main">
|
||||||
<div className="bs-cmp-sheet-cards">
|
<div
|
||||||
{sheetCards.map((c) => (
|
className={`bs-cmp-strip${summary.ok ? " is-ok" : " is-warn"}`}
|
||||||
<button
|
>
|
||||||
key={c.metric_id}
|
<div className="bs-cmp-strip__pass">
|
||||||
type="button"
|
<span className="bs-cmp-strip__pass-label">{t("bizCompare.passRate")}</span>
|
||||||
className={`bs-cmp-sheet-card${
|
<span className="bs-cmp-strip__pass-value">
|
||||||
resultSheetId === c.metric_id ? " is-active" : ""
|
{Number(summary.pass_rate ?? 0).toFixed(1)}%
|
||||||
}${Number(c.diff_count || 0) > 0 ? " has-diff" : " is-clean"}`}
|
</span>
|
||||||
onClick={() => setResultSheetId(c.metric_id)}
|
<span className="bs-cmp-strip__pass-sub">
|
||||||
>
|
{summary.ok ? t("bizCompare.passOk") : t("bizCompare.passWarn")}
|
||||||
<div className="bs-cmp-sheet-card__title">{c.metric_id}</div>
|
</span>
|
||||||
<div className="bs-cmp-sheet-card__pass">
|
</div>
|
||||||
{Number(c.pass_rate ?? 0).toFixed(1)}%
|
<div className="bs-cmp-strip__stats">
|
||||||
</div>
|
<span>
|
||||||
<div className="bs-cmp-sheet-card__stats">
|
<b>{summary.diff_count ?? 0}</b> {t("bizCompare.diffCountLabel")}
|
||||||
<span>+{c.added ?? 0}</span>
|
</span>
|
||||||
<span>−{c.removed ?? 0}</span>
|
<span>
|
||||||
<span>~{c.changed ?? 0}</span>
|
{summary.before_count ?? 0}→{summary.after_count ?? 0}
|
||||||
<span>={c.unchanged ?? 0}</span>
|
</span>
|
||||||
</div>
|
<span>
|
||||||
<div className="bs-cmp-sheet-card__meta muted">
|
{t("bizCompare.matchedRows")} {summary.matched_rows ?? 0}/
|
||||||
{c.mode === "presence"
|
{summary.total_rows ?? 0}
|
||||||
|
</span>
|
||||||
|
<span className="muted">{fmtTime(runDetail.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="bs-cmp-strip__kinds">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["added", summary.added ?? 0, "added"],
|
||||||
|
["removed", summary.removed ?? 0, "removed"],
|
||||||
|
["changed", summary.changed ?? 0, "changed"],
|
||||||
|
["unchanged", summary.unchanged ?? 0, "unchanged"],
|
||||||
|
] as const
|
||||||
|
).map(([id, n, cls]) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
className={`bs-cmp-strip__kind bs-cmp-strip__kind--${cls}${
|
||||||
|
kindFilter === id ? " is-active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setKindFilter(id)}
|
||||||
|
>
|
||||||
|
{kindLabel(id)} <b>{n}</b>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeSheetCard ? (
|
||||||
|
<div className="bs-cmp-sheet-bar">
|
||||||
|
<div className="bs-cmp-sheet-bar__title">
|
||||||
|
<strong>{activeSheetCard.metric_id}</strong>
|
||||||
|
<span className="muted">
|
||||||
|
{activeSheetCard.mode === "presence"
|
||||||
? t("bizCompare.presenceShort")
|
? t("bizCompare.presenceShort")
|
||||||
: t("bizCompare.modeFieldsShort")}{" "}
|
: t("bizCompare.modeFieldsShort")}{" "}
|
||||||
· {c.before_count ?? 0}→{c.after_count ?? 0}
|
· {activeSheetCard.before_count ?? 0}→
|
||||||
</div>
|
{activeSheetCard.after_count ?? 0} ·{" "}
|
||||||
</button>
|
{Number(activeSheetCard.pass_rate ?? 0).toFixed(1)}%
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{topChangedFields.length ? (
|
|
||||||
<div className="bs-cmp-top-fields">
|
|
||||||
<span className="bs-cmp-top-fields__label">{t("bizCompare.topChangedFields")}</span>
|
|
||||||
{topChangedFields.map((f) => (
|
|
||||||
<span key={f.field} className="bs-cmp-top-fields__chip">
|
|
||||||
{f.field}
|
|
||||||
<em>{f.count}</em>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="bs-cmp-kpis">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`bs-cmp-kpi bs-cmp-kpi--added${kindFilter === "added" ? " is-active" : ""}`}
|
|
||||||
onClick={() => setKindFilter("added")}
|
|
||||||
>
|
|
||||||
<span className="bs-cmp-kpi__label">{t("bizCompare.kindAddedShort")}</span>
|
|
||||||
<span className="bs-cmp-kpi__value">{summary.added ?? 0}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`bs-cmp-kpi bs-cmp-kpi--removed${kindFilter === "removed" ? " is-active" : ""}`}
|
|
||||||
onClick={() => setKindFilter("removed")}
|
|
||||||
>
|
|
||||||
<span className="bs-cmp-kpi__label">{t("bizCompare.kindRemovedShort")}</span>
|
|
||||||
<span className="bs-cmp-kpi__value">{summary.removed ?? 0}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`bs-cmp-kpi bs-cmp-kpi--changed${kindFilter === "changed" ? " is-active" : ""}`}
|
|
||||||
onClick={() => setKindFilter("changed")}
|
|
||||||
>
|
|
||||||
<span className="bs-cmp-kpi__label">{t("bizCompare.kindChangedShort")}</span>
|
|
||||||
<span className="bs-cmp-kpi__value">{summary.changed ?? 0}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`bs-cmp-kpi bs-cmp-kpi--unchanged${kindFilter === "unchanged" ? " is-active" : ""}`}
|
|
||||||
onClick={() => setKindFilter("unchanged")}
|
|
||||||
>
|
|
||||||
<span className="bs-cmp-kpi__label">{t("bizCompare.kindUnchangedShort")}</span>
|
|
||||||
<span className="bs-cmp-kpi__value">{summary.unchanged ?? 0}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bs-cmp-filter-bar">
|
|
||||||
<div className="bs-cmp-kind-pills" role="tablist">
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
["diff", t("bizCompare.kindDiff")],
|
|
||||||
["all", t("bizCompare.kindAll")],
|
|
||||||
["added", t("bizCompare.kindAddedShort")],
|
|
||||||
["removed", t("bizCompare.kindRemovedShort")],
|
|
||||||
["changed", t("bizCompare.kindChangedShort")],
|
|
||||||
["unchanged", t("bizCompare.kindUnchangedShort")],
|
|
||||||
] as const
|
|
||||||
).map(([id, label]) => (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
className={`bs-cmp-kind-pill${kindFilter === id ? " is-active" : ""}`}
|
|
||||||
onClick={() => setKindFilter(id)}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={resultKw}
|
|
||||||
placeholder={t("bizCompare.resultFilterPh")}
|
|
||||||
onChange={(e) => setResultKw(e.target.value)}
|
|
||||||
/>
|
|
||||||
<span className="muted bs-sheet-count">
|
|
||||||
{filteredDiffs.length}/{(activeRunSheet?.diffs || []).length}
|
|
||||||
{resultColumns.presence ? ` · ${t("bizCompare.presenceShort")}` : ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-list-table-wrap bs-sheet-table bs-cmp-result-table">
|
|
||||||
<table className="data-table pt-list-table bs-cmp-diff-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="bs-cmp-col-kind">{t("bizCompare.colKind")}</th>
|
|
||||||
{resultColumns.keys.map((k) => (
|
|
||||||
<th key={k}>{k}</th>
|
|
||||||
))}
|
|
||||||
{resultColumns.compare.map((f) => (
|
|
||||||
<th key={f}>{f}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredDiffs.slice(0, 2000).map((d, i) => {
|
|
||||||
const pre = (d.mapped_before || d.before || {}) as Record<string, unknown>;
|
|
||||||
const post = (d.after || {}) as Record<string, unknown>;
|
|
||||||
return (
|
|
||||||
<tr key={i} className={`bs-cmp-row bs-cmp-row--${d.kind}`}>
|
|
||||||
<td className="bs-cmp-col-kind">
|
|
||||||
<span className={`bs-cmp-badge bs-cmp-badge--${d.kind}`}>
|
|
||||||
{kindLabel(d.kind)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
{resultColumns.keys.map((k) => (
|
|
||||||
<td key={k} className="bs-cmp-key-cell">
|
|
||||||
{cellText(d.key?.[k] ?? pre[k] ?? post[k]) || "—"}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
{resultColumns.compare.map((f) => {
|
|
||||||
const pv = cellText(pre[f]);
|
|
||||||
const av = cellText(post[f]);
|
|
||||||
if (d.kind === "added") {
|
|
||||||
return (
|
|
||||||
<td key={f} className="bs-cmp-val-cell">
|
|
||||||
<span className="bs-cmp-val bs-cmp-val--post">{av || "—"}</span>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (d.kind === "removed") {
|
|
||||||
return (
|
|
||||||
<td key={f} className="bs-cmp-val-cell">
|
|
||||||
<span className="bs-cmp-val bs-cmp-val--pre">{pv || "—"}</span>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const mismatch =
|
|
||||||
Boolean(d.changes?.[f]) ||
|
|
||||||
(d.kind === "changed" && pv !== av);
|
|
||||||
if (!mismatch) {
|
|
||||||
return (
|
|
||||||
<td key={f} className="bs-cmp-val-cell">
|
|
||||||
<span className="bs-cmp-val">{pv || av || "—"}</span>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<td key={f} className="bs-cmp-val-cell bs-cmp-val-cell--diff">
|
|
||||||
<span className="bs-cmp-val bs-cmp-val--pre">{pv || "—"}</span>
|
|
||||||
<span className="bs-cmp-val-arrow" aria-hidden>
|
|
||||||
→
|
|
||||||
</span>
|
|
||||||
<span className="bs-cmp-val bs-cmp-val--post">{av || "—"}</span>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{runDetail && !filteredDiffs.length ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={
|
|
||||||
1 +
|
|
||||||
resultColumns.keys.length +
|
|
||||||
Math.max(resultColumns.compare.length, 0)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="pt-list-empty">{t("bizCompare.resultEmpty")}</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : null}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bs-sheet-tabs" role="tablist">
|
|
||||||
{runSheets.map((s) => {
|
|
||||||
const sc = s.summary || {};
|
|
||||||
const dirty =
|
|
||||||
Number(sc.added || 0) + Number(sc.removed || 0) + Number(sc.changed || 0);
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={s.metric_id}
|
|
||||||
type="button"
|
|
||||||
className={`bs-sheet-tab${resultSheetId === s.metric_id ? " is-active" : ""}`}
|
|
||||||
onClick={() => setResultSheetId(s.metric_id)}
|
|
||||||
>
|
|
||||||
{s.metric_id}
|
|
||||||
<span className="bs-sheet-tab__count">
|
|
||||||
{dirty > 0
|
|
||||||
? t("bizCompare.diffCount", { n: String(dirty) })
|
|
||||||
: t("bizCompare.kindUnchangedShort")}
|
|
||||||
{s.mode === "presence" ? ` · ${t("bizCompare.presenceShort")}` : ""}
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</div>
|
||||||
);
|
{topChangedFields.length ? (
|
||||||
})}
|
<div className="bs-cmp-top-fields bs-cmp-top-fields--inline">
|
||||||
|
<span className="bs-cmp-top-fields__label">
|
||||||
|
{t("bizCompare.topChangedFields")}
|
||||||
|
</span>
|
||||||
|
{topChangedFields.slice(0, 6).map((f) => (
|
||||||
|
<span key={f.field} className="bs-cmp-top-fields__chip">
|
||||||
|
{f.field}
|
||||||
|
<em>{f.count}</em>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="bs-cmp-filter-bar">
|
||||||
|
<div className="bs-cmp-kind-pills" role="tablist">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["diff", t("bizCompare.kindDiff")],
|
||||||
|
["all", t("bizCompare.kindAll")],
|
||||||
|
["added", t("bizCompare.kindAddedShort")],
|
||||||
|
["removed", t("bizCompare.kindRemovedShort")],
|
||||||
|
["changed", t("bizCompare.kindChangedShort")],
|
||||||
|
["unchanged", t("bizCompare.kindUnchangedShort")],
|
||||||
|
] as const
|
||||||
|
).map(([id, label]) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
className={`bs-cmp-kind-pill${kindFilter === id ? " is-active" : ""}`}
|
||||||
|
onClick={() => setKindFilter(id)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={resultKw}
|
||||||
|
placeholder={t("bizCompare.resultFilterPh")}
|
||||||
|
onChange={(e) => setResultKw(e.target.value)}
|
||||||
|
/>
|
||||||
|
<span className="muted bs-sheet-count">
|
||||||
|
{filteredDiffs.length}/{(activeRunSheet?.diffs || []).length}
|
||||||
|
{resultColumns.presence ? ` · ${t("bizCompare.presenceShort")}` : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-list-table-wrap bs-sheet-table bs-cmp-result-table">
|
||||||
|
<table className="data-table pt-list-table bs-cmp-diff-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="bs-cmp-col-kind">{t("bizCompare.colKind")}</th>
|
||||||
|
{resultColumns.keys.map((k) => (
|
||||||
|
<th key={k}>{k}</th>
|
||||||
|
))}
|
||||||
|
{resultColumns.compare.map((f) => (
|
||||||
|
<th key={f}>{f}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredDiffs.slice(0, 2000).map((d, i) => {
|
||||||
|
const pre = (d.mapped_before || d.before || {}) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
const post = (d.after || {}) as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
<tr key={i} className={`bs-cmp-row bs-cmp-row--${d.kind}`}>
|
||||||
|
<td className="bs-cmp-col-kind">
|
||||||
|
<span className={`bs-cmp-badge bs-cmp-badge--${d.kind}`}>
|
||||||
|
{kindLabel(d.kind)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{resultColumns.keys.map((k) => (
|
||||||
|
<td key={k} className="bs-cmp-key-cell">
|
||||||
|
{cellText(d.key?.[k] ?? pre[k] ?? post[k]) || "—"}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
{resultColumns.compare.map((f) => {
|
||||||
|
const pv = cellText(pre[f]);
|
||||||
|
const av = cellText(post[f]);
|
||||||
|
if (d.kind === "added") {
|
||||||
|
return (
|
||||||
|
<td key={f} className="bs-cmp-val-cell">
|
||||||
|
<span className="bs-cmp-val bs-cmp-val--post">
|
||||||
|
{av || "—"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (d.kind === "removed") {
|
||||||
|
return (
|
||||||
|
<td key={f} className="bs-cmp-val-cell">
|
||||||
|
<span className="bs-cmp-val bs-cmp-val--pre">
|
||||||
|
{pv || "—"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const mismatch =
|
||||||
|
Boolean(d.changes?.[f]) ||
|
||||||
|
(d.kind === "changed" && pv !== av);
|
||||||
|
if (!mismatch) {
|
||||||
|
return (
|
||||||
|
<td key={f} className="bs-cmp-val-cell">
|
||||||
|
<span className="bs-cmp-val">{pv || av || "—"}</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<td key={f} className="bs-cmp-val-cell bs-cmp-val-cell--diff">
|
||||||
|
<span className="bs-cmp-val bs-cmp-val--pre">
|
||||||
|
{pv || "—"}
|
||||||
|
</span>
|
||||||
|
<span className="bs-cmp-val-arrow" aria-hidden>
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
<span className="bs-cmp-val bs-cmp-val--post">
|
||||||
|
{av || "—"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{runDetail && !filteredDiffs.length ? (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={
|
||||||
|
1 +
|
||||||
|
resultColumns.keys.length +
|
||||||
|
Math.max(resultColumns.compare.length, 0)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="pt-list-empty">{t("bizCompare.resultEmpty")}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="pt-list-empty">{t("bizCompare.noRuns")}</div>
|
<div className="pt-list-empty">{t("bizCompare.noRuns")}</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue