forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Assistant] Move security AI assistant button into global na…
…v bar (elastic#203060) ## Summary More changes are needed within the observability and search solution to close the issue fully. Summarise your PR. If it involves visual changes include a screenshot or gif. Move the security AI assistant button from the solution header bar into the global nav bar. This is part of the AI assistant unification initiative. ### How to Test - Start kibana - Go to one of the security solution pages (e.g. attack discovery) - AI assistant button should be in the global nav bar. Clicking it opens the assistant. - The button can also be tested for security serverless deployment. It should look like the screenshot bellow. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... Classic: ![image](https://github.com/user-attachments/assets/b2a9c982-bc54-42f4-ab59-6f0c99d4d899) ![image](https://github.com/user-attachments/assets/1ae36af0-5d1a-4519-844a-563074646ddf) Serverless: ![image](https://github.com/user-attachments/assets/345280df-0e70-4203-b0d8-48ad11753f74) ![image](https://github.com/user-attachments/assets/7425c886-4528-4987-a00a-48bdc71728c7) Old: <img width="1728" alt="image" src="https://github.com/user-attachments/assets/5ef568c6-2d31-47da-8f5f-87dfdf10cb5c"> --------- Co-authored-by: kibanamachine <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
- Loading branch information
1 parent
4873fa1
commit 06cf554
Showing
24 changed files
with
363 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
161 changes: 161 additions & 0 deletions
161
.../packages/shared/kbn-elastic-assistant/impl/assistant_context/assistant_nav_link.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
/* | ||
* 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; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import React from 'react'; | ||
import { render, renderHook } from '@testing-library/react'; | ||
import { AssistantNavLink } from './assistant_nav_link'; | ||
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks'; | ||
import { ChromeNavControl } from '@kbn/core/public'; | ||
import { createHtmlPortalNode, OutPortal } from 'react-reverse-portal'; | ||
import { of } from 'rxjs'; | ||
import { useAssistantContext } from '.'; | ||
|
||
const MockNavigationBar = OutPortal; | ||
|
||
const mockShowAssistantOverlay = jest.fn(); | ||
const mockNavControls = chromeServiceMock.createStartContract().navControls; | ||
const mockGetChromeStyle = jest.fn(); | ||
|
||
const mockAssistantContext = { | ||
chrome: { | ||
getChromeStyle$: mockGetChromeStyle, | ||
navControls: mockNavControls, | ||
}, | ||
showAssistantOverlay: mockShowAssistantOverlay, | ||
assistantAvailability: { | ||
hasAssistantPrivilege: true, | ||
}, | ||
}; | ||
|
||
jest.mock('.', () => { | ||
return { | ||
...jest.requireActual('.'), | ||
useAssistantContext: jest.fn(), | ||
}; | ||
}); | ||
|
||
describe('AssistantNavLink', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
mockGetChromeStyle.mockReturnValue(of('classic')); | ||
(useAssistantContext as jest.Mock).mockReturnValue({ | ||
...mockAssistantContext, | ||
}); | ||
}); | ||
|
||
it('should register link in nav bar', () => { | ||
render(<AssistantNavLink />); | ||
expect(mockNavControls.registerRight).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('button has transparent background in project navigation', () => { | ||
const { result: portalNode } = renderHook(() => | ||
React.useMemo(() => createHtmlPortalNode(), []) | ||
); | ||
|
||
mockGetChromeStyle.mockReturnValue(of('project')); | ||
|
||
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => { | ||
chromeNavControl.mount(portalNode.current.element); | ||
}); | ||
|
||
const { queryByTestId } = render( | ||
<> | ||
<MockNavigationBar node={portalNode.current} /> | ||
<AssistantNavLink /> | ||
</> | ||
); | ||
expect(queryByTestId('assistantNavLink')).not.toHaveStyle( | ||
'background-color: rgb(204, 228, 245)' | ||
); | ||
}); | ||
|
||
it('button has opaque background in classic navigation', () => { | ||
const { result: portalNode } = renderHook(() => | ||
React.useMemo(() => createHtmlPortalNode(), []) | ||
); | ||
|
||
mockGetChromeStyle.mockReturnValue(of('classic')); | ||
|
||
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => { | ||
chromeNavControl.mount(portalNode.current.element); | ||
}); | ||
|
||
const { queryByTestId } = render( | ||
<> | ||
<MockNavigationBar node={portalNode.current} /> | ||
<AssistantNavLink /> | ||
</> | ||
); | ||
expect(queryByTestId('assistantNavLink')).toHaveStyle('background-color: rgb(204, 228, 245)'); | ||
}); | ||
|
||
it('should render the header link text', () => { | ||
const { result: portalNode } = renderHook(() => | ||
React.useMemo(() => createHtmlPortalNode(), []) | ||
); | ||
|
||
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => { | ||
chromeNavControl.mount(portalNode.current.element); | ||
}); | ||
|
||
const { queryByText, queryByTestId } = render( | ||
<> | ||
<MockNavigationBar node={portalNode.current} /> | ||
<AssistantNavLink /> | ||
</> | ||
); | ||
expect(queryByTestId('assistantNavLink')).toBeInTheDocument(); | ||
expect(queryByText('AI Assistant')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should not render the header link if not authorized', () => { | ||
const { result: portalNode } = renderHook(() => | ||
React.useMemo(() => createHtmlPortalNode(), []) | ||
); | ||
|
||
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => { | ||
chromeNavControl.mount(portalNode.current.element); | ||
}); | ||
|
||
(useAssistantContext as jest.Mock).mockReturnValue({ | ||
...mockAssistantContext, | ||
assistantAvailability: { | ||
hasAssistantPrivilege: false, | ||
}, | ||
}); | ||
|
||
const { queryByText, queryByTestId } = render( | ||
<> | ||
<MockNavigationBar node={portalNode.current} /> | ||
<AssistantNavLink /> | ||
</> | ||
); | ||
expect(queryByTestId('assistantNavLink')).not.toBeInTheDocument(); | ||
expect(queryByText('AI Assistant')).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('should call the assistant overlay to show on click', () => { | ||
const { result: portalNode } = renderHook(() => | ||
React.useMemo(() => createHtmlPortalNode(), []) | ||
); | ||
|
||
mockNavControls.registerRight.mockImplementation((chromeNavControl: ChromeNavControl) => { | ||
chromeNavControl.mount(portalNode.current.element); | ||
}); | ||
|
||
const { queryByTestId } = render( | ||
<> | ||
<MockNavigationBar node={portalNode.current} /> | ||
<AssistantNavLink /> | ||
</> | ||
); | ||
queryByTestId('assistantNavLink')?.click(); | ||
expect(mockShowAssistantOverlay).toHaveBeenCalledTimes(1); | ||
expect(mockShowAssistantOverlay).toHaveBeenCalledWith({ showOverlay: true }); | ||
}); | ||
}); |
94 changes: 94 additions & 0 deletions
94
...tform/packages/shared/kbn-elastic-assistant/impl/assistant_context/assistant_nav_link.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* 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; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import type { FC } from 'react'; | ||
import React, { useCallback, useEffect, useState } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
import { createHtmlPortalNode, OutPortal, InPortal } from 'react-reverse-portal'; | ||
import { EuiToolTip, EuiButton, EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { ChromeStyle } from '@kbn/core-chrome-browser'; | ||
import { AssistantIcon } from '@kbn/ai-assistant-icon'; | ||
import { useAssistantContext } from '.'; | ||
|
||
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; | ||
|
||
const TOOLTIP_CONTENT = i18n.translate( | ||
'xpack.elasticAssistant.assistantContext.assistantNavLinkShortcutTooltip', | ||
{ | ||
values: { keyboardShortcut: isMac ? '⌘ ;' : 'Ctrl ;' }, | ||
defaultMessage: 'Keyboard shortcut {keyboardShortcut}', | ||
} | ||
); | ||
const LINK_LABEL = i18n.translate('xpack.elasticAssistant.assistantContext.assistantNavLink', { | ||
defaultMessage: 'AI Assistant', | ||
}); | ||
|
||
export const AssistantNavLink: FC = () => { | ||
const { chrome, showAssistantOverlay, assistantAvailability, currentAppId } = | ||
useAssistantContext(); | ||
const portalNode = React.useMemo(() => createHtmlPortalNode(), []); | ||
const [chromeStyle, setChromeStyle] = useState<ChromeStyle | undefined>(undefined); | ||
|
||
// useObserverable would change the order of re-renders that are tested against closely. | ||
useEffect(() => { | ||
const s = chrome.getChromeStyle$().subscribe(setChromeStyle); | ||
return () => s.unsubscribe(); | ||
}, [chrome]); | ||
|
||
useEffect(() => { | ||
const registerPortalNode = () => { | ||
chrome.navControls.registerRight({ | ||
mount: (element: HTMLElement) => { | ||
ReactDOM.render(<OutPortal node={portalNode} />, element); | ||
return () => ReactDOM.unmountComponentAtNode(element); | ||
}, | ||
// right before the user profile | ||
order: 1001, | ||
}); | ||
}; | ||
|
||
if ( | ||
assistantAvailability.hasAssistantPrivilege && | ||
chromeStyle && | ||
currentAppId !== 'management' | ||
) { | ||
registerPortalNode(); | ||
} | ||
}, [chrome, portalNode, assistantAvailability.hasAssistantPrivilege, chromeStyle, currentAppId]); | ||
|
||
const showOverlay = useCallback( | ||
() => showAssistantOverlay({ showOverlay: true }), | ||
[showAssistantOverlay] | ||
); | ||
|
||
if (!assistantAvailability.hasAssistantPrivilege || !chromeStyle) { | ||
return null; | ||
} | ||
|
||
const EuiButtonBasicOrEmpty = chromeStyle === 'project' ? EuiButtonEmpty : EuiButton; | ||
|
||
return ( | ||
<InPortal node={portalNode}> | ||
<EuiToolTip content={TOOLTIP_CONTENT}> | ||
<EuiButtonBasicOrEmpty | ||
onClick={showOverlay} | ||
color="primary" | ||
size="s" | ||
data-test-subj="assistantNavLink" | ||
> | ||
<EuiFlexGroup gutterSize="s" alignItems="center"> | ||
<EuiFlexItem grow={false}> | ||
<AssistantIcon size="m" /> | ||
</EuiFlexItem> | ||
<EuiFlexItem grow={false}>{LINK_LABEL}</EuiFlexItem> | ||
</EuiFlexGroup> | ||
</EuiButtonBasicOrEmpty> | ||
</EuiToolTip> | ||
</InPortal> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.