mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-07 09:23:07 +02:00
feat(web): adopt TanStack Query for SPA data layer; consolidate shared UI
Migrate the React SPA off the bespoke useApiFetch hook and raw useEffect fetches to TanStack Query: useQuery/useQueries for reads, useMutation + invalidateQueries for writes; a central query-key factory and invalidation helpers mirroring the backend cache_invalidation prefixes (channels/routes/nodes/messages/profiles/dashboard/adoptions); polling via refetchInterval (useAutoRefresh now returns pause state only); RouteCard self-fetches its detail/history, dropping the hand-rolled caches. Adds QueryClientProvider in App, a renderWithProviders test helper, and deletes useApiFetch. Also consolidates recurring UI into reusable components (Breadcrumbs, ListToolbar, Modal/ConfirmDialog, NotFoundState, TimeAgo, Definition, CopyableValue, MeshQrCode, Badges, SectionGroup, PageHeader) and fixes the FilterForm clear button, merged toggle wrappers, and role-aware channels/messages cache keys so client invalidation matches the server. Verified: tsc --noEmit clean, vitest 113 passed, pytest test_web 251 + test_cache 119 passed, pre-commit --all-files green.
This commit is contained in:
@@ -1313,12 +1313,13 @@ class TestKeyBuilders:
|
||||
):
|
||||
scope = {
|
||||
"type": "http",
|
||||
"path": "/api/v1/channels",
|
||||
"query_string": b"",
|
||||
"headers": [],
|
||||
}
|
||||
request = Request(scope)
|
||||
key = _channels_key_builder(request)
|
||||
assert key == "channels:role=operator:"
|
||||
assert key == "/api/v1/channels:role=operator:"
|
||||
|
||||
def test_channels_key_builder_anonymous(self):
|
||||
from meshcore_hub.api.routes.channels import _channels_key_builder
|
||||
@@ -1329,6 +1330,7 @@ class TestKeyBuilders:
|
||||
):
|
||||
scope = {
|
||||
"type": "http",
|
||||
"path": "/api/v1/channels",
|
||||
"query_string": b"",
|
||||
"headers": [],
|
||||
}
|
||||
@@ -1345,14 +1347,54 @@ class TestKeyBuilders:
|
||||
):
|
||||
scope = {
|
||||
"type": "http",
|
||||
"path": "/api/v1/messages",
|
||||
"query_string": b"limit=10&offset=0",
|
||||
"headers": [],
|
||||
}
|
||||
request = Request(scope)
|
||||
key = _messages_key_builder(request)
|
||||
assert "role=admin" in key
|
||||
assert key.startswith("/api/v1/messages:role=admin:")
|
||||
assert "limit=10" in key
|
||||
|
||||
def test_role_aware_keys_match_invalidation_prefixes(self):
|
||||
"""The GET cache key must live under the prefix the matching
|
||||
invalidation helper drops, otherwise a mutation never clears the
|
||||
stale cached list (the store path and delete path must agree).
|
||||
"""
|
||||
from meshcore_hub.api.routes.channels import _channels_key_builder
|
||||
from meshcore_hub.api.routes.messages import _messages_key_builder
|
||||
|
||||
with (
|
||||
patch(
|
||||
"meshcore_hub.api.routes.channels.resolve_user_role",
|
||||
return_value="admin",
|
||||
),
|
||||
patch(
|
||||
"meshcore_hub.api.routes.messages.resolve_user_role",
|
||||
return_value="admin",
|
||||
),
|
||||
):
|
||||
channels_req = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"path": "/api/v1/channels",
|
||||
"query_string": b"",
|
||||
"headers": [],
|
||||
}
|
||||
)
|
||||
messages_req = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"path": "/api/v1/messages",
|
||||
"query_string": b"",
|
||||
"headers": [],
|
||||
}
|
||||
)
|
||||
# invalidate_channels drops "/api/v1/channels",
|
||||
# invalidate_messages drops "/api/v1/messages".
|
||||
assert _channels_key_builder(channels_req).startswith("/api/v1/channels")
|
||||
assert _messages_key_builder(messages_req).startswith("/api/v1/messages")
|
||||
|
||||
|
||||
def _make_request_with_cache(cache):
|
||||
"""Build a Request whose ``app.state.redis_cache`` is *cache* (or absent)."""
|
||||
|
||||
Reference in New Issue
Block a user