QUESTPIE
AdminCustom views

Dashboard widgets

A widget is your React on one dashboard tile. The server decides where it sits and what it loads, and hands the whole item to your component.

View markdown

The admin ships eight widget types: stats, value, chart, table, timeline, progress, recentItems and quickActions. When none of them says what you need, write the ninth.

The tile

The client half is one file under src/questpie/admin/widgets/.

src/questpie/admin/widgets/stale-drafts.tsx
import {
	useServerWidgetData,
	type WidgetComponentProps,
	widget,
} from "@questpie/admin/client";

function StaleDraftsWidget({ config }: WidgetComponentProps) {
	const { data, isLoading } = useServerWidgetData<{ titles: string[] }>(
		config.id,
		{ enabled: Boolean(config.hasLoader) },
	);

	return (
		<div className="rounded-lg border p-4">
			<h3 className="mb-2 text-sm font-medium">{config.title}</h3>
			{isLoading && <p className="text-muted-foreground text-sm">Loading</p>}
			<ul className="text-sm">
				{(data?.titles ?? []).map((title) => (
					<li key={title}>{title}</li>
				))}
			</ul>
		</div>
	);
}

export default widget("stale-drafts", { component: StaleDraftsWidget });

No card wraps your markup. The built-in widgets draw their own, and so must yours.

The item that places it

The server half is a dashboard item in config/admin.ts.

src/questpie/server/config/admin.ts
import { adminConfig } from "#questpie/factories";
import type { WidgetFetchContext } from "@questpie/admin/factories";

export default adminConfig({
	dashboard: {
		items: [
			{
				id: "stale-drafts",
				type: "custom",
				widgetType: "staleDrafts",
				label: "Stale drafts",
				span: 2,
				loader: async ({ collections }: WidgetFetchContext) => {
					const { docs } = await collections.posts.find({
						where: { status: "draft" },
						limit: 5,
					});
					return { titles: docs.map((doc: { _title: string }) => doc._title) };
				},
			},
		],
	},
});

Run questpie generate and the tile is on /admin.

`widgetType` is the file name, camelCased

stale-drafts.tsx registers as staleDrafts. The string you pass to widget() plays no part in the lookup. A widgetType that resolves to nothing renders an unknown-widget card naming the type it wanted.

What the component gets

The dashboard item arrives whole on config, minus the function keys the server keeps to itself.

KeyWhat it is
idRequired. useServerWidgetData looks the loader up by it
widgetTypeThe registry key, so the client can find your component
titleYour label, copied under the name the widgets expect
propsAny object you want. It arrives untouched
spanGrid columns. Clamped to 1 to 12. Also passed as a prop
rowSpanGrid rows
hasLoadertrue when the item declared a loader

Keys of your own survive too. A dashboard item is spread through as it stands. That is the opposite of .list() config on a view.

The loader

loader runs on the server and never reaches the browser. The config route strips it and sets hasLoader in its place. Your component then fetches the result by widget id.

The context carries collections, globals and db, plus the rest of the app context. The fetch route refuses anyone whose session is not an admin.

access is the other function key. Give it false or a predicate, and a tile the current user may not see never leaves the server. Collection-bound widgets are filtered by read access as well.

Next

Configuration covers the rest of the dashboard: sections, columns, header actions and the eight built-in widget types.

On this page