Adding a new widget type
The widget catalogue lives in habarico-enterprise, with the dashboard that renders it: nothing in
the free package resolves a widget type, so registering one needs the paid package. Compose on top
of enterpriseWidgetProvider to keep the built-in widgets alongside your own.
A widget type is registered the same way a field or an input is: through a provider that maps a
type name to a definition, with the built-in defaultWidgetProvider as the fallback.
type WidgetDefinition = {
Component: ComponentType<WidgetRenderProps>; // rendered on the dashboard
ConfigStep?: ComponentType<WidgetConfigStepProps>; // optional; omit for a label-only config step
defaultLayout: WidgetLayout; // initial grid placement — {x, y, w, h, minW?, minH?}
labelKey: string; // i18n key shown in the type step
dataSource: WidgetDataSource; // 'resource' | 'aggregate' | 'none'
requiresGroupBy?: boolean; // charts: block save until the first groupBy item is set
flushContent?: boolean; // drop the panel padding, e.g. for a datagrid
};
type WidgetRenderProps = {widget: WidgetConfig};
type WidgetConfigStepProps = {
availableFields: FieldDescriptor[];
availableFilters: FilterSchema[];
};
Step 1 — pick a type name
WidgetType is BuiltInWidgetType | (string & {}), so any custom string is accepted out of the
box — nothing in the library needs editing. Use a name that does not clash with a built-in type,
e.g. 'gauge'.
Step 2 — write the Component and the ConfigStep
The render component receives only the stored widget config; everything else it needs it reads
from context or fetches itself.
// GaugeWidget.tsx
import {type WidgetRenderProps, useSimpleAggregate} from 'habarico-enterprise';
export const GaugeWidget = ({widget}: WidgetRenderProps) => {
const {value, isLoading} = useSimpleAggregate({
resource: widget.resource ?? '',
function: widget.aggregateFunction ?? 'count',
field: widget.field,
filter: widget.filters,
});
return isLoading ? <Loading /> : <Gauge value={value} />;
};
The config step receives the resource's available fields and filters as props, and reads and
writes the wizard form through the react-hook-form context — the wizard owns a single
react-hook-form instance and provides it via FormProvider, so useFormContext() and useWatch
work inside any step. The form's value keys are the keys of the public WidgetConfigFormState
type (label, field, aggregateFunction, filters, color, …).
// GaugeConfigStep.tsx
import {type WidgetConfigFormState, type WidgetConfigStepProps} from 'habarico-enterprise';
import {useFormContext, useWatch} from 'react-hook-form';
export const GaugeConfigStep = ({availableFields, availableFilters}: WidgetConfigStepProps) => {
const {control, setValue} = useFormContext<WidgetConfigFormState>();
const {label, field, color} = useWatch({control});
// … render the pickers, writing back through setValue, e.g.
// setValue('field', name, {shouldDirty: true});
};
The form is not passed as a prop. A ConfigStep that expects props.form gets undefined —
read and write the form through useFormContext() / useWatch instead. They only work inside the
wizard, which owns the form context. (The built-in config steps do the same through an internal
facade hook that is not part of the public API.)
ConfigStep is optional: omit it and the wizard shows a generic step with just the required label
field.
Step 3 — register it in the widget catalogue
The catalogue is a context, not a prop: a resolver that answers by type, and the list of types the
"Add widget" dialog offers. Compose with enterpriseWidgetProvider so the built-in types keep
working, and append your own name to enterpriseWidgetTypes — a resolver can look a type up, but
it cannot enumerate types, so both halves are needed:
import {
EnterpriseSchemaAdmin,
enterpriseWidgetProvider,
enterpriseWidgetTypes,
} from 'habarico-enterprise';
import type {AdminWidgetProvider} from 'habarico-enterprise';
import {GaugeConfigStep} from './GaugeConfigStep';
import {GaugeWidget} from './GaugeWidget';
const widgetProvider: AdminWidgetProvider = ({type, resource}) => {
if (type === 'gauge') {
return {
Component: GaugeWidget,
ConfigStep: GaugeConfigStep,
defaultLayout: {x: 0, y: 0, w: 6, h: 4},
labelKey: 'ra.widget.definition.type_gauge',
dataSource: 'aggregate',
requiresGroupBy: true,
};
}
return enterpriseWidgetProvider({type, resource});
};
<EnterpriseSchemaAdmin
widgetProvider={widgetProvider}
supportedWidgetTypes={() => [...enterpriseWidgetTypes(), 'gauge']}
/* … */
>
…
</EnterpriseSchemaAdmin>;
Returning undefined from the resolver means "no such widget", and the wizard treats the type as
unavailable. The wizard's resource step filters the offered resources by the definition's
dataSource.
Rendering SchemaDashboard directly instead of EnterpriseSchemaAdmin works the same way — wrap
it in WidgetCatalogProvider, which takes the same two values:
import {SchemaDashboard, WidgetCatalogProvider} from 'habarico-enterprise';
<WidgetCatalogProvider
widgetProvider={widgetProvider}
supportedWidgetTypes={() => [...enterpriseWidgetTypes(), 'gauge']}
>
<SchemaDashboard />
</WidgetCatalogProvider>;
Overriding an existing built-in type — replacing the default bar chart, say — needs no change
to the type list, because the name is already in it.
Step 4 — i18n
Add the labelKey translation to each locale (e.g. ra.widget.definition.type_gauge).
See also
- Dashboard hooks —
useGetWidget, which reads the catalogue - Data sources — what
dataSourceselects - Widget configuration — the
WidgetConfiga widget is rendered from - Providers — the same resolution pattern for fields, inputs and boxes