Add tape pause toggles
This commit is contained in:
parent
053e8e6cea
commit
fd175260c9
2 changed files with 82 additions and 9 deletions
|
|
@ -109,6 +109,10 @@ h1 {
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-paused {
|
||||||
|
background: #fff3e4;
|
||||||
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
|
|
@ -135,6 +139,29 @@ h1 {
|
||||||
color: #6f5b39;
|
color: #6f5b39;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pause-button {
|
||||||
|
border: 1px solid rgba(47, 109, 79, 0.3);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(47, 109, 79, 0.12);
|
||||||
|
color: #2f6d4f;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-paused .pause-button {
|
||||||
|
border-color: rgba(196, 111, 42, 0.4);
|
||||||
|
background: rgba(196, 111, 42, 0.16);
|
||||||
|
color: #8c4a16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-button:focus-visible {
|
||||||
|
outline: 2px solid rgba(47, 109, 79, 0.4);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid var(--panel-border);
|
border: 1px solid var(--panel-border);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { EquityPrint, OptionPrint } from "@islandflow/types";
|
import type { EquityPrint, OptionPrint } from "@islandflow/types";
|
||||||
|
|
||||||
const MAX_ITEMS = 60;
|
const MAX_ITEMS = 60;
|
||||||
|
|
@ -19,6 +19,9 @@ type TapeState<T> = {
|
||||||
status: WsStatus;
|
status: WsStatus;
|
||||||
items: T[];
|
items: T[];
|
||||||
lastUpdate: number | null;
|
lastUpdate: number | null;
|
||||||
|
paused: boolean;
|
||||||
|
dropped: number;
|
||||||
|
togglePause: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildWsUrl = (path: string): string => {
|
const buildWsUrl = (path: string): string => {
|
||||||
|
|
@ -53,7 +56,11 @@ const formatTime = (ts: number): string => {
|
||||||
return new Date(ts).toLocaleTimeString();
|
return new Date(ts).toLocaleTimeString();
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusLabel = (status: WsStatus): string => {
|
const statusLabel = (status: WsStatus, paused: boolean): string => {
|
||||||
|
if (paused) {
|
||||||
|
return "Paused";
|
||||||
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "connected":
|
case "connected":
|
||||||
return "Live";
|
return "Live";
|
||||||
|
|
@ -69,9 +76,21 @@ const useTape = <T,>(path: string, expectedType: MessageType): TapeState<T> => {
|
||||||
const [status, setStatus] = useState<WsStatus>("connecting");
|
const [status, setStatus] = useState<WsStatus>("connecting");
|
||||||
const [items, setItems] = useState<T[]>([]);
|
const [items, setItems] = useState<T[]>([]);
|
||||||
const [lastUpdate, setLastUpdate] = useState<number | null>(null);
|
const [lastUpdate, setLastUpdate] = useState<number | null>(null);
|
||||||
|
const [paused, setPaused] = useState<boolean>(false);
|
||||||
|
const [dropped, setDropped] = useState<number>(0);
|
||||||
const reconnectRef = useRef<number | null>(null);
|
const reconnectRef = useRef<number | null>(null);
|
||||||
const socketRef = useRef<WebSocket | null>(null);
|
const socketRef = useRef<WebSocket | null>(null);
|
||||||
|
|
||||||
|
const togglePause = useCallback(() => {
|
||||||
|
setPaused((prev) => {
|
||||||
|
const next = !prev;
|
||||||
|
if (!next) {
|
||||||
|
setDropped(0);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
|
|
@ -103,6 +122,12 @@ const useTape = <T,>(path: string, expectedType: MessageType): TapeState<T> => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (paused) {
|
||||||
|
setDropped((prev) => prev + 1);
|
||||||
|
setLastUpdate(Date.now());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setItems((prev) => {
|
setItems((prev) => {
|
||||||
const next = [message.payload, ...prev];
|
const next = [message.payload, ...prev];
|
||||||
return next.slice(0, MAX_ITEMS);
|
return next.slice(0, MAX_ITEMS);
|
||||||
|
|
@ -145,26 +170,35 @@ const useTape = <T,>(path: string, expectedType: MessageType): TapeState<T> => {
|
||||||
socketRef.current.close();
|
socketRef.current.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [path, expectedType]);
|
}, [path, expectedType, paused]);
|
||||||
|
|
||||||
return { status, items, lastUpdate };
|
return { status, items, lastUpdate, paused, dropped, togglePause };
|
||||||
};
|
};
|
||||||
|
|
||||||
type TapeStatusProps = {
|
type TapeStatusProps = {
|
||||||
status: WsStatus;
|
status: WsStatus;
|
||||||
lastUpdate: number | null;
|
lastUpdate: number | null;
|
||||||
|
paused: boolean;
|
||||||
|
dropped: number;
|
||||||
|
onTogglePause: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TapeStatus = ({ status, lastUpdate }: TapeStatusProps) => {
|
const TapeStatus = ({ status, lastUpdate, paused, dropped, onTogglePause }: TapeStatusProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={`status status-${status} status-compact`}>
|
<div className={`status status-${status} status-compact ${paused ? "status-paused" : ""}`}>
|
||||||
<span className="status-dot" />
|
<span className="status-dot" />
|
||||||
<span>{statusLabel(status)}</span>
|
<span>{statusLabel(status, paused)}</span>
|
||||||
{lastUpdate ? (
|
{lastUpdate ? (
|
||||||
<span className="timestamp">Updated {formatTime(lastUpdate)}</span>
|
<span className="timestamp">Updated {formatTime(lastUpdate)}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="timestamp">Waiting for data</span>
|
<span className="timestamp">Waiting for data</span>
|
||||||
)}
|
)}
|
||||||
|
{paused && dropped > 0 ? (
|
||||||
|
<span className="timestamp">{dropped} new while paused</span>
|
||||||
|
) : null}
|
||||||
|
<button className="pause-button" type="button" onClick={onTogglePause}>
|
||||||
|
{paused ? "Resume" : "Pause"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -204,7 +238,13 @@ export default function HomePage() {
|
||||||
<h2>Options Tape</h2>
|
<h2>Options Tape</h2>
|
||||||
<p className="card-subtitle">Newest prints first (max {MAX_ITEMS}).</p>
|
<p className="card-subtitle">Newest prints first (max {MAX_ITEMS}).</p>
|
||||||
</div>
|
</div>
|
||||||
<TapeStatus status={options.status} lastUpdate={options.lastUpdate} />
|
<TapeStatus
|
||||||
|
status={options.status}
|
||||||
|
lastUpdate={options.lastUpdate}
|
||||||
|
paused={options.paused}
|
||||||
|
dropped={options.dropped}
|
||||||
|
onTogglePause={options.togglePause}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list">
|
<div className="list">
|
||||||
|
|
@ -237,7 +277,13 @@ export default function HomePage() {
|
||||||
<h2>Equities Tape</h2>
|
<h2>Equities Tape</h2>
|
||||||
<p className="card-subtitle">Off-exchange flag highlighted.</p>
|
<p className="card-subtitle">Off-exchange flag highlighted.</p>
|
||||||
</div>
|
</div>
|
||||||
<TapeStatus status={equities.status} lastUpdate={equities.lastUpdate} />
|
<TapeStatus
|
||||||
|
status={equities.status}
|
||||||
|
lastUpdate={equities.lastUpdate}
|
||||||
|
paused={equities.paused}
|
||||||
|
dropped={equities.dropped}
|
||||||
|
onTogglePause={equities.togglePause}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="list">
|
<div className="list">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue