New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Dashboard] Prevent unnecessary loss of dashboard unsaved state #167707
Changes from 12 commits
6ba1228
004f1f2
f1d07e7
8dccdd8
294c3ee
d101aa1
b90df40
4d4547d
19cdc32
a8a8866
1f3f4d3
9c5c15a
cad9af0
709a3e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
*/ | ||
|
||
import { firstValueFrom } from 'rxjs'; | ||
import { isEqual } from 'lodash'; | ||
|
||
import { set } from '@kbn/safer-lodash-set'; | ||
import { ViewMode } from '@kbn/embeddable-plugin/public'; | ||
|
@@ -110,7 +111,12 @@ class DashboardBackupService implements DashboardBackupServiceType { | |
} | ||
} | ||
|
||
public getDashboardIdsWithUnsavedChanges() { | ||
/** | ||
* Because we are storing these unsaved dashboard IDs in React component state, we only want things to be re-rendered | ||
* if the **contents** change, not if the array reference changes; therefore, in order to maintain this reference, this | ||
* method needs to take the old array in as an argument. | ||
*/ | ||
public getDashboardIdsWithUnsavedChanges(oldDashboardsWithUnsavedChanges: string[] = []) { | ||
try { | ||
const dashboardStatesInSpace = | ||
this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[this.activeSpaceId] || {}; | ||
|
@@ -125,7 +131,10 @@ class DashboardBackupService implements DashboardBackupServiceType { | |
) | ||
dashboardsWithUnsavedChanges.push(dashboardId); | ||
}); | ||
return dashboardsWithUnsavedChanges; | ||
|
||
return isEqual(oldDashboardsWithUnsavedChanges, dashboardsWithUnsavedChanges) | ||
? oldDashboardsWithUnsavedChanges | ||
Heenawter marked this conversation as resolved.
Show resolved
Hide resolved
|
||
: dashboardsWithUnsavedChanges; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only change the array reference if the contents of the array have changed - this prevents an infinite React loop that would happen when an error was caught in the |
||
} catch (e) { | ||
this.notifications.toasts.addDanger({ | ||
title: backupServiceStrings.getPanelsGetError(e.message), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,16 +6,16 @@ | |
* Side Public License, v 1. | ||
*/ | ||
|
||
import { Reference } from '@kbn/content-management-utils'; | ||
import { SavedObjectError, SavedObjectsFindOptionsReference } from '@kbn/core/public'; | ||
|
||
import { Reference } from '@kbn/content-management-utils'; | ||
import { | ||
DashboardItem, | ||
DashboardCrudTypes, | ||
DashboardAttributes, | ||
DashboardCrudTypes, | ||
DashboardItem, | ||
} from '../../../../common/content_management'; | ||
import { DashboardStartDependencies } from '../../../plugin'; | ||
import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants'; | ||
import { DashboardStartDependencies } from '../../../plugin'; | ||
import { dashboardContentManagementCache } from '../dashboard_content_management_service'; | ||
|
||
export interface SearchDashboardsArgs { | ||
|
@@ -83,19 +83,34 @@ export async function findDashboardById( | |
references: cachedDashboard.item.references, | ||
}; | ||
} | ||
|
||
/** Otherwise, fetch the dashboard from the content management client, add it to the cache, and return the result */ | ||
const response = await contentManagement.client | ||
.get<DashboardCrudTypes['GetIn'], DashboardCrudTypes['GetOut']>({ | ||
try { | ||
const response = await contentManagement.client.get< | ||
DashboardCrudTypes['GetIn'], | ||
DashboardCrudTypes['GetOut'] | ||
>({ | ||
contentTypeId: DASHBOARD_CONTENT_ID, | ||
id, | ||
}) | ||
.then((result) => { | ||
dashboardContentManagementCache.addDashboard(result); | ||
return { id, status: 'success', attributes: result.item.attributes }; | ||
}) | ||
.catch((e) => ({ status: 'error', error: e.body, id })); | ||
}); | ||
if (response.item.error) { | ||
throw response.item.error; | ||
} | ||
|
||
return response as FindDashboardsByIdResponse; | ||
dashboardContentManagementCache.addDashboard(response); | ||
return { | ||
id, | ||
status: 'success', | ||
attributes: response.item.attributes, | ||
references: response.item.references, | ||
}; | ||
} catch (e) { | ||
return { | ||
status: 'error', | ||
error: e.body || e.message, | ||
id, | ||
}; | ||
} | ||
Comment on lines
+88
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Had to refactor this - for some reason, the old code wasn't catching errors being thrown in |
||
} | ||
|
||
export async function findDashboardsByIds( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,7 @@ export interface DashboardContentManagementRequiredServices { | |
|
||
export interface DashboardContentManagementService { | ||
findDashboards: FindDashboardsService; | ||
deleteDashboards: (ids: string[]) => void; | ||
deleteDashboards: (ids: string[]) => Promise<void>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated, but noticed during my investigation: this method was |
||
loadDashboardState: (props: { id?: string }) => Promise<LoadDashboardReturn>; | ||
saveDashboardState: (props: SaveDashboardProps) => Promise<SaveDashboardReturn>; | ||
checkForDuplicateDashboardTitle: (meta: DashboardDuplicateTitleCheckProps) => Promise<boolean>; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,8 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { | |
let unsavedPanelCount = 0; | ||
const testQuery = 'Test Query'; | ||
|
||
// Failing: See https://github.com/elastic/kibana/issues/167661 | ||
describe.skip('dashboard unsaved state', () => { | ||
describe('dashboard unsaved state', () => { | ||
before(async () => { | ||
await kibanaServer.savedObjects.cleanStandardList(); | ||
await kibanaServer.importExport.load( | ||
|
@@ -140,6 +139,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { | |
await PageObjects.visualize.gotoVisualizationLandingPage(); | ||
await PageObjects.header.waitUntilLoadingHasFinished(); | ||
await PageObjects.dashboard.navigateToApp(); | ||
if (await PageObjects.dashboard.onDashboardLandingPage()) { | ||
await testSubjects.existOrFail('unsavedDashboardsCallout'); | ||
} | ||
Comment on lines
+142
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding this so that, if this failure happens again, it will be clearer why the failure is happening; because the failure (and the corresponding failure screenshot) happened on the dashboard itself, it took me awhile to confirm that the failure was happening due to the session storage being cleared. Failing on the listing page makes this clearer. |
||
await PageObjects.dashboard.loadSavedDashboard('few panels'); | ||
const currentPanelCount = await PageObjects.dashboard.getPanelCount(); | ||
expect(currentPanelCount).to.eql(unsavedPanelCount); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is ultimately what fixed this flakiness 🎉 We should only clear the state specifically if the CM service returned an "Object not found" error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good that you used the status code directly rather than testing against the error message!