[Dashboard] Fix styling of top nav bar (#159754)

Closes https://github.com/elastic/kibana/issues/159353
Closes https://github.com/elastic/kibana/issues/159756

## Summary

This PR adds styling so that, when the dashboard's chrome is hidden or
when there is a header banner present, the dashboard's top navigation
bar's `top` position is adjusted as necessary - this prevents it from
either (a) getting hidden behind the Kibana chrome or (b) floating in
the wrong position and overlapping the dashboard content, regardless of
if the dashboard is in embed mode (i.e. `embed=true` is present in the
URL) or not (i.e. `embed=false` is present in the URL **or**, more
commonly, there is no `embed` parameter in the URL).

### Embed Mode

- #### Before:


76d3c70c-936c-4bcc-985c-4fb433f0cff3

- #### After:


137bc103-666b-4fdd-ab4e-8994345e21b4

It also resolves a bug where the `isEmbeddedExternally` component state
was never actually being set, which meant that the
`ExitFullScreenButton` was always receiving `toggleChrome=true`. This
made it so that entering and exiting fullscreen mode in an embedded
dashboard would force the chrome to be visible (which should never
happen in embed mode).

#### How to Test
The easiest way to test this PR is to simply add `embed=true` to your
dashboard URL - because this PR also fixes
https://github.com/elastic/kibana/issues/159756, this will essentially
mimic the embedded experience.

Alternatively, if you want to test this in an actual iframe...

1. Start and login to Kibana with the default `kibana.yml` settings
2. Create and embed a dashboard using an iframe in an HTML file and open
that file in your browser - the iframe will show a prompt to login, but
you won't be able to. Instead...
3. Add the following settings to your `kibana.yml` file:<br><Br>
   ```
    xpack.security.secureCookies: true
    xpack.security.sameSiteCookies: 'None'
   ```
4. Wait for Kibana to re-load
5. Refresh the HTML page from step 2
6. The embedded dashboard should now be available for you to test 👍 

#### Scenarios Tested

-  **Non-fullscreen mode**

    <details>
<summary> Without filter pill, without header banner <i>(click to see
screenshot)</i></summary>
<img
src="f68bbcfb-74d8-497c-a2ae-33e8e0c02660"/>
    </details>

    <details>
<summary> Without filter pill, with header banner <i>(click to see
screenshot)</i></summary>
<img
src="7c19711c-61dc-499a-b1d0-01fab639a27e"/>
    </details>

    <details>
<summary> With filter pill, without header banner <i>(click to see
screenshot)</i></summary>
<img
src="36e848bd-f0d9-41e3-8a8a-a48571ad5cd2"/>
    </details>

    <details>
<summary> With filter pill, with header banner <i>(click to see
screenshot)</i></summary>
<img
src="cd7489f6-3f34-439a-a30e-3ef39f3970b5"/>
    </details>
    
    <details>
<summary> With filter pill, with header banner <b>and</b> notification
banner <i>(click to see GIF)</i></summary>
<img
src="bd67b4eb-4f68-4d9b-9e22-4d1b2d2e4d90"/>
    </details>

-  **Fullscreen mode**

    <details>
<summary> Without filter pill, without banner <i>(click to see
screenshot)</i></summary>
<img
src="d7d15560-7698-424f-b761-59b5557abe37"/>
    </details>

    <details>
<summary> Without filter pill, with header banner <i>(click to see
screenshot)</i></summary>
<img
src="311b6f3d-5152-4d16-ba39-160978c60c96"/>
    </details>

    <details>
<summary> With filter pill, without header banner <i>(click to see
screenshot)</i></summary>
<img
src="bff9e040-8169-40c7-a086-13a19e870383"/>
    </details>

    <details>
<summary> With filter pill, with header banner <i>(click to see
screenshot)</i></summary>
<img
src="3f453811-e65d-4ac4-9524-c396f9efdbdd"/>
    </details>

    <details>
<summary> With filter pill, with header banner <b>and</b> notification
banner <i>(click to see GIF)</i></summary>
<img
src="f79673e7-03f2-49fa-be56-b67bf7a12976"/>
    </details>

### Non-Embed Mode

- #### Before:


71ffc964-2844-41a6-98d6-353e84d674be

- #### After:


894aa292-b611-4e5e-a0d7-fe3d256fc3ba


### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Hannah Mudge 2023-06-20 16:23:06 -06:00 committed by GitHub
parent c0d3a93dff
commit a4d5209a9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 45 deletions

View file

@ -160,9 +160,11 @@ export function DashboardApp({
},
getInitialInput,
validateLoadedSavedObject: validateOutcome,
isEmbeddedExternally: Boolean(embedSettings), // embed settings are only sent if the dashboard URL has `embed=true`
});
}, [
history,
embedSettings,
validateOutcome,
getScopedHistory,
isScreenshotMode,

View file

@ -91,16 +91,16 @@ export async function mountApp({ core, element, appUnMounted, mountContext }: Da
routeParams: ParsedQuery<string>
): DashboardEmbedSettings | undefined => {
return {
forceShowTopNavMenu: Boolean(routeParams[dashboardUrlParams.showTopMenu]),
forceShowQueryInput: Boolean(routeParams[dashboardUrlParams.showQueryInput]),
forceShowDatePicker: Boolean(routeParams[dashboardUrlParams.showTimeFilter]),
forceHideFilterBar: Boolean(routeParams[dashboardUrlParams.hideFilterBar]),
forceShowTopNavMenu: routeParams[dashboardUrlParams.showTopMenu] === 'true',
forceShowQueryInput: routeParams[dashboardUrlParams.showQueryInput] === 'true',
forceShowDatePicker: routeParams[dashboardUrlParams.showTimeFilter] === 'true',
forceHideFilterBar: routeParams[dashboardUrlParams.hideFilterBar] === 'true',
};
};
const renderDashboard = (routeProps: RouteComponentProps<{ id?: string }>) => {
const routeParams = parse(routeProps.history.location.search);
if (routeParams.embed && !globalEmbedSettings) {
if (routeParams.embed === 'true' && !globalEmbedSettings) {
globalEmbedSettings = getDashboardEmbedSettings(routeParams);
}
return (

View file

@ -1,11 +1,26 @@
.dashboardTopNav {
width: 100%;
position: sticky;
z-index: $euiZLevel2;
background: $euiPageBackgroundColor;
.kbnBody {
.dashboardTopNav {
width: 100%;
position: sticky;
z-index: $euiZLevel2;
background: $euiPageBackgroundColor;
}
top: $kbnHeaderOffset;
&.dashboardTopNav-fullscreenMode {
top: 0;
&.kbnBody--noHeaderBanner {
&.kbnBody--chromeVisible .dashboardTopNav {
top: $kbnHeaderOffset;
}
&.kbnBody--chromeHidden .dashboardTopNav {
top: 0;
}
}
&.kbnBody--hasHeaderBanner {
&.kbnBody--chromeVisible .dashboardTopNav {
top: $kbnHeaderOffsetWithBanner;
}
&.kbnBody--chromeHidden .dashboardTopNav {
top: $kbnHeaderBannerHeight;
}
}
}

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import classNames from 'classnames';
import UseUnmount from 'react-use/lib/useUnmount';
import React, { useEffect, useMemo, useRef, useState } from 'react';
@ -77,6 +76,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
const fullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
const savedQueryId = dashboard.select((state) => state.componentState.savedQueryId);
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const query = dashboard.select((state) => state.explicitInput.query);
const title = dashboard.select((state) => state.explicitInput.title);
@ -207,11 +207,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr
});
return (
<div
className={classNames('dashboardTopNav', {
'dashboardTopNav-fullscreenMode': fullScreenMode,
})}
>
<div className="dashboardTopNav">
<h1
id="dashboardTitle"
className="euiScreenReaderOnly"

View file

@ -175,6 +175,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
...DEFAULT_DASHBOARD_INPUT,
id: initialInput.id,
},
isEmbeddedExternally: creationOptions?.isEmbeddedExternally,
animatePanelTransforms: false, // set panel transforms to false initially to avoid panels animating on initial render.
hasUnsavedChanges: false, // if there is initial unsaved changes, the initial diff will catch them.
lastSavedId: savedObjectId,

View file

@ -56,6 +56,8 @@ export interface DashboardCreationOptions {
unifiedSearchSettings?: { kbnUrlStateStorage: IKbnUrlStateStorage };
validateLoadedSavedObject?: (result: LoadDashboardReturn) => boolean;
isEmbeddedExternally?: boolean;
}
export class DashboardContainerFactoryDefinition

View file

@ -10,13 +10,19 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
export default function ({
getService,
getPageObjects,
updateBaselines,
}: FtrProviderContext & { updateBaselines: boolean }) {
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['dashboard', 'common']);
const browser = getService('browser');
const globalNav = getService('globalNav');
const screenshot = getService('screenshots');
const log = getService('log');
describe('embed mode', () => {
const urlParamExtensions = [
@ -36,40 +42,88 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
await PageObjects.common.navigateToApp('dashboard');
await PageObjects.dashboard.preserveCrossAppState();
await PageObjects.dashboard.loadSavedDashboard('few panels');
await PageObjects.dashboard.loadSavedDashboard('dashboard with everything');
await browser.setWindowSize(1300, 900);
});
it('hides the chrome', async () => {
const globalNavShown = await globalNav.exists();
expect(globalNavShown).to.be(true);
describe('default URL params', () => {
it('hides the chrome', async () => {
const globalNavShown = await globalNav.exists();
expect(globalNavShown).to.be(true);
const currentUrl = await browser.getCurrentUrl();
const newUrl = currentUrl + '&embed=true';
// Embed parameter only works on a hard refresh.
const useTimeStamp = true;
await browser.get(newUrl.toString(), useTimeStamp);
const currentUrl = await browser.getCurrentUrl();
const newUrl = currentUrl + '&embed=true';
// Embed parameter only works on a hard refresh.
const useTimeStamp = true;
await browser.get(newUrl.toString(), useTimeStamp);
await retry.try(async () => {
const globalNavHidden = !(await globalNav.exists());
expect(globalNavHidden).to.be(true);
await retry.try(async () => {
const globalNavHidden = !(await globalNav.exists());
expect(globalNavHidden).to.be(true);
});
});
it('expected elements are rendered and/or hidden', async () => {
await testSubjects.missingOrFail('top-nav');
await testSubjects.missingOrFail('queryInput');
await testSubjects.missingOrFail('superDatePickerToggleQuickMenuButton');
await testSubjects.existOrFail('globalQueryBar');
});
/**
* Skipping all render tests for now - there is a problem where the locally generated screenshots do not align with the
* CI screenshots due to (possibly) pixel density or something similar. This fix is super important to get in so we will
* have to resolve the issue with these new tests *after* FF for 8.9/8.8.2
*/
it.skip('renders as expected', async () => {
await PageObjects.dashboard.waitForRenderComplete();
const percentDifference = await screenshot.compareAgainstBaseline(
'dashboard_embed_mode',
updateBaselines
);
expect(percentDifference).to.be.lessThan(0.02);
});
});
it('shows or hides elements based on URL params', async () => {
await testSubjects.missingOrFail('top-nav');
await testSubjects.missingOrFail('queryInput');
await testSubjects.missingOrFail('superDatePickerToggleQuickMenuButton');
await testSubjects.existOrFail('showQueryBarMenu');
describe('non-default URL params', () => {
it('shows or hides elements based on URL params', async () => {
const currentUrl = await browser.getCurrentUrl();
const newUrl = [currentUrl].concat(urlParamExtensions).join('&');
// Embed parameter only works on a hard refresh.
const useTimeStamp = true;
await browser.get(newUrl.toString(), useTimeStamp);
const currentUrl = await browser.getCurrentUrl();
const newUrl = [currentUrl].concat(urlParamExtensions).join('&');
// Embed parameter only works on a hard refresh.
const useTimeStamp = true;
await browser.get(newUrl.toString(), useTimeStamp);
await testSubjects.existOrFail('top-nav');
await testSubjects.existOrFail('queryInput');
await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton');
});
await testSubjects.existOrFail('top-nav');
await testSubjects.existOrFail('queryInput');
await testSubjects.existOrFail('superDatePickerToggleQuickMenuButton');
it.skip('renders as expected', async () => {
await PageObjects.dashboard.waitForRenderComplete();
const percentDifference = await screenshot.compareAgainstBaseline(
'dashboard_embed_mode_with_url_params',
updateBaselines
);
expect(percentDifference).to.be.lessThan(0.02);
});
it.skip('renders as expected when scrolling', async () => {
const panels = await PageObjects.dashboard.getDashboardPanels();
const lastPanel = panels[panels.length - 1];
const lastPanelHeight = -parseInt(await lastPanel.getComputedStyle('height'), 10);
log.debug(
`scrolling to panel ${await lastPanel.getAttribute(
'data-test-embeddable-id'
)} with offset ${lastPanelHeight}...`
);
await lastPanel.scrollIntoViewIfNecessary(lastPanelHeight);
const percentDifference = await screenshot.compareAgainstBaseline(
'dashboard_embed_mode_scrolling',
updateBaselines
);
expect(percentDifference).to.be.lessThan(0.02);
});
});
after(async function () {

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB