-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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] Fix blank panel save and display issue. #120815
Changes from 3 commits
e18448e
7781d60
2a475ff
84afa07
3fcd626
4ee8a49
77c622f
12bae8b
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 |
---|---|---|
|
@@ -169,8 +169,9 @@ export class AttributeService< | |
// Remove unneeded attributes from the original input. | ||
const newInput = omit(input, ATTRIBUTE_SERVICE_KEY); | ||
|
||
// Combine input and wrapped input to preserve any passed in explicit Input. | ||
resolve({ ...newInput, ...wrappedInput }); | ||
// Combine input and wrapped input to preserve any passed in explicit Input while ensuring that the | ||
// library title ovewrites the original title | ||
resolve({ ...newInput, ...wrappedInput, title: newAttributes.title }); | ||
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. nit; This code overall is a little tricksy to follow 😅 it seems a little clearer to me if we did: const wrappedInput = (await this.wrapAttributes(newAttributes, true, {
title: props.newTitle,
})) as RefType; perhaps @Dosant has some input (pun intended) too. |
||
return { id: wrappedInput.savedObjectId }; | ||
} catch (error) { | ||
reject(error); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -153,7 +153,7 @@ export function PanelHeader({ | |
|
||
if (!showPanelBar) { | ||
return ( | ||
<div className={classes}> | ||
<div data-test-subj="dashboardPanelTitle__wrapper" className={classes}> | ||
<PanelOptionsMenu | ||
getActionContextMenuPanel={getActionContextMenuPanel} | ||
isViewMode={isViewMode} | ||
|
@@ -212,22 +212,24 @@ export function PanelHeader({ | |
}; | ||
|
||
return ( | ||
<figcaption | ||
className={classes} | ||
data-test-subj={`embeddablePanelHeading-${(title || '').replace(/\s/g, '')}`} | ||
> | ||
<h2 data-test-subj="dashboardPanelTitle" className="embPanel__title embPanel__dragger"> | ||
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly> | ||
{renderTitle()} | ||
{renderBadges(badges, embeddable)} | ||
</h2> | ||
{renderNotifications(notifications, embeddable)} | ||
<PanelOptionsMenu | ||
isViewMode={isViewMode} | ||
getActionContextMenuPanel={getActionContextMenuPanel} | ||
closeContextMenu={closeContextMenu} | ||
title={title} | ||
/> | ||
</figcaption> | ||
<span data-test-subj="dashboardPanelTitle__wrapper"> | ||
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. If you have the 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. Unfortunately, yup! The |
||
<figcaption | ||
className={classes} | ||
data-test-subj={`embeddablePanelHeading-${(title || '').replace(/\s/g, '')}`} | ||
> | ||
<h2 data-test-subj="dashboardPanelTitle" className="embPanel__title embPanel__dragger"> | ||
<EuiScreenReaderOnly>{getAriaLabel()}</EuiScreenReaderOnly> | ||
{renderTitle()} | ||
{renderBadges(badges, embeddable)} | ||
</h2> | ||
{renderNotifications(notifications, embeddable)} | ||
<PanelOptionsMenu | ||
isViewMode={isViewMode} | ||
getActionContextMenuPanel={getActionContextMenuPanel} | ||
closeContextMenu={closeContextMenu} | ||
title={title} | ||
/> | ||
</figcaption> | ||
</span> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -206,7 +206,8 @@ export class TestSubjects extends FtrService { | |
// call clearValue() and type() on the element that is focused after | ||
// clicking on the testSubject | ||
const input = await this.findService.activeElement(); | ||
if (clearWithKeyboard === true) { | ||
// if `text` is explicitely set to the empty string, then call clearValueWithKeyboard() rather than clearValue() | ||
if (clearWithKeyboard === true || text === '') { | ||
await input.clearValueWithKeyboard(); | ||
} else { | ||
await input.clearValue(); | ||
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. I will share more context here: both It would be better not to change default behaviour to keep consistency across all the tests. 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. But I wonder if
call means it was using the default way to clean the field 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. That makes perfect sense! Thanks for the feedback :) These changes to |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import expect from '@kbn/expect'; | ||
import { FtrProviderContext } from '../../ftr_provider_context'; | ||
|
||
export default function ({ getService, getPageObjects }: FtrProviderContext) { | ||
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. This is a very well put together test suite 🔥🔥 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. 💪 |
||
const esArchiver = getService('esArchiver'); | ||
const kibanaServer = getService('kibanaServer'); | ||
const testSubjects = getService('testSubjects'); | ||
const retry = getService('retry'); | ||
const dashboardPanelActions = getService('dashboardPanelActions'); | ||
const PageObjects = getPageObjects([ | ||
'common', | ||
'dashboard', | ||
'visualize', | ||
'visEditor', | ||
'timePicker', | ||
'lens', | ||
]); | ||
|
||
const DASHBOARD_NAME = 'Panel Title Test'; | ||
const EMPTY_TITLE = '[No Title]'; | ||
const NEW_CUSTOM_TITLE = 'Test Custom Title'; | ||
const LIBRARY_TITLE_FOR_EMPTY_TESTS = 'Library Title for Empty String'; | ||
|
||
describe('panel titles', () => { | ||
const clearUnsavedChanges = async () => { | ||
await retry.try(async () => { | ||
// avoid flaky test by surrounding in retry | ||
await testSubjects.existOrFail('dashboardUnsavedChangesBadge'); | ||
await PageObjects.dashboard.clickQuickSave(); | ||
await testSubjects.missingOrFail('dashboardUnsavedChangesBadge'); | ||
}); | ||
}; | ||
|
||
before(async () => { | ||
await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); | ||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); | ||
await kibanaServer.importExport.load( | ||
'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json' | ||
); | ||
await PageObjects.common.navigateToApp('dashboard'); | ||
await PageObjects.dashboard.preserveCrossAppState(); | ||
await PageObjects.dashboard.clickNewDashboard(); | ||
await PageObjects.dashboard.saveDashboard(DASHBOARD_NAME); | ||
}); | ||
|
||
it('new panel by value has empty title', async () => { | ||
await PageObjects.lens.createAndAddLensFromDashboard({}); | ||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(newPanelTitle).to.equal(EMPTY_TITLE); | ||
}); | ||
|
||
it('saving new panel with blank title clears "unsaved changes" badge', async () => { | ||
await dashboardPanelActions.setCustomPanelTitle(''); | ||
await clearUnsavedChanges(); | ||
}); | ||
|
||
it('custom title causes unsaved changes and saving clears it', async () => { | ||
await dashboardPanelActions.setCustomPanelTitle(NEW_CUSTOM_TITLE); | ||
const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(panelTitle).to.equal(NEW_CUSTOM_TITLE); | ||
await clearUnsavedChanges(); | ||
}); | ||
|
||
it('resetting title on a by reference panel sets it to the library title', async () => { | ||
const BY_REFERENCE_TITLE = 'Reset Title - By Reference'; | ||
await dashboardPanelActions.saveToLibrary(BY_REFERENCE_TITLE); | ||
await dashboardPanelActions.setCustomPanelTitle('This should go away'); | ||
|
||
await dashboardPanelActions.resetCustomPanelTitle(); | ||
const resetPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(resetPanelTitle).to.equal(BY_REFERENCE_TITLE); | ||
|
||
// unlink so that panel goes back to by value for next tests | ||
await dashboardPanelActions.unlinkFromLibary(); | ||
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. Tests should be fully isolated from each other so that if ordering ever changes everything should still work. This, and other post-test teardown logic should live inside of an Perhaps we can navigate to dashboard home if we need to refresh the page and create helpers for setting up a new panel in 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. This is great feedback, thanks! :) |
||
}); | ||
|
||
it('resetting title on a by value panel sets it to the empty string', async () => { | ||
const BY_VALUE_TITLE = 'Reset Title - By Value'; | ||
await dashboardPanelActions.setCustomPanelTitle(BY_VALUE_TITLE); | ||
|
||
await dashboardPanelActions.resetCustomPanelTitle(); | ||
const panelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(panelTitle).to.equal(EMPTY_TITLE); | ||
await clearUnsavedChanges(); | ||
}); | ||
|
||
it('blank titles are hidden in view mode', async () => { | ||
await PageObjects.dashboard.clickCancelOutOfEditMode(); | ||
|
||
const titleVisibility = (await PageObjects.dashboard.getVisibilityOfPanelTitles())[0]; | ||
expect(titleVisibility).to.be(false); | ||
}); | ||
|
||
it('custom titles are visible in view mode', async () => { | ||
await PageObjects.dashboard.switchToEditMode(); | ||
await dashboardPanelActions.setCustomPanelTitle(NEW_CUSTOM_TITLE); | ||
await PageObjects.dashboard.clickQuickSave(); | ||
await PageObjects.dashboard.clickCancelOutOfEditMode(); | ||
|
||
const titleVisibility = (await PageObjects.dashboard.getVisibilityOfPanelTitles())[0]; | ||
expect(titleVisibility).to.be(true); | ||
}); | ||
|
||
it('hiding an individual panel title hides it in view mode', async () => { | ||
await PageObjects.dashboard.switchToEditMode(); | ||
await dashboardPanelActions.toggleHidePanelTitle(); | ||
await PageObjects.dashboard.clickQuickSave(); | ||
await PageObjects.dashboard.clickCancelOutOfEditMode(); | ||
|
||
const titleVisibility = (await PageObjects.dashboard.getVisibilityOfPanelTitles())[0]; | ||
expect(titleVisibility).to.be(false); | ||
|
||
// undo the previous hide panel toggle (i.e. make the panel visible) to prepare for next tests | ||
await PageObjects.dashboard.switchToEditMode(); | ||
await dashboardPanelActions.toggleHidePanelTitle(); | ||
await PageObjects.dashboard.clickQuickSave(); | ||
}); | ||
|
||
it('linking a by value panel with a custom title to the library will overwrite the custom title with the library title', async () => { | ||
// note that the panel already has a custom title from the previous tests | ||
const BY_REFERENCE_TITLE = 'Test Custom Title on Link'; | ||
|
||
await PageObjects.dashboard.switchToEditMode(); | ||
await dashboardPanelActions.saveToLibrary(BY_REFERENCE_TITLE); | ||
await retry.try(async () => { | ||
// need to surround in 'retry' due to delays in HTML updates causing the title read to be behind | ||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(newPanelTitle).to.equal(BY_REFERENCE_TITLE); | ||
}); | ||
}); | ||
|
||
it('unlinking a by reference panel with a custom title will keep the current title', async () => { | ||
await dashboardPanelActions.setCustomPanelTitle(NEW_CUSTOM_TITLE); | ||
await dashboardPanelActions.unlinkFromLibary(); | ||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(newPanelTitle).to.equal(NEW_CUSTOM_TITLE); | ||
}); | ||
|
||
it("linking a by value panel with a blank title to the library will set the panel's title to the library title", async () => { | ||
await dashboardPanelActions.setCustomPanelTitle(''); | ||
await dashboardPanelActions.saveToLibrary(LIBRARY_TITLE_FOR_EMPTY_TESTS); | ||
await retry.try(async () => { | ||
// need to surround in 'retry' due to delays in HTML updates causing the title read to be behind | ||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(newPanelTitle).to.equal(LIBRARY_TITLE_FOR_EMPTY_TESTS); | ||
}); | ||
}); | ||
|
||
it('unlinking a by reference panel without a custom title will keep the library title', async () => { | ||
await dashboardPanelActions.unlinkFromLibary(); | ||
const newPanelTitle = (await PageObjects.dashboard.getPanelTitles())[0]; | ||
expect(newPanelTitle).to.equal(LIBRARY_TITLE_FOR_EMPTY_TESTS); | ||
}); | ||
}); | ||
} |
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.
👌