Docs / docs (push) Successful in 22s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m14s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m53s
pre-commit / pre-commit (push) Failing after 2m13s
Test Backend / test-backend (push) Successful in 2m38s
Compose Smoke Test / test-compose (push) Successful in 38s
Playwright Tests / merge-reports (push) Successful in 1m8s
`GET /api/v1/search/` hands the client one flat index of everything worth jumping to — flows and the nodes inside them, dashboards and the widgets on them, panels, secrets, modules, workers and alert channels — and cmdk matches it in the browser, so results narrow while typing without a round trip per keystroke. A node hit is the one thing no list endpoint could answer: it opens its flow with that node in focus. The panel is reached from **Search** above Documentation in the sidebar, or ⌘K anywhere. The flow canvas palette moves to ⌘P, being the narrower of the two. The panels dialog gains an address (`/dashboards?panels`) so a panel hit has somewhere to land, and the sidebar logo gets `shrink-0`: the rail's width animates while the logo is already back, and a flex item short of room is squeezed rather than clipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vGH7jqcXxWKP9wZFPyVdU
157 lines
4.8 KiB
Python
157 lines
4.8 KiB
Python
"""One index of everything in this installation worth jumping to by name."""
|
|
|
|
from typing import Any, Literal
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from pydantic import BaseModel
|
|
|
|
from fluksio.api.deps import (
|
|
CurrentUser,
|
|
DashboardStoreDep,
|
|
FlowControllerDep,
|
|
get_current_user,
|
|
)
|
|
from fluksio.api.routes.alerts import read_config as read_alerts_config
|
|
from fluksio.flow import modules, panels
|
|
from fluksio.flow.controller import FlowController
|
|
from fluksio.flow.dashboards import DashboardNotFound, DashboardStore
|
|
from fluksio.flow.secrets import get_secrets
|
|
from fluksio.flow.store import FlowNotFound
|
|
|
|
# A wall panel never reaches this route: ``deps._panel_may`` is a whitelist that
|
|
# ends in a 403, and a whole-installation index is the opposite of what a screen
|
|
# on a wall is allowed to read.
|
|
router = APIRouter(
|
|
prefix="/search", tags=["search"], dependencies=[Depends(get_current_user)]
|
|
)
|
|
|
|
Category = Literal[
|
|
"flow",
|
|
"node",
|
|
"dashboard",
|
|
"widget",
|
|
"panel",
|
|
"secret",
|
|
"module",
|
|
"worker",
|
|
"alert",
|
|
]
|
|
|
|
|
|
class SearchEntry(BaseModel):
|
|
"""One thing somebody might be looking for.
|
|
|
|
Deliberately not a route: where a category lands is the frontend's business,
|
|
and it already owns the router. This says what the thing is and what it is
|
|
called, which is all the matching needs.
|
|
"""
|
|
|
|
category: Category
|
|
#: The id the frontend routes on.
|
|
name: str
|
|
#: Human title, often empty — a flow is usually only its name.
|
|
title: str = ""
|
|
#: The flow a node sits in, or the dashboard a widget sits on.
|
|
parent: str = ""
|
|
#: Node type, widget type, channel kind.
|
|
kind: str = ""
|
|
|
|
|
|
def _build(
|
|
controller: FlowController, dashboards: DashboardStore, hub: Any, secrets: bool
|
|
) -> list[SearchEntry]:
|
|
"""Read every store once. Blocking: disk and git throughout.
|
|
|
|
# ponytail: rebuilt per call. Key it on ``controller.store.revision`` if a
|
|
# store large enough to feel it ever shows up in a profile.
|
|
"""
|
|
entries: list[SearchEntry] = []
|
|
|
|
for name in controller.store.list_flows():
|
|
try:
|
|
flow = controller.store.read_flow(name, draft=True)
|
|
except FlowNotFound:
|
|
continue
|
|
entries.append(SearchEntry(category="flow", name=flow.name, title=flow.title))
|
|
entries.extend(
|
|
SearchEntry(
|
|
category="node",
|
|
name=node.id,
|
|
title=node.title,
|
|
parent=flow.name,
|
|
kind=node.type,
|
|
)
|
|
for node in flow.nodes
|
|
)
|
|
|
|
for summary in dashboards.list():
|
|
try:
|
|
dashboard = dashboards.read(summary.name, draft=True)
|
|
except DashboardNotFound:
|
|
continue
|
|
entries.append(
|
|
SearchEntry(
|
|
category="dashboard", name=dashboard.name, title=dashboard.title
|
|
)
|
|
)
|
|
entries.extend(
|
|
SearchEntry(
|
|
category="widget",
|
|
name=widget.id,
|
|
title=widget.title,
|
|
parent=dashboard.name,
|
|
kind=widget.type,
|
|
)
|
|
for widget in dashboard.widgets
|
|
)
|
|
|
|
entries.extend(
|
|
SearchEntry(category="panel", name=panel.id, title=panel.title)
|
|
for panel in panels.read_config().panels
|
|
)
|
|
|
|
if secrets:
|
|
entries.extend(
|
|
SearchEntry(category="secret", name=name) for name in get_secrets().list()
|
|
)
|
|
|
|
entries.extend(
|
|
SearchEntry(category="module", name=package.name, kind=package.version)
|
|
for package in modules.info(controller.store).packages
|
|
)
|
|
|
|
entries.extend(
|
|
SearchEntry(category="worker", name=worker.name)
|
|
for worker in (hub.workers() if hub is not None else [])
|
|
)
|
|
|
|
entries.extend(
|
|
SearchEntry(category="alert", name=channel.name, kind=channel.kind)
|
|
for channel in read_alerts_config().channels
|
|
)
|
|
|
|
return entries
|
|
|
|
|
|
@router.get("/", response_model=list[SearchEntry])
|
|
async def read_search_index(
|
|
current_user: CurrentUser,
|
|
request: Request,
|
|
controller: FlowControllerDep,
|
|
dashboards: DashboardStoreDep,
|
|
) -> Any:
|
|
"""Everything searchable, for the client to match against as it is typed.
|
|
|
|
The whole index rather than a query: it is a few hundred short rows for an
|
|
installation of any ordinary size, so one fetch when the panel opens beats a
|
|
round trip per keystroke — and the client already has a matcher.
|
|
|
|
Secrets are named only to a superuser, which is who ``/secrets`` answers to.
|
|
"""
|
|
# Absent when remote workers are switched off — not a reason to fail a search.
|
|
hub = getattr(request.app.state, "worker_hub", None)
|
|
return await run_in_threadpool(
|
|
_build, controller, dashboards, hub, current_user.is_superuser
|
|
)
|