Say what a plugin settings tab can actually do, and catch a declaration that cannot work
Finding F12 of the architectural review, checked against the code and found worse than reported: the dynamic component import cannot be rewritten by the bundler, so the built file ships the unresolved path and a plugin tab answers 404 in every production build, staged or not. It works only under the dev server. Nothing shipping is affected, because the one plugin that declares a tab also registers the same page as an ordinary route, and that is the one sites reach. - The ADR draft now carries a "Constraints as implemented" section: only the model catalog hub registers plugin tabs, registration happens when that hub mounts so a tab is not deep-linkable and does not survive a refresh, and the build finding above with the bundler's own warning. The two ways out are stated as the open decision rather than taken. - The hook's docstring reads as instructions to a plugin author, and its example no longer names a path that does not exist at build time. - A route declared for a hub that cannot render it is dropped with an error naming the plugin, the hub and the hubs that do work, and the same is logged at startup. Enabling the plugin still succeeds: a tab nobody can reach is no reason to take a working backend offline. - Tests hold the two halves together: no bundled plugin may declare a hub the frontend does not implement, and the list in the server must equal the hubs that really register routes, so the two cannot drift apart again.
This commit is contained in:
@@ -19,6 +19,41 @@ from dataclasses import dataclass, field
|
||||
from flask import Flask, Blueprint
|
||||
|
||||
|
||||
# Settings hubs whose Vue page actually draws plugin-contributed tabs: it asks
|
||||
# /api/plugins/settings-routes, keeps the routes whose `hub` matches its own id,
|
||||
# and registers them with router.addRoute(). Every other hub renders its own
|
||||
# tabs and nothing else, so a route naming one would be returned by the API and
|
||||
# never drawn - a tab the author declared correctly that simply does not exist
|
||||
# for the user. Hubs are added here only when their .vue file gains that code;
|
||||
# tests/test_core/test_plugin_settings_hubs.py fails if the two drift apart.
|
||||
SETTINGS_HUBS_WITH_PLUGIN_TABS = ('model-catalog',)
|
||||
|
||||
|
||||
def renderable_settings_routes(plugin_name: str, routes: Optional[List[Dict]],
|
||||
logger) -> List[Dict]:
|
||||
"""The subset of a plugin's declared settings routes that a hub can draw.
|
||||
|
||||
Anything naming a hub outside SETTINGS_HUBS_WITH_PLUGIN_TABS is dropped and
|
||||
logged as an error naming the plugin, the hub and the supported set. The
|
||||
declaration is not a crash - the plugin's API, models and pages are all
|
||||
fine - so this refuses the dead tab rather than the plugin. The hard stop
|
||||
lives in the test suite, where the author still has a chance to fix it.
|
||||
"""
|
||||
kept = []
|
||||
for route in routes or []:
|
||||
hub = route.get('hub')
|
||||
if hub in SETTINGS_HUBS_WITH_PLUGIN_TABS:
|
||||
kept.append(route)
|
||||
continue
|
||||
logger.error(
|
||||
"Plugin %s declares settings route '%s' for hub '%s'. No settings "
|
||||
"hub page registers plugin tabs for that hub, so the tab would "
|
||||
"never render; dropping it. Hubs that support plugin tabs: %s",
|
||||
plugin_name, route.get('name'), hub,
|
||||
', '.join(SETTINGS_HUBS_WITH_PLUGIN_TABS))
|
||||
return kept
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginMeta:
|
||||
"""Plugin metadata container."""
|
||||
@@ -147,11 +182,17 @@ class BasePlugin(ABC):
|
||||
|
||||
Returns a list of route configurations for the plugin's settings pages.
|
||||
Each route is a dict with:
|
||||
hub - settings hub to attach to ('model-catalog', 'asset-config', etc.)
|
||||
hub - settings hub to attach to; ONLY a hub named in
|
||||
SETTINGS_HUBS_WITH_PLUGIN_TABS (top of this module) draws
|
||||
plugin tabs. A route for any other hub is dropped by
|
||||
/api/plugins/settings-routes with an error in the log.
|
||||
path - route path relative to hub (e.g. 'operatingsystems')
|
||||
name - unique route name (e.g. 'model-catalog-os')
|
||||
tab - tab label shown in hub UI (e.g. 'Operating Systems')
|
||||
component- path to Vue component (e.g. 'plugins/<name>/frontend/views/<Component>.vue')
|
||||
component- path to the Vue component AS IT EXISTS IN THE BUILD, i.e.
|
||||
relative to frontend/src, which for a plugin means
|
||||
'.plugins-staged/<name>/views/<Component>.vue' - the copy
|
||||
scripts/stage-frontend.mjs makes of plugins/<name>/frontend
|
||||
|
||||
Example:
|
||||
return [{
|
||||
@@ -159,9 +200,29 @@ class BasePlugin(ABC):
|
||||
'path': 'operatingsystems',
|
||||
'name': 'model-catalog-os',
|
||||
'tab': 'Operating Systems',
|
||||
'component': 'plugins/computers/frontend/views/OperatingSystemsList.vue'
|
||||
'component': '.plugins-staged/computers/views/OperatingSystemsList.vue'
|
||||
}]
|
||||
|
||||
Three constraints the shape of this hook does not show, so read them
|
||||
before you rely on it (ADR-019, "Constraints as implemented"):
|
||||
|
||||
1. Declare a hub only if it is in SETTINGS_HUBS_WITH_PLUGIN_TABS. Today
|
||||
that is 'model-catalog' alone. The other hubs have no code to draw
|
||||
your tab, so the route is accepted and dropped.
|
||||
2. Route the page yourself as well, in plugins/<name>/frontend/
|
||||
routes.js, and treat THAT as the real route. The hub interpolates
|
||||
`component` into a dynamic import with no static file extension,
|
||||
which Vite will not rewrite, so the tab loads under `npm run dev`
|
||||
and 404s in a production build. A page reachable only through the
|
||||
tab is a page nobody can open at a site. Your frontend also has to
|
||||
be staged into the build (SITE_PLUGINS decides that), so enabling a
|
||||
plugin whose UI was never staged needs a frontend rebuild.
|
||||
3. The tab is not a link you can hand out. The hub registers the route
|
||||
when it mounts, so the URL resolves only for someone who has already
|
||||
been through the parent hub in that page load. Do not link to it
|
||||
from an email, a doc, or another page, and do not expect a refresh
|
||||
on it to land anywhere.
|
||||
|
||||
Return [] (default) if the plugin adds no settings pages.
|
||||
"""
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user