Show that an operation is running
Apply Scale sat there live and silent for the whole round trip, so on a large slice the only sign anything was happening was the absence of a result -- and a second click applied the change twice. The button now spins, reads "Applying…", and refuses further clicks until the write returns, with a line beside it saying the pivot updates when it finishes. Cleared in a finally, so a failure releases it too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f2734ab5b2
commit
0d05dc3baa
@ -99,11 +99,15 @@ function Segmented({ options, value, onChange }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Submit({ onClick, children, disabled }) {
|
||||
function Submit({ onClick, children, disabled, busy }) {
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled}
|
||||
<button onClick={onClick} disabled={disabled || busy}
|
||||
className="self-start px-4 py-1.5 rounded text-xs font-medium bg-blue-600 text-white hover:bg-blue-700
|
||||
disabled:bg-gray-200 disabled:text-gray-600 disabled:cursor-not-allowed whitespace-nowrap">
|
||||
disabled:bg-gray-200 disabled:text-gray-600 disabled:cursor-not-allowed whitespace-nowrap
|
||||
inline-flex items-center gap-2">
|
||||
{busy && (
|
||||
<span className="inline-block w-3 h-3 rounded-full border-2 border-white/40 border-t-white animate-spin" />
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
@ -887,6 +891,7 @@ export default function OperationPanel({
|
||||
dock,
|
||||
slices, setSlices, distinctSlices,
|
||||
viewScope = [],
|
||||
opBusy = null,
|
||||
applyMode, setApplyMode,
|
||||
currentTotals,
|
||||
activeOp, setActiveOp,
|
||||
@ -912,6 +917,7 @@ export default function OperationPanel({
|
||||
const shifting = !!cloneOffset && cloneOffset.trim() !== '' && cloneOffset.trim() !== '0 days'
|
||||
const setNote = activeOp === 'scale' ? setScaleNote : activeOp === 'recode' ? setRecodeNote : setCloneNote
|
||||
const OP_LABEL = { scale: 'Apply Scale', recode: 'Apply Recode', clone: 'Apply Clone' }
|
||||
const OP_BUSY_LABEL = { scale: 'Applying…', recode: 'Recoding…', clone: 'Cloning…' }
|
||||
|
||||
return (
|
||||
<div className={horizontal ? 'flex flex-row items-start p-3 gap-5 min-w-0' : 'flex flex-col p-3 gap-3 min-w-0'}>
|
||||
@ -1084,8 +1090,15 @@ export default function OperationPanel({
|
||||
placeholder="optional" className={`${TEXT} w-48`} />
|
||||
</Field>
|
||||
|
||||
<div className="pt-1">
|
||||
<Submit onClick={() => submitOp(activeOp)}>{OP_LABEL[activeOp]}</Submit>
|
||||
<div className="pt-1 flex items-center gap-3">
|
||||
<Submit onClick={() => submitOp(activeOp)} busy={!!opBusy}>
|
||||
{opBusy ? OP_BUSY_LABEL[opBusy] : OP_LABEL[activeOp]}
|
||||
</Submit>
|
||||
{opBusy && (
|
||||
<span className="text-gray-500 leading-snug">
|
||||
Writing rows — the pivot updates when it finishes.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<RequestPreview payload={buildPayload(activeOp)} />
|
||||
</div>
|
||||
|
||||
@ -81,6 +81,8 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
// the same filter, for display: the panel has to show the scope it is acting
|
||||
// inside or the slice it prints is not the slice that gets written
|
||||
const [viewScope, setViewScope] = useState([])
|
||||
// which operation is in flight, so the panel can say so and refuse a second click
|
||||
const [opBusy, setOpBusy] = useState(null)
|
||||
const [msg, setMsg] = useState(null)
|
||||
|
||||
// layouts
|
||||
@ -1462,6 +1464,11 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
}
|
||||
}
|
||||
|
||||
// A large scale writes a row per coordinate and can run for a while, and
|
||||
// until now the button stayed live and said nothing -- so the only feedback
|
||||
// that anything was happening was the absence of feedback, and clicking
|
||||
// twice would apply it twice.
|
||||
setOpBusy(op)
|
||||
try {
|
||||
const res = await fetch(`/api/versions/${versionId}/${op}`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
||||
@ -1483,7 +1490,11 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
if (op === 'scale') { setScaleInputs({}); setScaleNote(''); fetchCurrentTotals(slices) }
|
||||
if (op === 'recode') { setRecodeNote('') }
|
||||
if (op === 'clone') { setCloneNote(''); setCloneScale('1') }
|
||||
} catch (err) { flash(err.message, 'error') }
|
||||
} catch (err) {
|
||||
flash(err.message, 'error')
|
||||
} finally {
|
||||
setOpBusy(null)
|
||||
}
|
||||
}
|
||||
|
||||
async function lookupDerivedCols(col, value, setter) {
|
||||
@ -1728,6 +1739,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
dock,
|
||||
slices, setSlices,
|
||||
viewScope,
|
||||
opBusy,
|
||||
applyMode, setApplyMode,
|
||||
currentTotals,
|
||||
activeOp, setActiveOp,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user