/*
* 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 { AwaitingControlGroupAPI, ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { css } from '@emotion/react';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { ControlsPanels } from '@kbn/controls-plugin/common';
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.registerCustomizationProfile('customization-examples', {
customize: 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 = (
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"
/>
);
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>
>([]);
useEffect(() => {
core.savedObjects.client
.find<{ title: string }>({ type: 'search' })
.then((response) => {
setSavedSearches(response.savedObjects);
});
}, []);
const currentSavedSearch = useObservable(
stateContainer.savedSearchState.getCurrent$(),
stateContainer.savedSearchState.getState()
);
return (
{currentSavedSearch.title ?? 'None selected'}
}
anchorClassName="eui-fullWidth"
isOpen={isPopoverOpen}
panelPaddingSize="none"
closePopover={closePopover}
>
({
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,
''
)}`,
})),
},
]}
/>
);
},
});
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>
>([]);
useEffect(() => {
core.savedObjects.client
.find<{ title: string }>({ type: 'search' })
.then((response) => {
setSavedSearches(response.savedObjects);
});
}, []);
const currentSavedSearch = useObservable(
stateContainer.savedSearchState.getCurrent$(),
stateContainer.savedSearchState.getState()
);
return (
{currentSavedSearch.title ?? 'None selected'}
}
anchorClassName="eui-fullWidth"
isOpen={isPopoverOpen}
panelPaddingSize="none"
closePopover={closePopover}
>
({
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,
''
)}`,
})),
},
]}
/>
);
},
PrependFilterBar: () => {
const [controlGroupAPI, setControlGroupAPI] = useState();
const stateStorage = stateContainer.stateStorage;
const dataView = useObservable(
stateContainer.internalState.state$,
stateContainer.internalState.getState()
).dataView;
useEffect(() => {
if (!controlGroupAPI) {
return;
}
const stateSubscription = stateStorage
.change$('controlPanels')
.subscribe((panels) =>
controlGroupAPI.updateInput({ panels: panels ?? undefined })
);
const inputSubscription = controlGroupAPI.getInput$().subscribe((input) => {
if (input && input.panels) stateStorage.set('controlPanels', input.panels);
});
const filterSubscription = controlGroupAPI.onFiltersPublished$.subscribe(
(newFilters) => {
stateContainer.internalState.transitions.setCustomFilters(newFilters);
stateContainer.actions.fetchData();
}
);
return () => {
stateSubscription.unsubscribe();
inputSubscription.unsubscribe();
filterSubscription.unsubscribe();
};
}, [controlGroupAPI, stateStorage]);
const fieldToFilterOn = dataView?.fields.filter((field) =>
field.esTypes?.includes('keyword')
)[0];
if (!fieldToFilterOn) {
return null;
}
return (
{
const panels = stateStorage.get('controlPanels');
if (!panels) {
await builder.addOptionsListControl(initialInput, {
dataViewId: dataView?.id!,
title: fieldToFilterOn.name.split('.')[0],
fieldName: fieldToFilterOn.name,
grow: false,
width: 'small',
});
}
return {
initialInput: {
...initialInput,
panels: panels ?? initialInput.panels,
viewMode: ViewMode.VIEW,
filters: stateContainer.appState.get().filters ?? [],
},
};
}}
/>
);
},
});
return () => {
// eslint-disable-next-line no-console
console.log('Cleaning up Logs explorer customizations');
};
},
});
}
}