mirror of
https://github.com/hansjone/netx.git
synced 2026-10-15 00:53:17 +08:00
Refine business compare configuration and history UI
This commit is contained in:
parent
4d5101ec3a
commit
051a10ad95
16 changed files with 1107 additions and 236 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import type { ReactNode, SelectHTMLAttributes } from "react";
|
||||
import { useId, type ReactNode, type SelectHTMLAttributes } from "react";
|
||||
import { Label } from "@heroui/react";
|
||||
|
||||
/** Compact labeled native select — HeroUI Select is heavy for dense ops forms. */
|
||||
|
|
@ -9,6 +9,8 @@ export function FieldSelect({
|
|||
fullWidth,
|
||||
className = "",
|
||||
children,
|
||||
id,
|
||||
"aria-describedby": describedBy,
|
||||
...props
|
||||
}: {
|
||||
label?: ReactNode;
|
||||
|
|
@ -18,17 +20,26 @@ export function FieldSelect({
|
|||
className?: string;
|
||||
children: ReactNode;
|
||||
} & SelectHTMLAttributes<HTMLSelectElement>) {
|
||||
const generatedId = useId();
|
||||
const selectId = id || generatedId;
|
||||
const hintId = `${selectId}-hint`;
|
||||
return (
|
||||
<label className={`ui-field${fullWidth ? " ui-field--full" : ""}${className ? ` ${className}` : ""}`}>
|
||||
<div className={`ui-field${fullWidth ? " ui-field--full" : ""}${className ? ` ${className}` : ""}`}>
|
||||
{label ? (
|
||||
<Label isRequired={required} className="ui-field__label">
|
||||
<Label htmlFor={selectId} isRequired={required} className="ui-field__label">
|
||||
{label}
|
||||
</Label>
|
||||
) : null}
|
||||
<select className="ui-field__select" required={required} {...props}>
|
||||
<select
|
||||
className="ui-field__select"
|
||||
required={required}
|
||||
{...props}
|
||||
id={selectId}
|
||||
aria-describedby={[describedBy, hint ? hintId : null].filter(Boolean).join(" ") || undefined}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
{hint ? <span className="ui-field__hint">{hint}</span> : null}
|
||||
</label>
|
||||
{hint ? <span id={hintId} className="ui-field__hint">{hint}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -562,6 +562,29 @@ const en = {
|
|||
tabConfig: "Job config",
|
||||
tabRuns: "Compare batches",
|
||||
tabResult: "Result",
|
||||
detailTabs: "Business comparison workspace",
|
||||
jobReady: "Ready",
|
||||
configBasics: "Job parameters",
|
||||
configSources: "Before and after sources",
|
||||
configScope: "Check scope",
|
||||
selectMapping: "Select mapping",
|
||||
mappingPairs: "Interface pairs",
|
||||
sourceBaseline: "Fixed baseline",
|
||||
sourceTarget: "Acceptance target",
|
||||
sourceLatest: "Latest batch",
|
||||
sourceRows: "{{n}} collected rows",
|
||||
autoSourceHint: "Keep the before batch fixed. Compare against the latest completed batch from the after task.",
|
||||
runHistoryTitle: "Execution history",
|
||||
runHistoryHint: "Showing the latest 50 runs. Execution status and business verdicts are displayed separately. Open results to inspect.",
|
||||
refreshRuns: "Refresh history",
|
||||
loadedRuns: "Loaded runs",
|
||||
activeRuns: "In progress",
|
||||
completedRuns: "Completed",
|
||||
stoppedRuns: "Failed / cancelled",
|
||||
runFinished: "Completed",
|
||||
runStatusQueued: "Queued",
|
||||
runsLoading: "Loading execution history…",
|
||||
runsLoadError: "Could not load execution history",
|
||||
runs: "Run history",
|
||||
runsHint: "Each “Run now” creates a batch. Stuck “Running” after restart is auto-cancelled; you can also cancel manually and re-run.",
|
||||
noRuns: "No runs yet — run a compare first",
|
||||
|
|
|
|||
|
|
@ -561,6 +561,29 @@ const zh = {
|
|||
tabConfig: "任务配置",
|
||||
tabRuns: "对比批次",
|
||||
tabResult: "比对结果",
|
||||
detailTabs: "业务对比工作区",
|
||||
jobReady: "已就绪",
|
||||
configBasics: "作业参数",
|
||||
configSources: "前后数据源",
|
||||
configScope: "检查范围",
|
||||
selectMapping: "选择映射",
|
||||
mappingPairs: "接口对应关系",
|
||||
sourceBaseline: "固定基准",
|
||||
sourceTarget: "验收目标",
|
||||
sourceLatest: "最新批次",
|
||||
sourceRows: "采集 {{n}} 条",
|
||||
autoSourceHint: "保持操作前批次固定;操作后任务采集完成时,自动取最新批次进行对比。",
|
||||
runHistoryTitle: "执行记录",
|
||||
runHistoryHint: "展示最近 50 条记录。执行状态与业务判定分别显示,可打开结果核查。",
|
||||
refreshRuns: "刷新记录",
|
||||
loadedRuns: "已载入批次",
|
||||
activeRuns: "正在执行",
|
||||
completedRuns: "执行完成",
|
||||
stoppedRuns: "失败 / 取消",
|
||||
runFinished: "已完成",
|
||||
runStatusQueued: "排队中",
|
||||
runsLoading: "正在读取执行记录…",
|
||||
runsLoadError: "执行记录读取失败",
|
||||
runs: "历史比对",
|
||||
runsHint: "每次「立即比对」生成一条批次。重启后卡住的「比对中」会自动标为已取消;也可手动终止后重跑。",
|
||||
noRuns: "尚无比对记录,请先执行比对",
|
||||
|
|
|
|||
|
|
@ -11856,6 +11856,594 @@ html.login-page--paused .login-page__flare {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Business compare workspace: compact, flat ops surfaces. */
|
||||
.bs-cmp-workspace,
|
||||
.bs-cmp-job-modal,
|
||||
.bs-cmp-create {
|
||||
--cmp-line: #2b3748;
|
||||
--cmp-muted: #9aa9bc;
|
||||
--cmp-surface: #111c2a;
|
||||
--cmp-accent: #6ba9ec;
|
||||
}
|
||||
|
||||
.panel.bs-cmp-workspace {
|
||||
border-radius: 6px;
|
||||
border-color: var(--cmp-line);
|
||||
background: #0c1521;
|
||||
}
|
||||
|
||||
.bs-cmp-workspace > .pt-list > .pt-list-table-wrap {
|
||||
border-radius: 4px;
|
||||
border-color: var(--cmp-line);
|
||||
}
|
||||
|
||||
.bs-cmp-workspace > .pt-list > .pt-list-table-wrap .pt-list-table td {
|
||||
border-bottom: 1px solid var(--cmp-line);
|
||||
}
|
||||
|
||||
.bs-cmp-workspace .button,
|
||||
.bs-cmp-workspace .input {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bs-cmp-workspace .bs-cmp-runs__delete {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .modal__dialog,
|
||||
.bs-cmp-create .modal__dialog {
|
||||
border-radius: 6px;
|
||||
background: #0c1521;
|
||||
border-color: var(--cmp-line);
|
||||
max-height: min(92dvh, 1000px);
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal:not(.bs-cmp-board-modal) .modal__dialog {
|
||||
width: min(1180px, 96vw);
|
||||
max-width: min(1180px, 96vw);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal:not(.bs-cmp-board-modal) .modal__body.bs-cmp-job-body {
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .button,
|
||||
.bs-cmp-create .button,
|
||||
.bs-cmp-job-modal .ui-field__select,
|
||||
.bs-cmp-create .ui-field__select,
|
||||
.bs-cmp-job-modal .input,
|
||||
.bs-cmp-create .input {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bs-cmp-job-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.bs-cmp-job-heading__id {
|
||||
color: var(--cmp-muted);
|
||||
font: 10px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-job-tabs {
|
||||
margin: 0;
|
||||
gap: 0;
|
||||
flex-wrap: nowrap;
|
||||
border-bottom: 1px solid var(--cmp-line);
|
||||
}
|
||||
|
||||
.bs-cmp-job-tabs .button {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
height: 38px;
|
||||
padding: 0 15px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--cmp-muted);
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-job-tabs .button.is-active {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
border-bottom-color: var(--cmp-accent);
|
||||
color: #edf5ff;
|
||||
background: rgba(107, 169, 236, 0.08);
|
||||
}
|
||||
|
||||
.bs-cmp-job-tabs__index {
|
||||
font: 10px ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.bs-cmp-job-tabs__count {
|
||||
padding: 1px 5px;
|
||||
background: #263347;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bs-cmp-config {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bs-cmp-config__section {
|
||||
min-width: 0;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--cmp-line);
|
||||
border-radius: 4px;
|
||||
background: var(--cmp-surface);
|
||||
}
|
||||
|
||||
.bs-cmp-config .is-wide,
|
||||
.bs-cmp-config__fields > .is-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.bs-cmp-config__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin: 0 0 14px;
|
||||
color: #e7eef7;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.bs-cmp-config__title > span {
|
||||
color: var(--cmp-accent);
|
||||
font: 11px ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
|
||||
.bs-cmp-config__fields,
|
||||
.bs-cmp-mapping__fields,
|
||||
.bs-cmp-sources {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bs-cmp-config__fields > .ui-field--full,
|
||||
.bs-cmp-mapping__fields > .ui-field--full {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.bs-cmp-config__hint {
|
||||
color: var(--cmp-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.65;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.bs-cmp-source {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--cmp-line);
|
||||
border-radius: 3px;
|
||||
background: #0c1521;
|
||||
}
|
||||
|
||||
.bs-cmp-source__head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-cmp-source__head > span:last-child {
|
||||
margin-left: auto;
|
||||
color: var(--cmp-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bs-cmp-source__tag {
|
||||
padding: 2px 5px;
|
||||
border: 1px solid #465569;
|
||||
color: #c6d3e3;
|
||||
font: 10px ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
|
||||
.bs-cmp-source.is-after .bs-cmp-source__tag {
|
||||
border-color: #3f6a97;
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.bs-cmp-source__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
margin-top: auto;
|
||||
padding-top: 9px;
|
||||
border-top: 1px solid var(--cmp-line);
|
||||
color: var(--cmp-muted);
|
||||
font: 11px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
|
||||
.bs-cmp-source__auto {
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__count {
|
||||
color: var(--cmp-accent);
|
||||
font: 12px ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
|
||||
.bs-cmp-scope .bm-hint {
|
||||
margin: 8px 0 12px;
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid var(--cmp-line);
|
||||
border-radius: 3px;
|
||||
background: #0c1521;
|
||||
color: var(--cmp-muted);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__item.is-on {
|
||||
border-color: #3c5f89;
|
||||
background: rgba(61, 108, 160, 0.1);
|
||||
color: #dbeafe;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__item:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__check {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex: 0 0 15px;
|
||||
height: 15px;
|
||||
border: 1px solid #53677f;
|
||||
border-radius: 2px;
|
||||
color: var(--cmp-accent);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__content strong {
|
||||
font-size: 12px;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__content > span {
|
||||
color: var(--cmp-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bs-cmp-mapping {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bs-cmp-mapping__editor {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--cmp-line);
|
||||
border-radius: 3px;
|
||||
background: #0a121d;
|
||||
color: #dce8f6;
|
||||
resize: vertical;
|
||||
font: 11px/1.7 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
}
|
||||
|
||||
.bs-cmp-mapping__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bs-cmp-create .bm-create__pane,
|
||||
.bs-cmp-create .bm-steps__item {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__intro {
|
||||
flex: 1 1 300px;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__intro h3 {
|
||||
margin: 0 0 5px;
|
||||
color: #e7eef7;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__hint {
|
||||
color: var(--cmp-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin: 0;
|
||||
border: 1px solid var(--cmp-line);
|
||||
border-radius: 4px;
|
||||
background: var(--cmp-surface);
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats > div {
|
||||
padding: 12px 15px;
|
||||
border-right: 1px solid var(--cmp-line);
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats > div:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats dt {
|
||||
color: var(--cmp-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats dd {
|
||||
margin: 5px 0 0;
|
||||
color: #e7eef7;
|
||||
font: 24px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats .is-active dd {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.bs-cmp-run-stats .is-stopped dd {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs .pt-list-table-wrap {
|
||||
max-height: min(54vh, 580px);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--cmp-line);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__table {
|
||||
min-width: 1000px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__table th:nth-child(1) { width: 12%; }
|
||||
.bs-cmp-runs__table th:nth-child(2) { width: 18%; }
|
||||
.bs-cmp-runs__table th:nth-child(3) { width: 27%; }
|
||||
.bs-cmp-runs__table th:nth-child(4) { width: 25%; }
|
||||
.bs-cmp-runs__table th:nth-child(5) { width: 18%; }
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__table th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: #162232;
|
||||
color: #a7b6ca;
|
||||
font-size: 11px;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__table td {
|
||||
height: 80px;
|
||||
border-bottom: 1px solid var(--cmp-line);
|
||||
background: #0c1521;
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__table tr.is-selected td {
|
||||
background: #142237;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__table tr td:first-child {
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__table tr.is-selected td:first-child,
|
||||
.bs-cmp-runs__table tr[data-status="running"] td:first-child,
|
||||
.bs-cmp-runs__table tr[data-status="queued"] td:first-child {
|
||||
border-left-color: var(--cmp-accent);
|
||||
}
|
||||
|
||||
.bs-cmp-runs__table tr[data-status="failed"] td:first-child {
|
||||
border-left-color: #f87171;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__id {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: var(--cmp-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__sides {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-width: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__sides > div {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__side-tag,
|
||||
.bs-cmp-runs__sides time {
|
||||
color: var(--cmp-muted);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__sides strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
.bs-cmp-run-verdict {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bs-cmp-run-verdict > span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: var(--cmp-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.bs-cmp-run-verdict strong {
|
||||
color: #dbe7f5;
|
||||
font: 16px/1.2 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.bs-cmp-run-verdict .is-fail strong { color: #fca5a5; }
|
||||
.bs-cmp-run-verdict .is-pass strong { color: #86efac; }
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__summary,
|
||||
.bs-cmp-job-modal .bs-cmp-run-prog__meta,
|
||||
.bs-cmp-job-modal .bs-cmp-runs__elapsed {
|
||||
color: var(--cmp-muted);
|
||||
font-size: 11px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__summary.is-stopped {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-runs__delete {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.bs-cmp-runs__error {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border: 1px solid #7f4343;
|
||||
border-radius: 4px;
|
||||
color: #fecaca;
|
||||
background: rgba(127, 67, 67, 0.1);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-board,
|
||||
.bs-cmp-job-modal .bs-cmp-strip,
|
||||
.bs-cmp-job-modal .bs-cmp-sides,
|
||||
.bs-cmp-job-modal .bs-cmp-nav,
|
||||
.bs-cmp-job-modal .bs-cmp-progress {
|
||||
border-radius: 4px;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-progress__track,
|
||||
.bs-cmp-job-modal .bs-cmp-run-prog__track {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.bs-cmp-job-modal .bs-cmp-progress__fill,
|
||||
.bs-cmp-job-modal .bs-cmp-run-prog__fill {
|
||||
background: #5795d9;
|
||||
}
|
||||
|
||||
.bs-cmp-scope__item:focus-visible,
|
||||
.bs-cmp-job-modal .bs-cmp-job-tabs .button:focus-visible,
|
||||
.bs-cmp-mapping__editor:focus-visible {
|
||||
outline: 2px solid var(--cmp-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.bs-cmp-config { grid-template-columns: minmax(0, 1fr); }
|
||||
.bs-cmp-job-tabs .button { padding: 0 10px; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.bs-cmp-sources,
|
||||
.bs-cmp-config__fields,
|
||||
.bs-cmp-mapping__fields { grid-template-columns: minmax(0, 1fr); }
|
||||
.bs-cmp-config__section { padding: 11px; }
|
||||
.bs-cmp-run-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.bs-cmp-run-stats > div:nth-child(2) { border-right: 0; }
|
||||
.bs-cmp-run-stats > div:nth-child(-n+2) { border-bottom: 1px solid var(--cmp-line); }
|
||||
.bs-cmp-job-tabs__index { display: none; }
|
||||
.bs-cmp-job-modal .bs-cmp-job-tabs .button { flex: 1 1 0; padding: 0 7px; font-size: 12px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bs-cmp-job-modal .bs-cmp-progress__fill,
|
||||
.bs-cmp-job-modal .bs-cmp-run-prog__fill { transition: none; }
|
||||
.bs-cmp-job-modal .is-indeterminate { animation: none; }
|
||||
}
|
||||
|
||||
/* Native :fullscreen + CSS immersive fallback (class only when FS API blocked) */
|
||||
.bs-cmp-board.is-fullscreen,
|
||||
.bs-cmp-board:fullscreen {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Button, Input, Modal } from "@heroui/react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
import { ListPager } from "../../components/ListPager";
|
||||
import { AppModalShell } from "../../components/ui/AppModalShell";
|
||||
import { FieldSelect } from "../../components/ui/FieldSelect";
|
||||
|
|
@ -145,6 +146,21 @@ type RunSheet = {
|
|||
diffs?: DiffRow[];
|
||||
};
|
||||
|
||||
type RunRecord = {
|
||||
id: string;
|
||||
status?: string;
|
||||
created_at?: string;
|
||||
before?: SideInfo;
|
||||
after?: SideInfo;
|
||||
summary?: {
|
||||
added?: number; removed?: number; changed?: number; unchanged?: number;
|
||||
before_count?: number; after_count?: number; duration_ms?: number;
|
||||
progress?: RunProgressInfo;
|
||||
};
|
||||
progress?: RunProgressInfo;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function fmtTime(v?: string | null) {
|
||||
if (!v) return "—";
|
||||
return formatSystemTime(v) || v;
|
||||
|
|
@ -947,7 +963,9 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const [mapName, setMapName] = useState("端口映射");
|
||||
const [mapText, setMapText] = useState("");
|
||||
const [validateOut, setValidateOut] = useState<any>(null);
|
||||
const [runs, setRuns] = useState<any[]>([]);
|
||||
const [runs, setRuns] = useState<RunRecord[]>([]);
|
||||
const [runsLoading, setRunsLoading] = useState(false);
|
||||
const [runsError, setRunsError] = useState("");
|
||||
const [runDetail, setRunDetail] = useState<any>(null);
|
||||
const [resultSheetId, setResultSheetId] = useState("");
|
||||
|
||||
|
|
@ -1055,6 +1073,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setCreateStep(0);
|
||||
setJobId("");
|
||||
setRuns([]);
|
||||
setRunsLoading(false);
|
||||
setRunsError("");
|
||||
setRunDetail(null);
|
||||
setResultSheetId("");
|
||||
if (document.fullscreenElement === boardRef.current) {
|
||||
|
|
@ -1083,21 +1103,22 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
return () => { cancelled = true; };
|
||||
}, [beforeTaskId, showError]);
|
||||
|
||||
const effectiveAfterTaskId = afterTaskId || beforeTaskId;
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setAfterBatches([]);
|
||||
void (async () => {
|
||||
if (!afterTaskId) return;
|
||||
if (!effectiveAfterTaskId) return;
|
||||
try {
|
||||
const b = await cutoverCachedGet(`bizCompare:batches:${afterTaskId}`,
|
||||
() => bizStateListBatches(afterTaskId), { ttlMs: 5000 });
|
||||
const b = await cutoverCachedGet(`bizCompare:batches:${effectiveAfterTaskId}`,
|
||||
() => bizStateListBatches(effectiveAfterTaskId), { ttlMs: 5000 });
|
||||
if (!cancelled) setAfterBatches((b.items || []) as BatchOpt[]);
|
||||
} catch (e) {
|
||||
if (!cancelled) showError(formatErr(e));
|
||||
}
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, [afterTaskId, showError]);
|
||||
}, [effectiveAfterTaskId, showError]);
|
||||
|
||||
const templatesById = useMemo(() => new Map(templates.map((x) => [x.id, x])), [templates]);
|
||||
|
||||
|
|
@ -2034,6 +2055,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setJobDetailTab("config");
|
||||
setRunDetail(null);
|
||||
setRuns([]);
|
||||
setRunsLoading(true);
|
||||
setRunsError("");
|
||||
setResultSheetId("");
|
||||
setKindFilter("diff");
|
||||
setResultKw("");
|
||||
|
|
@ -2042,20 +2065,53 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
try {
|
||||
const r = await bizCompareListRuns(id, 50);
|
||||
if (request !== jobRequestRef.current) return;
|
||||
const items = r.items || [];
|
||||
const items = (r.items || []) as RunRecord[];
|
||||
setRuns(items);
|
||||
if (items.length) {
|
||||
const latest = await bizCompareGetRun(String((items as any[])[0].id));
|
||||
const latest = await bizCompareGetRun(items[0].id);
|
||||
if (request !== jobRequestRef.current) return;
|
||||
setRunDetail(latest);
|
||||
// Land on batch list so stuck/cancelled runs are visible and actionable
|
||||
setJobDetailTab("runs");
|
||||
}
|
||||
} catch (e) {
|
||||
if (request === jobRequestRef.current) showError(formatErr(e));
|
||||
if (request === jobRequestRef.current) {
|
||||
setRunsError(formatErr(e));
|
||||
showError(formatErr(e));
|
||||
}
|
||||
} finally {
|
||||
if (request === jobRequestRef.current) setRunsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshRuns = async () => {
|
||||
if (!jobId || runsLoading) return;
|
||||
const request = jobRequestRef.current;
|
||||
setRunsLoading(true);
|
||||
setRunsError("");
|
||||
try {
|
||||
const response = await bizCompareListRuns(jobId, 50);
|
||||
if (request === jobRequestRef.current) setRuns((response.items || []) as RunRecord[]);
|
||||
} catch (e) {
|
||||
if (request === jobRequestRef.current) setRunsError(formatErr(e));
|
||||
} finally {
|
||||
if (request === jobRequestRef.current) setRunsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJobTabKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {
|
||||
const tabs: JobDetailTab[] = ["config", "runs", "result"];
|
||||
const current = tabs.indexOf(jobDetailTab);
|
||||
const next = event.key === "ArrowRight" ? (current + 1) % tabs.length
|
||||
: event.key === "ArrowLeft" ? (current + tabs.length - 1) % tabs.length
|
||||
: event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : -1;
|
||||
if (next < 0) return;
|
||||
event.preventDefault();
|
||||
setJobDetailTab(tabs[next]);
|
||||
if (tabs[next] === "runs") void refreshRuns();
|
||||
event.currentTarget.querySelector<HTMLButtonElement>(`#compare-${tabs[next]}-tab`)?.focus();
|
||||
};
|
||||
|
||||
const closeJob = () => {
|
||||
jobRequestRef.current += 1;
|
||||
runRequestRef.current += 1;
|
||||
|
|
@ -2065,6 +2121,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
setBoardFs(false);
|
||||
setJobId("");
|
||||
setRuns([]);
|
||||
setRunsLoading(false);
|
||||
setRunsError("");
|
||||
setRunDetail(null);
|
||||
setResultSheetId("");
|
||||
};
|
||||
|
|
@ -2090,9 +2148,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
const runStatus = String(runDetail?.status || "");
|
||||
const runIsActive = runStatus === "running" || runStatus === "queued";
|
||||
const jobHasActiveRun = runs.some((r) => {
|
||||
const st = String((r as any).status || "");
|
||||
const st = String(r.status || "");
|
||||
return st === "running" || st === "queued";
|
||||
});
|
||||
const runCounts = useMemo(() => ({
|
||||
total: runs.length,
|
||||
active: runs.filter((r) => r.status === "running" || r.status === "queued").length,
|
||||
completed: runs.filter((r) => r.status === "success").length,
|
||||
stopped: runs.filter((r) => r.status === "failed" || r.status === "cancelled").length,
|
||||
}), [runs]);
|
||||
|
||||
const runNow = async () => {
|
||||
if (!jobId) return;
|
||||
|
|
@ -2118,7 +2182,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
showOk(t("bizCompare.runStarted"));
|
||||
const r = await bizCompareListRuns(jobId, 50);
|
||||
if (request !== jobRequestRef.current) return;
|
||||
setRuns(r.items || []);
|
||||
setRuns((r.items || []) as RunRecord[]);
|
||||
await refresh({ force: true });
|
||||
} catch (e) {
|
||||
if (request === jobRequestRef.current) {
|
||||
|
|
@ -2148,7 +2212,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
runIsActive && runId ? bizCompareGetRun(runId) : Promise.resolve(null),
|
||||
]);
|
||||
if (cancelled || jobRequest !== jobRequestRef.current) return;
|
||||
setRuns(r.items || []);
|
||||
setRuns((r.items || []) as RunRecord[]);
|
||||
if (!d || runRequest !== runRequestRef.current) return;
|
||||
setRunDetail((prev: typeof runDetail) => String(prev?.id || "") === runId ? d : prev);
|
||||
const st = String(d.status || "");
|
||||
|
|
@ -2205,7 +2269,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
if (jobId) {
|
||||
const r = await bizCompareListRuns(jobId, 50);
|
||||
if (request !== jobRequestRef.current) return;
|
||||
setRuns(r.items || []);
|
||||
setRuns((r.items || []) as RunRecord[]);
|
||||
}
|
||||
showOk(t("bizCompare.runCancelled"));
|
||||
} catch (e) {
|
||||
|
|
@ -2229,7 +2293,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
if (jobId) {
|
||||
const r = await bizCompareListRuns(jobId, 50);
|
||||
if (request !== jobRequestRef.current) return;
|
||||
nextRuns = r.items || [];
|
||||
nextRuns = (r.items || []) as RunRecord[];
|
||||
} else {
|
||||
nextRuns = (runs || []).filter((r) => String(r.id) !== runId);
|
||||
}
|
||||
|
|
@ -2249,15 +2313,15 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
};
|
||||
|
||||
const renderSheetChips = () => (
|
||||
<div className="bm-create__pane" style={{ padding: 0, border: "none", background: "transparent" }}>
|
||||
<div className="bm-mapping__label">
|
||||
<div className="bs-cmp-scope">
|
||||
<div className="bs-cmp-scope__heading">
|
||||
{t("bizCompare.enabledSheets")}
|
||||
<span className="muted" style={{ fontWeight: 400, marginLeft: 8 }}>
|
||||
<span className="bs-cmp-scope__count">
|
||||
{enabledJobSheetCount}/{jobSheetAllIds.length || 0}
|
||||
</span>
|
||||
</div>
|
||||
<p className="muted bm-hint">{t("bizCompare.enabledSheetsHint")}</p>
|
||||
<div className="bm-metric-chips">
|
||||
<div className="bs-cmp-scope__items">
|
||||
{jobTemplateSheets.map((s) => {
|
||||
const sid = sheetIdentity(s);
|
||||
const on = isJobSheetOn(sid);
|
||||
|
|
@ -2265,11 +2329,17 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
<button
|
||||
key={sid}
|
||||
type="button"
|
||||
className={`bm-metric-chip${on ? " is-on" : ""}`}
|
||||
className={`bs-cmp-scope__item${on ? " is-on" : ""}`}
|
||||
aria-pressed={on}
|
||||
disabled={on && enabledJobSheetCount === 1}
|
||||
onClick={() => toggleJobSheet(sid)}
|
||||
title={s.metric_id !== sid ? s.metric_id : undefined}
|
||||
>
|
||||
{sheetLabel(s)}
|
||||
<span className="bs-cmp-scope__check" aria-hidden="true">{on ? "✓" : ""}</span>
|
||||
<span className="bs-cmp-scope__content">
|
||||
<strong>{sheetLabel(s)}</strong>
|
||||
<span>{s.compare_fields.length ? t("bizCompare.modeFieldsShort") : t("bizCompare.presenceShort")}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -2285,10 +2355,10 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
|
||||
const renderMappingBlock = () => (
|
||||
<div className="bs-cmp-mapping">
|
||||
<h4 style={{ margin: "8px 0" }}>{t("bizCompare.mapping")}</h4>
|
||||
<p className="muted">{t("bizCompare.mappingOptionalHint")}</p>
|
||||
<div className="filter-inline" style={{ marginBottom: 8 }}>
|
||||
<p className="bs-cmp-config__hint">{t("bizCompare.mappingOptionalHint")}</p>
|
||||
<div className="bs-cmp-mapping__fields">
|
||||
<FieldSelect
|
||||
label={t("bizCompare.selectMapping")}
|
||||
value={mappingId}
|
||||
onChange={(e) => {
|
||||
const id = e.target.value;
|
||||
|
|
@ -2306,7 +2376,23 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<Input value={mapName} placeholder={t("bizCompare.mapName")} onChange={(e) => setMapName(e.target.value)} />
|
||||
<label className="ui-field ui-field--full">
|
||||
<span className="ui-field__label">{t("bizCompare.mapName")}</span>
|
||||
<Input value={mapName} onChange={(e) => setMapName(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
<label className="ui-field ui-field--full">
|
||||
<span className="ui-field__label">{t("bizCompare.mappingPairs")}</span>
|
||||
<textarea
|
||||
className="bs-cmp-mapping__editor"
|
||||
value={mapText}
|
||||
onChange={(e) => setMapText(e.target.value)}
|
||||
placeholder={t("bizCompare.mapHint")}
|
||||
rows={4}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</label>
|
||||
<div className="bs-cmp-mapping__actions">
|
||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void saveMapping()}>
|
||||
{t("bizCompare.saveMapping")}
|
||||
</Button>
|
||||
|
|
@ -2314,13 +2400,6 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
{t("bizCompare.validateMapping")}
|
||||
</Button>
|
||||
</div>
|
||||
<textarea
|
||||
value={mapText}
|
||||
onChange={(e) => setMapText(e.target.value)}
|
||||
placeholder={t("bizCompare.mapHint")}
|
||||
rows={5}
|
||||
style={{ width: "100%", fontFamily: "ui-monospace, monospace" }}
|
||||
/>
|
||||
{validateOut ? (
|
||||
<pre className="muted" style={{ fontSize: 12, maxHeight: 120, overflow: "auto" }}>
|
||||
{JSON.stringify(validateOut, null, 2)}
|
||||
|
|
@ -2330,68 +2409,97 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
);
|
||||
|
||||
const renderBatchFields = () => (
|
||||
<>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.beforeTask")}
|
||||
value={beforeTaskId}
|
||||
onChange={(e) => setBeforeTaskId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.pick")}</option>
|
||||
{tasks.map((row) => (
|
||||
<option key={row.id} value={row.id}>
|
||||
{taskLabel(row)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.beforeBatch")}
|
||||
value={beforeBatchId}
|
||||
onChange={(e) => setBeforeBatchId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.pick")}</option>
|
||||
{beforeBatches.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{batchOptLabel(b)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.afterTask")}
|
||||
value={afterTaskId}
|
||||
onChange={(e) => setAfterTaskId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.sameAsBefore")}</option>
|
||||
{tasks.map((row) => (
|
||||
<option key={row.id} value={row.id}>
|
||||
{taskLabel(row)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
{mode === "manual" ? (
|
||||
<div className="bs-cmp-sources">
|
||||
<section className="bs-cmp-source" aria-label={t("bizCompare.sideBefore")}>
|
||||
<header className="bs-cmp-source__head">
|
||||
<span className="bs-cmp-source__tag" aria-hidden="true">PRE</span>
|
||||
<strong>{t("bizCompare.sideBefore")}</strong>
|
||||
<span>{t("bizCompare.sourceBaseline")}</span>
|
||||
</header>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.afterBatch")}
|
||||
value={afterBatchId}
|
||||
onChange={(e) => setAfterBatchId(e.target.value)}
|
||||
label={t("bizCompare.beforeTask")}
|
||||
value={beforeTaskId}
|
||||
onChange={(e) => {
|
||||
setBeforeTaskId(e.target.value);
|
||||
setBeforeBatchId("");
|
||||
if (!afterTaskId) setAfterBatchId("");
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.pick")}</option>
|
||||
{afterBatches.map((b) => (
|
||||
{tasks.map((row) => (
|
||||
<option key={row.id} value={row.id}>
|
||||
{taskLabel(row)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.beforeBatch")}
|
||||
value={beforeBatchId}
|
||||
onChange={(e) => setBeforeBatchId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.pick")}</option>
|
||||
{beforeBatches.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{batchOptLabel(b)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
) : (
|
||||
<p className="muted">{t("bizCompare.autoHint")}</p>
|
||||
)}
|
||||
</>
|
||||
<div className="bs-cmp-source__meta">
|
||||
<span>{tasks.find((row) => row.id === beforeTaskId)?.ne_ip || "—"}</span>
|
||||
<span>{t("bizCompare.sourceRows", { n: beforeBatches.find((b) => b.id === beforeBatchId)?.row_count?.toLocaleString() ?? "—" })}</span>
|
||||
</div>
|
||||
</section>
|
||||
<section className="bs-cmp-source is-after" aria-label={t("bizCompare.sideAfter")}>
|
||||
<header className="bs-cmp-source__head">
|
||||
<span className="bs-cmp-source__tag" aria-hidden="true">POST</span>
|
||||
<strong>{t("bizCompare.sideAfter")}</strong>
|
||||
<span>{mode === "auto" ? t("bizCompare.sourceLatest") : t("bizCompare.sourceTarget")}</span>
|
||||
</header>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.afterTask")}
|
||||
value={afterTaskId}
|
||||
onChange={(e) => {
|
||||
setAfterTaskId(e.target.value);
|
||||
setAfterBatchId("");
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.sameAsBefore")}</option>
|
||||
{tasks.map((row) => (
|
||||
<option key={row.id} value={row.id}>
|
||||
{taskLabel(row)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
{mode === "manual" ? (
|
||||
<FieldSelect
|
||||
label={t("bizCompare.afterBatch")}
|
||||
value={afterBatchId}
|
||||
onChange={(e) => setAfterBatchId(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
<option value="">{t("bizCompare.pick")}</option>
|
||||
{afterBatches.map((b) => (
|
||||
<option key={b.id} value={b.id}>
|
||||
{batchOptLabel(b)}
|
||||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
) : (
|
||||
<p className="bs-cmp-config__hint bs-cmp-source__auto">{t("bizCompare.autoSourceHint")}</p>
|
||||
)}
|
||||
<div className="bs-cmp-source__meta">
|
||||
<span>{tasks.find((row) => row.id === (afterTaskId || beforeTaskId))?.ne_ip || "—"}</span>
|
||||
<span>{mode === "auto" ? t("bizCompare.sourceLatest") : t("bizCompare.sourceRows", { n: afterBatches.find((b) => b.id === afterBatchId)?.row_count?.toLocaleString() ?? "—" })}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderJobForm = () => (
|
||||
<div className="bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
||||
const renderBasicFields = () => (
|
||||
<div className="bs-cmp-config__fields">
|
||||
<label className="ui-field ui-field--full">
|
||||
<span className="ui-field__label">{t("bizCompare.jobName")}</span>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
|
|
@ -2421,15 +2529,33 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
<option value="auto">{t("bizCompare.modeAuto")}</option>
|
||||
</FieldSelect>
|
||||
{renderStoreUnchangedField()}
|
||||
<p className="muted bm-hint">{t("bizCompare.storeUnchangedHint")}</p>
|
||||
{renderSheetChips()}
|
||||
{renderBatchFields()}
|
||||
{renderMappingBlock()}
|
||||
<p className="bs-cmp-config__hint is-wide">{t("bizCompare.storeUnchangedHint")}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderJobForm = () => (
|
||||
<div className="bs-cmp-form bs-cmp-config" role="tabpanel" id="compare-config-panel" aria-labelledby="compare-config-tab">
|
||||
<section className="bs-cmp-config__section is-wide" aria-labelledby="compare-basic-title">
|
||||
<h3 className="bs-cmp-config__title" id="compare-basic-title"><span>01</span>{t("bizCompare.configBasics")}</h3>
|
||||
{renderBasicFields()}
|
||||
</section>
|
||||
<section className="bs-cmp-config__section is-wide" aria-labelledby="compare-source-title">
|
||||
<h3 className="bs-cmp-config__title" id="compare-source-title"><span>02</span>{t("bizCompare.configSources")}</h3>
|
||||
{renderBatchFields()}
|
||||
</section>
|
||||
<section className="bs-cmp-config__section" aria-labelledby="compare-scope-title">
|
||||
<h3 className="bs-cmp-config__title" id="compare-scope-title"><span>03</span>{t("bizCompare.configScope")}</h3>
|
||||
{renderSheetChips()}
|
||||
</section>
|
||||
<section className="bs-cmp-config__section" aria-labelledby="compare-mapping-title">
|
||||
<h3 className="bs-cmp-config__title" id="compare-mapping-title"><span>04</span>{t("bizCompare.mapping")}</h3>
|
||||
{renderMappingBlock()}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="panel nm-page-panel">
|
||||
<section className={`panel nm-page-panel${activeTab === "jobs" ? " bs-cmp-workspace" : ""}`}>
|
||||
<div className="panel__toolbar">
|
||||
<h2>
|
||||
{pageMode === "templates" ? t("bizCompare.templates") : t("bizCompare.title")}
|
||||
|
|
@ -2492,6 +2618,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
<div className="filter-inline">
|
||||
<Input
|
||||
value={listKw}
|
||||
aria-label={activeTab === "jobs" ? t("bizCompare.jobFilterPh") : t("bizCompare.templateFilterPh")}
|
||||
placeholder={
|
||||
activeTab === "jobs" ? t("bizCompare.jobFilterPh") : t("bizCompare.templateFilterPh")
|
||||
}
|
||||
|
|
@ -2591,14 +2718,17 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
<tr key={j.id}>
|
||||
<td>
|
||||
<div className="pt-list-task-name">{j.name}</div>
|
||||
<code className="bs-cmp-runs__id" title={j.id}>{j.id.slice(0, 12)}</code>
|
||||
</td>
|
||||
<td className="muted">
|
||||
{tpl?.name || j.template_id.slice(0, 8)}
|
||||
{n ? ` · ${n} ${t("bizCompare.sheetsUnit")}` : ""}
|
||||
</td>
|
||||
<td>{j.mode}</td>
|
||||
<td>{j.mode === "auto" ? t("bizCompare.modeAuto") : t("bizCompare.modeManual")}</td>
|
||||
<td>
|
||||
<NmStatusChip color={jobChipColor(j.status)}>{j.status}</NmStatusChip>
|
||||
<NmStatusChip color={jobChipColor(j.status)}>
|
||||
{j.status === "ready" ? t("bizCompare.jobReady") : j.status === "auto" ? t("bizCompare.modeAuto") : j.status}
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
<div className="pt-list-actions">
|
||||
|
|
@ -2607,7 +2737,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
variant="ghost"
|
||||
className="bs-cmp-runs__delete"
|
||||
isDisabled={busy}
|
||||
onPress={() => void removeJob(j.id)}
|
||||
>
|
||||
|
|
@ -3014,7 +3145,7 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
open={jobCreateOpen}
|
||||
onClose={closeCreateJob}
|
||||
size="lg"
|
||||
className="bm-create-modal"
|
||||
className="bm-create-modal bs-cmp-create"
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>{t("bizCompare.createCompare")}</Modal.Heading>
|
||||
|
|
@ -3059,45 +3190,12 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
})}
|
||||
</nav>
|
||||
|
||||
{createStep === 0 ? (
|
||||
<div className="bm-create__pane bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
||||
<label className="ui-field ui-field--full">
|
||||
<span className="ui-field__label">{t("bizCompare.jobName")}</span>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</label>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.template")}
|
||||
value={templateId}
|
||||
onChange={(e) => setJobTemplateAndSheets(e.target.value)}
|
||||
fullWidth
|
||||
>
|
||||
{templates.map((tpl) => {
|
||||
const n = templateSheets(tpl).length;
|
||||
return (
|
||||
<option key={tpl.id} value={tpl.id}>
|
||||
{tpl.name} ({n} {t("bizCompare.sheetsUnit")})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</FieldSelect>
|
||||
<FieldSelect
|
||||
label={t("bizCompare.mode")}
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value as "manual" | "auto")}
|
||||
fullWidth
|
||||
>
|
||||
<option value="manual">{t("bizCompare.modeManual")}</option>
|
||||
<option value="auto">{t("bizCompare.modeAuto")}</option>
|
||||
</FieldSelect>
|
||||
{renderStoreUnchangedField()}
|
||||
<p className="muted bm-hint">{t("bizCompare.storeUnchangedHint")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{createStep === 0 ? <div className="bm-create__pane">{renderBasicFields()}</div> : null}
|
||||
|
||||
{createStep === 1 ? <div className="bm-create__pane">{renderSheetChips()}</div> : null}
|
||||
|
||||
{createStep === 2 ? (
|
||||
<div className="bm-create__pane bs-cmp-form" style={{ display: "grid", gap: 8 }}>
|
||||
<div className="bm-create__pane bs-cmp-form">
|
||||
{renderBatchFields()}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -3132,68 +3230,98 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
onClose={closeJob}
|
||||
dismissible={!boardFs}
|
||||
size={jobDetailTab === "result" ? "cover" : "lg"}
|
||||
className={`app-heroui-modal--xl${jobDetailTab === "result" ? " bs-cmp-board-modal" : ""}`}
|
||||
className={`app-heroui-modal--xl bs-cmp-job-modal${jobDetailTab === "result" ? " bs-cmp-board-modal" : ""}`}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Heading>
|
||||
{name || t("bizCompare.detail")} · {jobId.slice(0, 8)}…
|
||||
<Modal.Header className="bs-cmp-job-header">
|
||||
<Modal.Heading className="bs-cmp-job-heading">
|
||||
<span className="bs-cmp-job-heading__id" title={jobId}>JOB / {jobId.slice(0, 12)}</span>
|
||||
<span>{name || t("bizCompare.detail")}</span>
|
||||
</Modal.Heading>
|
||||
<Modal.CloseTrigger />
|
||||
</Modal.Header>
|
||||
<Modal.Body className="flex flex-col gap-3 bs-cmp-job-body">
|
||||
<div className="btn-row nm-config-modal__tabs" role="tablist">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={jobDetailTab === "config" ? "primary" : "secondary"}
|
||||
className={jobDetailTab === "config" ? "is-active" : undefined}
|
||||
onPress={() => setJobDetailTab("config")}
|
||||
<div className="btn-row nm-config-modal__tabs bs-cmp-job-tabs" role="tablist" aria-label={t("bizCompare.detailTabs")} onKeyDown={handleJobTabKeyDown}>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="compare-config-tab"
|
||||
aria-selected={jobDetailTab === "config"}
|
||||
aria-controls="compare-config-panel"
|
||||
tabIndex={jobDetailTab === "config" ? 0 : -1}
|
||||
className={`button${jobDetailTab === "config" ? " is-active" : ""}`}
|
||||
onClick={() => setJobDetailTab("config")}
|
||||
>
|
||||
{t("bizCompare.tabConfig")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={jobDetailTab === "runs" ? "primary" : "secondary"}
|
||||
className={jobDetailTab === "runs" ? "is-active" : undefined}
|
||||
onPress={() => {
|
||||
<span className="bs-cmp-job-tabs__index" aria-hidden="true">01</span>{t("bizCompare.tabConfig")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="compare-runs-tab"
|
||||
aria-label={t("bizCompare.tabRuns")}
|
||||
aria-selected={jobDetailTab === "runs"}
|
||||
aria-controls="compare-runs-panel"
|
||||
tabIndex={jobDetailTab === "runs" ? 0 : -1}
|
||||
className={`button${jobDetailTab === "runs" ? " is-active" : ""}`}
|
||||
onClick={() => {
|
||||
setJobDetailTab("runs");
|
||||
const request = jobRequestRef.current;
|
||||
if (jobId) void bizCompareListRuns(jobId, 50).then((r) => {
|
||||
if (request === jobRequestRef.current) setRuns(r.items || []);
|
||||
}).catch((e) => {
|
||||
if (request === jobRequestRef.current) showError(formatErr(e));
|
||||
});
|
||||
void refreshRuns();
|
||||
}}
|
||||
>
|
||||
{t("bizCompare.tabRuns")}
|
||||
{runs.length ? ` (${runs.length})` : ""}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant={jobDetailTab === "result" ? "primary" : "secondary"}
|
||||
className={jobDetailTab === "result" ? "is-active" : undefined}
|
||||
onPress={() => setJobDetailTab("result")}
|
||||
<span className="bs-cmp-job-tabs__index" aria-hidden="true">02</span>{t("bizCompare.tabRuns")}
|
||||
{runs.length ? <span className="bs-cmp-job-tabs__count">{runs.length}</span> : null}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="compare-result-tab"
|
||||
aria-selected={jobDetailTab === "result"}
|
||||
aria-controls="compare-result-panel"
|
||||
tabIndex={jobDetailTab === "result" ? 0 : -1}
|
||||
className={`button${jobDetailTab === "result" ? " is-active" : ""}`}
|
||||
onClick={() => setJobDetailTab("result")}
|
||||
>
|
||||
{t("bizCompare.tabResult")}
|
||||
</Button>
|
||||
<span className="bs-cmp-job-tabs__index" aria-hidden="true">03</span>{t("bizCompare.tabResult")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{jobDetailTab === "config" ? (
|
||||
renderJobForm()
|
||||
) : jobDetailTab === "runs" ? (
|
||||
<div className="bs-cmp-runs">
|
||||
<div className="bs-cmp-runs" role="tabpanel" id="compare-runs-panel" aria-labelledby="compare-runs-tab" aria-busy={runsLoading}>
|
||||
<div className="bs-cmp-runs__toolbar">
|
||||
<p className="muted bm-hint bs-cmp-runs__hint">{t("bizCompare.runsHint")}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
isDisabled={busy || jobHasActiveRun}
|
||||
onPress={() => void runNow()}
|
||||
>
|
||||
{jobHasActiveRun ? t("bizCompare.runStatusRunning") : t("bizCompare.runNow")}
|
||||
</Button>
|
||||
<div className="bs-cmp-runs__intro">
|
||||
<h3>{t("bizCompare.runHistoryTitle")}</h3>
|
||||
<p className="bs-cmp-runs__hint">{t("bizCompare.runHistoryHint")}</p>
|
||||
</div>
|
||||
<div className="btn-row">
|
||||
<Button size="sm" variant="secondary" isDisabled={busy || runsLoading} isPending={runsLoading} onPress={() => void refreshRuns()}>
|
||||
{t("bizCompare.refreshRuns")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
isDisabled={busy || jobHasActiveRun}
|
||||
onPress={() => void runNow()}
|
||||
>
|
||||
{jobHasActiveRun ? t("bizCompare.runStatusRunning") : t("bizCompare.runNow")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="bs-cmp-run-stats">
|
||||
<div><dt>{t("bizCompare.loadedRuns")}</dt><dd>{runCounts.total}</dd></div>
|
||||
<div className={runCounts.active ? "is-active" : ""}><dt>{t("bizCompare.activeRuns")}</dt><dd>{runCounts.active}</dd></div>
|
||||
<div><dt>{t("bizCompare.completedRuns")}</dt><dd>{runCounts.completed}</dd></div>
|
||||
<div className={runCounts.stopped ? "is-stopped" : ""}><dt>{t("bizCompare.stoppedRuns")}</dt><dd>{runCounts.stopped}</dd></div>
|
||||
</dl>
|
||||
{runsError ? (
|
||||
<div className="bs-cmp-runs__error" role="alert">
|
||||
<span>{t("bizCompare.runsLoadError")} · {runsError}</span>
|
||||
<Button size="sm" variant="secondary" isDisabled={runsLoading} onPress={() => void refreshRuns()}>{t("bizCompare.retry")}</Button>
|
||||
</div>
|
||||
) : null}
|
||||
{!runs.length ? (
|
||||
<div className="pt-list-empty">{t("bizCompare.noRuns")}</div>
|
||||
runsLoading ? <div className="pt-list-empty" role="status">{t("bizCompare.runsLoading")}</div>
|
||||
: !runsError ? <div className="pt-list-empty">{t("bizCompare.noRuns")}</div> : null
|
||||
) : (
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table bs-cmp-runs__table">
|
||||
|
|
@ -3208,68 +3336,64 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</thead>
|
||||
<tbody>
|
||||
{runs.map((r) => {
|
||||
const st = String((r as any).status || "");
|
||||
const st = String(r.status || "");
|
||||
const active = st === "running" || st === "queued";
|
||||
const selected = String(runDetail?.id || "") === String(r.id);
|
||||
const before = enrichSide(
|
||||
(r as any).before,
|
||||
r.before,
|
||||
beforeTaskId,
|
||||
tasks,
|
||||
beforeBatches,
|
||||
);
|
||||
const after = enrichSide(
|
||||
(r as any).after,
|
||||
r.after,
|
||||
afterTaskId || beforeTaskId,
|
||||
tasks,
|
||||
afterBatches.length ? afterBatches : beforeBatches,
|
||||
);
|
||||
const when = formatSystemTime((r as any).created_at) || "—";
|
||||
const sum = ((r as any).summary || {}) as Record<string, number>;
|
||||
const when = formatSystemTime(r.created_at) || "—";
|
||||
const sum = r.summary || {};
|
||||
const fail =
|
||||
Number(sum.removed || 0) + Number(sum.changed || 0);
|
||||
const ok = Number(sum.unchanged || 0);
|
||||
const durMs = Number(sum.duration_ms || 0);
|
||||
const prog = ((r as any).progress ||
|
||||
(r as any).summary?.progress ||
|
||||
const prog = (r.progress ||
|
||||
r.summary?.progress ||
|
||||
{}) as RunProgressInfo;
|
||||
const pct = active ? runProgressPercent(prog) : 100;
|
||||
const elapsedSec = Math.round(Number(prog.elapsed_ms || 0) / 1000);
|
||||
const stLabel =
|
||||
st === "running" || st === "queued"
|
||||
? t("bizCompare.runStatusRunning")
|
||||
st === "queued" ? t("bizCompare.runStatusQueued")
|
||||
: st === "running" ? t("bizCompare.runStatusRunning")
|
||||
: st === "failed"
|
||||
? t("bizCompare.runStatusFailed")
|
||||
: st === "cancelled"
|
||||
? t("bizCompare.runStatusCancelled")
|
||||
: t("bizCompare.runStatusDoneSec", {
|
||||
s: String(durMs > 0 ? Math.round(durMs / 1000) : 0),
|
||||
});
|
||||
: st === "success" ? t("bizCompare.runFinished") : st || "—";
|
||||
const chipColor = active
|
||||
? "accent"
|
||||
: st === "cancelled"
|
||||
? "warning"
|
||||
: jobChipColor(st);
|
||||
: st === "failed" ? "danger" : "default";
|
||||
const meta = active ? runProgressMetaLine(prog, t) : "";
|
||||
return (
|
||||
<tr key={String(r.id)} className={selected ? "is-selected" : undefined}>
|
||||
<tr key={String(r.id)} className={selected ? "is-selected" : undefined} data-status={st} aria-selected={selected}>
|
||||
<td>
|
||||
<NmStatusChip color={chipColor}>{stLabel}</NmStatusChip>
|
||||
{active && elapsedSec > 0 ? (
|
||||
{(active && elapsedSec > 0) || (!active && durMs > 0) ? (
|
||||
<div className="bs-cmp-runs__elapsed">
|
||||
{t("bizCompare.runElapsed", { s: String(elapsedSec) })}
|
||||
{t("bizCompare.runElapsed", { s: String(active ? elapsedSec : Math.round(durMs / 1000)) })}
|
||||
</div>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="pt-list-time">{when}</td>
|
||||
<td className="pt-list-time">
|
||||
<span>{when}</span>
|
||||
<code className="bs-cmp-runs__id" title={r.id}>{r.id.slice(0, 12)}</code>
|
||||
</td>
|
||||
<td>
|
||||
<div className="bs-cmp-runs__sides" title={`${sideDeviceName(before)} → ${sideDeviceName(after)}`}>
|
||||
<span>{sideDeviceName(before)}</span>
|
||||
<span className="muted">{sideCollectTime(before)}</span>
|
||||
<span className="bs-cmp-runs__arrow" aria-hidden>
|
||||
→
|
||||
</span>
|
||||
<span>{sideDeviceName(after)}</span>
|
||||
<span className="muted">{sideCollectTime(after)}</span>
|
||||
<div><span className="bs-cmp-runs__side-tag">{t("bizCompare.sideBefore")}</span><strong>{sideDeviceName(before)}</strong><time>{sideCollectTime(before)}</time></div>
|
||||
<div><span className="bs-cmp-runs__side-tag">{t("bizCompare.sideAfter")}</span><strong>{sideDeviceName(after)}</strong><time>{sideCollectTime(after)}</time></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
|
@ -3278,7 +3402,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
<div
|
||||
className="bs-cmp-run-prog__track"
|
||||
role="progressbar"
|
||||
aria-valuenow={pct}
|
||||
aria-label={t("bizCompare.colProgress")}
|
||||
aria-valuenow={Number(prog.sheet_total || 0) > 0 ? pct : undefined}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
|
|
@ -3298,22 +3423,22 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bs-cmp-runs__summary">
|
||||
{st === "failed" || st === "cancelled"
|
||||
? String((r as any).message || stLabel).slice(0, 96)
|
||||
: t("bizCompare.runBatchSummary", {
|
||||
fail: String(fail),
|
||||
ok: String(ok),
|
||||
added: String(Number(sum.added || 0)),
|
||||
})}
|
||||
</div>
|
||||
st === "failed" || st === "cancelled" ? (
|
||||
<div className="bs-cmp-runs__summary is-stopped" title={r.message}>{String(r.message || stLabel).slice(0, 160)}</div>
|
||||
) : (
|
||||
<div className="bs-cmp-run-verdict">
|
||||
<span className={fail ? "is-fail" : ""}>{t("bizCompare.kindDiff")}<strong>{fail.toLocaleString()}</strong></span>
|
||||
<span className="is-pass">{t("bizCompare.kindUnchanged")}<strong>{ok.toLocaleString()}</strong></span>
|
||||
<span>{t("bizCompare.kindAdded")}<strong>{Number(sum.added || 0).toLocaleString()}</strong></span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="pt-list-actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
variant={selected ? "primary" : "secondary"}
|
||||
isDisabled={busy || active}
|
||||
onPress={() => void loadRun(String(r.id))}
|
||||
>
|
||||
|
|
@ -3331,7 +3456,8 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
variant="ghost"
|
||||
className="bs-cmp-runs__delete"
|
||||
isDisabled={busy}
|
||||
onPress={() => void removeRun(String(r.id))}
|
||||
>
|
||||
|
|
@ -3351,6 +3477,9 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
) : (
|
||||
<div
|
||||
ref={boardRef}
|
||||
role="tabpanel"
|
||||
id="compare-result-panel"
|
||||
aria-labelledby="compare-result-tab"
|
||||
className={`bs-cmp-board${boardFs ? " is-fullscreen" : ""}`}
|
||||
>
|
||||
<div className="bs-cmp-board__toolbar">
|
||||
|
|
@ -4214,36 +4343,27 @@ export function BizComparePage({ pageMode = "all" }: { pageMode?: BizComparePage
|
|||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Modal.Footer className="bs-cmp-job-footer">
|
||||
{jobDetailTab === "config" ? (
|
||||
<>
|
||||
<Button size="sm" variant="secondary" isDisabled={busy} onPress={() => void saveJobConfig()}>
|
||||
<Button size="sm" variant="primary" isDisabled={busy} onPress={() => void saveJobConfig()}>
|
||||
{t("bizCompare.saveJob")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
variant="secondary"
|
||||
isDisabled={busy || jobHasActiveRun}
|
||||
onPress={() => void runNow()}
|
||||
>
|
||||
{jobHasActiveRun ? t("bizCompare.runStatusRunning") : t("bizCompare.runNow")}
|
||||
</Button>
|
||||
{jobId ? (
|
||||
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeJob(jobId)}>
|
||||
<Button size="sm" variant="ghost" className="bs-cmp-runs__delete" isDisabled={busy} onPress={() => void removeJob(jobId)}>
|
||||
{t("bizCompare.deleteJob")}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
) : jobDetailTab === "runs" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
isDisabled={busy || jobHasActiveRun}
|
||||
onPress={() => void runNow()}
|
||||
>
|
||||
{jobHasActiveRun ? t("bizCompare.runStatusRunning") : t("bizCompare.runNow")}
|
||||
</Button>
|
||||
) : (
|
||||
) : jobDetailTab === "runs" ? null : (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue