Skip to content

Commit f0eccf0

Browse files
committed
Add requiresObject/requiresService capability gates for UI elements
1 parent 8cd7c40 commit f0eccf0

6 files changed

Lines changed: 70 additions & 6 deletions

File tree

content/docs/references/ui/dashboard.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ Dashboard header action
8888
| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'grouped-bar' \| 'stacked-bar' \| 'bi-polar-bar' \| 'line' \| 'area' \| 'stacked-area' \| 'step-line' \| 'spline' \| 'pie' \| 'donut' \| 'funnel' \| 'pyramid' \| 'scatter' \| 'bubble' \| 'treemap' \| 'sunburst' \| 'sankey' \| 'word-cloud' \| 'gauge' \| 'solid-gauge' \| 'metric' \| 'kpi' \| 'bullet' \| 'choropleth' \| 'bubble-map' \| 'gl-map' \| 'heatmap' \| 'radar' \| 'waterfall' \| 'box-plot' \| 'violin' \| 'candlestick' \| 'stock' \| 'table' \| 'pivot'>` || Visualization type |
8989
| **chartConfig** | `Object` | optional | Chart visualization configuration |
9090
| **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming |
91+
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered |
92+
| **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered |
9193
| **actionUrl** | `string` | optional | URL or target for the widget action button |
9294
| **actionType** | `Enum<'script' \| 'url' \| 'modal' \| 'flow' \| 'api'>` | optional | Type of action for the widget action button |
9395
| **actionIcon** | `string` | optional | Icon identifier for the widget action button |

packages/objectql/src/protocol.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -500,9 +500,16 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
500500
}
501501

502502
// 2. In-memory SchemaRegistry (artifact-loaded out-of-box values).
503-
// Project kernels skip the process-wide registry to avoid cross-
504-
// project leakage; their artifact source is MetadataService below.
505-
if (item === undefined && this.projectId === undefined) {
503+
// Both control-plane (unscoped) and project kernels consult the
504+
// registry. The previous guard that skipped the registry for
505+
// project kernels was meant to prevent cross-project leakage at
506+
// the LIST level — but for a single-item lookup the kernel's own
507+
// `engine.registry` is project-local (each ObjectQL instance has
508+
// its own SchemaRegistry), so reading from it is safe and
509+
// necessary. Without this, project-kernel callers of
510+
// `GET /api/v1/meta/object/<name>` 404 even though the object is
511+
// registered and visible via the list endpoint.
512+
if (item === undefined) {
506513
item = this.engine.registry.getItem(request.type, request.name);
507514
if (item === undefined) {
508515
const alt = PLURAL_TO_SINGULAR[request.type] ?? SINGULAR_TO_PLURAL[request.type];

packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export const SystemOverviewDashboard = Dashboard.create({
8181
title: 'Packages Installed',
8282
type: 'metric',
8383
object: 'sys_package_installation',
84+
// Cloud-only object — only registered when service-tenant is loaded.
85+
// Hide this widget gracefully in single-project runtimes.
86+
requiresObject: 'sys_package_installation',
8487
layout: {
8588
x: 9,
8689
y: 0,

packages/platform-objects/src/apps/setup.app.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,15 @@ export const SETUP_APP: App = {
7171
label: 'Platform',
7272
icon: 'layers',
7373
children: [
74-
{ id: 'nav_apps', type: 'object', label: 'Apps', objectName: 'sys_app', icon: 'layout-grid' },
75-
{ id: 'nav_packages', type: 'object', label: 'Packages', objectName: 'sys_package', icon: 'package' },
76-
{ id: 'nav_package_installations', type: 'object', label: 'Installations', objectName: 'sys_package_installation', icon: 'package-check' },
74+
// `sys_app` / `sys_package` / `sys_package_installation` are
75+
// contributed by `@objectstack/service-tenant` (control-plane scope).
76+
// Single-project runtimes do not register them — the `requiresObject`
77+
// capability flag tells the frontend to hide these entries when the
78+
// backing object is not in the SchemaRegistry, avoiding the
79+
// 404-when-clicked trap.
80+
{ id: 'nav_apps', type: 'object', label: 'Apps', objectName: 'sys_app', icon: 'layout-grid', requiresObject: 'sys_app' },
81+
{ id: 'nav_packages', type: 'object', label: 'Packages', objectName: 'sys_package', icon: 'package', requiresObject: 'sys_package' },
82+
{ id: 'nav_package_installations', type: 'object', label: 'Installations', objectName: 'sys_package_installation', icon: 'package-check', requiresObject: 'sys_package_installation' },
7783
{ id: 'nav_metadata', type: 'object', label: 'All Metadata', objectName: 'sys_metadata', icon: 'file-cog' },
7884
],
7985
},

packages/spec/src/ui/app.zod.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ const BaseNavItemSchema = z.object({
4848

4949
/** Permissions required to see/access this navigation item */
5050
requiredPermissions: z.array(z.string()).optional().describe('Permissions required to access this item'),
51+
52+
/**
53+
* Capability gate — registered object name.
54+
*
55+
* When set, the frontend MUST hide (or render disabled) this navigation
56+
* entry if the named object is not registered in the runtime's
57+
* SchemaRegistry. Useful for cloud-only objects (e.g. `sys_app`,
58+
* `sys_package`, `sys_package_installation`) that don't exist in
59+
* single-project runtimes — declaring the dependency here avoids
60+
* 404-when-clicked traps without hard-coding environment checks in the
61+
* UI.
62+
*
63+
* Independent of `visible` (CEL) and `requiredPermissions` (RBAC) —
64+
* this gates on runtime *capability*, not user authorization.
65+
*/
66+
requiresObject: z.string().optional().describe('Hide/disable this entry unless the named object is registered in the runtime'),
67+
68+
/**
69+
* Capability gate — registered service name.
70+
*
71+
* Same idea as `requiresObject` but keyed on a kernel service
72+
* (e.g. `'ai'`, `'tenant'`, `'realtime'`). Hide the entry when the
73+
* service isn't installed.
74+
*/
75+
requiresService: z.string().optional().describe('Hide/disable this entry unless the named kernel service is registered'),
5176
});
5277

5378
/**

packages/spec/src/ui/dashboard.zod.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ export const DashboardWidgetSchema = lazySchema(() => z.object({
107107
/** Color variant for the widget (e.g., KPI card accent color) */
108108
colorVariant: WidgetColorVariantSchema.optional().describe('Widget color variant for theming'),
109109

110+
/**
111+
* Runtime capability gate — widget is hidden when the named object is
112+
* not registered in the runtime's SchemaRegistry. Mirrors
113+
* `NavigationItem.requiresObject` so cloud-only widgets (e.g. those
114+
* keyed on `sys_app` / `sys_package_installation`) silently disappear
115+
* in single-project runtimes instead of rendering a 404 error.
116+
*
117+
* Defaults to the widget's `object` field when not explicitly set —
118+
* any widget that targets an object will be gated on that object's
119+
* registration. Set this to a different value (or empty string to
120+
* disable) when the widget should appear even if its `object` is
121+
* unavailable.
122+
*/
123+
requiresObject: z.string().optional().describe('Hide the widget unless the named object is registered'),
124+
125+
/**
126+
* Runtime capability gate — widget is hidden when the named kernel
127+
* service is not registered. Mirrors `NavigationItem.requiresService`.
128+
*/
129+
requiresService: z.string().optional().describe('Hide the widget unless the named kernel service is registered'),
130+
110131
/** Action URL for the widget header action button */
111132
actionUrl: z.string().optional().describe('URL or target for the widget action button'),
112133

0 commit comments

Comments
 (0)