From 34f6eb8640d0e5a0d18ea6792e16b2c52f3d1ba5 Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Wed, 18 Mar 2026 09:28:30 +0100 Subject: [PATCH 01/10] fix(intergral/dashboards): refresh permissions before redirect after save Await contextSrv.fetchUserPermissions() before navigating to a newly saved dashboard. Without this, the redirect can hit stale cached permissions, causing a transient "not allowed to view" error. - Add contextSrv import - Await permission refresh before locationService.replace() Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dashboard/components/SaveDashboard/useDashboardSave.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 360510b613..1d25a376c1 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -6,6 +6,7 @@ import { t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import appEvents from 'app/core/app_events'; +import { contextSrv } from 'app/core/services/context_srv'; import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; @@ -72,6 +73,9 @@ export const useDashboardSave = (isCopy = false) => { const newUrl = locationUtil.stripBaseFromUrl(result.url); if (newUrl !== currentPath && result.url) { + // Refresh permissions before redirect to avoid "not allowed to view" on new dashboards. + // The save creates new permission entries that the cached user permissions don't include yet. + await contextSrv.fetchUserPermissions(); setTimeout(() => locationService.replace(newUrl)); } if (dashboard.meta.isStarred) { From e88ef40f2b86d4bc05959d7d9ce1c1d1c7442cf2 Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Wed, 18 Mar 2026 09:34:46 +0100 Subject: [PATCH 02/10] fix(intergral/ci): fix Docker tag for PR builds Use github.head_ref for PRs (source branch name) instead of github.ref_name (which resolves to the merge ref e.g. "58/merge"). Add Compute image tag step to replace / with - in branch names. Matches the workflow from feat/alerting-partitioner. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/docker_build.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 2a11e0b9be..1485f71610 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -1,4 +1,5 @@ name: docker +# Trigger rebuild on: push: @@ -40,6 +41,14 @@ jobs: with: platforms: linux/amd64,linux/arm64 + - name: Compute image tag + id: meta + run: | + # For PRs use the source branch name; for pushes use the ref name + REF="${{ github.head_ref || github.ref_name }}" + # Replace / with - for valid Docker tags + echo "tag=${REF//\//-}" >> "$GITHUB_OUTPUT" + - name: Build and push uses: docker/build-push-action@v5 with: @@ -48,6 +57,6 @@ jobs: build-args: | BINGO=false COMMIT_SHA=${{ github.sha }} - BUILD_BRANCH=main + BUILD_BRANCH=${{ github.head_ref || github.ref_name }} push: true - tags: intergral/grafana:${{ github.ref_name }} + tags: intergral/grafana:${{ steps.meta.outputs.tag }} \ No newline at end of file From b27a1f145cea5c8c8243e7af17d0eb8b58b8db33 Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Wed, 18 Mar 2026 11:53:21 +0100 Subject: [PATCH 03/10] chore(intergral/ci): trigger rebuild Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/docker_build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml index 1485f71610..b9a09e4f1e 100644 --- a/.github/workflows/docker_build.yml +++ b/.github/workflows/docker_build.yml @@ -1,5 +1,5 @@ name: docker -# Trigger rebuild +# Trigger rebuild 2 on: push: From abeb3a2840744678adbbd0d71118825500d738e8 Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Wed, 18 Mar 2026 16:01:36 +0100 Subject: [PATCH 04/10] fix(intergral/dashboards): clear basic role permission caches on dashboard create - Expand ClearUserPermissionCache to also clear basic role caches (Editor/Viewer) so managed role permissions for newly created dashboards are picked up on the next request - Add ClearCacheFor method to Resolvers for targeted scope cache invalidation Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/services/accesscontrol/acimpl/service.go | 5 +++++ pkg/services/accesscontrol/resolvers.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 3c1deeb193..d9ad70f631 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -383,6 +383,11 @@ func (s *Service) getCachedTeamsPermissions(ctx context.Context, user identity.R func (s *Service) ClearUserPermissionCache(user identity.Requester) { s.cache.Delete(accesscontrol.GetUserDirectPermissionCacheKey(user)) + // Also clear basic role caches so managed role permissions for newly created + // resources are picked up on the next request. + for _, role := range accesscontrol.GetOrgRoles(user) { + s.cache.Delete(accesscontrol.GetBasicRolePermissionCacheKey(role, user.GetOrgID())) + } } func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error { diff --git a/pkg/services/accesscontrol/resolvers.go b/pkg/services/accesscontrol/resolvers.go index 56f2903735..5ae879468e 100644 --- a/pkg/services/accesscontrol/resolvers.go +++ b/pkg/services/accesscontrol/resolvers.go @@ -88,6 +88,11 @@ func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator } } +// ClearCacheFor removes the cached scope resolution for a specific scope in an org. +func (s *Resolvers) ClearCacheFor(orgID int64, scope string) { + s.cache.Delete(getScopeCacheKey(orgID, scope)) +} + // getScopeCacheKey creates an identifier to fetch and store resolution of scopes in the cache func getScopeCacheKey(orgID int64, scope string) string { return fmt.Sprintf("%s-%v", scope, orgID) From ec9a0b2ec9b41dbcb4aadcf5424fa1f69486f37a Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Thu, 19 Mar 2026 07:32:43 +0100 Subject: [PATCH 05/10] fix(intergral/dashboards): poll for dashboard availability before redirect after save In multi-instance deployments, the instance serving the redirect after saving a new dashboard may not have the dashboard or its permissions propagated yet, resulting in "Not found" or "Not allowed to view". - Add waitForDashboardReady() that polls the dashboard DTO API (up to 8 attempts, 500ms apart) before redirecting - Applies to new dashboards and save-as-copy Co-Authored-By: Claude Opus 4.6 (1M context) --- .../saving/useSaveDashboard.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index e9bb81b026..cd85df6dcf 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -9,6 +9,7 @@ import appEvents from 'app/core/app_events'; import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; +import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { SaveDashboardAsOptions, SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { DashboardSavedEvent } from 'app/types/events'; import { useDispatch } from 'app/types/store'; @@ -18,6 +19,26 @@ import { DashboardScene } from '../scene/DashboardScene'; import { DashboardInteractions } from '../utils/interactions'; import { trackDashboardSceneCreatedOrSaved } from '../utils/tracking'; +/** + * Wait until the dashboard is accessible via the API before redirecting. + * In multi-instance deployments, the instance serving the redirect may not + * have the dashboard or its permissions available yet. + */ +async function waitForDashboardReady(uid: string, maxAttempts = 8, intervalMs = 500): Promise { + const api = getDashboardAPI(); + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + await api.getDashboardDTO(uid); + return; + } catch { + if (attempt < maxAttempts - 1) { + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + } + } + // Proceed with redirect even if polling fails — worst case the user refreshes +} + export function useSaveDashboard(isCopy = false) { const dispatch = useDispatch(); const notifyApp = useAppNotification(); @@ -91,6 +112,13 @@ export function useSaveDashboard(isCopy = false) { const newUrl = locationUtil.stripBaseFromUrl(resultData.url); if (newUrl !== currentLocation.pathname) { + // Wait until the dashboard is accessible before redirecting. + // In multi-instance deployments, the instance serving the redirect may not have + // the dashboard or its permissions propagated yet. This applies to new dashboards + // and save-as-copy, both of which create a new UID. + if (resultData.uid) { + await waitForDashboardReady(resultData.uid); + } setTimeout(() => { locationService.push({ pathname: newUrl, search: currentLocation.search }); }); From 7308adfbb6b55cc007146b2e585b8b0fd46828e6 Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Thu, 19 Mar 2026 09:27:39 +0100 Subject: [PATCH 06/10] fix(intergral/dashboards): retry dashboard load after save for multi-instance deployments The previous poll-before-redirect approach didn't work because polls go through the load balancer and may hit a different instance than the one serving the redirect. - Add afterSave=1 query param to redirect URL after saving - Dashboard loader retries up to 6 times on 404/403 when afterSave is present, then strips the param on success - Remove waitForDashboardReady polling (ineffective through LB) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pages/DashboardScenePageStateManager.ts | 47 +++++++++++++++---- .../saving/useSaveDashboard.ts | 37 +++------------ 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index e17f7e36cf..98bc3dd9ba 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -573,19 +573,50 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag } return result; } - default: - // If reloadDashboardsOnParamsChange is on, we need to process query params for dashboard load - // Since the scene is not yet there, we need to process whatever came through URL - if (config.featureToggles.reloadDashboardsOnParamsChange) { - const queryParamsObject = processQueryParamsForDashboardLoad(); - rsp = await dashboardLoaderSrv.loadDashboard(type || 'db', slug || '', uid, queryParamsObject); - } else { - rsp = await dashboardLoaderSrv.loadDashboard(type || 'db', slug || '', uid); + default: { + // In multi-instance deployments, a post-save redirect may land on an instance + // that hasn't propagated the new dashboard yet. Retry on 404/403 when afterSave is set. + const searchParams = new URLSearchParams(locationService.getLocation().search); + const isAfterSave = searchParams.has('afterSave'); + const maxAttempts = isAfterSave ? 6 : 1; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + // If reloadDashboardsOnParamsChange is on, we need to process query params for dashboard load + // Since the scene is not yet there, we need to process whatever came through URL + if (config.featureToggles.reloadDashboardsOnParamsChange) { + const queryParamsObject = processQueryParamsForDashboardLoad(); + rsp = await dashboardLoaderSrv.loadDashboard(type || 'db', slug || '', uid, queryParamsObject); + } else { + rsp = await dashboardLoaderSrv.loadDashboard(type || 'db', slug || '', uid); + } + break; + } catch (e) { + const isRetryable = + isAfterSave && + attempt < maxAttempts - 1 && + isFetchError(e) && + (e.status === 404 || e.status === 403); + if (!isRetryable) { + throw e; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + // Strip afterSave param now that the dashboard loaded successfully + if (isAfterSave) { + searchParams.delete('afterSave'); + locationService.replace({ + ...locationService.getLocation(), + search: searchParams.toString(), + }); } if (route === DashboardRoutes.Embedded) { rsp.meta.isEmbedded = true; } + } } // Fix outdated URLs (e.g., old slugs from title changes) but skip during playlist navigation diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index cd85df6dcf..3f1a789e17 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -9,7 +9,6 @@ import appEvents from 'app/core/app_events'; import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; -import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api'; import { SaveDashboardAsOptions, SaveDashboardOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { DashboardSavedEvent } from 'app/types/events'; import { useDispatch } from 'app/types/store'; @@ -19,26 +18,6 @@ import { DashboardScene } from '../scene/DashboardScene'; import { DashboardInteractions } from '../utils/interactions'; import { trackDashboardSceneCreatedOrSaved } from '../utils/tracking'; -/** - * Wait until the dashboard is accessible via the API before redirecting. - * In multi-instance deployments, the instance serving the redirect may not - * have the dashboard or its permissions available yet. - */ -async function waitForDashboardReady(uid: string, maxAttempts = 8, intervalMs = 500): Promise { - const api = getDashboardAPI(); - for (let attempt = 0; attempt < maxAttempts; attempt++) { - try { - await api.getDashboardDTO(uid); - return; - } catch { - if (attempt < maxAttempts - 1) { - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - } - } - // Proceed with redirect even if polling fails — worst case the user refreshes -} - export function useSaveDashboard(isCopy = false) { const dispatch = useDispatch(); const notifyApp = useAppNotification(); @@ -112,15 +91,13 @@ export function useSaveDashboard(isCopy = false) { const newUrl = locationUtil.stripBaseFromUrl(resultData.url); if (newUrl !== currentLocation.pathname) { - // Wait until the dashboard is accessible before redirecting. - // In multi-instance deployments, the instance serving the redirect may not have - // the dashboard or its permissions propagated yet. This applies to new dashboards - // and save-as-copy, both of which create a new UID. - if (resultData.uid) { - await waitForDashboardReady(resultData.uid); - } + // Signal to the dashboard loader that this is a post-save redirect. + // In multi-instance deployments, the serving instance may not have the + // dashboard propagated yet — the loader will retry on 404/403. + const search = new URLSearchParams(currentLocation.search); + search.set('afterSave', '1'); setTimeout(() => { - locationService.push({ pathname: newUrl, search: currentLocation.search }); + locationService.push({ pathname: newUrl, search: search.toString() }); }); } @@ -141,4 +118,4 @@ export function useSaveDashboard(isCopy = false) { ); return { state, onSaveDashboard }; -} +} \ No newline at end of file From 6976cc615f5bd991c2da89fc3d6f6a6db677ba8f Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Thu, 19 Mar 2026 13:17:14 +0100 Subject: [PATCH 07/10] fix(intergral/dashboards): add HTTP 500 to retryable statuses on post-save load - Apiserver returns 500 (not 403/404) when permission check fails with a non-metav1.Status error during cross-pod dashboard load after save Co-Authored-By: Claude Opus 4.6 (1M context) --- .../dashboard-scene/pages/DashboardScenePageStateManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 98bc3dd9ba..d8028126f5 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -596,7 +596,7 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag isAfterSave && attempt < maxAttempts - 1 && isFetchError(e) && - (e.status === 404 || e.status === 403); + (e.status === 404 || e.status === 403 || e.status === 500); if (!isRetryable) { throw e; } From a99a081aecbb7edcff5e2a55b2da06e6b630f2ba Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Thu, 19 Mar 2026 15:07:14 +0100 Subject: [PATCH 08/10] fix(intergral/ci): fix pre-existing lint and test failures in UI build - Add missing newline at EOF in useSaveDashboard.ts - Fix import order in useDashboardSave.tsx (contextSrv after navBarTree) - Suppress expected jsdom XHR console.error in DashboardPageProxy test Co-Authored-By: Claude Opus 4.6 (1M context) --- public/app/features/dashboard-scene/saving/useSaveDashboard.ts | 2 +- .../dashboard/components/SaveDashboard/useDashboardSave.tsx | 2 +- .../features/dashboard/containers/DashboardPageProxy.test.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts index 3f1a789e17..355e3f89c7 100644 --- a/public/app/features/dashboard-scene/saving/useSaveDashboard.ts +++ b/public/app/features/dashboard-scene/saving/useSaveDashboard.ts @@ -118,4 +118,4 @@ export function useSaveDashboard(isCopy = false) { ); return { state, onSaveDashboard }; -} \ No newline at end of file +} diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 1d25a376c1..dea1a33850 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -6,9 +6,9 @@ import { t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; import { Dashboard } from '@grafana/schema'; import appEvents from 'app/core/app_events'; -import { contextSrv } from 'app/core/services/context_srv'; import { useAppNotification } from 'app/core/copy/appNotification'; import { updateDashboardName } from 'app/core/reducers/navBarTree'; +import { contextSrv } from 'app/core/services/context_srv'; import { useSaveDashboardMutation } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions'; diff --git a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx index e26b1099d2..ac766634c8 100644 --- a/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx +++ b/public/app/features/dashboard/containers/DashboardPageProxy.test.tsx @@ -169,6 +169,7 @@ describe('DashboardPageProxy', () => { }); it('should not render DashboardScenePage if route is Normal and has uid', async () => { + jest.spyOn(console, 'error').mockImplementation(); getDashboardScenePageStateManager().setDashboardCache('abc-def', dashMockEditable); act(() => { setup({ From 91b1d121555eebb8a3a1780e74e7ab364703d889 Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Thu, 19 Mar 2026 17:18:45 +0100 Subject: [PATCH 09/10] fix(intergral/dashboards): suppress permissions toast during post-save retry in V2 state manager - Add retry loop (6 attempts on 404/403/500) to V2 fetchDashboard(), mirroring V1 pattern - Strip afterSave query param after successful load in V2 path - Treat HTTP 500 as permission error in folder metadata fetch (v1.ts and v2.ts) to match apiserver behaviour Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pages/DashboardScenePageStateManager.ts | 35 +++++++++++++++++-- public/app/features/dashboard/api/v1.ts | 2 +- public/app/features/dashboard/api/v2.ts | 2 +- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index d8028126f5..4738eb5cc8 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -801,13 +801,44 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan case DashboardRoutes.Public: { return await this.dashboardLoader.loadDashboard('public', '', uid); } - default: - rsp = await this.dashboardLoader.loadDashboard(type || 'db', slug || '', uid); + default: { + // In multi-instance deployments, a post-save redirect may land on an instance + // that hasn't propagated the new dashboard yet. Retry on 404/403/500 when afterSave is set. + const searchParams = new URLSearchParams(locationService.getLocation().search); + const isAfterSave = searchParams.has('afterSave'); + const maxAttempts = isAfterSave ? 6 : 1; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + rsp = await this.dashboardLoader.loadDashboard(type || 'db', slug || '', uid); + break; + } catch (e) { + const isRetryable = + isAfterSave && + attempt < maxAttempts - 1 && + isFetchError(e) && + (e.status === 404 || e.status === 403 || e.status === 500); + if (!isRetryable) { + throw e; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + + // Strip afterSave param now that the dashboard loaded successfully + if (isAfterSave) { + searchParams.delete('afterSave'); + locationService.replace({ + ...locationService.getLocation(), + search: searchParams.toString(), + }); + } if (route === DashboardRoutes.Embedded) { rsp.metadata.annotations = rsp.metadata.annotations || {}; rsp.metadata.annotations[AnnoKeyEmbedded] = 'embedded'; } + } } // Fix outdated URLs (e.g., old slugs from title changes) but skip during playlist navigation // Playlists manage their own URL generation and redirects would break the navigation flow diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index 6f67451178..142ba6f39e 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -172,7 +172,7 @@ export class K8sDashboardAPI implements DashboardAPI { result.meta.folderId = folder.id; } catch (e) { // If user has access to dashboard but not to folder, continue without folder info - if (getStatusFromError(e) !== 403) { + if (getStatusFromError(e) !== 403 && getStatusFromError(e) !== 500) { throw new Error('Failed to load folder'); } // we still want to save the folder uid so that we can properly handle disabling the folder picker in Settings -> General diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index dfcbcc638f..784ba074dc 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -64,7 +64,7 @@ export class K8sDashboardV2API dashboard.metadata.annotations[AnnoKeyFolderUrl] = folder.url; } catch (e) { // If user has access to dashboard but not to folder, continue without folder info - if (getStatusFromError(e) !== 403) { + if (getStatusFromError(e) !== 403 && getStatusFromError(e) !== 500) { throw new Error('Failed to load folder'); } } From 56d5a4612e722b639c12c385e9e14bce3cd50d0e Mon Sep 17 00:00:00 2001 From: "John Hawksley (Intergral)" Date: Thu, 19 Mar 2026 19:32:37 +0100 Subject: [PATCH 10/10] fix(intergral/dashboards): suppress error toast at source via showErrorAlert on DTO requests - Add optional `options` param to `ScopedResourceClient.subresource()` and `ResourceClient` interface - Pass `{ showErrorAlert: false }` in V1 and V2 `getDashboardDTO()` calls - Prevents backendSrv.processRequestError from scheduling permissions toast - Both API classes already handle errors explicitly in their catch blocks Co-Authored-By: Claude Opus 4.6 (1M context) --- public/app/features/apiserver/client.ts | 11 ++++++++--- public/app/features/apiserver/types.ts | 4 +++- public/app/features/dashboard/api/v1.ts | 4 +++- public/app/features/dashboard/api/v2.ts | 4 +++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index 9aeda6c53e..1dbc2fff57 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -1,7 +1,7 @@ import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs'; import { isLiveChannelMessageEvent, LiveChannelScope } from '@grafana/data'; -import { config, getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime'; +import { BackendSrvRequest, config, getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getAPINamespace } from '../../api/utils'; @@ -98,8 +98,13 @@ export class ScopedResourceClient implements ); } - public async subresource(name: string, path: string, params?: Record): Promise { - return getBackendSrv().get(`${this.url}/${name}/${path}`, params); + public async subresource( + name: string, + path: string, + params?: Record, + options?: Partial + ): Promise { + return getBackendSrv().get(`${this.url}/${name}/${path}`, params, undefined, options); } public async list(opts?: ListOptions | undefined): Promise> { diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts index 3e77035f13..17d75e5ca2 100644 --- a/public/app/features/apiserver/types.ts +++ b/public/app/features/apiserver/types.ts @@ -10,6 +10,8 @@ import { Observable } from 'rxjs'; +import { BackendSrvRequest } from '@grafana/runtime'; + /** The object type and version */ export interface TypeMeta { apiVersion: string; @@ -261,7 +263,7 @@ export interface ResourceClient { update(obj: ResourceForCreate, params?: ResourceClientWriteParams): Promise>; delete(name: string, showSuccessAlert?: boolean): Promise; list(opts?: ListOptions): Promise>; - subresource(name: string, path: string, params?: Record): Promise; + subresource(name: string, path: string, params?: Record, options?: Partial): Promise; watch(opts?: WatchOptions): Observable>; } diff --git a/public/app/features/dashboard/api/v1.ts b/public/app/features/dashboard/api/v1.ts index 142ba6f39e..8dddfffb39 100644 --- a/public/app/features/dashboard/api/v1.ts +++ b/public/app/features/dashboard/api/v1.ts @@ -121,7 +121,9 @@ export class K8sDashboardAPI implements DashboardAPI { async getDashboardDTO(uid: string, params?: UrlQueryMap) { try { - const dash = await this.client.subresource>(uid, 'dto', params); + const dash = await this.client.subresource>( + uid, 'dto', params, { showErrorAlert: false } + ); // This could come as conversion error from v0 or v2 to V1. if (dash.status?.conversion?.failed && isV2StoredVersion(dash.status.conversion.storedVersion)) { diff --git a/public/app/features/dashboard/api/v2.ts b/public/app/features/dashboard/api/v2.ts index 784ba074dc..0da3d87009 100644 --- a/public/app/features/dashboard/api/v2.ts +++ b/public/app/features/dashboard/api/v2.ts @@ -42,7 +42,9 @@ export class K8sDashboardV2API async getDashboardDTO(uid: string) { try { - const dashboard = await this.client.subresource>(uid, 'dto'); + const dashboard = await this.client.subresource>( + uid, 'dto', undefined, { showErrorAlert: false } + ); // FOR /dto calls returning v2 spec we are ignoring the conversion status to avoid runtime errors caused by the status // being saved for v2 resources that's been client-side converted to v2 and then PUT to the API server.