diff --git a/public/app/fn-app/fn-dashboard-page/fn-dashboard.test.tsx b/public/app/fn-app/fn-dashboard-page/fn-dashboard.test.tsx index b8253d2a78cc7..813abf554490c 100644 --- a/public/app/fn-app/fn-dashboard-page/fn-dashboard.test.tsx +++ b/public/app/fn-app/fn-dashboard-page/fn-dashboard.test.tsx @@ -106,8 +106,10 @@ describe('FNDashboard', () => { afterEach(() => { cleanup(); act(() => { - for (const uid of dashboardUIDs) { + for (const uid of Object.keys(mfeGetStoreState().fnGlobalReducer.dashboards)) { mfeDispatch(removeGrafanaStoreAndDashboard(uid)); + } + for (const uid of dashboardUIDs) { document.getElementById(`${uid}-portal`)?.remove(); } mfeDispatch(updateRenderingDashboardUID('')); @@ -140,6 +142,50 @@ describe('FNDashboard', () => { expect(consoleError.mock.calls.flat().join(' ')).not.toContain('Cannot update a component'); }); + it('replaces the dashboard owning a stable portal through draft creation and save', () => { + const publishedUid = dashboardUIDs[0]; + const draftUid = 'custom-dashboard-draft'; + const portalContainerID = `${publishedUid}-portal`; + render( + + ); + + for (const uid of [draftUid, publishedUid, draftUid, publishedUid]) { + const previousUid = uid === draftUid ? publishedUid : draftUid; + act(() => mfeDispatch(updatePartialMfeStates({ uid, portalContainerID }))); + + expect(document.getElementById(portalContainerID)!.querySelectorAll('.page-dashboard')).toHaveLength(1); + expect(screen.getByTestId(`dashboard-${uid}`)).toBeInTheDocument(); + expect(screen.queryByTestId(`dashboard-${previousUid}`)).not.toBeInTheDocument(); + expect(mfeGetStoreState().fnGlobalReducer.grafanaStores[previousUid]).toBeUndefined(); + expect(mfeGetStoreState().fnGlobalReducer.renderingDashboardUID).toBe(uid); + } + }); + + it('preserves the existing store on same-UID updates and unrelated portals on replacement', () => { + mountDashboard(dashboardUIDs[1]); + const parentStore = mfeGetStoreState().fnGlobalReducer.grafanaStores[dashboardUIDs[0]]; + const drawerStore = mfeGetStoreState().fnGlobalReducer.grafanaStores[dashboardUIDs[1]]; + mfeDispatch(updatePartialMfeStates({ uid: dashboardUIDs[0], refreshRevision: 3 })); + mfeDispatch(updatePartialMfeStates({ uid: dashboardUIDs[0], portalContainerID: `${dashboardUIDs[0]}-portal` })); + expect(mfeGetStoreState().fnGlobalReducer.grafanaStores[dashboardUIDs[0]]).toBe(parentStore); + expect(mfeGetStoreState().fnGlobalReducer.dashboards[dashboardUIDs[0]].refreshRevision).toBe(3); + + mfeDispatch(updatePartialMfeStates({ uid: 'draft', portalContainerID: `${dashboardUIDs[0]}-portal` })); + expect(mfeGetStoreState().fnGlobalReducer.grafanaStores[dashboardUIDs[1]]).toBe(drawerStore); + expect(mfeGetStoreState().fnGlobalReducer.dashboards[dashboardUIDs[1]].portalContainerID).toBe( + `${dashboardUIDs[1]}-portal` + ); + expect(mfeGetStoreState().fnGlobalReducer.grafanaStores[dashboardUIDs[0]]).toBeUndefined(); + }); + it('restores the preserved parent time owner after drawer removal without refreshing it', () => { const parent = makeModel(); mfeGetStoreState().fnGlobalReducer.grafanaStores[dashboardUIDs[0]].dispatch({ type: 'test/model', model: parent }); diff --git a/public/app/store/configureMfeStore.ts b/public/app/store/configureMfeStore.ts index 476270149e1a9..993b9ee9931d0 100644 --- a/public/app/store/configureMfeStore.ts +++ b/public/app/store/configureMfeStore.ts @@ -50,6 +50,19 @@ function setGrafanaStore(state: WritableDraft, uid: string, init const reducers: SliceCaseReducers = { updatePartialMfeStates: (state, action: UpdateFNGlobalStateAction) => { const { uid, ...partialState } = action.payload; + // A host portal has one dashboard owner. Custom dashboards keep the DOM + // container while switching between published and draft UIDs; retaining + // both stores would render both dashboards into that same container. + // Only explicit container updates transfer ownership, not async refresh + // completions or other UID-only state patches. + if (uid && partialState.portalContainerID) { + for (const [previousUid, dashboard] of Object.entries(state.dashboards)) { + if (previousUid !== uid && dashboard.portalContainerID === partialState.portalContainerID) { + delete state.dashboards[previousUid]; + delete state.grafanaStores[previousUid]; + } + } + } setGrafanaStore(state, uid, partialState); const fnState = state.dashboards[uid]; state.FNDashboard = true;