Skip to content

Commit 606b578

Browse files
KDKHDkibanamachine
andauthored
[8.x] [Security Assistant] Move security AI assistant button into global nav bar (#203060) (#205884)
# Backport This will backport the following commits from `main` to `8.x`: - [[Security Assistant] Move security AI assistant button into global nav bar (#203060)](#203060) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Kenneth Kreindler","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-08T10:03:54Z","message":"[Security Assistant] Move security AI assistant button into global nav bar (#203060)\n\n## Summary\r\n\r\nMore changes are needed within the observability and search solution to\r\nclose the issue fully.\r\n\r\nSummarise your PR. If it involves visual changes include a screenshot or\r\ngif.\r\n\r\nMove the security AI assistant button from the solution header bar into\r\nthe global nav bar. This is part of the AI assistant unification\r\ninitiative.\r\n\r\n### How to Test\r\n- Start kibana\r\n- Go to one of the security solution pages (e.g. attack discovery)\r\n- AI assistant button should be in the global nav bar. Clicking it opens\r\nthe assistant.\r\n\r\n- The button can also be tested for security serverless deployment. It\r\nshould look like the screenshot bellow.\r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies following conditions. \r\n\r\nReviewers should verify this PR satisfies this list as well.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] This was checked for breaking HTTP API changes, and any breaking\r\nchanges have been approved by the breaking-change committee. The\r\n`release_note:breaking` label should be applied in these situations.\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [x] The PR description includes the appropriate Release Notes section,\r\nand the correct `release_note:*` label is applied per the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n### Identify risks\r\n\r\nDoes this PR introduce any risks? For example, consider risks like hard\r\nto test bugs, performance regression, potential of data loss.\r\n\r\nDescribe the risk, its severity, and mitigation for each identified\r\nrisk. Invite stakeholders and evaluate how to proceed before merging.\r\n\r\n- [ ] [See some risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\r\n- [ ] ...\r\n\r\nClassic:\r\n\r\n![image](https://github.com/user-attachments/assets/b2a9c982-bc54-42f4-ab59-6f0c99d4d899)\r\n\r\n![image](https://github.com/user-attachments/assets/1ae36af0-5d1a-4519-844a-563074646ddf)\r\n\r\nServerless:\r\n\r\n![image](https://github.com/user-attachments/assets/345280df-0e70-4203-b0d8-48ad11753f74)\r\n\r\n![image](https://github.com/user-attachments/assets/7425c886-4528-4987-a00a-48bdc71728c7)\r\n\r\nOld:\r\n<img width=\"1728\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/5ef568c6-2d31-47da-8f5f-87dfdf10cb5c\">\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <[email protected]>\r\nCo-authored-by: Elastic Machine <[email protected]>","sha":"06cf554981845fa2e1d9505952e559568d3e0479","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-major","Feature:Security Assistant","Team:Security Generative AI"],"number":203060,"url":"https://github.com/elastic/kibana/pull/203060","mergeCommit":{"message":"[Security Assistant] Move security AI assistant button into global nav bar (#203060)\n\n## Summary\r\n\r\nMore changes are needed within the observability and search solution to\r\nclose the issue fully.\r\n\r\nSummarise your PR. If it involves visual changes include a screenshot or\r\ngif.\r\n\r\nMove the security AI assistant button from the solution header bar into\r\nthe global nav bar. This is part of the AI assistant unification\r\ninitiative.\r\n\r\n### How to Test\r\n- Start kibana\r\n- Go to one of the security solution pages (e.g. attack discovery)\r\n- AI assistant button should be in the global nav bar. Clicking it opens\r\nthe assistant.\r\n\r\n- The button can also be tested for security serverless deployment. It\r\nshould look like the screenshot bellow.\r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies following conditions. \r\n\r\nReviewers should verify this PR satisfies this list as well.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] This was checked for breaking HTTP API changes, and any breaking\r\nchanges have been approved by the breaking-change committee. The\r\n`release_note:breaking` label should be applied in these situations.\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [x] The PR description includes the appropriate Release Notes section,\r\nand the correct `release_note:*` label is applied per the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n### Identify risks\r\n\r\nDoes this PR introduce any risks? For example, consider risks like hard\r\nto test bugs, performance regression, potential of data loss.\r\n\r\nDescribe the risk, its severity, and mitigation for each identified\r\nrisk. Invite stakeholders and evaluate how to proceed before merging.\r\n\r\n- [ ] [See some risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\r\n- [ ] ...\r\n\r\nClassic:\r\n\r\n![image](https://github.com/user-attachments/assets/b2a9c982-bc54-42f4-ab59-6f0c99d4d899)\r\n\r\n![image](https://github.com/user-attachments/assets/1ae36af0-5d1a-4519-844a-563074646ddf)\r\n\r\nServerless:\r\n\r\n![image](https://github.com/user-attachments/assets/345280df-0e70-4203-b0d8-48ad11753f74)\r\n\r\n![image](https://github.com/user-attachments/assets/7425c886-4528-4987-a00a-48bdc71728c7)\r\n\r\nOld:\r\n<img width=\"1728\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/5ef568c6-2d31-47da-8f5f-87dfdf10cb5c\">\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <[email protected]>\r\nCo-authored-by: Elastic Machine <[email protected]>","sha":"06cf554981845fa2e1d9505952e559568d3e0479"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/203060","number":203060,"mergeCommit":{"message":"[Security Assistant] Move security AI assistant button into global nav bar (#203060)\n\n## Summary\r\n\r\nMore changes are needed within the observability and search solution to\r\nclose the issue fully.\r\n\r\nSummarise your PR. If it involves visual changes include a screenshot or\r\ngif.\r\n\r\nMove the security AI assistant button from the solution header bar into\r\nthe global nav bar. This is part of the AI assistant unification\r\ninitiative.\r\n\r\n### How to Test\r\n- Start kibana\r\n- Go to one of the security solution pages (e.g. attack discovery)\r\n- AI assistant button should be in the global nav bar. Clicking it opens\r\nthe assistant.\r\n\r\n- The button can also be tested for security serverless deployment. It\r\nshould look like the screenshot bellow.\r\n\r\n### Checklist\r\n\r\nCheck the PR satisfies following conditions. \r\n\r\nReviewers should verify this PR satisfies this list as well.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] This was checked for breaking HTTP API changes, and any breaking\r\nchanges have been approved by the breaking-change committee. The\r\n`release_note:breaking` label should be applied in these situations.\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed\r\n- [x] The PR description includes the appropriate Release Notes section,\r\nand the correct `release_note:*` label is applied per the\r\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n### Identify risks\r\n\r\nDoes this PR introduce any risks? For example, consider risks like hard\r\nto test bugs, performance regression, potential of data loss.\r\n\r\nDescribe the risk, its severity, and mitigation for each identified\r\nrisk. Invite stakeholders and evaluate how to proceed before merging.\r\n\r\n- [ ] [See some risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)\r\n- [ ] ...\r\n\r\nClassic:\r\n\r\n![image](https://github.com/user-attachments/assets/b2a9c982-bc54-42f4-ab59-6f0c99d4d899)\r\n\r\n![image](https://github.com/user-attachments/assets/1ae36af0-5d1a-4519-844a-563074646ddf)\r\n\r\nServerless:\r\n\r\n![image](https://github.com/user-attachments/assets/345280df-0e70-4203-b0d8-48ad11753f74)\r\n\r\n![image](https://github.com/user-attachments/assets/7425c886-4528-4987-a00a-48bdc71728c7)\r\n\r\nOld:\r\n<img width=\"1728\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/5ef568c6-2d31-47da-8f5f-87dfdf10cb5c\">\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <[email protected]>\r\nCo-authored-by: Elastic Machine <[email protected]>","sha":"06cf554981845fa2e1d9505952e559568d3e0479"}}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <[email protected]>
1 parent 5413cff commit 606b578

File tree

24 files changed

+396
-166
lines changed

24 files changed

+396
-166
lines changed

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/prompt_editor/system_prompt/index.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { DEFAULT_CONVERSATION_TITLE } from '../../use_conversation/translations'
1515
import { TestProviders } from '../../../mock/test_providers/test_providers';
1616
import { WELCOME_CONVERSATION } from '../../use_conversation/sample_conversations';
1717
import { PromptResponse } from '@kbn/elastic-assistant-common';
18+
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
19+
import { of } from 'rxjs';
1820

1921
const BASE_CONVERSATION: Conversation = {
2022
...WELCOME_CONVERSATION,
@@ -36,6 +38,13 @@ const mockUseAssistantContext = {
3638
setConversations: jest.fn(),
3739
setAllSystemPrompts: jest.fn(),
3840
allSystemPrompts: mockSystemPrompts,
41+
chrome: {
42+
getChromeStyle$: jest.fn(() => of('classic')),
43+
navControls: chromeServiceMock.createStartContract().navControls,
44+
},
45+
assistantAvailability: {
46+
hasAssistantPrivilege: true,
47+
},
3948
};
4049

4150
jest.mock('../../../assistant_context', () => {

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompt_settings/quick_prompt_settings.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { QuickPromptSettings } from './quick_prompt_settings';
1111
import { TestProviders } from '../../../mock/test_providers/test_providers';
1212
import { MOCK_QUICK_PROMPTS } from '../../../mock/quick_prompt';
1313
import { mockPromptContexts } from '../../../mock/prompt_context';
14+
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
15+
import { of } from 'rxjs';
1416

1517
const onSelectedQuickPromptChange = jest.fn();
1618
const setPromptsBulkActions = jest.fn();
@@ -28,6 +30,13 @@ const testProps = {
2830
};
2931
const mockContext = {
3032
basePromptContexts: MOCK_QUICK_PROMPTS,
33+
chrome: {
34+
getChromeStyle$: jest.fn(() => of('classic')),
35+
navControls: chromeServiceMock.createStartContract().navControls,
36+
},
37+
assistantAvailability: {
38+
hasAssistantPrivilege: true,
39+
},
3140
};
3241

3342
jest.mock('../../../assistant_context', () => ({

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/quick_prompts/quick_prompts.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { TestProviders } from '../../mock/test_providers/test_providers';
1111
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
1212
import { QUICK_PROMPTS_TAB } from '../settings/const';
1313
import { QuickPrompts } from './quick_prompts';
14+
import { of } from 'rxjs';
15+
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
1416

1517
const setInput = jest.fn();
1618
const setIsSettingsModalVisible = jest.fn();
@@ -26,6 +28,13 @@ const mockUseAssistantContext = {
2628
setSelectedSettingsTab,
2729
promptContexts: {},
2830
allQuickPrompts: MOCK_QUICK_PROMPTS,
31+
chrome: {
32+
getChromeStyle$: jest.fn(() => of('classic')),
33+
navControls: chromeServiceMock.createStartContract().navControls,
34+
},
35+
assistantAvailability: {
36+
hasAssistantPrivilege: true,
37+
},
2938
};
3039

3140
const testTitle = 'SPL_QUERY_CONVERSION_TITLE';
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { render, renderHook } from '@testing-library/react';
10+
import { AssistantNavLink } from './assistant_nav_link';
11+
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
12+
import { ChromeNavControl } from '@kbn/core/public';
13+
import { createHtmlPortalNode, OutPortal } from 'react-reverse-portal';
14+
import { of } from 'rxjs';
15+
import { useAssistantContext } from '.';
16+
17+
const MockNavigationBar = OutPortal;
18+
19+
const mockShowAssistantOverlay = jest.fn();
20+
const mockNavControls = chromeServiceMock.createStartContract().navControls;
21+
const mockGetChromeStyle = jest.fn();
22+
23+
const mockAssistantContext = {
24+
chrome: {
25+
getChromeStyle$: mockGetChromeStyle,
26+
navControls: mockNavControls,
27+
},
28+
showAssistantOverlay: mockShowAssistantOverlay,
29+
assistantAvailability: {
30+
hasAssistantPrivilege: true,
31+
},
32+
};
33+
34+
jest.mock('.', () => {
35+
return {
36+
...jest.requireActual('.'),
37+
useAssistantContext: jest.fn(),
38+
};
39+
});
40+
41+
describe('AssistantNavLink', () => {
42+
beforeEach(() => {
43+
jest.clearAllMocks();
44+
mockGetChromeStyle.mockReturnValue(of('classic'));
45+
(useAssistantContext as jest.Mock).mockReturnValue({
46+
...mockAssistantContext,
47+
});
48+
});
49+
50+
it('should register link in nav bar', () => {
51+
render(<AssistantNavLink />);
52+
expect(mockNavControls.registerRight).toHaveBeenCalledTimes(1);
53+
});
54+
55+
it('button has transparent background in project navigation', () => {
56+
const { result: portalNode } = renderHook(() =>
57+
React.useMemo(() => createHtmlPortalNode(), [])
58+
);
59+
60+
mockGetChromeStyle.mockReturnValue(of('project'));
61+
62+
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => {
63+
chromeNavControl.mount(portalNode.current.element);
64+
});
65+
66+
const { queryByTestId } = render(
67+
<>
68+
<MockNavigationBar node={portalNode.current} />
69+
<AssistantNavLink />
70+
</>
71+
);
72+
expect(queryByTestId('assistantNavLink')).not.toHaveStyle(
73+
'background-color: rgb(204, 228, 245)'
74+
);
75+
});
76+
77+
it('button has opaque background in classic navigation', () => {
78+
const { result: portalNode } = renderHook(() =>
79+
React.useMemo(() => createHtmlPortalNode(), [])
80+
);
81+
82+
mockGetChromeStyle.mockReturnValue(of('classic'));
83+
84+
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => {
85+
chromeNavControl.mount(portalNode.current.element);
86+
});
87+
88+
const { queryByTestId } = render(
89+
<>
90+
<MockNavigationBar node={portalNode.current} />
91+
<AssistantNavLink />
92+
</>
93+
);
94+
expect(queryByTestId('assistantNavLink')).toHaveStyle('background-color: rgb(204, 228, 245)');
95+
});
96+
97+
it('should render the header link text', () => {
98+
const { result: portalNode } = renderHook(() =>
99+
React.useMemo(() => createHtmlPortalNode(), [])
100+
);
101+
102+
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => {
103+
chromeNavControl.mount(portalNode.current.element);
104+
});
105+
106+
const { queryByText, queryByTestId } = render(
107+
<>
108+
<MockNavigationBar node={portalNode.current} />
109+
<AssistantNavLink />
110+
</>
111+
);
112+
expect(queryByTestId('assistantNavLink')).toBeInTheDocument();
113+
expect(queryByText('AI Assistant')).toBeInTheDocument();
114+
});
115+
116+
it('should not render the header link if not authorized', () => {
117+
const { result: portalNode } = renderHook(() =>
118+
React.useMemo(() => createHtmlPortalNode(), [])
119+
);
120+
121+
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => {
122+
chromeNavControl.mount(portalNode.current.element);
123+
});
124+
125+
(useAssistantContext as jest.Mock).mockReturnValue({
126+
...mockAssistantContext,
127+
assistantAvailability: {
128+
hasAssistantPrivilege: false,
129+
},
130+
});
131+
132+
const { queryByText, queryByTestId } = render(
133+
<>
134+
<MockNavigationBar node={portalNode.current} />
135+
<AssistantNavLink />
136+
</>
137+
);
138+
expect(queryByTestId('assistantNavLink')).not.toBeInTheDocument();
139+
expect(queryByText('AI Assistant')).not.toBeInTheDocument();
140+
});
141+
142+
it('should call the assistant overlay to show on click', () => {
143+
const { result: portalNode } = renderHook(() =>
144+
React.useMemo(() => createHtmlPortalNode(), [])
145+
);
146+
147+
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => {
148+
chromeNavControl.mount(portalNode.current.element);
149+
});
150+
151+
const { queryByTestId } = render(
152+
<>
153+
<MockNavigationBar node={portalNode.current} />
154+
<AssistantNavLink />
155+
</>
156+
);
157+
queryByTestId('assistantNavLink')?.click();
158+
expect(mockShowAssistantOverlay).toHaveBeenCalledTimes(1);
159+
expect(mockShowAssistantOverlay).toHaveBeenCalledWith({ showOverlay: true });
160+
});
161+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import type { FC } from 'react';
9+
import React, { useCallback, useEffect, useState } from 'react';
10+
import ReactDOM from 'react-dom';
11+
import { createHtmlPortalNode, OutPortal, InPortal } from 'react-reverse-portal';
12+
import { EuiToolTip, EuiButton, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
13+
import { i18n } from '@kbn/i18n';
14+
import { ChromeStyle } from '@kbn/core-chrome-browser';
15+
import { AssistantIcon } from '@kbn/ai-assistant-icon';
16+
import { useAssistantContext } from '.';
17+
18+
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
19+
20+
const TOOLTIP_CONTENT = i18n.translate(
21+
'xpack.elasticAssistant.assistantContext.assistantNavLinkShortcutTooltip',
22+
{
23+
values: { keyboardShortcut: isMac ? '⌘ ;' : 'Ctrl ;' },
24+
defaultMessage: 'Keyboard shortcut {keyboardShortcut}',
25+
}
26+
);
27+
const LINK_LABEL = i18n.translate('xpack.elasticAssistant.assistantContext.assistantNavLink', {
28+
defaultMessage: 'AI Assistant',
29+
});
30+
31+
export const AssistantNavLink: FC = () => {
32+
const { chrome, showAssistantOverlay, assistantAvailability, currentAppId } =
33+
useAssistantContext();
34+
const portalNode = React.useMemo(() => createHtmlPortalNode(), []);
35+
const [chromeStyle, setChromeStyle] = useState<ChromeStyle | undefined>(undefined);
36+
37+
// useObserverable would change the order of re-renders that are tested against closely.
38+
useEffect(() => {
39+
const s = chrome.getChromeStyle$().subscribe(setChromeStyle);
40+
return () => s.unsubscribe();
41+
}, [chrome]);
42+
43+
useEffect(() => {
44+
const registerPortalNode = () => {
45+
chrome.navControls.registerRight({
46+
mount: (element: HTMLElement) => {
47+
ReactDOM.render(<OutPortal node={portalNode} />, element);
48+
return () => ReactDOM.unmountComponentAtNode(element);
49+
},
50+
// right before the user profile
51+
order: 1001,
52+
});
53+
};
54+
55+
if (
56+
assistantAvailability.hasAssistantPrivilege &&
57+
chromeStyle &&
58+
currentAppId !== 'management'
59+
) {
60+
registerPortalNode();
61+
}
62+
}, [chrome, portalNode, assistantAvailability.hasAssistantPrivilege, chromeStyle, currentAppId]);
63+
64+
const showOverlay = useCallback(
65+
() => showAssistantOverlay({ showOverlay: true }),
66+
[showAssistantOverlay]
67+
);
68+
69+
if (!assistantAvailability.hasAssistantPrivilege || !chromeStyle) {
70+
return null;
71+
}
72+
73+
const EuiButtonBasicOrEmpty = chromeStyle === 'project' ? EuiButtonEmpty : EuiButton;
74+
75+
return (
76+
<InPortal node={portalNode}>
77+
<EuiToolTip content={TOOLTIP_CONTENT}>
78+
<EuiButtonBasicOrEmpty
79+
onClick={showOverlay}
80+
color="primary"
81+
size="s"
82+
data-test-subj="assistantNavLink"
83+
>
84+
<EuiFlexGroup gutterSize="s" alignItems="center">
85+
<EuiFlexItem grow={false}>
86+
<AssistantIcon size="m" />
87+
</EuiFlexItem>
88+
<EuiFlexItem grow={false}>{LINK_LABEL}</EuiFlexItem>
89+
</EuiFlexGroup>
90+
</EuiButtonBasicOrEmpty>
91+
</EuiToolTip>
92+
</InPortal>
93+
);
94+
};

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant_context/index.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import useLocalStorage from 'react-use/lib/useLocalStorage';
1414
import useSessionStorage from 'react-use/lib/useSessionStorage';
1515
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
1616
import { AssistantFeatures, defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
17-
import { NavigateToAppOptions, UserProfileService } from '@kbn/core/public';
17+
import { ChromeStart, NavigateToAppOptions, UserProfileService } from '@kbn/core/public';
1818
import { useQuery } from '@tanstack/react-query';
1919
import { updatePromptContexts } from './helpers';
2020
import type {
@@ -43,6 +43,7 @@ import {
4343
import { useCapabilities } from '../assistant/api/capabilities/use_capabilities';
4444
import { WELCOME_CONVERSATION_TITLE } from '../assistant/use_conversation/translations';
4545
import { SettingsTabs } from '../assistant/settings/types';
46+
import { AssistantNavLink } from './assistant_nav_link';
4647

4748
export interface ShowAssistantOverlayProps {
4849
showOverlay: boolean;
@@ -77,6 +78,7 @@ export interface AssistantProviderProps {
7778
toasts?: IToasts;
7879
currentAppId: string;
7980
userProfileService: UserProfileService;
81+
chrome: ChromeStart;
8082
}
8183

8284
export interface UserAvatar {
@@ -128,6 +130,7 @@ export interface UseAssistantContext {
128130
currentAppId: string;
129131
codeBlockRef: React.MutableRefObject<(codeBlock: string) => void>;
130132
userProfileService: UserProfileService;
133+
chrome: ChromeStart;
131134
}
132135

133136
const AssistantContext = React.createContext<UseAssistantContext | undefined>(undefined);
@@ -151,6 +154,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
151154
toasts,
152155
currentAppId,
153156
userProfileService,
157+
chrome,
154158
}) => {
155159
/**
156160
* Session storage for traceOptions, including APM URL and LangSmith Project/API Key
@@ -303,6 +307,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
303307
currentAppId,
304308
codeBlockRef,
305309
userProfileService,
310+
chrome,
306311
}),
307312
[
308313
actionTypeRegistry,
@@ -338,10 +343,16 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
338343
currentAppId,
339344
codeBlockRef,
340345
userProfileService,
346+
chrome,
341347
]
342348
);
343349

344-
return <AssistantContext.Provider value={value}>{children}</AssistantContext.Provider>;
350+
return (
351+
<AssistantContext.Provider value={value}>
352+
<AssistantNavLink />
353+
{children}
354+
</AssistantContext.Provider>
355+
);
345356
};
346357

347358
export const useAssistantContext = () => {

0 commit comments

Comments
 (0)