Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@
*/

import React from 'react';
import { useAiButton } from './hooks';
import { HeaderExtension } from '../shared/header_extension';
import { useAiButtons } from './hooks';

/**
* Renders the AI button in the Chrome-Next project header.
* The plugin owns the component; Chrome just places it in the slot.
*/
export const AiButtonSlot = React.memo(() => {
const node = useAiButton();
const buttons = useAiButtons();

if (!node) {
if (buttons.length === 0) {
return null;
}

return <>{node}</>;
return (
<>
{buttons.map((button, index) => (
<HeaderExtension key={index} extension={button.content} />
))}
</>
);
});

AiButtonSlot.displayName = 'AiButtonSlot';
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { useAiButton } from './use_ai_button';
export { useAiButtons } from './use_ai_button';
export { useProjectNextAppMenu } from './use_project_next_app_menu';
export { useBackButton } from './use_back_button';
export { useReportTopBarHeight } from './use_report_top_bar_height';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,15 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { ReactNode } from 'react';
import { useMemo } from 'react';
import type { ChromeNextAiButton } from '@kbn/core-chrome-browser';
import { useObservable } from '@kbn/use-observable';
import { useChromeService } from '@kbn/core-chrome-browser-context';

/**
* Returns the AI button ReactNode set via `chrome.next.aiButton.set()`,
* or `undefined` if not set. Used by the Chrome-Next header.
*/
export function useAiButton(): ReactNode | undefined {
const EMPTY: ChromeNextAiButton[] = [];

export function useAiButtons(): ChromeNextAiButton[] {
const chrome = useChromeService();
const node$ = useMemo(() => chrome.next.aiButton.get$(), [chrome]);
return useObservable(node$, undefined);
const buttons$ = useMemo(() => chrome.next.aiButton.get$(), [chrome]);
return useObservable(buttons$, EMPTY);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import React, { useMemo } from 'react';
import { useHasLegacyActionMenu } from '../shared/chrome_hooks';
import { AiButtonSlot } from './ai_button_slot';
import { ProjectNextAppMenu } from './app_menu';
import { useAiButton, useProjectNextAppMenu } from './hooks';
import { useAiButtons, useProjectNextAppMenu } from './hooks';

const useTrailingStyles = () => {
const { euiTheme } = useEuiTheme();
Expand All @@ -38,10 +38,10 @@ const useTrailingStyles = () => {
export const ProjectNextTrailingActions = React.memo(() => {
const appMenuConfig = useProjectNextAppMenu();
const hasLegacyActionMenu = useHasLegacyActionMenu();
const aiButton = useAiButton();
const aiButtons = useAiButtons();
const styles = useTrailingStyles();

const hasTrailingContent = !!appMenuConfig || hasLegacyActionMenu || !!aiButton;
const hasTrailingContent = !!appMenuConfig || hasLegacyActionMenu || aiButtons.length > 0;

if (!hasTrailingContent) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const HeaderExtension = ({ extension, display, containerClassName }: Prop
if (!isMountPoint(extension)) {
return (
<Suspense fallback={null}>
<div className={containerClassName} style={style}>
<div css={mountPointContainerCss} className={containerClassName} style={style}>
{extension}
</div>
</Suspense>
Expand Down
25 changes: 15 additions & 10 deletions src/core/packages/chrome/browser-internal-types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type {
ChromeBadge,
ChromeBreadcrumb,
ChromeBreadcrumbsAppendExtension,
ChromeNext,
ChromeNextAiButton,
ChromeNextHeaderConfig,
ChromeNextGlobalSearchConfig,
ChromeProjectNavigationNode,
Expand Down Expand Up @@ -108,15 +110,18 @@ export interface InternalChromeStart extends ChromeStart {
};

/** @internal Extends public `next` with `get$` for Chrome layout components. */
next: ChromeStart['next'] & {
header: ChromeStart['next']['header'] & {
get$(): Observable<ChromeNextHeaderConfig | undefined>;
};
aiButton: ChromeStart['next']['aiButton'] & {
get$(): Observable<ReactNode | undefined>;
};
globalSearch: ChromeStart['next']['globalSearch'] & {
get$(): Observable<ChromeNextGlobalSearchConfig | undefined>;
};
next: InternalChromeNext;
}

/** @internal */
export interface InternalChromeNext extends ChromeNext {
header: ChromeNext['header'] & {
get$(): Observable<ChromeNextHeaderConfig | undefined>;
};
aiButton: ChromeNext['aiButton'] & {
get$(): Observable<ChromeNextAiButton[]>;
};
globalSearch: ChromeNext['globalSearch'] & {
get$(): Observable<ChromeNextGlobalSearchConfig | undefined>;
};
}
14 changes: 12 additions & 2 deletions src/core/packages/chrome/browser-internal/src/chrome_api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import React, { type ReactNode } from 'react';
import { distinctUntilChanged, map, shareReplay } from 'rxjs';
import type { ChromeNextAiButton } from '@kbn/core-chrome-browser';
import type { RecentlyAccessedService } from '@kbn/recently-accessed';
import { SidebarServiceProvider } from '@kbn/core-chrome-sidebar-context';
import { ChromeServiceProvider } from '@kbn/core-chrome-browser-context';
Expand Down Expand Up @@ -176,8 +177,17 @@ export function createChromeApi({ state, services, sidebar }: ChromeApiDeps): In
set: services.nextHeader.set,
},
aiButton: {
get$: () => state.aiButton.$,
set: state.aiButton.set,
get$: () => state.aiButton.$.pipe(map((buttons) => [...buttons])),
register: (button: ChromeNextAiButton) => {
state.aiButton.update((prev) => new Set([...prev, button]));
return () => {
state.aiButton.update((prev) => {
const next = new Set(prev);
next.delete(button);
return next;
});
};
},
},
globalSearch: {
get$: () => state.globalSearch.$,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
ChromeGlobalHelpExtensionMenuLink,
ChromeHelpExtension,
ChromeNavLink,
ChromeNextAiButton,
ChromeNextGlobalSearchConfig,
ChromeUserBanner,
} from '@kbn/core-chrome-browser';
Expand Down Expand Up @@ -65,7 +66,7 @@ export interface ChromeState {
globalFooter: State<ReactNode>;
customNavLink: State<ChromeNavLink | undefined>;
appMenu: State<AppMenuConfig | undefined>;
aiButton: State<ReactNode>;
aiButton: State<ReadonlySet<ChromeNextAiButton>>;
globalSearch: State<ChromeNextGlobalSearchConfig | undefined>;

/** Help system */
Expand Down Expand Up @@ -110,7 +111,7 @@ export function createChromeState({ application, docLinks }: ChromeStateDeps): C

// UI Elements (not reset on app change)
const globalFooter = createState<ReactNode>(null);
const aiButton = createState<ReactNode>(undefined);
const aiButton = createState<ReadonlySet<ChromeNextAiButton>>(new Set());
const globalSearch = createState<ChromeNextGlobalSearchConfig | undefined>(undefined);
const customNavLink = createState<ChromeNavLink | undefined>(undefined);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ const createStartContractMock = () => {
}),
}),
aiButton: lazyObject({
get$: jest.fn().mockReturnValue(new BehaviorSubject(undefined)),
set: jest.fn(),
get$: jest.fn().mockReturnValue(new BehaviorSubject([])),
register: jest.fn().mockReturnValue(() => {}),
}),
globalSearch: lazyObject({
get$: jest.fn().mockReturnValue(nextGlobalSearchState$),
Expand Down
8 changes: 5 additions & 3 deletions src/core/packages/chrome/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ export type {
SidebarAppDefinition,
SidebarSetup,
SidebarStart,
ChromeNext,
ChromeNextAiButton,
ChromeNextGlobalSearchConfig,
ChromeNextHeaderBack,
ChromeNextHeaderBadge,
ChromeNextHeaderCallout,
ChromeNextHeaderConfig,
ChromeNextHeaderMetadataSlotItem,
ChromeNextHeaderGlobalActions,
ChromeNextHeaderMetadataSlotItem,
ChromeNextHeaderTab,
ChromeNextHeaderCallout,
ChromeNextGlobalSearchConfig,
} from './src';
15 changes: 15 additions & 0 deletions src/core/packages/chrome/browser/src/chrome_next/ai_button.ts
Original file line number Diff line number Diff line change
@@ -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".
*/

import type { ChromeExtensionContent } from '@kbn/core-mount-utils-browser';

/** @public */
export interface ChromeNextAiButton {
content: ChromeExtensionContent;
}
47 changes: 47 additions & 0 deletions src/core/packages/chrome/browser/src/chrome_next/chrome_next.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 { ChromeNextAiButton } from './ai_button';
import type { ChromeNextGlobalSearchConfig } from './global_search';
import type { ChromeNextHeaderConfig } from './header';

/**
* Chrome-Next APIs: header configuration, AI button slot, global search, and future slots.
* @public
*/
export interface ChromeNext {
header: {
/**
* Set the Chrome-Next header configuration for the current page.
* Chrome renders the title, metadata, global actions, and app menu.
*
* Pass `undefined` to clear (e.g. on unmount or route change).
* Automatically cleared on app change.
*/
set(config?: ChromeNextHeaderConfig): void;
};
aiButton: {
/**
* Register an AI button for the Chrome-Next header.
* Multiple plugins can register buttons; each owns its own visibility logic.
* Chrome renders all registered buttons sorted by `order` in a fixed slot
* at the far right of the header.
* Returns an unregister callback. Global — persists across app changes.
*/
register(button: ChromeNextAiButton): () => void;
};
globalSearch: {
/**
* Set the global search configuration for the Chrome-Next sidenav.
* Chrome renders a search icon button in the sidenav header items; clicking it fires `onClick`.
* Pass `undefined` to remove the button. Global — persists across app changes.
*/
set(config?: ChromeNextGlobalSearchConfig): void;
};
}
21 changes: 21 additions & 0 deletions src/core/packages/chrome/browser/src/chrome_next/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 { ChromeNextAiButton } from './ai_button';
export type { ChromeNext } from './chrome_next';
export type { ChromeNextGlobalSearchConfig } from './global_search';
export type {
ChromeNextHeaderBack,
ChromeNextHeaderBadge,
ChromeNextHeaderCallout,
ChromeNextHeaderConfig,
ChromeNextHeaderGlobalActions,
ChromeNextHeaderMetadataSlotItem,
ChromeNextHeaderTab,
} from './header';
37 changes: 3 additions & 34 deletions src/core/packages/chrome/browser/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
import type { ReactNode } from 'react';
import type { Observable } from 'rxjs';
import type { AppMenuConfig } from '@kbn/core-chrome-app-menu-components';
import type { ChromeNextHeaderConfig } from './next_header';
import type { ChromeNextGlobalSearchConfig } from './next_global_search';
import type { ChromeNext } from './chrome_next/chrome_next';
import type { ChromeNavLink, ChromeNavLinks } from './nav_links';
import type { ChromeRecentlyAccessed } from './recently_accessed';
import type { ChromeDocTitle } from './doc_title';
Expand Down Expand Up @@ -324,38 +323,8 @@ export interface ChromeStart {
*/
getActiveSolutionNavId(): SolutionId | null;

/**
* Chrome-Next APIs: header configuration, AI button slot, and future slots.
*/
next: {
header: {
/**
* Set the Chrome-Next header configuration for the current page.
* Chrome renders the title, metadata, global actions, and app menu.
*
* Pass `undefined` to clear (e.g. on unmount or route change).
* Automatically cleared on app change.
*/
set(config?: ChromeNextHeaderConfig): void;
};
aiButton: {
/**
* Set the AI button component for the Chrome-Next header.
* The plugin owns the full component (rendering, state, tooltips, shortcuts).
* Chrome renders it in a fixed slot at the far right of the header.
* Pass `undefined` to remove. Global — persists across app changes.
*/
set(node?: ReactNode): void;
};
globalSearch: {
/**
* Set the global search configuration for the Chrome-Next sidenav.
* Chrome renders a search icon button in the sidenav header items; clicking it fires `onClick`.
* Pass `undefined` to remove the button. Global — persists across app changes.
*/
set(config?: ChromeNextGlobalSearchConfig): void;
};
};
/** {@inheritdoc ChromeNext} */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL about @inheritdoc. I've been copy pasting docs everywhere

next: ChromeNext;

/**
* Used only by the rendering service and KibanaRenderingContextProvider to wrap the rendering tree in the Chrome context providers
Expand Down
11 changes: 6 additions & 5 deletions src/core/packages/chrome/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ export type {
} from './sidebar';

export type {
ChromeNext,
ChromeNextAiButton,
ChromeNextGlobalSearchConfig,
ChromeNextHeaderBack,
ChromeNextHeaderBadge,
ChromeNextHeaderCallout,
ChromeNextHeaderConfig,
ChromeNextHeaderMetadataSlotItem,
ChromeNextHeaderGlobalActions,
ChromeNextHeaderMetadataSlotItem,
ChromeNextHeaderTab,
ChromeNextHeaderCallout,
} from './next_header';

export type { ChromeNextGlobalSearchConfig } from './next_global_search';
} from './chrome_next';
Loading
Loading