[Workspace Chrome] Bootstrap grid layout for classic nav (#224255)

> [!IMPORTANT]
> **Should be no user-facing changes!!!** The new layout work is behind
a feature flag!

> [!IMPORTANT]  
> This bootstraps new grid layout for chrome using a feature flag. It
only works with classic nav and hack a lot of bugs and EUI-related
workarounds, but the overall code structure and approach can be reviewed
and merged to main.


## Summary

Part of [workspace
chrome](https://github.com/elastic/kibana-team/issues/1581 ) work. In
this PR we lay down the ground work for new grid layout that will power
Kibana's chrome. This is done by introducing **a feature flag** with
which Kibana can switch between "legacy-fixed" layout and new "grid"
layout.


![Image](https://github.com/user-attachments/assets/8d91ef37-f17e-4cee-980b-23834d81290e)

Proper detailed figma link:
https://www.figma.com/design/10ca4AhnWDkyJklUDXnHg5/Sidebar?node-id=5192-259808&p=f&m=dev


kibana.yml:
```
feature_flags.overrides:
  core.chrome.layoutType: 'grid'
```

For this, in-between `rendering_service` and `chrome_service` a new
`layout_service` was introduced the goal of which is to aggregate stuff
from chrome service and compose it together using the needed layout.
There are two implementations for `layout_service`:
- `LegacyFixedLayout` - old one, just code refactor, should still work
as in main
- `GridLayout`- new one, mostly works, but only for classic nav, for
now, and with bunch of hacks and bugs that we will resolve over time

The switch is in `rendering_service` based on a feature flag: 

```tsx
const layout: LayoutService =
      layoutType === 'grid'
        ? new GridLayout(renderCoreDeps)
        : new LegacyFixedLayout(renderCoreDeps);

    const Layout = layout.getComponent();

    ReactDOM.render(
      <KibanaRootContextProvider {...startServices} globalStyles={true}>
        <Layout />
      </KibanaRootContextProvider>,
      targetDomElement
    );`
```

To see the grid and new layout in action there is a helpful `debug` flag
that displays not yet used elements of new layout:

kibana.yml:
```
feature_flags.overrides:
  core.chrome.layoutType: 'grid'
  core.chrome.layoutDebug: true
```


https://github.com/user-attachments/assets/9e4ad1d9-ed23-41ab-b029-254f7511136d




### Other clean ups 

- Migrate `.chrHeaderBadge__wrapper`, `. chrHeaderHelpMenu__version`,
`breadcrumbsWithExtensionContainer` to emotion on simplify global css of
chrome
- remove `getIsNavDrawerLocked` and related css since not used 
- Small unzyme 

### TODO

- [x] fix solution nav in management 
- [x] make sure solution nav works with header 
- [x] fix dashboard full screen mode
- [x] check discover eui grid full screen
- [x] check chromeless mode
- [x] Follow up on EUI related hacks
https://github.com/elastic/eui/issues/8820
- [ ] Misaligned console in search solution 
- [ ] Miaaligned secondary nav in security solutions
- [ ] double scroll in discover push flyout


## How to review 

1. Most importantly, we need to ensure that nothing is broken in the old
layout during the refactor. - Functional tests + visual/manual testing
2. Then for the new layout: 

kibana.yml:
```
feature_flags.overrides:
  core.chrome.layoutType: 'grid'
  core.chrome.layoutDebug: true
```

- Check that it mostly works (some specific edge cases and bugs are
fine)
- Code-review:  focus on the layout implementation split approach
This commit is contained in:
Anton Dosov 2025-06-27 16:08:47 +02:00 committed by GitHub
parent f43138c059
commit fe9dcf751a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1050 additions and 273 deletions

1
.github/CODEOWNERS vendored
View file

@ -125,6 +125,7 @@ src/core/packages/capabilities/server-mocks @elastic/kibana-core
src/core/packages/chrome/browser @elastic/appex-sharedux
src/core/packages/chrome/browser-internal @elastic/appex-sharedux
src/core/packages/chrome/browser-mocks @elastic/appex-sharedux
src/core/packages/chrome/layout/core-chrome-layout @elastic/appex-sharedux
src/core/packages/chrome/layout/core-chrome-layout-components @elastic/appex-sharedux
src/core/packages/config/server-internal @elastic/kibana-core
src/core/packages/custom-branding/browser @elastic/appex-sharedux

View file

@ -284,6 +284,7 @@
"@kbn/core-capabilities-server-internal": "link:src/core/packages/capabilities/server-internal",
"@kbn/core-chrome-browser": "link:src/core/packages/chrome/browser",
"@kbn/core-chrome-browser-internal": "link:src/core/packages/chrome/browser-internal",
"@kbn/core-chrome-layout": "link:src/core/packages/chrome/layout/core-chrome-layout",
"@kbn/core-chrome-layout-components": "link:src/core/packages/chrome/layout/core-chrome-layout-components",
"@kbn/core-config-server-internal": "link:src/core/packages/config/server-internal",
"@kbn/core-custom-branding-browser": "link:src/core/packages/custom-branding/browser",

View file

@ -10,4 +10,3 @@
export type { AppCategory } from './src/app_category';
export { APP_WRAPPER_CLASS } from './src/app_wrapper_class';
export { DEFAULT_APP_CATEGORIES } from './src/default_app_categories';
export { GlobalAppStyle } from './src/global_app_style';

View file

@ -8,7 +8,7 @@
*/
import { registerAnalyticsContextProviderMock } from './chrome_service.test.mocks';
import { shallow, mount } from 'enzyme';
import { render, screen } from '@testing-library/react';
import React from 'react';
import * as Rx from 'rxjs';
import { toArray, firstValueFrom } from 'rxjs';
@ -27,7 +27,7 @@ import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { getAppInfo } from '@kbn/core-application-browser-internal';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { findTestSubject } from '@kbn/test-jest-helpers';
import { ChromeService } from './chrome_service';
const mockhandleSystemColorModeChange = jest.fn();
@ -242,7 +242,8 @@ describe('start', () => {
// Have to do some fanagling to get the type system and enzyme to accept this.
// Don't capture the snapshot because it's 600+ lines long.
expect(shallow(React.createElement(() => chrome.getHeaderComponent()))).toBeDefined();
// Render and assert that no error is thrown
render(React.createElement(() => chrome.getLegacyHeaderComponentForFixedLayout()));
});
it('renders the custom project side navigation', async () => {
@ -256,20 +257,15 @@ describe('start', () => {
chrome.setChromeStyle('project');
chrome.project.setSideNavComponent(MyNav);
const component = mount(
render(
<KibanaRenderContextProvider {...startDeps}>
{chrome.getHeaderComponent()}
{chrome.getLegacyHeaderComponentForFixedLayout()}
</KibanaRenderContextProvider>
);
const projectHeader = findTestSubject(component, 'kibanaProjectHeader');
expect(projectHeader.length).toBe(1);
const defaultProjectSideNav = findTestSubject(component, 'defaultProjectSideNav');
expect(defaultProjectSideNav.length).toBe(0); // Default side nav not mounted
const customProjectSideNav = findTestSubject(component, 'customProjectSideNav');
expect(customProjectSideNav.text()).toBe('HELLO');
expect(screen.getByTestId('kibanaProjectHeader')).toBeInTheDocument();
expect(screen.queryByTestId('defaultProjectSideNav')).not.toBeInTheDocument();
expect(screen.getByTestId('customProjectSideNav')).toHaveTextContent('HELLO');
});
it('renders chromeless header', async () => {
@ -277,14 +273,13 @@ describe('start', () => {
chrome.setIsVisible(false);
const component = mount(
render(
<KibanaRenderContextProvider {...startDeps}>
{chrome.getHeaderComponent()}
{chrome.getLegacyHeaderComponentForFixedLayout()}
</KibanaRenderContextProvider>
);
const chromeless = findTestSubject(component, 'kibanaHeaderChromeless');
expect(chromeless.length).toBe(1);
expect(screen.getByTestId('kibanaHeaderChromeless')).toBeInTheDocument();
});
});
@ -741,10 +736,9 @@ describe('start', () => {
});
describe('stop', () => {
it('completes applicationClass$, getIsNavDrawerLocked, breadcrumbs$, isVisible$, and brand$ observables', async () => {
it('completes applicationClass$, breadcrumbs$, isVisible$, and brand$ observables', async () => {
const { chrome, service } = await start();
const promise = Rx.combineLatest([
chrome.getIsNavDrawerLocked$(),
chrome.getBreadcrumbs$(),
chrome.getIsVisible$(),
chrome.getHelpExtension$(),
@ -760,7 +754,6 @@ describe('stop', () => {
await expect(
Rx.combineLatest([
chrome.getIsNavDrawerLocked$(),
chrome.getBreadcrumbs$(),
chrome.getIsVisible$(),
chrome.getHelpExtension$(),

View file

@ -60,7 +60,6 @@ import type { InternalChromeStart } from './types';
import { HeaderTopBanner } from './ui/header/header_top_banner';
import { handleSystemColorModeChange } from './handle_system_colormode_change';
const IS_LOCKED_KEY = 'core.chrome.isLocked';
const IS_SIDENAV_COLLAPSED_KEY = 'core.chrome.isSideNavCollapsed';
const SNAPSHOT_REGEX = /-snapshot/i;
@ -276,7 +275,6 @@ export class ChromeService {
const badge$ = new BehaviorSubject<ChromeBadge | undefined>(undefined);
const customNavLink$ = new BehaviorSubject<ChromeNavLink | undefined>(undefined);
const helpSupportUrl$ = new BehaviorSubject<string>(docLinks.links.kibana.askElastic);
const isNavDrawerLocked$ = new BehaviorSubject(localStorage.getItem(IS_LOCKED_KEY) === 'true');
// ChromeStyle is set to undefined by default, which means that no header will be rendered until
// setChromeStyle(). This is to avoid a flickering between the "classic" and "project" header meanwhile
// we load the user profile to check if the user opted out of the new solution navigation.
@ -341,13 +339,6 @@ export class ChromeService {
docTitle.reset();
});
const setIsNavDrawerLocked = (isLocked: boolean) => {
isNavDrawerLocked$.next(isLocked);
localStorage.setItem(IS_LOCKED_KEY, `${isLocked}`);
};
const getIsNavDrawerLocked$ = isNavDrawerLocked$.pipe(takeUntil(this.stop$));
const validateChromeStyle = () => {
const chromeStyle = chromeStyleSubject$.getValue();
if (chromeStyle !== 'project') {
@ -419,7 +410,66 @@ export class ChromeService {
});
}
const getHeaderComponent = () => {
/**
* Classic header is a header for the "classic" navigation with all solutions
* It can be customized to be used with either legacy fixed layout or new grid layout.
* In fixed layout it is fixed to the top of the page, with display: fixed; and should be responsible for rendering the banner
*
* @param isFixed
* @param includeBanner
*/
const getClassicHeader = ({
isFixed,
includeBanner,
as,
}: {
/**
* Whether the header should be rendered as a header element, or as a div element.
*/
as: 'header' | 'div';
/**
* Whether the header should be fixed to the top of the page, with display: fixed;
*/
isFixed: boolean;
/**
* Whether the header should be also responsible the top banner, which is displayed above the header
*/
includeBanner: boolean;
}) => (
<Header
/* customizable header variations */
as={as}
headerBanner$={includeBanner ? headerBanner$.pipe(takeUntil(this.stop$)) : null}
isFixed={isFixed}
/* consistent header properties */
isServerless={this.isServerless}
loadingCount$={http.getLoadingCount$()}
application={application}
badge$={badge$.pipe(takeUntil(this.stop$))}
basePath={http.basePath}
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$.pipe(takeUntil(this.stop$))}
customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))}
kibanaDocLink={docLinks.links.kibana.guide}
docLinks={docLinks}
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
helpMenuLinks$={helpMenuLinks$}
homeHref={http.basePath.prepend('/app/home')}
kibanaVersion={injectedMetadata.getKibanaVersion()}
navLinks$={navLinks.getNavLinks$()}
recentlyAccessed$={recentlyAccessed.get$()}
navControlsLeft$={navControls.getLeft$()}
navControlsCenter$={navControls.getCenter$()}
navControlsRight$={navControls.getRight$()}
navControlsExtension$={navControls.getExtension$()}
customBranding$={customBranding$}
/>
);
const getLegacyHeaderComponentForFixedLayout = () => {
const defaultChromeStyle = chromeStyleSubject$.getValue();
const HeaderComponent = () => {
@ -494,48 +544,42 @@ export class ChromeService {
return <ProjectHeaderWithNavigationComponent />;
}
return (
<Header
isServerless={this.isServerless}
loadingCount$={http.getLoadingCount$()}
application={application}
headerBanner$={headerBanner$.pipe(takeUntil(this.stop$))}
badge$={badge$.pipe(takeUntil(this.stop$))}
basePath={http.basePath}
breadcrumbs$={breadcrumbs$.pipe(takeUntil(this.stop$))}
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$.pipe(takeUntil(this.stop$))}
customNavLink$={customNavLink$.pipe(takeUntil(this.stop$))}
kibanaDocLink={docLinks.links.kibana.guide}
docLinks={docLinks}
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
globalHelpExtensionMenuLinks$={globalHelpExtensionMenuLinks$}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
helpSupportUrl$={helpSupportUrl$.pipe(takeUntil(this.stop$))}
helpMenuLinks$={helpMenuLinks$}
homeHref={http.basePath.prepend('/app/home')}
kibanaVersion={injectedMetadata.getKibanaVersion()}
navLinks$={navLinks.getNavLinks$()}
recentlyAccessed$={recentlyAccessed.get$()}
navControlsLeft$={navControls.getLeft$()}
navControlsCenter$={navControls.getCenter$()}
navControlsRight$={navControls.getRight$()}
navControlsExtension$={navControls.getExtension$()}
onIsLockedUpdate={setIsNavDrawerLocked}
isLocked$={getIsNavDrawerLocked$}
customBranding$={customBranding$}
/>
);
return getClassicHeader({ isFixed: true, includeBanner: true, as: 'header' });
};
return <HeaderComponent />;
};
const getClassicHeaderComponentForGridLayout = () => {
return getClassicHeader({ isFixed: false, includeBanner: false, as: 'div' });
};
return {
// TODO: this service does too much and doesn't have to compose these headers components.
// let's get rid of this in the future https://github.com/elastic/kibana/issues/225264
getLegacyHeaderComponentForFixedLayout,
getClassicHeaderComponentForGridLayout,
getHeaderBanner: () => {
return (
<HeaderTopBanner
headerBanner$={headerBanner$.pipe(takeUntil(this.stop$))}
position={'static'}
/>
);
},
getChromelessHeader: () => {
return (
<div data-test-subj="kibanaHeaderChromeless">
<LoadingIndicator loadingCount$={http.getLoadingCount$()} showAsBar />
</div>
);
},
// chrome APIs
navControls,
navLinks,
recentlyAccessed,
docTitle,
getHeaderComponent,
getIsVisible$: () => this.isVisible$,
@ -592,8 +636,6 @@ export class ChromeService {
getHelpSupportUrl$: () => helpSupportUrl$.pipe(takeUntil(this.stop$)),
getIsNavDrawerLocked$: () => getIsNavDrawerLocked$,
getCustomNavLink$: () => customNavLink$.pipe(takeUntil(this.stop$)),
setCustomNavLink: (customNavLink?: ChromeNavLink) => {

View file

@ -27,8 +27,50 @@ export interface InternalChromeStart extends ChromeStart {
/**
* Used only by the rendering service to render the header UI
* @internal
*
* @remarks
* LegacyHeader is a fixed layout header component that is used in the legacy fixed layout.
* Apart from the header, it also includes the navigations, banner and the chromeless header state.
* It decides which header - classic or project based on the chromeStyle$ observable.
*
* @deprecated - clean up https://github.com/elastic/kibana/issues/225264
*/
getHeaderComponent(): JSX.Element;
getLegacyHeaderComponentForFixedLayout(): JSX.Element;
/**
* Used only by the rendering service to render the header UI
* @internal
*
* @remarks
* Header that is used in the grid layout with the "classic" navigation.
* It includes the header and the overlay classic navigation.
* It doesn't include the banner or the chromeless header state, which are rendered separately by the layout service.
*
* @deprecated - clean up https://github.com/elastic/kibana/issues/225264
*/
getClassicHeaderComponentForGridLayout(): JSX.Element;
/**
* Used only by the rendering service to render the header banner UI
* @internal
*
* @remarks
* Can be used by layout service to render a banner separate from the header.
*
* @deprecated - clean up https://github.com/elastic/kibana/issues/225264
*/
getHeaderBanner(): JSX.Element;
/**
* Used only by the rendering service to render the chromeless header UI
* @internal
*
* @remarks
* Includes global loading indicator for chromeless state.
*
* @deprecated - clean up https://github.com/elastic/kibana/issues/225264
*/
getChromelessHeader(): JSX.Element;
/**
* Used only by the rendering service to retrieve the set of classNames

View file

@ -816,7 +816,6 @@ exports[`CollapsibleNav renders the default nav 1`] = `
}
navigateToApp={[Function]}
navigateToUrl={[Function]}
onIsLockedUpdate={[Function]}
recentlyAccessed$={
BehaviorSubject {
"_value": Array [],
@ -1083,7 +1082,6 @@ exports[`CollapsibleNav renders the default nav 2`] = `
}
navigateToApp={[Function]}
navigateToUrl={[Function]}
onIsLockedUpdate={[Function]}
recentlyAccessed$={
BehaviorSubject {
"_value": Array [],

View file

@ -13,12 +13,32 @@ import type { ChromeBreadcrumbsAppendExtension } from '@kbn/core-chrome-browser'
import useObservable from 'react-use/lib/useObservable';
import { EuiFlexGroup } from '@elastic/eui';
import classnames from 'classnames';
import { css } from '@emotion/react';
import { HeaderExtension } from './header_extension';
export interface Props {
breadcrumbsAppendExtensions$: Observable<ChromeBreadcrumbsAppendExtension[]>;
}
const styles = {
breadcrumbsWithExtensionContainer: css`
overflow: hidden; // enables text-ellipsis in the last breadcrumb
.euiHeaderBreadcrumbs,
.euiBreadcrumbs {
// stop breadcrumbs from growing.
// this makes the extension appear right next to the last breadcrumb
flex-grow: 0;
margin-right: 0;
overflow: hidden; // enables text-ellipsis in the last breadcrumb
}
.header__breadcrumbsAppendExtension--last {
flex-grow: 1;
}
`,
};
export const BreadcrumbsWithExtensionsWrapper = ({
breadcrumbsAppendExtensions$,
children,
@ -32,8 +52,8 @@ export const BreadcrumbsWithExtensionsWrapper = ({
responsive={false}
wrap={false}
alignItems={'center'}
className={'header__breadcrumbsWithExtensionContainer'}
gutterSize={'none'}
css={styles.breadcrumbsWithExtensionContainer}
>
{children}
{breadcrumbsAppendExtensions.map((breadcrumbsAppendExtension, index) => {

View file

@ -55,7 +55,6 @@ function mockProps() {
navLinks$: new BehaviorSubject([]),
recentlyAccessed$: new BehaviorSubject([]),
storage: new StubBrowserStorage(),
onIsLockedUpdate: () => {},
closeNav: () => {},
navigateToApp: () => Promise.resolve(),
navigateToUrl: () => Promise.resolve(),

View file

@ -28,7 +28,6 @@ import type { HttpStart } from '@kbn/core-http-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import type { AppCategory } from '@kbn/core-application-common';
import type { ChromeNavLink, ChromeRecentlyAccessedHistoryItem } from '@kbn/core-chrome-browser';
import type { OnIsLockedUpdate } from './types';
import {
createEuiListItem,
createRecentNavLink,
@ -81,7 +80,6 @@ interface Props {
navLinks$: Rx.Observable<ChromeNavLink[]>;
recentlyAccessed$: Rx.Observable<ChromeRecentlyAccessedHistoryItem[]>;
storage?: Storage;
onIsLockedUpdate: OnIsLockedUpdate;
closeNav: () => void;
navigateToApp: InternalApplicationStart['navigateToApp'];
navigateToUrl: InternalApplicationStart['navigateToUrl'];
@ -104,7 +102,6 @@ export function CollapsibleNav({
isNavOpen,
homeHref,
storage = window.localStorage,
onIsLockedUpdate,
closeNav,
navigateToApp,
navigateToUrl,

View file

@ -46,7 +46,7 @@ function mockProps() {
basePath: http.basePath,
isLocked$: new BehaviorSubject(false),
loadingCount$: new BehaviorSubject(0),
onIsLockedUpdate: () => {},
isFixed: true,
};
}
@ -91,7 +91,6 @@ describe('Header', () => {
breadcrumbs$={breadcrumbs$}
navLinks$={navLinks$}
recentlyAccessed$={recentlyAccessed$}
isLocked$={isLocked$}
customNavLink$={customNavLink$}
breadcrumbsAppendExtensions$={breadcrumbsAppendExtensions$}
headerBanner$={headerBanner$}

View file

@ -37,7 +37,6 @@ import { CustomBranding } from '@kbn/core-custom-branding-common';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { css } from '@emotion/react';
import type { OnIsLockedUpdate } from './types';
import { CollapsibleNav } from './collapsible_nav';
import { HeaderBadge } from './header_badge';
import { HeaderBreadcrumbs } from './header_breadcrumbs';
@ -53,7 +52,7 @@ import { ScreenReaderRouteAnnouncements, SkipToMainContent } from './screen_read
export interface HeaderProps {
kibanaVersion: string;
application: InternalApplicationStart;
headerBanner$: Observable<ChromeUserBanner | undefined>;
headerBanner$?: Observable<ChromeUserBanner | undefined> | null;
badge$: Observable<ChromeBadge | undefined>;
breadcrumbs$: Observable<ChromeBreadcrumb[]>;
breadcrumbsAppendExtensions$: Observable<ChromeBreadcrumbsAppendExtension[]>;
@ -73,11 +72,11 @@ export interface HeaderProps {
navControlsRight$: Observable<readonly ChromeNavControl[]>;
navControlsExtension$: Observable<readonly ChromeNavControl[]>;
basePath: HttpStart['basePath'];
isLocked$: Observable<boolean>;
loadingCount$: ReturnType<HttpStart['getLoadingCount$']>;
onIsLockedUpdate: OnIsLockedUpdate;
customBranding$: Observable<CustomBranding>;
isServerless: boolean;
isFixed: boolean;
as?: 'div' | 'header';
}
export function Header({
@ -86,12 +85,13 @@ export function Header({
docLinks,
application,
basePath,
onIsLockedUpdate,
homeHref,
breadcrumbsAppendExtensions$,
globalHelpExtensionMenuLinks$,
customBranding$,
isServerless,
isFixed,
as = 'header',
...observables
}: HeaderProps) {
const [isNavOpen, setIsNavOpen] = useState(false);
@ -102,6 +102,7 @@ export function Header({
const className = classnames('hide-for-sharing', 'headerGlobalNav');
const Breadcrumbs = <HeaderBreadcrumbs breadcrumbs$={observables.breadcrumbs$} />;
const HeaderElement = as === 'header' ? 'header' : 'div';
return (
<>
@ -112,12 +113,12 @@ export function Header({
/>
<SkipToMainContent />
<HeaderTopBanner headerBanner$={observables.headerBanner$} />
<header className={className} data-test-subj="headerGlobalNav">
{observables.headerBanner$ && <HeaderTopBanner headerBanner$={observables.headerBanner$} />}
<HeaderElement className={className} data-test-subj="headerGlobalNav">
<div id="globalHeaderBars" className="header__bars">
<EuiHeader
theme="dark"
position="fixed"
position={isFixed ? 'fixed' : 'static'}
className="header__firstBar"
sections={[
{
@ -169,7 +170,7 @@ export function Header({
]}
/>
<EuiHeader position="fixed" className="header__secondBar">
<EuiHeader position={isFixed ? 'fixed' : 'static'} className="header__secondBar">
<EuiHeaderSection grow={false}>
<EuiHeaderSectionItem className="header__toggleNavButtonSection">
<CollapsibleNav
@ -182,7 +183,6 @@ export function Header({
basePath={basePath}
navigateToApp={application.navigateToApp}
navigateToUrl={application.navigateToUrl}
onIsLockedUpdate={onIsLockedUpdate}
closeNav={() => {
setIsNavOpen(false);
}}
@ -227,7 +227,7 @@ export function Header({
</EuiHeaderSection>
</EuiHeader>
</div>
</header>
</HeaderElement>
</>
);
}

View file

@ -52,7 +52,7 @@ export class HeaderBadge extends Component<Props, State> {
}
return (
<div className="chrHeaderBadge__wrapper">
<div css={({ euiTheme }) => ({ alignSelf: 'center', marginLeft: euiTheme.size.base })}>
<EuiBetaBadge
data-test-subj="headerBadge"
data-test-badge-label={this.state.badge.text}

View file

@ -183,7 +183,7 @@ class HelpMenu extends Component<Props & WithEuiThemeProps, State> {
{!this.props.isServerless && (
<EuiFlexItem
grow={false}
className="chrHeaderHelpMenu__version"
css={{ textTransform: 'none' }}
data-test-subj="kbnVersionString"
>
<FormattedMessage

View file

@ -11,20 +11,56 @@ import React, { FC } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { Observable } from 'rxjs';
import type { ChromeUserBanner } from '@kbn/core-chrome-browser';
import { css } from '@emotion/react';
import { HeaderExtension } from './header_extension';
export interface HeaderTopBannerProps {
headerBanner$: Observable<ChromeUserBanner | undefined>;
position?: 'fixed' | 'static';
}
export const HeaderTopBanner: FC<HeaderTopBannerProps> = ({ headerBanner$ }) => {
const styles = {
root: {
fixed: css`
position: fixed;
top: 0;
left: 0;
height: var(--kbnHeaderBannerHeight);
width: 100%;
`,
static: css`
height: var(--kbnHeaderBannerHeight);
width: 100%;
`,
},
container: css`
.header__topBannerContainer {
height: 100%;
width: 100%;
}
`,
};
export const HeaderTopBanner: FC<HeaderTopBannerProps> = ({
headerBanner$,
position = 'fixed',
}) => {
const headerBanner = useObservable(headerBanner$, undefined);
if (!headerBanner) {
return null;
}
return (
<div className="header__topBanner" data-test-subj="headerTopBanner">
<div
css={[
styles.root[position],
styles.container,
({ euiTheme }) => ({
zIndex: euiTheme.levels.header,
}),
]}
data-test-subj="headerTopBanner"
>
<HeaderExtension
containerClassName="header__topBannerContainer"
display="block"

View file

@ -9,4 +9,3 @@
export { Header } from './header';
export type { HeaderProps } from './header';
export type { OnIsLockedUpdate, NavType } from './types';

View file

@ -6,6 +6,3 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export type OnIsLockedUpdate = (isLocked: boolean) => void;
export type NavType = 'modern' | 'legacy';

View file

@ -10,4 +10,3 @@
export { Header } from './header';
export { ProjectHeader } from './project';
export { LoadingIndicator } from './loading_indicator';
export type { NavType } from './header';

View file

@ -8,7 +8,8 @@
"react",
"@kbn/ambient-ui-types",
"@kbn/ambient-storybook-types",
"@emotion/react/types/css-prop"
"@emotion/react/types/css-prop",
"../../../../../typings/emotion.d.ts"
]
},
"include": [

View file

@ -15,7 +15,10 @@ import type { ChromeService, InternalChromeStart } from '@kbn/core-chrome-browse
const createStartContractMock = () => {
const startContract: DeeplyMockedKeys<InternalChromeStart> = {
getHeaderComponent: jest.fn(),
getLegacyHeaderComponentForFixedLayout: jest.fn(),
getClassicHeaderComponentForGridLayout: jest.fn(),
getChromelessHeader: jest.fn(),
getHeaderBanner: jest.fn(),
navLinks: {
getNavLinks$: jest.fn(),
has: jest.fn(),
@ -68,7 +71,6 @@ const createStartContractMock = () => {
setHelpMenuLinks: jest.fn(),
setHelpSupportUrl: jest.fn(),
getHelpSupportUrl$: jest.fn(() => of('https://www.elastic.co/support')),
getIsNavDrawerLocked$: jest.fn(),
getCustomNavLink$: jest.fn(),
setCustomNavLink: jest.fn(),
setHeaderBanner: jest.fn(),
@ -99,7 +101,6 @@ const createStartContractMock = () => {
startContract.getCustomNavLink$.mockReturnValue(new BehaviorSubject(undefined));
startContract.getGlobalHelpExtensionMenuLinks$.mockReturnValue(new BehaviorSubject([]));
startContract.getHelpExtension$.mockReturnValue(new BehaviorSubject(undefined));
startContract.getIsNavDrawerLocked$.mockReturnValue(new BehaviorSubject(false));
startContract.getBodyClasses$.mockReturnValue(new BehaviorSubject([]));
startContract.hasHeaderBanner$.mockReturnValue(new BehaviorSubject(false));
startContract.sideNav.getIsCollapsed$.mockReturnValue(new BehaviorSubject(false));

View file

@ -150,11 +150,6 @@ export interface ChromeStart {
*/
getHelpSupportUrl$(): Observable<string>;
/**
* Get an observable of the current locked state of the nav drawer.
*/
getIsNavDrawerLocked$(): Observable<boolean>;
/**
* Set the banner that will appear on top of the chrome header.
*

View file

@ -17,6 +17,9 @@ const root: EmotionFn = ({ euiTheme }) =>
position: relative;
width: 100%;
z-index: ${euiTheme.levels.content};
display: flex;
flex-direction: column;
`;
export const styles = {

View file

@ -8,13 +8,15 @@
*/
import { css } from '@emotion/react';
import { EmotionFn } from '../types';
const root = css`
const root: EmotionFn = ({ euiTheme }) => css`
grid-area: banner;
overflow: hidden;
position: sticky;
width: var(--kbn-layout--banner-width);
height: var(--kbn-layout--banner-height);
z-index: var(--kbn-layout--aboveFlyoutLevel);
`;
export const styles = {

View file

@ -0,0 +1,83 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { useRef, useLayoutEffect, useState } from 'react';
interface SimpleDebugOverlayProps {
label?: string;
background?: string;
color?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
/**
* A minimal debug component for temporary overlays (sidebar, footer, banner, etc.)
* Fills its parent container, with customizable background and text.
*
* @param props - {@link SimpleDebugOverlayProps}
* @returns The rendered debug overlay.
*/
export const SimpleDebugOverlay: React.FC<SimpleDebugOverlayProps> = ({
label = 'Debug Overlay',
background = '#e6f4ff',
color = '#0099ff',
style = {},
children,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isVertical, setIsVertical] = useState(false);
useLayoutEffect(() => {
const handleResize = () => {
if (containerRef.current) {
const width = containerRef.current.offsetWidth;
setIsVertical(width > 0 && width < 200);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return (
<div
ref={containerRef}
style={{
width: '100%',
height: '100%',
background,
color,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'monospace',
fontSize: 16,
border: '2px dashed #0099ff',
boxSizing: 'border-box',
...style,
}}
>
<span
style={
isVertical
? {
writingMode: 'vertical-rl',
textOrientation: 'mixed',
whiteSpace: 'nowrap',
}
: {}
}
>
{children || label}
</span>
</div>
);
};

View file

@ -15,6 +15,7 @@ const root = css`
grid-area: footer;
width: var(--kbn-layout--footer-width);
height: var(--kbn-layout--footer-height);
z-index: var(--kbn-layout--aboveFlyoutLevel);
`;
export const styles = {

View file

@ -8,12 +8,14 @@
*/
import { css } from '@emotion/react';
import { EmotionFn } from '../types';
const root = css`
const root: EmotionFn = ({ euiTheme }) => css`
position: sticky;
overflow: hidden;
grid-area: header;
height: var(--kbn-layout--header-height);
z-index: var(--kbn-layout--aboveFlyoutLevel);
`;
export const styles = {

View file

@ -14,3 +14,5 @@ export {
type LayoutConfig as ChromeLayoutConfig,
type LayoutConfigProviderProps as ChromeLayoutConfigProviderProps,
} from './layout_config_context';
export { SimpleDebugOverlay } from './debug/simple_debug_overlay';
export { LayoutDebugOverlay } from './debug/layout_debug_overlay';

View file

@ -122,6 +122,13 @@ export const LayoutGlobalCSS = () => {
--kbn-layout--footer-width: var(--kbn-layout--application-width);
`;
// we want to place layout slots (like sidebar) on top of eui's flyouts (1000),
// so that when they are open, they are animating from under the sidebars
// this part of EUI flyout workaround and should be gone with https://github.com/elastic/eui/issues/8820
const common = css`
--kbn-layout--aboveFlyoutLevel: 1050;
`;
const styles = css`
:root {
${banner}
@ -130,6 +137,7 @@ export const LayoutGlobalCSS = () => {
${sidebar}
${application}
${footer}
${common}
}
`;

View file

@ -18,6 +18,7 @@ const root: EmotionFn = ({ euiTheme }) => css`
align-self: start;
height: 100%;
width: var(--kbn-layout--navigation-width);
z-index: var(--kbn-layout--aboveFlyoutLevel);
`;
export const styles = {

View file

@ -20,6 +20,7 @@ const root = css`
align-items: center;
height: 100%;
width: var(--kbn-layout--sidebar-width);
z-index: var(--kbn-layout--aboveFlyoutLevel);
`;
export const styles = {

View file

@ -17,6 +17,7 @@ const root = css`
overflow: hidden;
position: sticky;
width: var(--kbn-layout--sidebar-panel-width);
z-index: var(--kbn-layout--aboveFlyoutLevel);
`;
export const styles = {

View file

@ -0,0 +1,31 @@
# @kbn/core-chrome-layout
The `core-chrome-layout` package provides implementation for different chrome layouts. Each implementation is a layout service that provides a layout component. A layout service is used by the rendering service to render the layout based on the selected layout type.
## Layouts
- `grid`: Grid-based layout (WIP)
- `legacy-fixed`: Legacy fixed layout (default)
## Usage
Import the layout service or components as needed:
```tsx
import { LayoutService } from './layout_service';
import { GridLayout } from './layouts/grid';
import { LegacyFixedLayout } from './layouts/legacy-fixed';
const layout = featureFlag.getStringValue<LayoutFeatureFlag>(
LAYOUT_FEATURE_FLAG_KEY,
'legacy-fixed'
);
const Layout = layout === 'grid' ? new GridLayout(deps) : new LegacyFixedLayout(deps);
ReactDOM.render(
<KibanaRootContextProvider {...startServices} globalStyles={true}>
<Layout />
</KibanaRootContextProvider>,
targetDomElement
);
```

View file

@ -9,7 +9,7 @@
import { BehaviorSubject } from 'rxjs';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import { render } from '@testing-library/react';
import React from 'react';
import { AppWrapper } from './app_containers';
@ -17,9 +17,12 @@ import { AppWrapper } from './app_containers';
describe('AppWrapper', () => {
it('toggles the `hidden-chrome` class depending on the chrome visibility state', () => {
const chromeVisible$ = new BehaviorSubject<boolean>(true);
const { getByTestId, rerender } = render(
<AppWrapper chromeVisible={chromeVisible$.value}>app-content</AppWrapper>
);
const component = mount(<AppWrapper chromeVisible$={chromeVisible$}>app-content</AppWrapper>);
expect(component.getDOMNode()).toMatchInlineSnapshot(`
// The data-test-subj attribute is used for querying
expect(getByTestId('kbnAppWrapper visibleChrome')).toMatchInlineSnapshot(`
<div
class="kbnAppWrapper"
data-test-subj="kbnAppWrapper visibleChrome"
@ -29,8 +32,8 @@ describe('AppWrapper', () => {
`);
act(() => chromeVisible$.next(false));
component.update();
expect(component.getDOMNode()).toMatchInlineSnapshot(`
rerender(<AppWrapper chromeVisible={chromeVisible$.value}>app-content</AppWrapper>);
expect(getByTestId('kbnAppWrapper hiddenChrome')).toMatchInlineSnapshot(`
<div
class="kbnAppWrapper kbnAppWrapper--hiddenChrome"
data-test-subj="kbnAppWrapper hiddenChrome"
@ -40,8 +43,8 @@ describe('AppWrapper', () => {
`);
act(() => chromeVisible$.next(true));
component.update();
expect(component.getDOMNode()).toMatchInlineSnapshot(`
rerender(<AppWrapper chromeVisible={chromeVisible$.value}>app-content</AppWrapper>);
expect(getByTestId('kbnAppWrapper visibleChrome')).toMatchInlineSnapshot(`
<div
class="kbnAppWrapper"
data-test-subj="kbnAppWrapper visibleChrome"

View file

@ -8,21 +8,18 @@
*/
import React, { FC, PropsWithChildren } from 'react';
import { Observable } from 'rxjs';
import useObservable from 'react-use/lib/useObservable';
import classNames from 'classnames';
import { APP_WRAPPER_CLASS } from '@kbn/core-application-common';
export const AppWrapper: FC<
PropsWithChildren<{
chromeVisible$: Observable<boolean>;
chromeVisible: boolean;
}>
> = ({ chromeVisible$, children }) => {
const visible = useObservable(chromeVisible$);
> = ({ chromeVisible, children }) => {
return (
<div
className={classNames(APP_WRAPPER_CLASS, { 'kbnAppWrapper--hiddenChrome': !visible })}
data-test-subj={`kbnAppWrapper ${visible ? 'visible' : 'hidden'}Chrome`}
className={classNames(APP_WRAPPER_CLASS, { 'kbnAppWrapper--hiddenChrome': !chromeVisible })}
data-test-subj={`kbnAppWrapper ${chromeVisible ? 'visible' : 'hidden'}Chrome`}
>
{children}
</div>

View file

@ -0,0 +1,15 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { type LayoutService } from './layout_service';
export {
LAYOUT_FEATURE_FLAG_KEY,
LAYOUT_DEBUG_FEATURE_FLAG_KEY,
type LayoutFeatureFlag,
} from './layout_feature_flag';

View file

@ -0,0 +1,14 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../../..',
roots: ['<rootDir>/src/core/packages/chrome/layout/core-chrome-layout'],
};

View file

@ -0,0 +1,7 @@
{
"type": "shared-browser",
"id": "@kbn/core-chrome-layout",
"owner": "@elastic/appex-sharedux",
"group": "platform",
"visibility": "private"
}

View file

@ -0,0 +1,12 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export type LayoutFeatureFlag = 'legacy-fixed' | 'grid';
export const LAYOUT_FEATURE_FLAG_KEY = 'core.chrome.layoutType';
export const LAYOUT_DEBUG_FEATURE_FLAG_KEY = 'core.chrome.layoutDebug';

View file

@ -0,0 +1,36 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import type { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
import type { OverlayStart } from '@kbn/core-overlays-browser';
export interface LayoutServiceStartDeps {
application: InternalApplicationStart;
chrome: InternalChromeStart;
overlays: OverlayStart;
}
export interface LayoutServiceParams {
debug?: boolean;
}
/**
* The LayoutService responsible for layout management of Kibana.
* Kibana can swap between different layout service implementation to support different layout types.
*
* @internal
*/
export interface LayoutService {
/**
* Returns a layout component with the provided dependencies
*/
getComponent: () => React.ComponentType;
}

View file

@ -0,0 +1,44 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { type UseEuiTheme, useEuiTheme } from '@elastic/eui';
import { css, Global } from '@emotion/react';
import React from 'react';
// Due to pure HTML and the scope being large, we decided to temporarily apply following 3 style blocks globally.
// TODO: refactor within github issue #223571
const hackGlobalFieldFormattersPluginStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
// Styles applied to the span.ffArray__highlight from FieldFormat class that is used to visually distinguish array delimiters when rendering array values as HTML in Kibana field formatters
.ffArray__highlight {
color: ${euiTheme.colors.mediumShade};
}
// Styles applied to the span.ffString__emptyValue from FieldFormat class that is used to visually distinguish empty string values when rendering string values as HTML in Kibana field formatters
.ffString__emptyValue {
color: ${euiTheme.colors.darkShade};
}
.lnsTableCell--colored .ffString__emptyValue {
color: unset;
}
`;
/**
* Global styles that are common for any type of layout.
*/
export const CommonGlobalAppStyles = () => {
const { euiTheme } = useEuiTheme();
return (
<Global
styles={css`
${hackGlobalFieldFormattersPluginStyles(euiTheme)}
`}
/>
);
};

View file

@ -0,0 +1,122 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import { css, Global } from '@emotion/react';
import { logicalCSS, useEuiTheme, type UseEuiTheme } from '@elastic/eui';
import { CommonGlobalAppStyles } from '../common/global_app_styles';
import {
useHackSyncPushFlyout,
hackEuiPushFlyoutPaddingInlineEnd,
} from './hack_use_sync_push_flyout';
const globalLayoutStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
:root {
// TODO: these variables are legacy and we keep them for backward compatibility
// https://github.com/elastic/kibana/issues/225264
// there is no fixed header in the grid layout, so we want to set the offset to 0
--euiFixedHeadersOffset: 0px;
// height of the header banner
--kbnHeaderBannerHeight: var(--kbn-layout--banner-height, 0px);
// the total height of all app-area headers
--kbnAppHeadersOffset: 0px;
}
#kibana-body {
// DO NOT ADD ANY OVERFLOW BEHAVIORS HERE
// It will break the sticky navigation
min-height: 100%;
display: flex;
flex-direction: column;
}
// Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header
#app-fixed-viewport {
pointer-events: none;
visibility: hidden;
position: fixed;
top: var(--kbn-layout--application-top, 0px);
right: var(--kbn-layout--application-right, 0px);
bottom: var(--kbn-layout--application-bottom, 0px);
left: var(--kbn-layout--application-left, 0px);
}
.kbnAppWrapper {
// DO NOT ADD ANY OTHER STYLES TO THIS SELECTOR
// This a very nested dependency happening in "all" apps
display: flex;
flex-flow: column nowrap;
flex-grow: 1;
z-index: 0; // This effectively puts every high z-index inside the scope of this wrapper to it doesn't interfere with the header and/or overlay mask
position: relative; // This is temporary for apps that relied on this being present on \`.application\`
}
#kibana-body .euiDataGrid--fullScreen {
height: calc(100vh - var(--kbnHeaderBannerHeight));
top: var(--kbnHeaderBannerHeight);
}
`;
// temporary hacks that need to be removed after better flyout and global sidenav customization support in EUI
// https://github.com/elastic/eui/issues/8820
const globalTempHackStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
// adjust position of the classic navigation overlay
.kbnBody .euiFlyout.euiCollapsibleNav {
${logicalCSS('top', 'var(--kbn-layout--application-top, 0px)')};
${logicalCSS('left', 'var(--kbn-layout--application-left, 0px)')};
${logicalCSS('bottom', 'var(--kbn-layout--application-bottom, 0px)')};
}
// adjust position of the overlay flyouts
.kbnBody .euiFlyout:not(.euiCollapsibleNavBeta),
.kbnBody .euiFlyout:not(.euiCollapsibleNav) {
// overlay flyout should only cover the application area
&[class*='right']:not([class*='push']) {
${logicalCSS('top', 'var(--kbn-layout--application-top, 0px)')};
${logicalCSS('bottom', 'var(--kbn-layout--application-bottom, 0px)')};
${logicalCSS('right', 'var(--kbn-layout--application-right, 0px)')};
}
// push flyout should only cover the application area
&[class*='right'][class*='push'] {
${logicalCSS('top', 'var(--kbn-layout--application-top, 0px)')};
${logicalCSS('bottom', 'var(--kbn-layout--application-bottom, 0px)')};
${logicalCSS('right', 'var(--kbn-layout--application-right, 0px)')};
}
}
// push flyout should be pushing the application area, instead of body
main[class*='LayoutApplication'] {
${logicalCSS('padding-right', `var(${hackEuiPushFlyoutPaddingInlineEnd}, 0px)`)};
}
.kbnBody {
${logicalCSS('padding-right', `0px !important`)};
}
// overlay mask "belowHeader" should only cover the application area
.kbnBody .euiOverlayMask[class*='belowHeader'] {
${logicalCSS('top', 'var(--kbn-layout--application-top, 0px)')};
${logicalCSS('left', 'var(--kbn-layout--application-left, 0px)')};
${logicalCSS('right', 'var(--kbn-layout--application-right, 0px)')};
${logicalCSS('bottom', 'var(--kbn-layout--application-bottom, 0px)')};
}
`;
export const GridLayoutGlobalStyles = () => {
const { euiTheme } = useEuiTheme();
useHackSyncPushFlyout();
return (
<>
<Global styles={[globalLayoutStyles(euiTheme), globalTempHackStyles(euiTheme)]} />
<CommonGlobalAppStyles />
</>
);
};

View file

@ -0,0 +1,124 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React, { ReactNode } from 'react';
import {
ChromeLayout,
ChromeLayoutConfigProvider,
ChromeLayoutConfig,
SimpleDebugOverlay,
} from '@kbn/core-chrome-layout-components';
import useObservable from 'react-use/lib/useObservable';
import { GridLayoutGlobalStyles } from './grid_global_app_style';
import type {
LayoutService,
LayoutServiceStartDeps,
LayoutServiceParams,
} from '../../layout_service';
import { AppWrapper } from '../../app_containers';
import { APP_FIXED_VIEWPORT_ID } from '../../app_fixed_viewport';
const layoutConfig: ChromeLayoutConfig = {
headerHeight: 96,
bannerHeight: 32,
/** for debug for now */
sidebarWidth: 48,
footerHeight: 48,
navigationWidth: 48,
};
/**
* Service for providing layout component wired to other core services.
*/
export class GridLayout implements LayoutService {
constructor(
private readonly deps: LayoutServiceStartDeps,
private readonly params: LayoutServiceParams
) {}
/**
* Returns a layout component with the provided dependencies
*/
public getComponent(): React.ComponentType {
const { application, chrome, overlays } = this.deps;
const appComponent = application.getComponent();
const appBannerComponent = overlays.banners.getComponent();
const hasHeaderBanner$ = chrome.hasHeaderBanner$();
const chromeVisible$ = chrome.getIsVisible$();
const debug = this.params.debug ?? false;
const chromeHeader = chrome.getClassicHeaderComponentForGridLayout();
const headerBanner = chrome.getHeaderBanner();
// chromeless header is used when chrome is not visible and responsible for displaying the data-test-subj and fixed loading bar
const chromelessHeader = chrome.getChromelessHeader();
return React.memo(() => {
// TODO: Get rid of observables https://github.com/elastic/kibana/issues/225265
const chromeVisible = useObservable(chromeVisible$, false);
const hasHeaderBanner = useObservable(hasHeaderBanner$, false);
// Assign main layout parts first
const header: ReactNode = chromeVisible && chromeHeader;
let banner: ReactNode = hasHeaderBanner ? headerBanner : undefined;
// not implemented
let sidebar: ReactNode;
let footer: ReactNode;
let navigation: ReactNode;
// If debug, override/add debug overlays
if (debug) {
if (chromeVisible) {
if (!sidebar) sidebar = <SimpleDebugOverlay label="Debug Sidebar" />;
if (!footer) footer = <SimpleDebugOverlay label="Debug Footer" />;
if (!navigation)
navigation = (
<SimpleDebugOverlay label="Debug Nav" style={{ transform: 'rotate(180deg)' }} />
);
}
// banner is visible even when chrome is not visible
if (!banner) {
banner = <SimpleDebugOverlay label="Debug Banner" />;
}
}
return (
<>
<GridLayoutGlobalStyles />
<ChromeLayoutConfigProvider value={layoutConfig}>
<ChromeLayout
header={header}
sidebar={sidebar}
footer={footer}
navigation={navigation}
banner={banner}
>
<>
{/* If chrome is not visible, we use the chromeless header to display the*/}
{/* data-test-subj and fixed loading bar*/}
{!chromeVisible && chromelessHeader}
<div id="globalBannerList">{appBannerComponent}</div>
<AppWrapper chromeVisible={chromeVisible}>
{/* Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header */}
<div id={APP_FIXED_VIEWPORT_ID} />
{/* The actual plugin/app */}
{appComponent}
</AppWrapper>
</>
</ChromeLayout>
</ChromeLayoutConfigProvider>
</>
);
});
}
}

View file

@ -0,0 +1,84 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { useEffect } from 'react';
import { useEuiThemeCSSVariables } from '@elastic/eui';
export const hackEuiPushFlyoutPaddingInlineEnd = '--eui-push-flyout-padding';
/**
* This is definitely a hack for experimental purposes.
* Currently, EUI push flyouts visually push the content to the right by adding a padding to the body
* This hook listens to styles changes on the body and updates a CSS variable that is used to push the workspace content
* https://github.com/elastic/eui/issues/8820
*/
export function useHackSyncPushFlyout() {
const { setGlobalCSSVariables } = useEuiThemeCSSVariables();
useEffect(() => {
const targetNode = document.body;
const callback: MutationCallback = (mutationsList: MutationRecord[]) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
// const newPaddingInlineStart = window.getComputedStyle(targetNode).paddingInlineStart;
const styleAttribute = targetNode.getAttribute('style') ?? '';
function parseCSSDeclaration(styleAttr: string) {
return styleAttr
.trim()
.split(';')
.filter((s) => s !== '')
.map((declaration) => {
const colonIndex = declaration.indexOf(':');
if (colonIndex === -1) {
throw new Error('Invalid CSS declaration: no colon found.');
}
const property = declaration.slice(0, colonIndex).trim();
const value = declaration.slice(colonIndex + 1).trim();
return { property, value };
});
}
const parsedStyle = parseCSSDeclaration(styleAttribute);
const paddingInline = parsedStyle.find(
(style) => style.property === 'padding-inline'
)?.value;
let paddingInlineEnd = parsedStyle.find(
(style) => style.property === 'padding-inline-end'
)?.value;
const [, end] = paddingInline?.split(' ') ?? ['', ''];
paddingInlineEnd = paddingInlineEnd ?? end;
if (paddingInlineEnd) {
setGlobalCSSVariables({
[hackEuiPushFlyoutPaddingInlineEnd]: paddingInlineEnd,
});
} else {
setGlobalCSSVariables({
[hackEuiPushFlyoutPaddingInlineEnd]: null,
});
}
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, { attributes: true, attributeFilter: ['style'] });
return () => {
observer.disconnect();
};
}, [setGlobalCSSVariables]);
}

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { GridLayout } from './grid_layout';

View file

@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { LegacyFixedLayout } from './legacy_fixed_layout';

View file

@ -10,8 +10,20 @@
import React from 'react';
import { css, Global } from '@emotion/react';
import { useEuiTheme, type UseEuiTheme } from '@elastic/eui';
import { CommonGlobalAppStyles } from '../common/global_app_styles';
const globalLayoutStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
:root {
// height of the header banner
--kbnHeaderBannerHeight: ${euiTheme.size.xl};
// total height of all fixed headers (when the banner is *not* present) inherited from EUI
--kbnHeaderOffset: var(--euiFixedHeadersOffset, 0px);
// total height of everything when the banner is present
--kbnHeaderOffsetWithBanner: calc(var(--kbnHeaderBannerHeight) + var(--kbnHeaderOffset));
// height of the action menu in the header in serverless projects
--kbnProjectHeaderAppActionMenuHeight: ${euiTheme.base * 3}px;
}
export const renderingOverrides = (euiTheme: UseEuiTheme['euiTheme']) => css`
#kibana-body {
// DO NOT ADD ANY OVERFLOW BEHAVIORS HERE
// It will break the sticky navigation
@ -43,6 +55,11 @@ export const renderingOverrides = (euiTheme: UseEuiTheme['euiTheme']) => css`
.kbnBody {
padding-top: var(--euiFixedHeadersOffset, 0);
// forward compatibility with new grid layout variables,
--kbn-layout--application-height: calc(
100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0))
);
}
// Conditionally override :root CSS fixed header variable. Updating \`--euiFixedHeadersOffset\`
@ -89,41 +106,6 @@ export const renderingOverrides = (euiTheme: UseEuiTheme['euiTheme']) => css`
}
}
// Due to pure HTML and the scope being large, we decided to temporarily apply following 3 style blocks globally.
// TODO: refactor within github issue #223571
// Styles applied to the span.ffArray__highlight from FieldFormat class that is used to visually distinguish array delimiters when rendering array values as HTML in Kibana field formatters
.ffArray__highlight {
color: ${euiTheme.colors.mediumShade};
}
// Styles applied to the span.ffString__emptyValue from FieldFormat class that is used to visually distinguish empty string values when rendering string values as HTML in Kibana field formatters
.ffString__emptyValue {
color: ${euiTheme.colors.darkShade};
}
.lnsTableCell--colored .ffString__emptyValue {
color: unset;
}
`;
export const bannerStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
.header__topBanner {
position: fixed;
top: 0;
left: 0;
height: var(--kbnHeaderBannerHeight);
width: 100%;
z-index: ${euiTheme.levels.header};
}
.header__topBannerContainer {
height: 100%;
width: 100%;
}
`;
export const chromeStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
.euiDataGrid__restrictBody {
.headerGlobalNav,
.kbnQueryBar {
@ -137,48 +119,14 @@ export const chromeStyles = (euiTheme: UseEuiTheme['euiTheme']) => css`
height: 100%;
}
}
.chrHeaderHelpMenu__version {
text-transform: none;
}
.chrHeaderBadge__wrapper {
align-self: center;
margin-right: ${euiTheme.size.base};
}
.header__toggleNavButtonSection {
.euiBody--collapsibleNavIsDocked & {
display: none;
}
}
.header__breadcrumbsWithExtensionContainer {
overflow: hidden; // enables text-ellipsis in the last breadcrumb
.euiHeaderBreadcrumbs,
.euiBreadcrumbs {
// stop breadcrumbs from growing.
// this makes the extension appear right next to the last breadcrumb
flex-grow: 0;
margin-right: 0;
overflow: hidden; // enables text-ellipsis in the last breadcrumb
}
}
.header__breadcrumbsAppendExtension--last {
flex-grow: 1;
}
`;
export const GlobalAppStyle = () => {
export const LegacyFixedLayoutGlobalStyles = () => {
const { euiTheme } = useEuiTheme();
return (
<Global
styles={css`
${bannerStyles(euiTheme)}
${chromeStyles(euiTheme)}
${renderingOverrides(euiTheme)}
`}
/>
<>
<Global styles={globalLayoutStyles(euiTheme)} />
<CommonGlobalAppStyles />
</>
);
};

View file

@ -0,0 +1,58 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import React from 'react';
import useObservable from 'react-use/lib/useObservable';
import { LegacyFixedLayoutGlobalStyles } from './legacy_fixed_global_app_style';
import { LayoutService, LayoutServiceStartDeps } from '../../layout_service';
import { AppWrapper } from '../../app_containers';
import { APP_FIXED_VIEWPORT_ID } from '../../app_fixed_viewport';
/**
* Service for providing layout component wired to other core services.
*/
export class LegacyFixedLayout implements LayoutService {
constructor(private deps: LayoutServiceStartDeps) {}
/**
* Returns a layout component with the provided dependencies
*/
public getComponent(): React.ComponentType {
const { chrome, overlays, application } = this.deps;
const chromeHeader = chrome.getLegacyHeaderComponentForFixedLayout();
const bannerComponent = overlays.banners.getComponent();
const appComponent = application.getComponent();
const chromeVisible$ = chrome.getIsVisible$();
return React.memo(() => {
// TODO: Get rid of observables https://github.com/elastic/kibana/issues/225265
const chromeVisible = useObservable(chromeVisible$, false);
return (
<>
{/* Global Styles that apply across the entire app */}
{<LegacyFixedLayoutGlobalStyles />}
{/* Fixed headers */}
{chromeHeader}
{/* banners$.subscribe() for things like the No data banner */}
<div id="globalBannerList">{bannerComponent}</div>
{/* The App Wrapper outside of the fixed headers that accepts custom class names from apps */}
<AppWrapper chromeVisible={chromeVisible}>
{/* Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header */}
<div id={APP_FIXED_VIEWPORT_ID} />
{/* The actual plugin/app */}
{appComponent}
</AppWrapper>
</>
);
});
}
}

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/core-chrome-layout",
"private": true,
"version": "1.0.0",
"license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
}

View file

@ -0,0 +1,29 @@
{
"extends": "../../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react",
"@kbn/ambient-ui-types",
"@kbn/ambient-storybook-types",
"@emotion/react/types/css-prop",
"../../../../../../typings/emotion.d.ts",
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": [
"@kbn/core-application-common",
"@kbn/core-application-browser-internal",
"@kbn/core-chrome-browser-internal",
"@kbn/core-overlays-browser",
"@kbn/core-chrome-layout-components",
]
}

View file

@ -33,6 +33,7 @@ import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-browser-mocks';
import { RenderingService } from './rendering_service';
describe('RenderingService', () => {
@ -44,6 +45,7 @@ describe('RenderingService', () => {
let i18n: ReturnType<typeof i18nServiceMock.createStartContract>;
let theme: ReturnType<typeof themeServiceMock.createStartContract>;
let userProfile: ReturnType<typeof userProfileServiceMock.createStart>;
let featureFlags: ReturnType<typeof coreFeatureFlagsMock.createStart>;
let targetDomElement: HTMLDivElement;
let rendering: RenderingService;
@ -54,7 +56,7 @@ describe('RenderingService', () => {
application.getComponent.mockReturnValue(<div>Hello application!</div>);
chrome = chromeServiceMock.createStartContract();
chrome.getHeaderComponent.mockReturnValue(<div>Hello chrome!</div>);
chrome.getLegacyHeaderComponentForFixedLayout.mockReturnValue(<div>Hello chrome!</div>);
overlays = overlayServiceMock.createStartContract();
overlays.banners.getComponent.mockReturnValue(<div>I&apos;m a banner!</div>);
@ -63,6 +65,7 @@ describe('RenderingService', () => {
userProfile = userProfileServiceMock.createStart();
theme = themeServiceMock.createStartContract();
i18n = i18nServiceMock.createStartContract();
featureFlags = coreFeatureFlagsMock.createStart();
targetDomElement = document.createElement('div');
rendering = new RenderingService();
@ -81,7 +84,7 @@ describe('RenderingService', () => {
it('renders application service into provided DOM element', () => {
const service = startService();
service.renderCore({ chrome, application, overlays }, targetDomElement);
service.renderCore({ chrome, application, overlays, featureFlags }, targetDomElement);
expect(targetDomElement.querySelector('div.kbnAppWrapper')).toMatchInlineSnapshot(`
<div
class="kbnAppWrapper kbnAppWrapper--hiddenChrome"
@ -101,7 +104,7 @@ describe('RenderingService', () => {
const isVisible$ = new BehaviorSubject(true);
chrome.getIsVisible$.mockReturnValue(isVisible$);
const service = startService();
service.renderCore({ chrome, application, overlays }, targetDomElement);
service.renderCore({ chrome, application, overlays, featureFlags }, targetDomElement);
const appWrapper = targetDomElement.querySelector('div.kbnAppWrapper')!;
expect(appWrapper.className).toEqual('kbnAppWrapper');
@ -120,7 +123,7 @@ describe('RenderingService', () => {
it('renders the banner UI', () => {
const service = startService();
service.renderCore({ chrome, application, overlays }, targetDomElement);
service.renderCore({ chrome, application, overlays, featureFlags }, targetDomElement);
expect(targetDomElement.querySelector('#globalBannerList')).toMatchInlineSnapshot(`
<div
id="globalBannerList"

View file

@ -15,18 +15,24 @@ import { BehaviorSubject, pairwise, startWith } from 'rxjs';
import { EuiLoadingSpinner } from '@elastic/eui';
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
import { GlobalAppStyle } from '@kbn/core-application-common';
import type { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
import type { ExecutionContextStart } from '@kbn/core-execution-context-browser';
import type { I18nStart } from '@kbn/core-i18n-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
import { APP_FIXED_VIEWPORT_ID } from '@kbn/core-rendering-browser';
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
import type { UserProfileService } from '@kbn/core-user-profile-browser';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
import { FeatureFlagsStart } from '@kbn/core-feature-flags-browser';
import { RenderingService as IRenderingService } from '@kbn/core-rendering-browser';
import { AppWrapper } from './app_containers';
import {
LayoutService,
LayoutFeatureFlag,
LAYOUT_FEATURE_FLAG_KEY,
LAYOUT_DEBUG_FEATURE_FLAG_KEY,
} from '@kbn/core-chrome-layout';
import { GridLayout } from '@kbn/core-chrome-layout/layouts/grid';
import { LegacyFixedLayout } from '@kbn/core-chrome-layout/layouts/legacy-fixed';
export interface RenderingServiceContextDeps {
analytics: AnalyticsServiceStart;
@ -40,6 +46,7 @@ export interface RenderingServiceRenderCoreDeps {
application: InternalApplicationStart;
chrome: InternalChromeStart;
overlays: OverlayStart;
featureFlags: FeatureFlagsStart;
}
export interface RenderingServiceInternalStart extends IRenderingService {
@ -80,11 +87,14 @@ export class RenderingService implements IRenderingService {
renderCoreDeps: RenderingServiceRenderCoreDeps,
targetDomElement: HTMLDivElement
) {
const { chrome, application, overlays } = renderCoreDeps;
const { chrome, featureFlags } = renderCoreDeps;
const layoutType = featureFlags.getStringValue<LayoutFeatureFlag>(
LAYOUT_FEATURE_FLAG_KEY,
'legacy-fixed'
);
const debugLayout = featureFlags.getBooleanValue(LAYOUT_DEBUG_FEATURE_FLAG_KEY, false);
const startServices = this.contextDeps.getValue()!;
const chromeHeader = chrome.getHeaderComponent();
const appComponent = application.getComponent();
const bannerComponent = overlays.banners.getComponent();
const body = document.querySelector('body')!;
chrome
@ -95,27 +105,16 @@ export class RenderingService implements IRenderingService {
body.classList.add(...newClasses);
});
const layout: LayoutService =
layoutType === 'grid'
? new GridLayout(renderCoreDeps, { debug: debugLayout })
: new LegacyFixedLayout(renderCoreDeps);
const Layout = layout.getComponent();
ReactDOM.render(
<KibanaRootContextProvider {...startServices} globalStyles={true}>
<>
{/* Global Styles that apply across the entire app */}
<GlobalAppStyle />
{/* Fixed headers */}
{chromeHeader}
{/* banners$.subscribe() for things like the No data banner */}
<div id="globalBannerList">{bannerComponent}</div>
{/* The App Wrapper outside of the fixed headers that accepts custom class names from apps */}
<AppWrapper chromeVisible$={chrome.getIsVisible$()}>
{/* Affixes a div to restrict the position of charts tooltip to the visible viewport minus the header */}
<div id={APP_FIXED_VIEWPORT_ID} />
{/* The actual plugin/app */}
{appComponent}
</AppWrapper>
</>
<Layout />
</KibanaRootContextProvider>,
targetDomElement
);

View file

@ -14,7 +14,6 @@
"**/*.tsx"
],
"kbn_references": [
"@kbn/core-application-common",
"@kbn/core-application-browser-internal",
"@kbn/core-overlays-browser",
"@kbn/core-chrome-browser-internal",
@ -33,7 +32,10 @@
"@kbn/core-user-profile-browser-mocks",
"@kbn/core-execution-context-browser-mocks",
"@kbn/core-execution-context-browser",
"@kbn/react-kibana-context-render"
"@kbn/react-kibana-context-render",
"@kbn/core-feature-flags-browser",
"@kbn/core-chrome-layout",
"@kbn/core-feature-flags-browser-mocks"
],
"exclude": [
"target/**/*"

View file

@ -7,5 +7,8 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { APP_FIXED_VIEWPORT_ID, useAppFixedViewport } from './use_app_fixed_viewport';
export {
APP_FIXED_VIEWPORT_ID,
useAppFixedViewport,
} from '@kbn/core-chrome-layout/app_fixed_viewport';
export type { RenderingService } from './rendering_service';

View file

@ -12,7 +12,9 @@
"**/*.ts",
"**/*.tsx",
],
"kbn_references": [],
"kbn_references": [
"@kbn/core-chrome-layout",
],
"exclude": [
"target/**/*",
]

View file

@ -494,6 +494,7 @@ describe('#start()', () => {
application: expect.any(Object),
chrome: expect.any(Object),
overlays: expect.any(Object),
featureFlags: expect.any(Object),
},
expect.any(HTMLElement)
);

View file

@ -430,7 +430,10 @@ export class CoreSystem {
`;
this.rootDomElement.classList.add(coreSystemRootDomElement);
this.rendering.renderCore({ chrome, application, overlays }, coreUiTargetDomElement);
this.rendering.renderCore(
{ chrome, application, overlays, featureFlags },
coreUiTargetDomElement
);
performance.mark(KBN_LOAD_MARKS, {
detail: LOAD_START_DONE,

View file

@ -1,13 +0,0 @@
:root {
// height of the header banner
--kbnHeaderBannerHeight: #{$euiSizeXL};
// total height of all fixed headers (when the banner is *not* present) inherited from EUI
--kbnHeaderOffset: var(--euiFixedHeadersOffset, 0);
// total height of everything when the banner is present
--kbnHeaderOffsetWithBanner: calc(var(--kbnHeaderBannerHeight) + var(--kbnHeaderOffset));
// height of the action menu in the header in serverless projects
--kbnProjectHeaderAppActionMenuHeight: #{$euiSize * 3};
}
// Quick note: This shouldn't be mixed with Sass variable declarations,
// as each import will cause :root to be re-declared unnecessarily

View file

@ -1,2 +1 @@
@import './css_variables';
@import './styles/index';

View file

@ -25,10 +25,6 @@
}
}
.euiBody--collapsibleNavIsDocked .euiBottomBar {
margin-left: 320px; // Hard-coded for now -- @cchaos
}
// Add support for serverless navbar
.euiBody--hasFlyout .euiBottomBar--fixed {
margin-left: var(--euiCollapsibleNavOffset, 0);

View file

@ -7,14 +7,11 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
// This file replaces scss core/public/_mixins.scss
import { css } from '@emotion/react';
// The `--kbnAppHeadersOffset` CSS variable is automatically updated by
// styles/rendering/_base.scss, based on whether the Kibana chrome has a
// header banner, app menu, and is visible or hidden
// The `--kbn-layout--application-height` CSS variable is automatically updated by chrome's layout system
// to reflect the height of the application container, minus any fixed headers or footers.
export const kbnFullBodyHeightCss = (additionalOffset = '0px') =>
css({
height: `calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)) - ${additionalOffset})`,
height: `calc(var(--kbn-layout--application-height) - ${additionalOffset})`,
});

View file

@ -3,7 +3,7 @@
exports[`KibanaPageTemplate render basic template 1`] = `
<div
class="euiPageTemplate kbnPageTemplate emotion-euiPageOuter-row-grow"
style="min-block-size:calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)));padding-block-start:0"
style="min-block-size:var(--kbn-layout--application-height);padding-block-start:0"
>
<main
class="emotion-euiPageInner"

View file

@ -24,7 +24,7 @@ exports[`KibanaPageTemplateInner isEmpty pageHeader & children 1`] = `
<_EuiPageTemplate
className="kbnPageTemplate"
grow={false}
minHeight="calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)))"
minHeight="var(--kbn-layout--application-height)"
offset={0}
>
<_EuiPageHeader
@ -80,7 +80,14 @@ exports[`KibanaPageTemplateInner page sidebar 1`] = `
offset={0}
>
<_EuiPageSidebar
sticky={true}
sticky={false}
style={
Object {
"maxHeight": "var(--kbn-layout--application-height, 100vh)",
"position": "sticky",
"top": "var(--euiFixedHeadersOffset, 0px)",
}
}
>
Test
</_EuiPageSidebar>

View file

@ -62,7 +62,15 @@ export const KibanaPageTemplateInner: FC<Props> = ({
let sideBar;
if (pageSideBar) {
const sideBarProps = { ...pageSideBarProps };
sideBarProps.sticky = true;
// TODO: instead of using sticky = true here, we reproduce the same behavior to account for both legacy fixed layout and new grid layout.
// https://github.com/elastic/eui/issues/8820
sideBarProps.style = {
maxHeight: 'var(--kbn-layout--application-height, 100vh)',
top: 'var(--euiFixedHeadersOffset, 0px)',
position: 'sticky',
};
sideBarProps.sticky = false; // This is a temporary fix to avoid the sidebar being incorrectly sticky in the new grid layout.
sideBar = <EuiPageTemplate.Sidebar {...sideBarProps}>{pageSideBar}</EuiPageTemplate.Sidebar>;
}
@ -75,9 +83,7 @@ export const KibanaPageTemplateInner: FC<Props> = ({
// the following props can be removed to allow the template to auto-handle
// the fixed header and banner heights.
offset={0}
minHeight={
header ? 'calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)))' : 0
}
minHeight={header ? 'var(--kbn-layout--application-height)' : 0}
grow={header ? false : undefined}
{...rest}
>

View file

@ -310,6 +310,8 @@
"@kbn/core-chrome-browser-internal/*": ["src/core/packages/chrome/browser-internal/*"],
"@kbn/core-chrome-browser-mocks": ["src/core/packages/chrome/browser-mocks"],
"@kbn/core-chrome-browser-mocks/*": ["src/core/packages/chrome/browser-mocks/*"],
"@kbn/core-chrome-layout": ["src/core/packages/chrome/layout/core-chrome-layout"],
"@kbn/core-chrome-layout/*": ["src/core/packages/chrome/layout/core-chrome-layout/*"],
"@kbn/core-chrome-layout-components": ["src/core/packages/chrome/layout/core-chrome-layout-components"],
"@kbn/core-chrome-layout-components/*": ["src/core/packages/chrome/layout/core-chrome-layout-components/*"],
"@kbn/core-config-server-internal": ["src/core/packages/config/server-internal"],

View file

@ -5,11 +5,6 @@ body.canvas-isFullscreen {
padding-top: 0;
}
// following rule is for docked navigation
&.euiBody--collapsibleNavIsDocked {
padding-left: 0 !important; // sass-lint:disable-line no-important
}
// hide global loading indicator
.kbnLoadingIndicator {
display: none;

View file

@ -8,21 +8,19 @@
import { FtrService } from '../ftr_provider_context';
export class BannersPageObject extends FtrService {
private readonly find = this.ctx.getService('find');
private readonly testSubjects = this.ctx.getService('testSubjects');
isTopBannerVisible() {
return this.find.existsByCssSelector(
'.header__topBanner [data-test-subj="bannerInnerWrapper"]'
);
return this.testSubjects.exists('bannerInnerWrapper');
}
async getTopBannerText() {
if (!(await this.isTopBannerVisible())) {
return '';
}
const bannerContainer = await this.find.byCssSelector(
'.header__topBanner [data-test-subj="bannerInnerWrapper"]'
);
const bannerContainer = await this.testSubjects.find('bannerInnerWrapper');
return bannerContainer.getVisibleText();
}
}

View file

@ -4401,6 +4401,10 @@
version "0.0.0"
uid ""
"@kbn/core-chrome-layout@link:src/core/packages/chrome/layout/core-chrome-layout":
version "0.0.0"
uid ""
"@kbn/core-config-server-internal@link:src/core/packages/config/server-internal":
version "0.0.0"
uid ""