web: playground — initialisation depuis l'URL avant toute réécriture (course au montage qui perdait les liens profonds) ; listes de clés tolérantes aux enveloppes inattendues
4 changed files +18 −8
modified
hfmarketdata/web/src/pages/dashboard/Keys.jsx
+6 −1
@@ -21,7 +21,12 @@ export default function Keys() { | ||
| 21 | 21 | const navigate = useNavigate() |
| 22 | 22 | |
| 23 | 23 | const load = useCallback(async () => { |
| 24 | − try { const r = await api('/v1/me/keys'); setKeys(r.data?.data || r.data || []); setErr(null) } catch (e) { setErr(e); setKeys([]) } | |
| 24 | + try { | |
| 25 | + const r = await api('/v1/me/keys') | |
| 26 | + const d = r.data | |
| 27 | + setKeys(Array.isArray(d) ? d : Array.isArray(d?.data) ? d.data : Array.isArray(d?.keys) ? d.keys : []) | |
| 28 | + setErr(null) | |
| 29 | + } catch (e) { setErr(e); setKeys([]) } | |
| 25 | 30 | }, []) |
| 26 | 31 | useEffect(() => { load() }, [load]) |
| 27 | 32 | |
modified
hfmarketdata/web/src/pages/dashboard/Overview.jsx
+1 −1
@@ -22,7 +22,7 @@ export default function Overview() { | ||
| 22 | 22 | useEffect(() => { |
| 23 | 23 | let alive = true |
| 24 | 24 | api('/v1/me/usage?range=24h').then(r => alive && setUsage(r.data?.data || r.data)).catch(e => alive && setErr(e)) |
| 25 | − api('/v1/me/keys').then(r => alive && setKeys(r.data?.data || r.data || [])).catch(() => alive && setKeys([])) | |
| 25 | + api('/v1/me/keys').then(r => alive && setKeys(Array.isArray(r.data) ? r.data : Array.isArray(r.data?.data) ? r.data.data : [])).catch(() => alive && setKeys([])) | |
| 26 | 26 | return () => { alive = false } |
| 27 | 27 | }, []) |
| 28 | 28 | const tier = TIERS.find(t => t.id === (user.tier || 'free')) || TIERS[1] |
modified
hfmarketdata/web/src/pages/dashboard/PlaygroundTab.jsx
+1 −1
@@ -12,7 +12,7 @@ export default function PlaygroundTab() { | ||
| 12 | 12 | const [keys, setKeys] = useState(null) |
| 13 | 13 | const [busy, setBusy] = useState(false) |
| 14 | 14 | const [err, setErr] = useState(null) |
| 15 | − useEffect(() => { let alive = true; api('/v1/me/keys').then(r => alive && setKeys(r.data?.data || r.data || [])).catch(() => alive && setKeys([])); return () => { alive = false } }, []) | |
| 15 | + useEffect(() => { let alive = true; api('/v1/me/keys').then(r => alive && setKeys(Array.isArray(r.data) ? r.data : Array.isArray(r.data?.data) ? r.data.data : [])).catch(() => alive && setKeys([])); return () => { alive = false } }, []) | |
| 16 | 16 | |
| 17 | 17 | const usePasted = e => { |
| 18 | 18 | e.preventDefault() |
modified
hfmarketdata/web/src/playground/Playground.jsx
+10 −5
@@ -89,6 +89,7 @@ export default function Playground({ apiKey, embedded = false }) { | ||
| 89 | 89 | const [lastRate, setLastRate] = useState(null) |
| 90 | 90 | const [filter, setFilter] = useState('') |
| 91 | 91 | const lastWritten = useRef(null) |
| 92 | + const ready = useRef(false) // URL → state initialisation done; only then do we write state → URL | |
| 92 | 93 | const abortRef = useRef(null) |
| 93 | 94 | const resultRef = useRef(null) |
| 94 | 95 | |
@@ -99,9 +100,7 @@ export default function Playground({ apiKey, embedded = false }) { | ||
| 99 | 100 | const ref = sp.get('ep') |
| 100 | 101 | const urlValues = {} |
| 101 | 102 | sp.forEach((v, k) => { if (k !== 'ep') urlValues[k] = v }) |
| 102 | − let cancelled = false | |
| 103 | − resolveEntry(ref, urlValues).then(({ entry: e, generic, unknown }) => { | |
| 104 | − if (cancelled) return | |
| 103 | + const apply = ({ entry: e, generic, unknown }) => { | |
| 105 | 104 | setEntry(e) |
| 106 | 105 | const dv = defaultValues(e) |
| 107 | 106 | for (const [k, v] of Object.entries(urlValues)) if (k in dv || e.fromSpec) dv[k] = v |
@@ -110,13 +109,19 @@ export default function Playground({ apiKey, embedded = false }) { | ||
| 110 | 109 | if (unknown) setNotice({ kind: 'warn', text: `Unknown request type "${unknown}" — showing ${e.title} instead.` }) |
| 111 | 110 | else if (generic) setNotice({ kind: 'info', text: `${e.title}: form generated from the OpenAPI spec (no curated preset yet).` }) |
| 112 | 111 | else setNotice(null) |
| 113 | − }) | |
| 112 | + ready.current = true | |
| 113 | + } | |
| 114 | + // synchronous fast path (catalog id / known operationId) avoids a flash of the default form | |
| 115 | + const direct = ref ? findEntry(ref, urlValues) : byId(DEFAULT_EP) | |
| 116 | + if (direct) { apply({ entry: direct }); return undefined } | |
| 117 | + let cancelled = false | |
| 118 | + resolveEntry(ref, urlValues).then(res => { if (!cancelled) apply(res) }) | |
| 114 | 119 | return () => { cancelled = true } |
| 115 | 120 | }, [location.search]) |
| 116 | 121 | |
| 117 | 122 | // ---- write the URL from the state (replace, no history spam) |
| 118 | 123 | useEffect(() => { |
| 119 | − if (!entry) return | |
| 124 | + if (!entry || !ready.current) return | |
| 120 | 125 | const sp = new URLSearchParams() |
| 121 | 126 | sp.set('ep', entry.fromSpec ? entry.operationIds[0] : entry.id) |
| 122 | 127 | const dv = defaultValues(entry) |
| 123 | 128 | |