Your first widget
A widget is a Vue component on a database's dashboard. You declare it in the manifest with its settings. Flare draws the settings form, places the widget on the grid and mounts your component.
On this page you'll build a widget that counts the rows in a table, then turn it into a chart. If you don't have a plugin yet, start with Getting started.
Declare it
Add the widget to contributes.widgets:
{
"name": "flare-plugin-row-count",
"version": "1.0.0",
"flare": {
"id": "flare.row-count",
"title": "Row count",
"description": "How many rows a table or collection holds, on the dashboard.",
"author": "Flare",
"permissions": { "data": "read" },
"contributes": {
"widgets": [
{
"id": "count",
"title": "Row count",
"description": "How many rows a table or collection holds.",
"icon": "hash",
"size": { "w": 4, "h": 3 },
"settings": [
{
"name": "container",
"title": "Table or collection",
"type": "container",
"required": true
}
]
}
]
}
}
}A widget reads data, so the manifest needs permissions.data set to read.
The container setting is a picker of the open database's tables and collections. Flare fills it in. Other setting types include field, text, number and dropdown. See Form fields for all of them.
size is measured in the dashboard's grid of 24 columns and 40px rows, so { "w": 4, "h": 3 } is a small tile. Add minSize to stop people shrinking it below what it can draw. icon takes any Lucide icon name in kebab case.
Implement it
In src/index.ts, map the id to your component:
import { definePlugin } from '@flare/plugin';
import Count from './widgets/Count.vue';
export default definePlugin({
widgets: { count: Count },
});Read its settings
Call useWidget() in the component. Pass the shape of your settings to get them typed:
<script lang="ts" setup>
import { useWidget } from '@flare/plugin/vue';
const { settings, size } = useWidget<{ container?: string }>();
</script>Both are computed refs, so templates and computeds update when the user changes a setting. size is the widget's size in grid cells. Use it to show more when there's room.
Read the data
Components read data through Flare's data composables. Pass a function that returns the request:
<script lang="ts" setup>
import { useAggregate, useWidget } from '@flare/plugin/vue';
const { settings } = useWidget<{ container?: string }>();
const { data, loading, error } = useAggregate(() =>
settings.value.container
? { container: settings.value.container, aggregate: 'count' }
: null
);
</script>Returning null means "not yet", so nothing is read until a table is picked. The composable reads again when the request changes, when the dashboard refreshes and when the user opens another database.
The database does the counting, so the number is exact on any size of table. data keeps the last answer during a refresh, so the widget never flashes empty. Answers that arrive out of order are dropped.
Each kind of read has its own composable:
useReadfor recordsuseAggregatefor a count, sum or averageuseQueryfor a read only query in the database's own languageuseContainersfor the list of tablesuseFlareDatafor any read you write yourself
They're all in Vue composables.
Draw it
Use Flare's components from @flare/plugin/vue and its Tailwind theme classes. Your widget then matches the rest of the dashboard in every theme:
<script lang="ts" setup>
import { EmptyState, Stat, useAggregate, useWidget } from '@flare/plugin/vue';
const { settings } = useWidget<{ container?: string }>();
const { data, loading, error } = useAggregate(() =>
settings.value.container
? { container: settings.value.container, aggregate: 'count' }
: null
);
</script>
<template>
<div v-if="!settings.container" class="h-full px-3 pb-3">
<EmptyState
title="Choose a table"
description="Pick the table or collection to count in this widget's settings."
/>
</div>
<p v-else-if="error && data === null" class="px-4 pt-2 text-2xs text-flare-danger">
{{ error.message }}
</p>
<Stat v-else :value="data" :label="`in ${settings.container}`" :loading="loading" />
</template>Stat is the same figure Flare's built in Stat widget draws, so the two line up side by side. Flare draws the title. Your component fills the space below it, so add your own padding, such as px-3 pb-3.
Use theme classes for colour
Use classes such as text-flare-muted, bg-flare-panel and border-flare-border instead of your own colours. Flare sends theme changes to your frame, so the widget follows whatever theme the user picks.
Turn it into a chart
This widget counts the rows that arrived on each of the last few days, by a date field, and draws bars. Its manifest entry adds a field setting, which lists the fields of the table picked in container, and a dropdown:
{
"id": "per-day",
"title": "Arrivals per day",
"icon": "chart-column",
"size": { "w": 8, "h": 5 },
"settings": [
{
"name": "container",
"title": "Table or collection",
"type": "container",
"required": true
},
{
"name": "field",
"title": "Date field",
"type": "field",
"of": "container",
"required": true
},
{
"name": "days",
"title": "Days",
"type": "dropdown",
"default": "14",
"options": [
{ "title": "7 days", "value": "7" },
{ "title": "14 days", "value": "14" },
{ "title": "30 days", "value": "30" }
]
}
]
}useFlareData runs a read you write yourself. Here it's one count per day, run in parallel:
<script lang="ts" setup>
import { BarChart, useFlareData, useWidget } from '@flare/plugin/vue';
import { computed } from 'vue';
const { settings } = useWidget<{ container?: string; field?: string; days?: string }>();
const days = computed(() => Number(settings.value.days ?? 14));
/** Midnight on each of the last `count` days, and the midnight after the last. */
function midnights(count: number): Date[] {
const today = new Date();
today.setHours(0, 0, 0, 0);
return Array.from({ length: count + 1 }, (_, index) => {
const day = new Date(today);
day.setDate(today.getDate() - count + 1 + index);
return day;
});
}
const { data } = useFlareData(
() =>
settings.value.container && settings.value.field
? {
container: settings.value.container,
field: settings.value.field,
count: days.value,
}
: null,
async ({ container, field, count }, flare) => {
const edges = midnights(count);
const counts = await Promise.all(
edges.slice(0, -1).map((from, index) =>
flare.data.aggregate({
container,
aggregate: 'count',
filters: [
{ field, operator: '>=', value: from },
{ field, operator: '<', value: edges[index + 1] },
],
})
)
);
return counts.map((value, index) => ({
label: edges[index]!.toLocaleDateString(undefined, {
day: 'numeric',
month: 'short',
}),
value,
}));
}
);
</script>
<template>
<div class="h-full px-3 pb-2">
<BarChart :data="data ?? []" :title="`${settings.container} per day`" />
</div>
</template>The database runs each count between two midnights, so the numbers are exact however many rows there are. Only the counts reach your frame.
When and where it runs
A widget reads its data:
- every 30 seconds while the dashboard is open, and when the user turns on Live.
useWidget().refreshKeygoes up on each refresh, and the data composables already follow it. - again when the user opens another database, with the same settings.
It's offered on every database unless you limit it with engines, such as "engines": ["postgres", "mysql"].
Next
- Cell formatters: change how values look in every table.
- Vue composables and Components: everything a view can use.
- Debugging: where your widget's logs and errors go.