[dashboard] Do not reset panel to undefined or empty last saved state (#203158)

Part of https://github.com/elastic/kibana/issues/201627

This is a short term fix for serverless and 8.x branches (long term fix
is an architectural change that will only be merged into 9.0 and 8.18
branches).

Fix prevents users from reseting a panel edited via embeddable transfer
service. This prevents panel from getting into an invalid state.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-12-09 20:04:15 -07:00 committed by GitHub
parent 418e50b4e0
commit e103a253d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 31 additions and 7 deletions

View file

@ -250,6 +250,7 @@ export function getPageApi() {
children$.next(children);
}
newPanels = {};
return true;
},
timeRange$,
unsavedChanges: unsavedChanges$ as PublishingSubject<object | undefined>,

View file

@ -14,11 +14,11 @@ import { waitFor } from '@testing-library/react';
describe('childrenUnsavedChanges$', () => {
const child1Api = {
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
resetUnsavedChanges: () => undefined,
resetUnsavedChanges: () => true,
};
const child2Api = {
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
resetUnsavedChanges: () => undefined,
resetUnsavedChanges: () => true,
};
const children$ = new BehaviorSubject<{ [key: string]: unknown }>({});
const onFireMock = jest.fn();
@ -99,7 +99,7 @@ describe('childrenUnsavedChanges$', () => {
...children$.value,
child3: {
unsavedChanges: new BehaviorSubject<object | undefined>({ key1: 'modified value' }),
resetUnsavedChanges: () => undefined,
resetUnsavedChanges: () => true,
},
});

View file

@ -95,10 +95,19 @@ export const initializeUnsavedChanges = <RuntimeState extends {} = {}>(
unsavedChanges,
resetUnsavedChanges: () => {
const lastSaved = lastSavedState$.getValue();
// Do not reset to undefined or empty last saved state
// Temporary fix for https://github.com/elastic/kibana/issues/201627
// TODO remove when architecture fix resolves issue.
if (comparatorKeys.length && (!lastSaved || Object.keys(lastSaved).length === 0)) {
return false;
}
for (const key of comparatorKeys) {
const setter = comparators[key][1]; // setter function is the 1st element of the tuple
setter(lastSaved?.[key] as RuntimeState[typeof key]);
}
return true;
},
snapshotRuntimeState,
} as PublishesUnsavedChanges<RuntimeState> & HasSnapshottableState<RuntimeState>,

View file

@ -11,7 +11,7 @@ import { PublishingSubject } from '../publishing_subject';
export interface PublishesUnsavedChanges<Runtime extends object = object> {
unsavedChanges: PublishingSubject<Partial<Runtime> | undefined>;
resetUnsavedChanges: () => void;
resetUnsavedChanges: () => boolean;
}
export const apiPublishesUnsavedChanges = (api: unknown): api is PublishesUnsavedChanges => {

View file

@ -43,7 +43,9 @@ export const getMockedBuildApi =
uuid,
parentApi: controlGroupApi ?? getMockedControlGroupApi(),
unsavedChanges: new BehaviorSubject<Partial<StateType> | undefined>(undefined),
resetUnsavedChanges: () => {},
resetUnsavedChanges: () => {
return true;
},
type: factory.type,
};
};

View file

@ -48,7 +48,9 @@ describe('TimesliderControlApi', () => {
uuid,
parentApi: controlGroupApi,
unsavedChanges: new BehaviorSubject<Partial<TimesliderControlState> | undefined>(undefined),
resetUnsavedChanges: () => {},
resetUnsavedChanges: () => {
return true;
},
type: factory.type,
};
}

View file

@ -68,6 +68,7 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
import { i18n } from '@kbn/i18n';
import { DASHBOARD_CONTAINER_TYPE, DashboardApi, DashboardLocatorParams } from '../..';
import type { DashboardAttributes } from '../../../server/content_management';
import { DashboardContainerInput, DashboardPanelMap, DashboardPanelState } from '../../../common';
@ -957,7 +958,16 @@ export class DashboardContainer
for (const panelId of Object.keys(currentChildren)) {
if (this.getInput().panels[panelId]) {
const child = currentChildren[panelId];
if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges();
if (apiPublishesUnsavedChanges(child)) {
const success = child.resetUnsavedChanges();
if (!success) {
coreServices.notifications.toasts.addWarning(
i18n.translate('dashboard.reset.panelError', {
defaultMessage: 'Unable to reset panel changes',
})
);
}
}
} else {
// if reset resulted in panel removal, we need to update the list of children
delete currentChildren[panelId];