Hold live tape updates while scrolled away
- pause incoming rows when a tape is not at top - show missed item count only while new data is hidden - wire list refs so scroll state can resume cleanly
This commit is contained in:
parent
623f7df113
commit
749528c114
2 changed files with 66 additions and 19 deletions
|
|
@ -835,11 +835,26 @@ h3 {
|
|||
}
|
||||
|
||||
.missed-count {
|
||||
width: 86px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 0.72rem;
|
||||
color: var(--accent);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transform: translateX(6px);
|
||||
transition:
|
||||
max-width 180ms ease,
|
||||
opacity 140ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.missed-count-visible {
|
||||
max-width: 92px;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.list {
|
||||
|
|
|
|||
|
|
@ -1127,6 +1127,7 @@ export const getOptionTableSnapshot = (
|
|||
|
||||
type ListScrollState = {
|
||||
listRef: React.RefObject<HTMLDivElement>;
|
||||
setListRef: (node: HTMLDivElement | null) => void;
|
||||
isAtTop: boolean;
|
||||
isAtTopRef: React.MutableRefObject<boolean>;
|
||||
missed: number;
|
||||
|
|
@ -1137,12 +1138,18 @@ type ListScrollState = {
|
|||
|
||||
const useListScroll = (): ListScrollState => {
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const [listNode, setListNode] = useState<HTMLDivElement | null>(null);
|
||||
const [isAtTop, setIsAtTop] = useState(true);
|
||||
const [missed, setMissed] = useState(0);
|
||||
const [resumeTick, setResumeTick] = useState(0);
|
||||
const isAtTopRef = useRef(true);
|
||||
const prevAtTopRef = useRef(true);
|
||||
|
||||
const setListRef = useCallback((node: HTMLDivElement | null) => {
|
||||
listRef.current = node;
|
||||
setListNode(node);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
isAtTopRef.current = isAtTop;
|
||||
}, [isAtTop]);
|
||||
|
|
@ -1169,8 +1176,7 @@ const useListScroll = (): ListScrollState => {
|
|||
}, [isAtTopRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = listRef.current;
|
||||
if (!el) {
|
||||
if (!listNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1179,12 +1185,12 @@ const useListScroll = (): ListScrollState => {
|
|||
};
|
||||
|
||||
updateScrollState();
|
||||
el.addEventListener("scroll", onScroll);
|
||||
listNode.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener("scroll", onScroll);
|
||||
listNode.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [updateScrollState]);
|
||||
}, [listNode, updateScrollState]);
|
||||
|
||||
const onNewItems = useCallback((count: number) => {
|
||||
if (count <= 0) {
|
||||
|
|
@ -1212,6 +1218,7 @@ const useListScroll = (): ListScrollState => {
|
|||
|
||||
return {
|
||||
listRef,
|
||||
setListRef,
|
||||
isAtTop,
|
||||
isAtTopRef,
|
||||
missed,
|
||||
|
|
@ -1846,6 +1853,8 @@ type PausableTapeViewConfig<T extends SortableItem & { seq: number }> = {
|
|||
captureScroll?: () => void;
|
||||
getItemTs?: (item: T) => number;
|
||||
retentionLimit?: number;
|
||||
shouldHold?: () => boolean;
|
||||
resumeSignal?: number;
|
||||
};
|
||||
|
||||
const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||
|
|
@ -1872,11 +1881,12 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
return;
|
||||
}
|
||||
|
||||
const holdForScroll = config.shouldHold ? config.shouldHold() : false;
|
||||
setData((current) => {
|
||||
const next = reducePausableTapeData(
|
||||
current,
|
||||
config.sourceItems,
|
||||
paused,
|
||||
paused || holdForScroll,
|
||||
config.retentionLimit ?? LIVE_HOT_WINDOW
|
||||
);
|
||||
if (next === current) {
|
||||
|
|
@ -1897,6 +1907,7 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
config.onNewItems,
|
||||
config.captureScroll,
|
||||
config.retentionLimit,
|
||||
config.shouldHold,
|
||||
paused
|
||||
]);
|
||||
|
||||
|
|
@ -1905,6 +1916,11 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
return;
|
||||
}
|
||||
|
||||
const holdForScroll = config.shouldHold ? config.shouldHold() : false;
|
||||
if (holdForScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
setData((current) => {
|
||||
const next = flushPausableTapeData(current, config.retentionLimit ?? LIVE_HOT_WINDOW);
|
||||
if (next === current) {
|
||||
|
|
@ -1918,7 +1934,15 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
|||
|
||||
return next;
|
||||
});
|
||||
}, [config.captureScroll, config.enabled, config.onNewItems, config.retentionLimit, paused]);
|
||||
}, [
|
||||
config.captureScroll,
|
||||
config.enabled,
|
||||
config.onNewItems,
|
||||
config.retentionLimit,
|
||||
config.resumeSignal,
|
||||
config.shouldHold,
|
||||
paused
|
||||
]);
|
||||
|
||||
const togglePause = useCallback(() => {
|
||||
setPaused((current) => !current);
|
||||
|
|
@ -2841,7 +2865,9 @@ const TapeControls = ({ paused, onTogglePause, isAtTop, missed, onJump }: TapeCo
|
|||
<button className="jump-button" type="button" onClick={onJump} disabled={isAtTop}>
|
||||
Jump to top
|
||||
</button>
|
||||
<span className="missed-count">{active ? `+${missed} new` : ""}</span>
|
||||
<span className={`missed-count${active ? " missed-count-visible" : ""}`} aria-hidden={!active}>
|
||||
+{missed} new
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -4233,7 +4259,9 @@ const useTerminalState = () => {
|
|||
freshnessMs: LIVE_OPTIONS_STALE_MS,
|
||||
retentionLimit: LIVE_HOT_WINDOW_OPTIONS,
|
||||
captureScroll: optionsAnchor.capture,
|
||||
onNewItems: optionsScroll.onNewItems
|
||||
onNewItems: optionsScroll.onNewItems,
|
||||
shouldHold: () => !optionsScroll.isAtTopRef.current,
|
||||
resumeSignal: optionsScroll.resumeTick
|
||||
});
|
||||
const liveEquities = usePausableTapeView<EquityPrint>({
|
||||
enabled: mode === "live",
|
||||
|
|
@ -4242,7 +4270,9 @@ const useTerminalState = () => {
|
|||
lastUpdate: liveSession.lastUpdate,
|
||||
freshnessMs: LIVE_EQUITIES_STALE_MS,
|
||||
captureScroll: equitiesAnchor.capture,
|
||||
onNewItems: equitiesScroll.onNewItems
|
||||
onNewItems: equitiesScroll.onNewItems,
|
||||
shouldHold: () => !equitiesScroll.isAtTopRef.current,
|
||||
resumeSignal: equitiesScroll.resumeTick
|
||||
});
|
||||
const liveFlow = usePausableTapeView<FlowPacket>({
|
||||
enabled: mode === "live",
|
||||
|
|
@ -4252,6 +4282,8 @@ const useTerminalState = () => {
|
|||
freshnessMs: LIVE_FLOW_STALE_MS,
|
||||
captureScroll: flowAnchor.capture,
|
||||
onNewItems: flowScroll.onNewItems,
|
||||
shouldHold: () => !flowScroll.isAtTopRef.current,
|
||||
resumeSignal: flowScroll.resumeTick,
|
||||
getItemTs: (item) => item.source_ts
|
||||
});
|
||||
|
||||
|
|
@ -5494,7 +5526,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
|
|||
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table-wrap" ref={state.optionsScroll.listRef}>
|
||||
<div className="data-table-wrap" ref={state.optionsScroll.setListRef}>
|
||||
<div className="data-table data-table-options" role="table" aria-label="Options tape">
|
||||
<div className="data-table-head" role="row">
|
||||
<span className="data-table-cell">TIME</span>
|
||||
|
|
@ -5660,7 +5692,7 @@ const EquitiesPane = ({ limit }: EquitiesPaneProps) => {
|
|||
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table-wrap" ref={state.equitiesScroll.listRef}>
|
||||
<div className="data-table-wrap" ref={state.equitiesScroll.setListRef}>
|
||||
<div className="data-table data-table-equities" role="table" aria-label="Equity prints">
|
||||
<div className="data-table-head" role="row">
|
||||
<span className="data-table-cell">TIME</span>
|
||||
|
|
@ -5753,7 +5785,7 @@ const FlowPane = ({ limit, title = "Flow" }: FlowPaneProps) => {
|
|||
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table-wrap" ref={state.flowScroll.listRef}>
|
||||
<div className="data-table-wrap" ref={state.flowScroll.setListRef}>
|
||||
<div className="data-table data-table-flow" role="table" aria-label="Flow packets">
|
||||
<div className="data-table-head" role="row">
|
||||
<span className="data-table-cell">TIME</span>
|
||||
|
|
@ -5892,7 +5924,7 @@ const AlertsPane = ({ limit, withStrip = false, className }: AlertsPaneProps) =>
|
|||
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table-wrap" ref={state.alertsScroll.listRef}>
|
||||
<div className="data-table-wrap" ref={state.alertsScroll.setListRef}>
|
||||
<div className="data-table data-table-alerts" role="table" aria-label="Alerts">
|
||||
<div className="data-table-head" role="row">
|
||||
<span className="data-table-cell">TIME</span>
|
||||
|
|
@ -5988,7 +6020,7 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
|
|||
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table-wrap" ref={state.classifierScroll.listRef}>
|
||||
<div className="data-table-wrap" ref={state.classifierScroll.setListRef}>
|
||||
<div className="data-table data-table-classifier" role="table" aria-label="Classifier hits">
|
||||
<div className="data-table-head" role="row">
|
||||
<span className="data-table-cell">TIME</span>
|
||||
|
|
@ -6073,7 +6105,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => {
|
|||
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table-wrap" ref={state.darkScroll.listRef}>
|
||||
<div className="data-table-wrap" ref={state.darkScroll.setListRef}>
|
||||
<div className="data-table data-table-dark" role="table" aria-label="Dark events">
|
||||
<div className="data-table-head" role="row">
|
||||
<span className="data-table-cell">TIME</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue