mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Discover] Implement Discover customization framework (#158603)
## Summary
This PR includes the initial implementation of the Discover
customization framework based on the `2023-04 Discover Customizations`
RFC.

Notes:
- I've included two initial extension points in this PR: `top_nav` and
`search_bar`. To my knowledge, these are the ones o11y want to start
with, but we don't yet have product alignment on these decisions. ~~I've
left them in for now for testing purposes, but I'll need to update this
PR before merging to either add tests for these extensions points if we
decide to include them, or remove them if we decide not to include
them.~~ Tests have now been added for these customizations.
- I'm planning to open a separate PR with documentation about the
framework once this is merged, but merging this first will unblock o11y.
- In order to enable customization profiles, Discover has been updated
to user locators for all of its navigation, which will allow the current
profile to be maintained when navigating between routes. This is because
the current customization profile is stored in the URL path as
`/p/{profile_name}/{discover_route}`.
Resolves #158625.
### Checklist
- [ ] ~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)~
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~
- [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] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] ~Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))~
- [ ] ~If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~
- [ ] ~This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))~
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
### For maintainers
- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4172236eb2
commit
b78c798971
67 changed files with 2245 additions and 204 deletions
11
examples/discover_customization_examples/kibana.jsonc
Normal file
11
examples/discover_customization_examples/kibana.jsonc
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/discover-customization-examples-plugin",
|
||||
"owner": "@elastic/kibana-data-discovery",
|
||||
"plugin": {
|
||||
"id": "discoverCustomizationExamples",
|
||||
"server": false,
|
||||
"browser": true,
|
||||
"requiredPlugins": ["developerExamples", "discover"]
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 331 KiB |
13
examples/discover_customization_examples/public/index.ts
Normal file
13
examples/discover_customization_examples/public/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { DiscoverCustomizationExamplesPlugin } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new DiscoverCustomizationExamplesPlugin();
|
||||
}
|
236
examples/discover_customization_examples/public/plugin.tsx
Normal file
236
examples/discover_customization_examples/public/plugin.tsx
Normal file
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiContextMenu,
|
||||
EuiFlexItem,
|
||||
EuiPopover,
|
||||
EuiWrappingPopover,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AppNavLinkStatus,
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
Plugin,
|
||||
SimpleSavedObject,
|
||||
} from '@kbn/core/public';
|
||||
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
|
||||
import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import { noop } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import image from './discover_customization_examples.png';
|
||||
|
||||
export interface DiscoverCustomizationExamplesSetupPlugins {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
discover: DiscoverSetup;
|
||||
}
|
||||
|
||||
export interface DiscoverCustomizationExamplesStartPlugins {
|
||||
discover: DiscoverStart;
|
||||
}
|
||||
|
||||
const PLUGIN_ID = 'discoverCustomizationExamples';
|
||||
const PLUGIN_NAME = 'Discover Customizations';
|
||||
|
||||
export class DiscoverCustomizationExamplesPlugin implements Plugin {
|
||||
setup(core: CoreSetup, plugins: DiscoverCustomizationExamplesSetupPlugins) {
|
||||
core.application.register({
|
||||
id: PLUGIN_ID,
|
||||
title: PLUGIN_NAME,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
mount() {
|
||||
plugins.discover?.locator?.navigate(
|
||||
{ profile: 'customization-examples' },
|
||||
{ replace: true }
|
||||
);
|
||||
return noop;
|
||||
},
|
||||
});
|
||||
|
||||
plugins.developerExamples.register({
|
||||
appId: PLUGIN_ID,
|
||||
title: PLUGIN_NAME,
|
||||
description: 'Example plugin that uses the Discover customization framework.',
|
||||
image,
|
||||
});
|
||||
}
|
||||
|
||||
start(core: CoreStart, plugins: DiscoverCustomizationExamplesStartPlugins) {
|
||||
const { discover } = plugins;
|
||||
|
||||
let isOptionsOpen = false;
|
||||
const optionsContainer = document.createElement('div');
|
||||
const closeOptionsPopover = () => {
|
||||
ReactDOM.unmountComponentAtNode(optionsContainer);
|
||||
document.body.removeChild(optionsContainer);
|
||||
isOptionsOpen = false;
|
||||
};
|
||||
|
||||
discover.customize('customization-examples', async ({ customizations, stateContainer }) => {
|
||||
customizations.set({
|
||||
id: 'top_nav',
|
||||
defaultMenu: {
|
||||
newItem: { disabled: true },
|
||||
openItem: { disabled: true },
|
||||
shareItem: { order: 200 },
|
||||
alertsItem: { disabled: true },
|
||||
inspectItem: { disabled: true },
|
||||
saveItem: { order: 400 },
|
||||
},
|
||||
getMenuItems: () => [
|
||||
{
|
||||
data: {
|
||||
id: 'options',
|
||||
label: 'Options',
|
||||
iconType: 'arrowDown',
|
||||
iconSide: 'right',
|
||||
testId: 'customOptionsButton',
|
||||
run: (anchorElement: HTMLElement) => {
|
||||
if (isOptionsOpen) {
|
||||
closeOptionsPopover();
|
||||
return;
|
||||
}
|
||||
|
||||
isOptionsOpen = true;
|
||||
document.body.appendChild(optionsContainer);
|
||||
|
||||
const element = (
|
||||
<EuiWrappingPopover
|
||||
ownFocus
|
||||
button={anchorElement}
|
||||
isOpen={true}
|
||||
panelPaddingSize="s"
|
||||
closePopover={closeOptionsPopover}
|
||||
>
|
||||
<EuiContextMenu
|
||||
size="s"
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
name: 'Create new',
|
||||
icon: 'plusInCircle',
|
||||
onClick: () => alert('Create new clicked'),
|
||||
},
|
||||
{
|
||||
name: 'Make a copy',
|
||||
icon: 'copy',
|
||||
onClick: () => alert('Make a copy clicked'),
|
||||
},
|
||||
{
|
||||
name: 'Manage saved searches',
|
||||
icon: 'gear',
|
||||
onClick: () => alert('Manage saved searches clicked'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
data-test-subj="customOptionsPopover"
|
||||
/>
|
||||
</EuiWrappingPopover>
|
||||
);
|
||||
|
||||
ReactDOM.render(element, optionsContainer);
|
||||
},
|
||||
},
|
||||
order: 100,
|
||||
},
|
||||
{
|
||||
data: {
|
||||
id: 'documentExplorer',
|
||||
label: 'Document explorer',
|
||||
iconType: 'discoverApp',
|
||||
testId: 'documentExplorerButton',
|
||||
run: () => {
|
||||
discover.locator?.navigate({});
|
||||
},
|
||||
},
|
||||
order: 300,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
customizations.set({
|
||||
id: 'search_bar',
|
||||
CustomDataViewPicker: () => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const togglePopover = () => setIsPopoverOpen((open) => !open);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
const [savedSearches, setSavedSearches] = useState<
|
||||
Array<SimpleSavedObject<{ title: string }>>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
core.savedObjects.client
|
||||
.find<{ title: string }>({ type: 'search' })
|
||||
.then((response) => {
|
||||
setSavedSearches(response.savedObjects);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const currentSavedSearch = useObservable(
|
||||
stateContainer.savedSearchState.getCurrent$(),
|
||||
stateContainer.savedSearchState.getState()
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButton
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
fullWidth
|
||||
onClick={togglePopover}
|
||||
data-test-subj="logsViewSelectorButton"
|
||||
>
|
||||
{currentSavedSearch.title ?? 'None selected'}
|
||||
</EuiButton>
|
||||
}
|
||||
anchorClassName="eui-fullWidth"
|
||||
isOpen={isPopoverOpen}
|
||||
panelPaddingSize="none"
|
||||
closePopover={closePopover}
|
||||
>
|
||||
<EuiContextMenu
|
||||
size="s"
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
title: 'Saved logs views',
|
||||
items: savedSearches.map((savedSearch) => ({
|
||||
name: savedSearch.get('title'),
|
||||
onClick: () => stateContainer.actions.onOpenSavedSearch(savedSearch.id),
|
||||
icon: savedSearch.id === currentSavedSearch.id ? 'check' : 'empty',
|
||||
'data-test-subj': `logsViewSelectorOption-${savedSearch.attributes.title.replace(
|
||||
/[^a-zA-Z0-9]/g,
|
||||
''
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Cleaning up Logs explorer customizations');
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
13
examples/discover_customization_examples/tsconfig.json
Normal file
13
examples/discover_customization_examples/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*", "../../typings/**/*"],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue