@mzzsfy/dsh-cron-board 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client.js +25 -6
- package/test/switch-guard.test.mjs +18 -0
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -679,7 +679,7 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
679
679
|
h('button', { className: 'cb-button cb-button--danger', onClick: onConfirm }, '删除')))
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
-
function JobsTab({ jobs, status, now, reload, wsModel }) {
|
|
682
|
+
function JobsTab({ jobs, status, now, reload, applyJobPatch, wsModel }) {
|
|
683
683
|
const [formJob, setFormJob] = useState(null)
|
|
684
684
|
const [notice, setNotice] = useState(null)
|
|
685
685
|
const [kindFilter, setKindFilter] = useState('all')
|
|
@@ -691,7 +691,13 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
691
691
|
setTimeout(reload, 300)
|
|
692
692
|
}
|
|
693
693
|
const toggleJob = async (job) => {
|
|
694
|
-
|
|
694
|
+
// 乐观翻转:受控开关即时到位;失败回滚并提示,不静默
|
|
695
|
+
applyJobPatch(job.id, { enabled: !job.enabled })
|
|
696
|
+
const outcome = await request('PATCH', 'jobs/' + job.id, { enabled: !job.enabled })
|
|
697
|
+
if (!outcome.ok) {
|
|
698
|
+
applyJobPatch(job.id, { enabled: job.enabled })
|
|
699
|
+
setNotice('切换失败:' + outcome.error)
|
|
700
|
+
}
|
|
695
701
|
reload()
|
|
696
702
|
}
|
|
697
703
|
const deleteJob = async (job) => {
|
|
@@ -811,14 +817,20 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
811
817
|
h('button', { className: 'cb-button cb-button--danger', onClick: () => doApply(true) }, '覆盖导入')))
|
|
812
818
|
}
|
|
813
819
|
|
|
814
|
-
function EnvsTab({ envs, reload }) {
|
|
820
|
+
function EnvsTab({ envs, reload, applyEnvPatch }) {
|
|
815
821
|
const [formRow, setFormRow] = useState(null)
|
|
816
822
|
const [importing, setImporting] = useState(false)
|
|
817
823
|
const [exportText, setExportText] = useState(null)
|
|
818
824
|
const [notice, setNotice] = useState(null)
|
|
819
825
|
|
|
820
826
|
const toggleEnv = async (row) => {
|
|
821
|
-
|
|
827
|
+
// 乐观翻转:受控开关即时到位;失败回滚并提示,不静默
|
|
828
|
+
applyEnvPatch(row.id, { enabled: !row.enabled })
|
|
829
|
+
const outcome = await request('PATCH', 'envs/' + row.id, { enabled: !row.enabled })
|
|
830
|
+
if (!outcome.ok) {
|
|
831
|
+
applyEnvPatch(row.id, { enabled: row.enabled })
|
|
832
|
+
setNotice('切换失败:' + outcome.error)
|
|
833
|
+
}
|
|
822
834
|
reload()
|
|
823
835
|
}
|
|
824
836
|
const deleteEnv = async (row) => {
|
|
@@ -921,6 +933,13 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
921
933
|
const [now, setNow] = useState(Date.now())
|
|
922
934
|
const [reloadFlag, setReloadFlag] = useState(0)
|
|
923
935
|
const reload = useCallback(() => setReloadFlag((value) => value + 1), [])
|
|
936
|
+
// 乐观更新:受控开关点击即时到位,PATCH 失败由调用方回滚;reload 拉权威态对账
|
|
937
|
+
const applyJobPatch = useCallback((id, patch) => {
|
|
938
|
+
setJobs((prev) => prev.map((job) => (job.id === id ? { ...job, ...patch } : job)))
|
|
939
|
+
}, [])
|
|
940
|
+
const applyEnvPatch = useCallback((id, patch) => {
|
|
941
|
+
setEnvs((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row)))
|
|
942
|
+
}, [])
|
|
924
943
|
|
|
925
944
|
useEffect(() => {
|
|
926
945
|
if (!active) return undefined
|
|
@@ -949,8 +968,8 @@ if (typeof window !== 'undefined' && window.__ModuleLoader__) {
|
|
|
949
968
|
status ? h('span', { className: 'cb-meta' },
|
|
950
969
|
'运行中 ' + status.scheduler.active + ' · 队列 ' + status.scheduler.queued +
|
|
951
970
|
(status.nextAt ? ' · 下次 ' + relativeTime(status.nextAt, now) : '')) : null),
|
|
952
|
-
tab === 'jobs' ? h(JobsTab, { key: 'jobs', jobs, status, now, reload, wsModel }) : null,
|
|
953
|
-
tab === 'envs' ? h(EnvsTab, { key: 'envs', envs, reload }) : null,
|
|
971
|
+
tab === 'jobs' ? h(JobsTab, { key: 'jobs', jobs, status, now, reload, applyJobPatch, wsModel }) : null,
|
|
972
|
+
tab === 'envs' ? h(EnvsTab, { key: 'envs', envs, reload, applyEnvPatch }) : null,
|
|
954
973
|
tab === 'logs' ? h(LogsTab, { key: 'logs', jobs, reload, reloadFlag }) : null)
|
|
955
974
|
}
|
|
956
975
|
|
|
@@ -60,6 +60,24 @@ test('client.js 无顶层词法声明(经典 script 书挡内安全)', () => {
|
|
|
60
60
|
assert.ok(trimmed.endsWith('})()'), '必须以 IIFE 书挡结尾')
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
+
test('启停开关乐观更新契约:即时翻转 + 失败回滚提示,禁止等待往返才反馈', () => {
|
|
64
|
+
// Given 受控 switch(checked 锁定 props)点击后无本地 state 变化不产生视觉反馈
|
|
65
|
+
// When toggleJob/toggleEnv 发起 PATCH
|
|
66
|
+
// Then 先乐观翻转,失败回滚并 notice,最后 reload 对账权威态(顺序锁定)
|
|
67
|
+
for (const fn of ['toggleJob', 'toggleEnv']) {
|
|
68
|
+
const start = source.indexOf('const ' + fn + ' = async')
|
|
69
|
+
assert.ok(start > 0, `缺少 ${fn}`)
|
|
70
|
+
const body = source.slice(start, source.indexOf('reload()', start))
|
|
71
|
+
assert.match(body, /apply(Job|Env)Patch\([^)]*, \{ enabled: !/m, `${fn} 必须先乐观翻转`)
|
|
72
|
+
assert.match(body, /if \(!outcome\.ok\)/m, `${fn} 必须检查 PATCH 结果`)
|
|
73
|
+
assert.match(body, /apply(Job|Env)Patch\([^)]*, \{ enabled: (job|row)\.enabled \}\)/m, `${fn} 失败必须回滚`)
|
|
74
|
+
assert.match(body, /切换失败:/m, `${fn} 失败必须提示,禁止静默`)
|
|
75
|
+
}
|
|
76
|
+
// 两个 Tab 均接收乐观更新通道(Board 下传)
|
|
77
|
+
assert.match(source, /applyJobPatch, wsModel/)
|
|
78
|
+
assert.match(source, /envs, reload, applyEnvPatch/)
|
|
79
|
+
})
|
|
80
|
+
|
|
63
81
|
test('client.js 主页面双形态挂载契约(默认主界面;设置开关手动移入侧边栏;无自动回退)', () => {
|
|
64
82
|
// Given 用户要求:默认永远主界面;检测到 better-sidebar 提供设置项,由用户手动移入;
|
|
65
83
|
// 移入后页签关闭/禁用不再自动返回主界面(挂载权完全交给用户与侧边栏)
|