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;