From 238244fe5c36ce4048d6217be3dedf3b1d3c38fd Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Tue, 28 Oct 2025 09:34:18 -0400 Subject: [PATCH] SaveProvisionedDashboardForm: Show preview banner when pushing to non-configured existing branch (#112947) * SaveProvisionedDashboardForm: Show preview banner when pushing to non-configured existing branch * useProvisionedRequestHandler: use ref to prevent handler triggered twice --- .../SaveProvisionedDashboardForm.tsx | 92 ++++++++++++------- .../hooks/useProvisionedRequestHandler.ts | 9 +- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx b/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx index f278c2e436c..6af33a5cbea 100644 --- a/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/provisioning/components/Dashboards/SaveProvisionedDashboardForm.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { Controller, useForm, FormProvider } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; @@ -56,7 +56,7 @@ export function SaveProvisionedDashboardForm({ const methods = useForm({ defaultValues }); const { handleSubmit, watch, control, reset, register } = methods; - const [workflow] = watch(['workflow']); + const [workflow, ref, path] = watch(['workflow', 'ref', 'path']); // Update the form if default values change useEffect(() => { @@ -70,46 +70,35 @@ export function SaveProvisionedDashboardForm({ }); }; - const handleNewDashboard = (upsert: Resource) => { - // Navigation for new dashboards - const url = locationUtil.assureBaseUrl( - getDashboardUrl({ - uid: upsert.metadata.name, - slug: kbn.slugifyForUrl(upsert.spec.title ?? ''), - currentQueryParams: window.location.search, - }) - ); - navigate(url); - }; + const handleNewDashboard = useCallback( + (upsert: Resource) => { + // Navigation for new dashboards + const url = locationUtil.assureBaseUrl( + getDashboardUrl({ + uid: upsert.metadata.name, + slug: kbn.slugifyForUrl(upsert.spec.title ?? ''), + currentQueryParams: window.location.search, + }) + ); + navigate(url); + }, + [navigate] + ); - const onWriteSuccess = (_: ProvisionedOperationInfo, upsert: Resource) => { - handleDismiss(); - if (isNew && upsert?.metadata.name) { - handleNewDashboard(upsert); - } else { - locationService.partial({ - viewPanel: null, - editPanel: null, - }); - } - }; - - const onBranchSuccess = (ref: string, path: string, info: ProvisionedOperationInfo, upsert: Resource) => { - handleDismiss(); - if (isNew && upsert?.metadata?.name) { - handleNewDashboard(upsert); - } else { + const navigateToPreview = useCallback( + (ref: string, path: string, repoType: string) => { const url = buildResourceBranchRedirectUrl({ baseUrl: `${PROVISIONING_URL}/${defaultValues.repo}/dashboard/preview/${path}`, paramName: 'ref', paramValue: ref, - repoType: info.repoType, + repoType, }); navigate(url); - } - }; + }, + [navigate, defaultValues.repo] + ); - const handleDismiss = () => { + const handleDismiss = useCallback(() => { panelEditor?.onDiscard(); const model = dashboard.getSaveModel(); @@ -118,7 +107,40 @@ export function SaveProvisionedDashboardForm({ dashboard.saveCompleted(model, saveResponse, defaultValues.folder?.uid); drawer.onClose(); - }; + }, [dashboard, defaultValues.folder?.uid, drawer, panelEditor, request?.data?.resource]); + + const onWriteSuccess = useCallback( + ({ repoType }: ProvisionedOperationInfo, upsert: Resource) => { + handleDismiss(); + if (isNew && upsert?.metadata.name) { + handleNewDashboard(upsert); + } + + // if pushed to an existing but non-configured branch, navigate to preview page + if (ref !== repository?.branch && ref) { + navigateToPreview(ref, path, repoType); + return; + } + + locationService.partial({ + viewPanel: null, + editPanel: null, + }); + }, + [isNew, path, ref, repository?.branch, handleDismiss, handleNewDashboard, navigateToPreview] + ); + + const onBranchSuccess = useCallback( + (ref: string, path: string, info: ProvisionedOperationInfo, upsert: Resource) => { + handleDismiss(); + if (isNew && upsert?.metadata?.name) { + handleNewDashboard(upsert); + } else { + navigateToPreview(ref, path, info.repoType); + } + }, + [isNew, navigateToPreview, handleNewDashboard, handleDismiss] + ); useProvisionedRequestHandler({ folderUID: defaultValues.folder?.uid, diff --git a/public/app/features/provisioning/hooks/useProvisionedRequestHandler.ts b/public/app/features/provisioning/hooks/useProvisionedRequestHandler.ts index 98675545294..7fc3bb052fe 100644 --- a/public/app/features/provisioning/hooks/useProvisionedRequestHandler.ts +++ b/public/app/features/provisioning/hooks/useProvisionedRequestHandler.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { AppEvents } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -74,6 +74,9 @@ export function useProvisionedRequestHandler({ resourceType, }: Props) { const dispatch = useDispatch(); + // useRef to ensure handlers are only called once per request + const hasHandled = useRef(false); + useEffect(() => { const repoType = repository?.type || 'git'; const info: ProvisionedOperationInfo = { @@ -83,11 +86,13 @@ export function useProvisionedRequestHandler({ }; if (request.isError) { + hasHandled.current = true; handlers.onError?.(request.error, info); return; } - if (request.isSuccess && request.data) { + if (request.isSuccess && request.data && !hasHandled.current) { + hasHandled.current = true; const { ref, path, urls, resource } = request.data; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const resourceData = resource.upsert as Resource;