Reusable widgets: reference widgets across dashboards + detach to clone
Widgets configured on one dashboard (e.g., a Grafana service's Overview)
can now be live-referenced on other dashboards. Editing the widget config
updates it everywhere it's referenced. References can be detached into
independent clones.
Backend: new widget_references table (dashboard_scope, widget_id,
sort_order) with ON DELETE CASCADE. CRUD methods + 4 endpoints:
GET/POST /api/widgets/references, DELETE /api/widgets/references/{id},
POST /api/widgets/references/{id}/detach (clones the widget into a
standalone instance, then removes the reference).
Frontend: WidgetConfigDialog gains a dashboardScope prop. When set
(the main Dashboard passes 'main'), the dialog shows:
- Owned + referenced widgets in a combined list, with a link badge on
references.
- 'Add existing widget' picker: searchable list of ALL widget instances
not already on this dashboard. Click to create a reference.
- Detach button on references: clones the widget (service_id=NULL) and
removes the reference.
- Delete on a reference removes the REFERENCE (not the original widget).
Dashboard renders referenced widgets alongside owned widgets.
Detaching a service-bound widget clones it with service_id=NULL — the
clone may need re-binding to a service to render correctly. Named
dashboards don't pass dashboardScope yet (pinned-links-only); when they
gain widget support, the backend already handles any scope string.
282 backend tests pass (+2 reference lifecycle); 127 frontend tests
pass; ruff/eslint/tsc/vite all green.
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
createWidgetInstance,
|
||||
createWidgetReference,
|
||||
deleteWidgetInstance,
|
||||
deleteWidgetReference,
|
||||
detachWidgetReference,
|
||||
fetchBuiltinWidgetKinds,
|
||||
fetchWidgetData,
|
||||
fetchWidgetInstances,
|
||||
fetchWidgetReferences,
|
||||
updateWidgetInstance,
|
||||
} from "../api/widgets";
|
||||
import type { WidgetInstanceInput } from "../types";
|
||||
@@ -58,3 +62,42 @@ export function useBuiltinWidgetKinds() {
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWidgetReferences(dashboardScope: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ["widgets", "references", dashboardScope ?? null],
|
||||
queryFn: () => fetchWidgetReferences(dashboardScope!),
|
||||
enabled: !!dashboardScope,
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateWidgetReference() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: createWidgetReference,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["widgets", "references"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteWidgetReference() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteWidgetReference,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["widgets", "references"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDetachWidgetReference() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: detachWidgetReference,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["widgets"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user