mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[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.  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:
parent
f43138c059
commit
fe9dcf751a
68 changed files with 1050 additions and 273 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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$(),
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [],
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -55,7 +55,6 @@ function mockProps() {
|
|||
navLinks$: new BehaviorSubject([]),
|
||||
recentlyAccessed$: new BehaviorSubject([]),
|
||||
storage: new StubBrowserStorage(),
|
||||
onIsLockedUpdate: () => {},
|
||||
closeNav: () => {},
|
||||
navigateToApp: () => Promise.resolve(),
|
||||
navigateToUrl: () => Promise.resolve(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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$}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -9,4 +9,3 @@
|
|||
|
||||
export { Header } from './header';
|
||||
export type { HeaderProps } from './header';
|
||||
export type { OnIsLockedUpdate, NavType } from './types';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -10,4 +10,3 @@
|
|||
export { Header } from './header';
|
||||
export { ProjectHeader } from './project';
|
||||
export { LoadingIndicator } from './loading_indicator';
|
||||
export type { NavType } from './header';
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
31
src/core/packages/chrome/layout/core-chrome-layout/README.md
Normal file
31
src/core/packages/chrome/layout/core-chrome-layout/README.md
Normal 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
|
||||
);
|
||||
```
|
|
@ -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"
|
|
@ -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>
|
15
src/core/packages/chrome/layout/core-chrome-layout/index.ts
Normal file
15
src/core/packages/chrome/layout/core-chrome-layout/index.ts
Normal 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';
|
|
@ -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'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/core-chrome-layout",
|
||||
"owner": "@elastic/appex-sharedux",
|
||||
"group": "platform",
|
||||
"visibility": "private"
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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)}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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",
|
||||
]
|
||||
}
|
|
@ -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'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"
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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/**/*"
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"kbn_references": [],
|
||||
"kbn_references": [
|
||||
"@kbn/core-chrome-layout",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -1,2 +1 @@
|
|||
@import './css_variables';
|
||||
@import './styles/index';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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})`,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue