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 {
|
.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;
|
font-size: 0.72rem;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-align: right;
|
opacity: 0;
|
||||||
white-space: nowrap;
|
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 {
|
.list {
|
||||||
|
|
|
||||||
|
|
@ -1127,6 +1127,7 @@ export const getOptionTableSnapshot = (
|
||||||
|
|
||||||
type ListScrollState = {
|
type ListScrollState = {
|
||||||
listRef: React.RefObject<HTMLDivElement>;
|
listRef: React.RefObject<HTMLDivElement>;
|
||||||
|
setListRef: (node: HTMLDivElement | null) => void;
|
||||||
isAtTop: boolean;
|
isAtTop: boolean;
|
||||||
isAtTopRef: React.MutableRefObject<boolean>;
|
isAtTopRef: React.MutableRefObject<boolean>;
|
||||||
missed: number;
|
missed: number;
|
||||||
|
|
@ -1137,12 +1138,18 @@ type ListScrollState = {
|
||||||
|
|
||||||
const useListScroll = (): ListScrollState => {
|
const useListScroll = (): ListScrollState => {
|
||||||
const listRef = useRef<HTMLDivElement | null>(null);
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [listNode, setListNode] = useState<HTMLDivElement | null>(null);
|
||||||
const [isAtTop, setIsAtTop] = useState(true);
|
const [isAtTop, setIsAtTop] = useState(true);
|
||||||
const [missed, setMissed] = useState(0);
|
const [missed, setMissed] = useState(0);
|
||||||
const [resumeTick, setResumeTick] = useState(0);
|
const [resumeTick, setResumeTick] = useState(0);
|
||||||
const isAtTopRef = useRef(true);
|
const isAtTopRef = useRef(true);
|
||||||
const prevAtTopRef = useRef(true);
|
const prevAtTopRef = useRef(true);
|
||||||
|
|
||||||
|
const setListRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
listRef.current = node;
|
||||||
|
setListNode(node);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isAtTopRef.current = isAtTop;
|
isAtTopRef.current = isAtTop;
|
||||||
}, [isAtTop]);
|
}, [isAtTop]);
|
||||||
|
|
@ -1169,8 +1176,7 @@ const useListScroll = (): ListScrollState => {
|
||||||
}, [isAtTopRef]);
|
}, [isAtTopRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = listRef.current;
|
if (!listNode) {
|
||||||
if (!el) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1179,12 +1185,12 @@ const useListScroll = (): ListScrollState => {
|
||||||
};
|
};
|
||||||
|
|
||||||
updateScrollState();
|
updateScrollState();
|
||||||
el.addEventListener("scroll", onScroll);
|
listNode.addEventListener("scroll", onScroll);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
el.removeEventListener("scroll", onScroll);
|
listNode.removeEventListener("scroll", onScroll);
|
||||||
};
|
};
|
||||||
}, [updateScrollState]);
|
}, [listNode, updateScrollState]);
|
||||||
|
|
||||||
const onNewItems = useCallback((count: number) => {
|
const onNewItems = useCallback((count: number) => {
|
||||||
if (count <= 0) {
|
if (count <= 0) {
|
||||||
|
|
@ -1212,6 +1218,7 @@ const useListScroll = (): ListScrollState => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
listRef,
|
listRef,
|
||||||
|
setListRef,
|
||||||
isAtTop,
|
isAtTop,
|
||||||
isAtTopRef,
|
isAtTopRef,
|
||||||
missed,
|
missed,
|
||||||
|
|
@ -1846,6 +1853,8 @@ type PausableTapeViewConfig<T extends SortableItem & { seq: number }> = {
|
||||||
captureScroll?: () => void;
|
captureScroll?: () => void;
|
||||||
getItemTs?: (item: T) => number;
|
getItemTs?: (item: T) => number;
|
||||||
retentionLimit?: number;
|
retentionLimit?: number;
|
||||||
|
shouldHold?: () => boolean;
|
||||||
|
resumeSignal?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||||
|
|
@ -1872,11 +1881,12 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const holdForScroll = config.shouldHold ? config.shouldHold() : false;
|
||||||
setData((current) => {
|
setData((current) => {
|
||||||
const next = reducePausableTapeData(
|
const next = reducePausableTapeData(
|
||||||
current,
|
current,
|
||||||
config.sourceItems,
|
config.sourceItems,
|
||||||
paused,
|
paused || holdForScroll,
|
||||||
config.retentionLimit ?? LIVE_HOT_WINDOW
|
config.retentionLimit ?? LIVE_HOT_WINDOW
|
||||||
);
|
);
|
||||||
if (next === current) {
|
if (next === current) {
|
||||||
|
|
@ -1897,6 +1907,7 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||||
config.onNewItems,
|
config.onNewItems,
|
||||||
config.captureScroll,
|
config.captureScroll,
|
||||||
config.retentionLimit,
|
config.retentionLimit,
|
||||||
|
config.shouldHold,
|
||||||
paused
|
paused
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -1905,6 +1916,11 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const holdForScroll = config.shouldHold ? config.shouldHold() : false;
|
||||||
|
if (holdForScroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setData((current) => {
|
setData((current) => {
|
||||||
const next = flushPausableTapeData(current, config.retentionLimit ?? LIVE_HOT_WINDOW);
|
const next = flushPausableTapeData(current, config.retentionLimit ?? LIVE_HOT_WINDOW);
|
||||||
if (next === current) {
|
if (next === current) {
|
||||||
|
|
@ -1918,7 +1934,15 @@ const usePausableTapeView = <T extends SortableItem & { seq: number }>(
|
||||||
|
|
||||||
return next;
|
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(() => {
|
const togglePause = useCallback(() => {
|
||||||
setPaused((current) => !current);
|
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}>
|
<button className="jump-button" type="button" onClick={onJump} disabled={isAtTop}>
|
||||||
Jump to top
|
Jump to top
|
||||||
</button>
|
</button>
|
||||||
<span className="missed-count">{active ? `+${missed} new` : ""}</span>
|
<span className={`missed-count${active ? " missed-count-visible" : ""}`} aria-hidden={!active}>
|
||||||
|
+{missed} new
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -4233,7 +4259,9 @@ const useTerminalState = () => {
|
||||||
freshnessMs: LIVE_OPTIONS_STALE_MS,
|
freshnessMs: LIVE_OPTIONS_STALE_MS,
|
||||||
retentionLimit: LIVE_HOT_WINDOW_OPTIONS,
|
retentionLimit: LIVE_HOT_WINDOW_OPTIONS,
|
||||||
captureScroll: optionsAnchor.capture,
|
captureScroll: optionsAnchor.capture,
|
||||||
onNewItems: optionsScroll.onNewItems
|
onNewItems: optionsScroll.onNewItems,
|
||||||
|
shouldHold: () => !optionsScroll.isAtTopRef.current,
|
||||||
|
resumeSignal: optionsScroll.resumeTick
|
||||||
});
|
});
|
||||||
const liveEquities = usePausableTapeView<EquityPrint>({
|
const liveEquities = usePausableTapeView<EquityPrint>({
|
||||||
enabled: mode === "live",
|
enabled: mode === "live",
|
||||||
|
|
@ -4242,7 +4270,9 @@ const useTerminalState = () => {
|
||||||
lastUpdate: liveSession.lastUpdate,
|
lastUpdate: liveSession.lastUpdate,
|
||||||
freshnessMs: LIVE_EQUITIES_STALE_MS,
|
freshnessMs: LIVE_EQUITIES_STALE_MS,
|
||||||
captureScroll: equitiesAnchor.capture,
|
captureScroll: equitiesAnchor.capture,
|
||||||
onNewItems: equitiesScroll.onNewItems
|
onNewItems: equitiesScroll.onNewItems,
|
||||||
|
shouldHold: () => !equitiesScroll.isAtTopRef.current,
|
||||||
|
resumeSignal: equitiesScroll.resumeTick
|
||||||
});
|
});
|
||||||
const liveFlow = usePausableTapeView<FlowPacket>({
|
const liveFlow = usePausableTapeView<FlowPacket>({
|
||||||
enabled: mode === "live",
|
enabled: mode === "live",
|
||||||
|
|
@ -4252,6 +4282,8 @@ const useTerminalState = () => {
|
||||||
freshnessMs: LIVE_FLOW_STALE_MS,
|
freshnessMs: LIVE_FLOW_STALE_MS,
|
||||||
captureScroll: flowAnchor.capture,
|
captureScroll: flowAnchor.capture,
|
||||||
onNewItems: flowScroll.onNewItems,
|
onNewItems: flowScroll.onNewItems,
|
||||||
|
shouldHold: () => !flowScroll.isAtTopRef.current,
|
||||||
|
resumeSignal: flowScroll.resumeTick,
|
||||||
getItemTs: (item) => item.source_ts
|
getItemTs: (item) => item.source_ts
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -5494,7 +5526,7 @@ const OptionsPane = ({ limit }: OptionsPaneProps) => {
|
||||||
: "Replay queue empty. Ensure ClickHouse has data."}
|
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||||
</div>
|
</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 data-table-options" role="table" aria-label="Options tape">
|
||||||
<div className="data-table-head" role="row">
|
<div className="data-table-head" role="row">
|
||||||
<span className="data-table-cell">TIME</span>
|
<span className="data-table-cell">TIME</span>
|
||||||
|
|
@ -5660,7 +5692,7 @@ const EquitiesPane = ({ limit }: EquitiesPaneProps) => {
|
||||||
: "Replay queue empty. Ensure ClickHouse has data."}
|
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||||
</div>
|
</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 data-table-equities" role="table" aria-label="Equity prints">
|
||||||
<div className="data-table-head" role="row">
|
<div className="data-table-head" role="row">
|
||||||
<span className="data-table-cell">TIME</span>
|
<span className="data-table-cell">TIME</span>
|
||||||
|
|
@ -5753,7 +5785,7 @@ const FlowPane = ({ limit, title = "Flow" }: FlowPaneProps) => {
|
||||||
: "Replay queue empty. Ensure ClickHouse has data."}
|
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||||
</div>
|
</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 data-table-flow" role="table" aria-label="Flow packets">
|
||||||
<div className="data-table-head" role="row">
|
<div className="data-table-head" role="row">
|
||||||
<span className="data-table-cell">TIME</span>
|
<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."}
|
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||||
</div>
|
</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 data-table-alerts" role="table" aria-label="Alerts">
|
||||||
<div className="data-table-head" role="row">
|
<div className="data-table-head" role="row">
|
||||||
<span className="data-table-cell">TIME</span>
|
<span className="data-table-cell">TIME</span>
|
||||||
|
|
@ -5988,7 +6020,7 @@ const ClassifierPane = ({ limit, className }: ClassifierPaneProps) => {
|
||||||
: "Replay queue empty. Ensure ClickHouse has data."}
|
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||||
</div>
|
</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 data-table-classifier" role="table" aria-label="Classifier hits">
|
||||||
<div className="data-table-head" role="row">
|
<div className="data-table-head" role="row">
|
||||||
<span className="data-table-cell">TIME</span>
|
<span className="data-table-cell">TIME</span>
|
||||||
|
|
@ -6073,7 +6105,7 @@ const DarkPane = ({ limit, className }: DarkPaneProps) => {
|
||||||
: "Replay queue empty. Ensure ClickHouse has data."}
|
: "Replay queue empty. Ensure ClickHouse has data."}
|
||||||
</div>
|
</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 data-table-dark" role="table" aria-label="Dark events">
|
||||||
<div className="data-table-head" role="row">
|
<div className="data-table-head" role="row">
|
||||||
<span className="data-table-cell">TIME</span>
|
<span className="data-table-cell">TIME</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue