Skip to main content
Premium

Adding a new widget type

Premium

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});
};
caution

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​