diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx index b897c1c73b89b..91f4a5e6be471 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.test.tsx @@ -18,7 +18,11 @@ import { } from '../../../../core/public'; import { FieldSetting } from './types'; import { AdvancedSettings } from './advanced_settings'; -import { notificationServiceMock, docLinksServiceMock } from '../../../../core/public/mocks'; +import { + notificationServiceMock, + docLinksServiceMock, + themeServiceMock, +} from '../../../../core/public/mocks'; import { ComponentRegistry } from '../component_registry'; import { Search } from './components/search'; @@ -251,6 +255,7 @@ describe('AdvancedSettings', () => { dockLinks={docLinksServiceMock.createStartContract().links} uiSettings={mockConfig().core.uiSettings} componentRegistry={new ComponentRegistry().start} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -273,6 +278,7 @@ describe('AdvancedSettings', () => { dockLinks={docLinksServiceMock.createStartContract().links} uiSettings={mockConfig().core.uiSettings} componentRegistry={new ComponentRegistry().start} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -299,6 +305,7 @@ describe('AdvancedSettings', () => { dockLinks={docLinksServiceMock.createStartContract().links} uiSettings={mockConfig().core.uiSettings} componentRegistry={new ComponentRegistry().start} + theme={themeServiceMock.createStartContract().theme$} /> ); diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index 1391312df5231..c0decf516fbad 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -19,6 +19,7 @@ import { DocLinksStart, ToastsStart, ScopedHistory, + ThemeServiceStart, } from '../../../../core/public'; import { url } from '../../../kibana_utils/public'; @@ -41,6 +42,7 @@ interface AdvancedSettingsProps { uiSettings: IUiSettingsClient; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + theme: ThemeServiceStart['theme$']; componentRegistry: ComponentRegistry['start']; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } @@ -270,6 +272,7 @@ export class AdvancedSettings extends Component { enableSaving={true} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -146,6 +148,7 @@ describe('Form', () => { enableSaving={false} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -165,6 +168,7 @@ describe('Form', () => { enableSaving={true} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -184,6 +188,7 @@ describe('Form', () => { enableSaving={true} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -203,6 +208,7 @@ describe('Form', () => { enableSaving={false} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); (wrapper.instance() as Form).setState({ @@ -233,6 +239,7 @@ describe('Form', () => { enableSaving={false} toasts={toasts} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); (wrapper.instance() as Form).setState({ @@ -269,6 +276,7 @@ describe('Form', () => { enableSaving={false} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); @@ -297,6 +305,7 @@ describe('Form', () => { enableSaving={false} toasts={{} as any} dockLinks={{} as any} + theme={themeServiceMock.createStartContract().theme$} /> ); diff --git a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx index 2356cc7690c7c..44e2fc7e90e78 100644 --- a/src/plugins/advanced_settings/public/management_app/components/form/form.tsx +++ b/src/plugins/advanced_settings/public/management_app/components/form/form.tsx @@ -26,8 +26,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; -import { toMountPoint } from '../../../../../kibana_react/public'; -import { DocLinksStart, ToastsStart } from '../../../../../../core/public'; +import { KibanaThemeProvider, toMountPoint } from '../../../../../kibana_react/public'; +import { DocLinksStart, ThemeServiceStart, ToastsStart } from '../../../../../../core/public'; import { getCategoryName } from '../../lib'; import { Field, getEditableValue } from '../field'; @@ -46,6 +46,7 @@ interface FormProps { enableSaving: boolean; dockLinks: DocLinksStart['links']; toasts: ToastsStart; + theme: ThemeServiceStart['theme$']; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; queryText?: string; } @@ -191,7 +192,7 @@ export class Form extends PureComponent { defaultMessage: 'One or more settings require you to reload the page to take effect.', }), text: toMountPoint( - <> + window.location.reload()}> @@ -201,7 +202,7 @@ export class Form extends PureComponent { - + ), color: 'success', }); diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index 3a4ec83e28963..423296dfb7b84 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -14,6 +14,7 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { LocationDescriptor } from 'history'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { url } from '../../../kibana_utils/public'; import { ManagementAppMountParams } from '../../../management/public'; import { UsageCollectionSetup } from '../../../usage_collection/public'; @@ -70,25 +71,28 @@ export async function mountManagementSection( chrome.docTitle.change(title); ReactDOM.render( - - - - {/* TODO: remove route param (`query`) in 7.13 */} - {(props) => } - - - - - - , + + + + + {/* TODO: remove route param (`query`) in 7.13 */} + {(props) => } + + + + + + + , params.element ); return () => { diff --git a/src/plugins/chart_expressions/expression_heatmap/kibana.json b/src/plugins/chart_expressions/expression_heatmap/kibana.json index 23e0ad9e904c9..604919ec54592 100755 --- a/src/plugins/chart_expressions/expression_heatmap/kibana.json +++ b/src/plugins/chart_expressions/expression_heatmap/kibana.json @@ -10,7 +10,7 @@ "server": true, "ui": true, "requiredPlugins": ["expressions", "fieldFormats", "charts", "visualizations", "presentationUtil", "data"], - "requiredBundles": ["kibanaUtils"], + "requiredBundles": ["kibanaUtils", "kibanaReact"], "optionalPlugins": [], "extraPublicDirs": ["common"] } diff --git a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx index 5671386145d4b..efdb6aee7782d 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx +++ b/src/plugins/chart_expressions/expression_heatmap/public/expression_renderers/heatmap_renderer.tsx @@ -9,6 +9,8 @@ import { i18n } from '@kbn/i18n'; import React, { memo } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import type { PersistedState } from '../../../../visualizations/public'; +import { ThemeServiceStart } from '../../../../../core/public'; +import { KibanaThemeProvider } from '../../../../kibana_react/public'; import { ExpressionRenderDefinition } from '../../../../expressions/common/expression_renderers'; import { EXPRESSION_HEATMAP_NAME, @@ -23,7 +25,13 @@ import HeatmapComponent from '../components/heatmap_component'; import './index.scss'; const MemoizedChart = memo(HeatmapComponent); -export const heatmapRenderer = (): ExpressionRenderDefinition => ({ +interface ExpressioHeatmapRendererDependencies { + theme: ThemeServiceStart; +} + +export const heatmapRenderer: ( + deps: ExpressioHeatmapRendererDependencies +) => ExpressionRenderDefinition = ({ theme }) => ({ name: EXPRESSION_HEATMAP_NAME, displayName: i18n.translate('expressionHeatmap.visualizationName', { defaultMessage: 'Heatmap', @@ -42,18 +50,20 @@ export const heatmapRenderer = (): ExpressionRenderDefinition - - , + +
+ +
+
, domNode, () => { handlers.done(); diff --git a/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts b/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts index 87e7984ed1951..cabb938f6f6b4 100644 --- a/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_heatmap/public/plugin.ts @@ -35,7 +35,7 @@ export class ExpressionHeatmapPlugin { expressions.registerFunction(heatmapFunction); expressions.registerFunction(heatmapLegendConfig); expressions.registerFunction(heatmapGridConfig); - expressions.registerRenderer(heatmapRenderer()); + expressions.registerRenderer(heatmapRenderer({ theme: core.theme })); } public start(core: CoreStart, { fieldFormats }: ExpressionHeatmapPluginStart) { diff --git a/src/plugins/chart_expressions/expression_metric/kibana.json b/src/plugins/chart_expressions/expression_metric/kibana.json index c662dc1310323..dec818b1f17df 100755 --- a/src/plugins/chart_expressions/expression_metric/kibana.json +++ b/src/plugins/chart_expressions/expression_metric/kibana.json @@ -10,6 +10,6 @@ "server": true, "ui": true, "requiredPlugins": ["expressions", "fieldFormats", "charts", "visualizations", "presentationUtil"], - "requiredBundles": ["kibanaUtils"], + "requiredBundles": ["kibanaUtils", "kibanaReact"], "optionalPlugins": [] } diff --git a/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx b/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx index 748ef15a6c9c9..18f97f23538c2 100644 --- a/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/__stories__/metric_renderer.stories.tsx @@ -8,11 +8,12 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; +import { from } from 'rxjs'; import { ExpressionValueVisDimension } from '../../../../visualizations/common'; import { Datatable, DatatableColumn } from '../../../../expressions'; import { Render } from '../../../../presentation_util/public/__stories__'; import { ColorMode, CustomPaletteState } from '../../../../charts/common'; -import { metricVisRenderer } from '../expression_renderers'; +import { getMetricVisRenderer } from '../expression_renderers'; import { MetricStyle, MetricVisRenderConfig, visType } from '../../common/types'; const palette: CustomPaletteState = { @@ -117,6 +118,8 @@ const containerSize = { height: '700px', }; +const metricVisRenderer = getMetricVisRenderer({ theme$: from([{ darkMode: false }]) }); + storiesOf('renderers/visMetric', module) .add('Default', () => { return ; diff --git a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/index.ts b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/index.ts index b4fb6cea84aa3..98a987db2fd43 100644 --- a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/index.ts +++ b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { metricVisRenderer } from './metric_vis_renderer'; +export { getMetricVisRenderer } from './metric_vis_renderer'; diff --git a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx index 6c3c7696fca40..f6cc73c366e4e 100644 --- a/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx +++ b/src/plugins/chart_expressions/expression_metric/public/expression_renderers/metric_vis_renderer.tsx @@ -9,6 +9,8 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../kibana_react/public'; import { VisualizationContainer } from '../../../../visualizations/public'; import { ExpressionRenderDefinition } from '../../../../expressions/common/expression_renderers'; import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig } from '../../common'; @@ -16,29 +18,35 @@ import { EXPRESSION_METRIC_NAME, MetricVisRenderConfig } from '../../common'; // @ts-ignore const MetricVisComponent = lazy(() => import('../components/metric_component')); -export const metricVisRenderer: () => ExpressionRenderDefinition = () => ({ - name: EXPRESSION_METRIC_NAME, - displayName: 'metric visualization', - reuseDomNode: true, - render: async (domNode, { visData, visConfig }, handlers) => { - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); +export const getMetricVisRenderer = ( + theme: ThemeServiceStart +): (() => ExpressionRenderDefinition) => { + return () => ({ + name: EXPRESSION_METRIC_NAME, + displayName: 'metric visualization', + reuseDomNode: true, + render: async (domNode, { visData, visConfig }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - render( - - - , - domNode - ); - }, -}); + render( + + + + + , + domNode + ); + }, + }); +}; diff --git a/src/plugins/chart_expressions/expression_metric/public/plugin.ts b/src/plugins/chart_expressions/expression_metric/public/plugin.ts index 6053cba597b4b..f941b4a9e2f22 100644 --- a/src/plugins/chart_expressions/expression_metric/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_metric/public/plugin.ts @@ -11,7 +11,7 @@ import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; import { metricVisFunction } from '../common'; import { setFormatService, setPaletteService } from './services'; -import { metricVisRenderer } from './expression_renderers'; +import { getMetricVisRenderer } from './expression_renderers'; import { FieldFormatsStart } from '../../../field_formats/public'; /** @internal */ @@ -29,7 +29,7 @@ export interface ExpressionMetricPluginStart { export class ExpressionMetricPlugin implements Plugin { public setup(core: CoreSetup, { expressions, charts }: ExpressionMetricPluginSetup) { expressions.registerFunction(metricVisFunction); - expressions.registerRenderer(metricVisRenderer); + expressions.registerRenderer(getMetricVisRenderer(core.theme)); charts.palettes.getPalettes().then((palettes) => { setPaletteService(palettes); }); diff --git a/src/plugins/chart_expressions/expression_tagcloud/kibana.json b/src/plugins/chart_expressions/expression_tagcloud/kibana.json index b1c3c1f020366..37a7e3be843b2 100755 --- a/src/plugins/chart_expressions/expression_tagcloud/kibana.json +++ b/src/plugins/chart_expressions/expression_tagcloud/kibana.json @@ -5,7 +5,7 @@ "server": true, "ui": true, "requiredPlugins": ["expressions", "visualizations", "charts", "presentationUtil", "fieldFormats"], - "requiredBundles": ["kibanaUtils"], + "requiredBundles": ["kibanaUtils", "kibanaReact"], "optionalPlugins": [], "owner": { "name": "Vis Editors", diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/theme.ts b/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/theme.ts new file mode 100644 index 0000000000000..d64fa429d5684 --- /dev/null +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__mocks__/theme.ts @@ -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 { from } from 'rxjs'; + +export const theme = { + theme$: from([{ darkMode: false }]), +}; diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx index 1e0dc2600d1a1..eca35918d7289 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/__stories__/tagcloud_renderer.stories.tsx @@ -12,6 +12,7 @@ import { tagcloudRenderer } from '../expression_renderers'; import { Render } from '../../../../presentation_util/public/__stories__'; import { TagcloudRendererConfig } from '../../common/types'; import { palettes } from '../__mocks__/palettes'; +import { theme } from '../__mocks__/theme'; const config: TagcloudRendererConfig = { visType: 'tagcloud', @@ -66,13 +67,17 @@ const containerSize = { storiesOf('renderers/tag_cloud_vis', module) .add('Default', () => { return ( - tagcloudRenderer({ palettes })} config={config} {...containerSize} /> + tagcloudRenderer({ palettes, theme })} + config={config} + {...containerSize} + /> ); }) .add('With log scale', () => { return ( tagcloudRenderer({ palettes })} + renderer={() => tagcloudRenderer({ palettes, theme })} config={{ ...config, visParams: { ...config.visParams, scale: 'log' } }} {...containerSize} /> @@ -81,7 +86,7 @@ storiesOf('renderers/tag_cloud_vis', module) .add('With square root scale', () => { return ( tagcloudRenderer({ palettes })} + renderer={() => tagcloudRenderer({ palettes, theme })} config={{ ...config, visParams: { ...config.visParams, scale: 'square root' } }} {...containerSize} /> @@ -90,7 +95,7 @@ storiesOf('renderers/tag_cloud_vis', module) .add('With right angled orientation', () => { return ( tagcloudRenderer({ palettes })} + renderer={() => tagcloudRenderer({ palettes, theme })} config={{ ...config, visParams: { ...config.visParams, orientation: 'right angled' } }} {...containerSize} /> @@ -99,7 +104,7 @@ storiesOf('renderers/tag_cloud_vis', module) .add('With multiple orientations', () => { return ( tagcloudRenderer({ palettes })} + renderer={() => tagcloudRenderer({ palettes, theme })} config={{ ...config, visParams: { ...config.visParams, orientation: 'multiple' } }} {...containerSize} /> @@ -108,7 +113,7 @@ storiesOf('renderers/tag_cloud_vis', module) .add('With hidden label', () => { return ( tagcloudRenderer({ palettes })} + renderer={() => tagcloudRenderer({ palettes, theme })} config={{ ...config, visParams: { ...config.visParams, showLabel: false } }} {...containerSize} /> @@ -117,7 +122,7 @@ storiesOf('renderers/tag_cloud_vis', module) .add('With empty results', () => { return ( tagcloudRenderer({ palettes })} + renderer={() => tagcloudRenderer({ palettes, theme })} config={{ ...config, visData: { ...config.visData, rows: [] } }} {...containerSize} /> diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx index 294371b3a5703..4b58c1dfb6c90 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx +++ b/src/plugins/chart_expressions/expression_tagcloud/public/expression_renderers/tagcloud_renderer.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { ClassNames } from '@emotion/react'; import { i18n } from '@kbn/i18n'; +import { KibanaThemeProvider } from '../../../../kibana_react/public'; import { VisualizationContainer } from '../../../../visualizations/public'; import { ExpressionRenderDefinition } from '../../../../expressions/common/expression_renderers'; import { ExpressioTagcloudRendererDependencies } from '../plugin'; @@ -36,7 +37,7 @@ const TagCloudChart = lazy(() => import('../components/tagcloud_component')); export const tagcloudRenderer: ( deps: ExpressioTagcloudRendererDependencies -) => ExpressionRenderDefinition = ({ palettes }) => ({ +) => ExpressionRenderDefinition = ({ palettes, theme }) => ({ name: EXPRESSION_NAME, displayName: strings.getDisplayName(), help: strings.getHelpDescription(), @@ -50,27 +51,29 @@ export const tagcloudRenderer: ( const showNoResult = config.visData.rows.length === 0; render( - - - {({ css, cx }) => ( - - - - )} - - , + + + + {({ css, cx }) => ( + + + + )} + + + , domNode ); }, diff --git a/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts b/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts index 5a8bcc8aa64bb..9385ab3b26772 100644 --- a/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts +++ b/src/plugins/chart_expressions/expression_tagcloud/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CoreSetup, CoreStart, Plugin } from '../../../../core/public'; +import { CoreSetup, CoreStart, Plugin, ThemeServiceStart } from '../../../../core/public'; import { ExpressionsStart, ExpressionsSetup } from '../../../expressions/public'; import { ChartsPluginSetup } from '../../../charts/public'; import { tagcloudRenderer } from './expression_renderers'; @@ -22,6 +22,7 @@ interface SetupDeps { /** @internal */ export interface ExpressioTagcloudRendererDependencies { palettes: ChartsPluginSetup['palettes']; + theme: ThemeServiceStart; } interface StartDeps { @@ -39,6 +40,7 @@ export class ExpressionTagcloudPlugin public setup(core: CoreSetup, { expressions, charts }: SetupDeps): ExpressionTagcloudPluginSetup { const rendererDependencies: ExpressioTagcloudRendererDependencies = { palettes: charts.palettes, + theme: core.theme, }; expressions.registerFunction(tagcloudFunction); expressions.registerRenderer(tagcloudRenderer(rendererDependencies)); diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index 0de5d4a9df120..293190fe56bfd 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -13,6 +13,8 @@ import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; import { IEditorController, EditorRenderProps } from 'src/plugins/visualize/public'; +import { KibanaThemeProvider } from '../../kibana_react/public'; +import { getTheme } from './services'; // @ts-ignore const DefaultEditor = lazy(() => import('./default_editor')); @@ -27,29 +29,31 @@ class DefaultEditorController implements IEditorController { render(props: EditorRenderProps) { render( - - - - - } - > - - - , + + + + + + } + > + + + + , this.el ); } diff --git a/src/plugins/vis_default_editor/public/plugin.ts b/src/plugins/vis_default_editor/public/plugin.ts index fd0d69bf297b6..5e67712e94856 100644 --- a/src/plugins/vis_default_editor/public/plugin.ts +++ b/src/plugins/vis_default_editor/public/plugin.ts @@ -10,6 +10,7 @@ import { CoreSetup, Plugin } from 'kibana/public'; import { VisualizePluginSetup } from '../../visualize/public'; import { DefaultEditorController } from './default_editor_controller'; +import { setTheme } from './services'; export interface VisDefaultEditorSetupDependencies { visualize: VisualizePluginSetup; @@ -19,6 +20,7 @@ export class VisDefaultEditorPlugin implements Plugin { public setup(core: CoreSetup, { visualize }: VisDefaultEditorSetupDependencies) { + setTheme(core.theme); if (visualize) { visualize.visEditorsRegistry.registerDefault(DefaultEditorController); } diff --git a/src/plugins/vis_default_editor/public/services.ts b/src/plugins/vis_default_editor/public/services.ts new file mode 100644 index 0000000000000..d76fbc4542f4c --- /dev/null +++ b/src/plugins/vis_default_editor/public/services.ts @@ -0,0 +1,12 @@ +/* + * 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 { ThemeServiceStart } from 'kibana/public'; +import { createGetterSetter } from '../../kibana_utils/common'; + +export const [getTheme, setTheme] = createGetterSetter('ThemeService'); diff --git a/src/plugins/vis_types/pie/kibana.json b/src/plugins/vis_types/pie/kibana.json index b3a5c1fc8aa07..52e02024ec31c 100644 --- a/src/plugins/vis_types/pie/kibana.json +++ b/src/plugins/vis_types/pie/kibana.json @@ -4,7 +4,7 @@ "ui": true, "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], - "requiredBundles": ["visDefaultEditor"], + "requiredBundles": ["visDefaultEditor", "kibanaReact"], "extraPublicDirs": ["common/index"], "owner": { "name": "Vis Editors", diff --git a/src/plugins/vis_types/pie/public/pie_renderer.tsx b/src/plugins/vis_types/pie/public/pie_renderer.tsx index e8fb6311904a6..1467f76bd43fd 100644 --- a/src/plugins/vis_types/pie/public/pie_renderer.tsx +++ b/src/plugins/vis_types/pie/public/pie_renderer.tsx @@ -12,6 +12,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { ExpressionRenderDefinition } from '../../../expressions/public'; import { VisualizationContainer } from '../../../visualizations/public'; import type { PersistedState } from '../../../visualizations/public'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { VisTypePieDependencies } from './plugin'; import { RenderValue, vislibPieName } from './pie_fn'; @@ -43,19 +44,21 @@ export const getPieVisRenderer: ( render( - - - + + + + + , domNode ); diff --git a/src/plugins/vis_types/pie/public/plugin.ts b/src/plugins/vis_types/pie/public/plugin.ts index 12be6dd5de10f..5de26975896ee 100644 --- a/src/plugins/vis_types/pie/public/plugin.ts +++ b/src/plugins/vis_types/pie/public/plugin.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CoreSetup, DocLinksStart } from 'src/core/public'; +import { CoreSetup, DocLinksStart, ThemeServiceStart } from 'src/core/public'; import { VisualizationsSetup } from '../../../visualizations/public'; import { Plugin as ExpressionsPublicPlugin } from '../../../expressions/public'; import { ChartsPluginSetup } from '../../../charts/public'; @@ -35,7 +35,11 @@ export interface VisTypePiePluginStartDependencies { export interface VisTypePieDependencies { theme: ChartsPluginSetup['theme']; palettes: ChartsPluginSetup['palettes']; - getStartDeps: () => Promise<{ data: DataPublicPluginStart; docLinks: DocLinksStart }>; + getStartDeps: () => Promise<{ + data: DataPublicPluginStart; + docLinks: DocLinksStart; + kibanaTheme: ThemeServiceStart; + }>; } export class VisTypePiePlugin { @@ -49,6 +53,7 @@ export class VisTypePiePlugin { return { data: deps.data, docLinks: coreStart.docLinks, + kibanaTheme: coreStart.theme, }; }; const trackUiMetric = usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_pie'); diff --git a/src/plugins/vis_types/table/public/table_vis_renderer.tsx b/src/plugins/vis_types/table/public/table_vis_renderer.tsx index e9f2002b71062..09c6c57150e62 100644 --- a/src/plugins/vis_types/table/public/table_vis_renderer.tsx +++ b/src/plugins/vis_types/table/public/table_vis_renderer.tsx @@ -13,6 +13,7 @@ import { CoreStart } from 'kibana/public'; import { VisualizationContainer } from '../../../visualizations/public'; import { ExpressionRenderDefinition } from '../../../expressions/common/expression_renderers'; import { TableVisRenderValue } from './table_vis_fn'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; const TableVisualizationComponent = lazy(() => import('./components/table_visualization')); @@ -30,18 +31,20 @@ export const getTableVisRenderer: ( visData.table?.rows.length === 0 || (!visData.table && visData.tables.length === 0); render( - - + - , + showNoResult={showNoResult} + > + + + , domNode ); }, diff --git a/src/plugins/vis_types/timelion/public/plugin.ts b/src/plugins/vis_types/timelion/public/plugin.ts index d37e15a9938b8..fb2b1df6f522e 100644 --- a/src/plugins/vis_types/timelion/public/plugin.ts +++ b/src/plugins/vis_types/timelion/public/plugin.ts @@ -13,6 +13,7 @@ import type { PluginInitializerContext, IUiSettingsClient, HttpSetup, + ThemeServiceStart, } from 'kibana/public'; import type { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public'; import type { @@ -37,6 +38,7 @@ export interface TimelionVisDependencies extends Partial { uiSettings: IUiSettingsClient; http: HttpSetup; timefilter: TimefilterContract; + theme: ThemeServiceStart; } /** @internal */ @@ -71,13 +73,14 @@ export class TimelionVisPlugin constructor(public initializerContext: PluginInitializerContext) {} public setup( - { uiSettings, http }: CoreSetup, + { uiSettings, http, theme }: CoreSetup, { expressions, visualizations, data, charts }: TimelionVisSetupDependencies ) { const dependencies: TimelionVisDependencies = { http, uiSettings, timefilter: data.query.timefilter.timefilter, + theme, }; expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx index 633f15a9824ea..1ee88e06bad8b 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx @@ -10,7 +10,7 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { KibanaContextProvider } from '../../../kibana_react/public'; +import { KibanaContextProvider, KibanaThemeProvider } from '../../../kibana_react/public'; import { VisualizationContainer } from '../../../visualizations/public'; import { TimelionVisDependencies } from './plugin'; import { TimelionRenderValue } from './timelion_vis_fn'; @@ -58,14 +58,16 @@ export const getTimelionVisRenderer: ( render( - - - + + + + + , domNode ); diff --git a/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx index 2f5ee2b8a631d..ffe1c90132360 100644 --- a/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx +++ b/src/plugins/vis_types/timeseries/public/application/editor_controller.tsx @@ -12,9 +12,10 @@ import type { EventEmitter } from 'events'; import type { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; import type { IEditorController, EditorRenderProps } from 'src/plugins/visualize/public'; -import { getUISettings, getI18n } from '../services'; +import { getUISettings, getI18n, getCoreStart } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; import type { TimeseriesVisParams } from '../types'; +import { KibanaThemeProvider } from '../../../../../../src/plugins/kibana_react/public'; export const TSVB_EDITOR_NAME = 'tsvbEditor'; @@ -31,16 +32,18 @@ export class EditorController implements IEditorController { render( - + + + , this.el ); diff --git a/src/plugins/vis_types/timeseries/public/plugin.ts b/src/plugins/vis_types/timeseries/public/plugin.ts index d6d83caa6eb0f..3d75c914826ce 100644 --- a/src/plugins/vis_types/timeseries/public/plugin.ts +++ b/src/plugins/vis_types/timeseries/public/plugin.ts @@ -56,6 +56,7 @@ export class MetricsPlugin implements Plugin { expressions.registerRenderer( getTimeseriesVisRenderer({ uiSettings: core.uiSettings, + theme: core.theme, }) ); setUISettings(core.uiSettings); diff --git a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx index 9edc05893e24f..93d3fd08b3efe 100644 --- a/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_types/timeseries/public/timeseries_vis_renderer.tsx @@ -11,7 +11,7 @@ import { get } from 'lodash'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, ThemeServiceStart } from 'kibana/public'; import { VisualizationContainer, PersistedState } from '../../../visualizations/public'; @@ -21,6 +21,7 @@ import { isVisTableData } from '../common/vis_data_utils'; import type { TimeseriesVisParams } from './types'; import type { ExpressionRenderDefinition } from '../../../expressions/common'; import type { TimeseriesRenderValue } from './metrics_fn'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; const TimeseriesVisualization = lazy( () => import('./application/components/timeseries_visualization') @@ -37,7 +38,8 @@ const checkIfDataExists = (visData: TimeseriesVisData | {}, model: TimeseriesVis export const getTimeseriesVisRenderer: (deps: { uiSettings: IUiSettingsClient; -}) => ExpressionRenderDefinition = ({ uiSettings }) => ({ + theme: ThemeServiceStart; +}) => ExpressionRenderDefinition = ({ uiSettings, theme }) => ({ name: 'timeseries_vis', reuseDomNode: true, render: async (domNode, config, handlers) => { @@ -54,22 +56,24 @@ export const getTimeseriesVisRenderer: (deps: { render( - - + - + showNoResult={showNoResult} + error={get(visData, [model.id, 'error'])} + > + + + , domNode, () => { diff --git a/src/plugins/vis_types/vega/public/vega_vis_renderer.tsx b/src/plugins/vis_types/vega/public/vega_vis_renderer.tsx index 77af6dfdcf042..e6a39f5a18e83 100644 --- a/src/plugins/vis_types/vega/public/vega_vis_renderer.tsx +++ b/src/plugins/vis_types/vega/public/vega_vis_renderer.tsx @@ -10,6 +10,7 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { VisualizationContainer } from '../../../visualizations/public'; import { VegaVisualizationDependencies } from './plugin'; import { RenderValue } from './vega_fn'; @@ -26,14 +27,16 @@ export const getVegaVisRenderer: ( }); render( - - - , + + + + + , domNode ); }, diff --git a/src/plugins/vis_types/vislib/kibana.json b/src/plugins/vis_types/vislib/kibana.json index ff81f6eed7b48..feb252f1bb0f5 100644 --- a/src/plugins/vis_types/vislib/kibana.json +++ b/src/plugins/vis_types/vislib/kibana.json @@ -4,7 +4,7 @@ "server": true, "ui": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations"], - "requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy", "visTypePie", "visTypeHeatmap", "fieldFormats"], + "requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy", "visTypePie", "visTypeHeatmap", "fieldFormats", "kibanaReact"], "owner": { "name": "Vis Editors", "githubTeam": "kibana-vis-editors" diff --git a/src/plugins/vis_types/vislib/public/plugin.ts b/src/plugins/vis_types/vislib/public/plugin.ts index 5cebaee60b64c..8c54df99bb988 100644 --- a/src/plugins/vis_types/vislib/public/plugin.ts +++ b/src/plugins/vis_types/vislib/public/plugin.ts @@ -19,7 +19,7 @@ import { heatmapVisTypeDefinition } from './heatmap'; import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn'; import { createPieVisFn } from './pie_fn'; import { visLibVisTypeDefinitions, pieVisTypeDefinition } from './vis_type_vislib_vis_types'; -import { setFormatService, setDataActions } from './services'; +import { setFormatService, setDataActions, setTheme } from './services'; import { getVislibVisRenderer } from './vis_renderer'; /** @internal */ @@ -67,5 +67,6 @@ export class VisTypeVislibPlugin public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { setFormatService(data.fieldFormats); setDataActions(data.actions); + setTheme(core.theme); } } diff --git a/src/plugins/vis_types/vislib/public/services.ts b/src/plugins/vis_types/vislib/public/services.ts index 21fb77a4d41ca..7afe16867bab4 100644 --- a/src/plugins/vis_types/vislib/public/services.ts +++ b/src/plugins/vis_types/vislib/public/services.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ThemeServiceStart } from 'kibana/public'; import { createGetterSetter } from '../../../kibana_utils/public'; import { DataPublicPluginStart } from '../../../data/public'; @@ -15,3 +16,5 @@ export const [getDataActions, setDataActions] = export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('vislib data.fieldFormats'); + +export const [getTheme, setTheme] = createGetterSetter('vislib theme service'); diff --git a/src/plugins/vis_types/vislib/public/vis_renderer.tsx b/src/plugins/vis_types/vislib/public/vis_renderer.tsx index 04c4c3cedc9d2..906c8efb69cf2 100644 --- a/src/plugins/vis_types/vislib/public/vis_renderer.tsx +++ b/src/plugins/vis_types/vislib/public/vis_renderer.tsx @@ -9,6 +9,7 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { ExpressionRenderDefinition } from '../../../expressions/public'; import { VisualizationContainer } from '../../../visualizations/public'; import { ChartsPluginSetup } from '../../../charts/public'; @@ -44,9 +45,11 @@ export const getVislibVisRenderer: ( handlers.onDestroy(() => unmountComponentAtNode(domNode)); render( - - - , + + + + + , domNode ); }, diff --git a/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx b/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx index 731fbed7482c4..4338b0bc99cca 100644 --- a/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx +++ b/src/plugins/vis_types/vislib/public/vislib/partials/touchdown_template.tsx @@ -9,6 +9,8 @@ import React from 'react'; import ReactDOM from 'react-dom/server'; import { EuiIcon } from '@elastic/eui'; +import { KibanaThemeProvider } from '../../../../../kibana_react/public'; +import { getTheme } from '../../services'; interface Props { wholeBucket: boolean; @@ -16,12 +18,14 @@ interface Props { export const touchdownTemplate = ({ wholeBucket }: Props) => { return ReactDOM.renderToStaticMarkup( -

- - - {wholeBucket ? 'Part of this bucket' : 'This area'} may contain partial data. The selected - time range does not fully cover it. - -

+ +

+ + + {wholeBucket ? 'Part of this bucket' : 'This area'} may contain partial data. The selected + time range does not fully cover it. + +

+
); }; diff --git a/src/plugins/vis_types/xy/kibana.json b/src/plugins/vis_types/xy/kibana.json index 1606af5944ad3..a37393b2439d4 100644 --- a/src/plugins/vis_types/xy/kibana.json +++ b/src/plugins/vis_types/xy/kibana.json @@ -4,7 +4,7 @@ "ui": true, "server": true, "requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"], - "requiredBundles": ["kibanaUtils", "visDefaultEditor"], + "requiredBundles": ["kibanaUtils", "visDefaultEditor", "kibanaReact"], "extraPublicDirs": ["common/index"], "owner": { "name": "Vis Editors", diff --git a/src/plugins/vis_types/xy/public/plugin.ts b/src/plugins/vis_types/xy/public/plugin.ts index 0f1de387161e3..9f81d6848d2e5 100644 --- a/src/plugins/vis_types/xy/public/plugin.ts +++ b/src/plugins/vis_types/xy/public/plugin.ts @@ -72,6 +72,7 @@ export class VisTypeXyPlugin expressions.registerRenderer( getXYVisRenderer({ uiSettings: core.uiSettings, + theme: core.theme, }) ); expressions.registerFunction(expressionFunctions.visTypeXyVisFn); diff --git a/src/plugins/vis_types/xy/public/vis_renderer.tsx b/src/plugins/vis_types/xy/public/vis_renderer.tsx index 77727761015a7..1fba921ab6275 100644 --- a/src/plugins/vis_types/xy/public/vis_renderer.tsx +++ b/src/plugins/vis_types/xy/public/vis_renderer.tsx @@ -9,8 +9,9 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { VisualizationContainer } from '../../../visualizations/public'; import type { PersistedState } from '../../../visualizations/public'; import type { ExpressionRenderDefinition } from '../../../expressions/public'; @@ -32,7 +33,8 @@ function shouldShowNoResultsMessage(visData: any, visType: XyVisType): boolean { export const getXYVisRenderer: (deps: { uiSettings: IUiSettingsClient; -}) => ExpressionRenderDefinition = ({ uiSettings }) => ({ + theme: ThemeServiceStart; +}) => ExpressionRenderDefinition = ({ uiSettings, theme }) => ({ name: visName, displayName: 'XY visualization', reuseDomNode: true, @@ -41,19 +43,21 @@ export const getXYVisRenderer: (deps: { handlers.onDestroy(() => unmountComponentAtNode(domNode)); render( - - - - - , + + + + + + + , domNode ); }, diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index 32430c9d4e4fd..5152ddff40890 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -12,7 +12,7 @@ "savedObjects" ], "optionalPlugins": ["usageCollection", "spaces", "savedObjectsTaggingOss"], - "requiredBundles": ["kibanaUtils", "discover"], + "requiredBundles": ["kibanaUtils", "discover", "kibanaReact"], "extraPublicDirs": ["common/constants", "common/prepare_log_table", "common/expression_functions"], "owner": { "name": "Vis Editors", diff --git a/src/plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx b/src/plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx index 99bd06bbc412e..b363553e6c9b2 100644 --- a/src/plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/disabled_lab_embeddable.tsx @@ -8,11 +8,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { Embeddable, EmbeddableOutput } from '../../../../plugins/embeddable/public'; import { DisabledLabVisualization } from './disabled_lab_visualization'; import { VisualizeInput } from './visualize_embeddable'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; +import { getTheme } from '../services'; export class DisabledLabEmbeddable extends Embeddable { private domNode?: HTMLElement; @@ -26,7 +28,12 @@ export class DisabledLabEmbeddable extends Embeddable, domNode); + ReactDOM.render( + + + , + domNode + ); } } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 37365fd613e5a..3a248732eac0c 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { render } from 'react-dom'; import { EuiLoadingChart } from '@elastic/eui'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import { IndexPattern, @@ -38,7 +39,7 @@ import { ExpressionAstExpression, } from '../../../../plugins/expressions/public'; import { Vis, SerializedVis } from '../vis'; -import { getExpressions, getUiActions } from '../services'; +import { getExpressions, getTheme, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { SavedObjectAttributes } from '../../../../core/types'; @@ -303,9 +304,11 @@ export class VisualizeEmbeddable super.render(this.domNode); render( -
- -
, + +
+ +
+
, this.domNode ); diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 60c50d018252b..4e671a145cc71 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -25,6 +25,7 @@ import { setEmbeddable, setDocLinks, setSpaces, + setTheme, } from './services'; import { VISUALIZE_EMBEDDABLE_TYPE, @@ -147,6 +148,7 @@ export class VisualizationsPlugin setUISettings(core.uiSettings); setUsageCollector(usageCollection); + setTheme(core.theme); expressions.registerFunction(rangeExpressionFunction); expressions.registerFunction(visDimensionExpressionFunction); diff --git a/src/plugins/visualizations/public/services.ts b/src/plugins/visualizations/public/services.ts index 95f5fa02c09a8..37aea45fa3f58 100644 --- a/src/plugins/visualizations/public/services.ts +++ b/src/plugins/visualizations/public/services.ts @@ -15,6 +15,7 @@ import type { OverlayStart, SavedObjectsStart, DocLinksStart, + ThemeServiceStart, } from '../../../core/public'; import type { TypesStart } from './vis_types'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; @@ -27,6 +28,8 @@ import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); +export const [getTheme, setTheme] = createGetterSetter('Theme'); + export const [getCapabilities, setCapabilities] = createGetterSetter('Capabilities'); export const [getHttp, setHttp] = createGetterSetter('Http'); diff --git a/src/plugins/visualizations/public/wizard/show_new_vis.tsx b/src/plugins/visualizations/public/wizard/show_new_vis.tsx index 76fabdedae48d..c8c945c08db81 100644 --- a/src/plugins/visualizations/public/wizard/show_new_vis.tsx +++ b/src/plugins/visualizations/public/wizard/show_new_vis.tsx @@ -10,6 +10,7 @@ import React, { lazy, Suspense } from 'react'; import ReactDOM from 'react-dom'; import { EuiPortal, EuiProgress } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; import { getHttp, getSavedObjects, @@ -19,6 +20,7 @@ import { getApplication, getEmbeddable, getDocLinks, + getTheme, } from '../services'; import type { BaseVisType } from '../vis_types'; @@ -61,33 +63,35 @@ export function showNewVisModal({ document.body.appendChild(container); const element = ( - - - - - } - > - - - + + + + + + } + > + + + + ); ReactDOM.render(element, container); diff --git a/src/plugins/visualize/public/application/components/visualize_no_match.tsx b/src/plugins/visualize/public/application/components/visualize_no_match.tsx index ad993af430086..0077230cff11c 100644 --- a/src/plugins/visualize/public/application/components/visualize_no_match.tsx +++ b/src/plugins/visualize/public/application/components/visualize_no_match.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; -import { useKibana, toMountPoint } from '../../../../kibana_react/public'; +import { useKibana, toMountPoint, KibanaThemeProvider } from '../../../../kibana_react/public'; import { VisualizeServices } from '../types'; import { VisualizeConstants } from '../visualize_constants'; @@ -35,21 +35,23 @@ export const VisualizeNoMatch = () => { bannerId = services.overlays.banners.replace( bannerId, toMountPoint( - -

- - {services.history.location.pathname} - - ), - }} - /> -

-
+ + +

+ + {services.history.location.pathname} + + ), + }} + /> +

+
+
) ); diff --git a/src/plugins/visualize/public/application/index.tsx b/src/plugins/visualize/public/application/index.tsx index 8a7936c7bc44c..c3bc01deff020 100644 --- a/src/plugins/visualize/public/application/index.tsx +++ b/src/plugins/visualize/public/application/index.tsx @@ -11,7 +11,7 @@ import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; import { AppMountParameters } from 'kibana/public'; -import { KibanaContextProvider } from '../../../kibana_react/public'; +import { KibanaContextProvider, KibanaThemeProvider } from '../../../kibana_react/public'; import { VisualizeApp } from './app'; import { VisualizeServices } from './types'; import { addHelpMenuToAppChrome, addBadgeToAppChrome } from './utils'; @@ -28,15 +28,17 @@ export const renderApp = ( } const app = ( - - - - - - - - - + + + + + + + + + + + ); ReactDOM.render(app, element); diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx index e8f163e30b153..dca1791d44149 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx @@ -12,7 +12,11 @@ import { map } from 'rxjs/operators'; import { EventEmitter } from 'events'; import { i18n } from '@kbn/i18n'; -import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; +import { + KibanaThemeProvider, + MarkdownSimple, + toMountPoint, +} from '../../../../../kibana_react/public'; import { migrateLegacyQuery } from '../migrate_legacy_query'; import { esFilters, connectToQueryState } from '../../../../../data/public'; import { @@ -121,7 +125,11 @@ export const useVisualizeAppState = ( title: i18n.translate('visualize.visualizationLoadingFailedErrorMessage', { defaultMessage: 'Failed to load the visualization', }), - text: toMountPoint({error.message}), + text: toMountPoint( + + {error.message} + + ), }); services.history.replace( diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 77445d88356ce..8ab02c462cfa4 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -166,6 +166,9 @@ export const outputRoutesService = { getUpdatePath: (outputId: string) => OUTPUT_API_ROUTES.UPDATE_PATTERN.replace('{outputId}', outputId), getListPath: () => OUTPUT_API_ROUTES.LIST_PATTERN, + getDeletePath: (outputId: string) => + OUTPUT_API_ROUTES.DELETE_PATTERN.replace('{outputId}', outputId), + getCreatePath: () => OUTPUT_API_ROUTES.CREATE_PATTERN, }; export const settingsRoutesService = { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/output.ts b/x-pack/plugins/fleet/common/types/rest_spec/output.ts index f8eb20b51f208..4e380feeb83a8 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/output.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/output.ts @@ -26,9 +26,25 @@ export interface PutOutputRequest { outputId: string; }; body: { + type?: 'elasticsearch'; + name?: string; hosts?: string[]; ca_sha256?: string; - config?: Record; + config_yaml?: string; + is_default?: boolean; + is_default_monitoring?: boolean; + }; +} + +export interface PostOutputRequest { + body: { + id?: string; + type: 'elasticsearch'; + name: string; + hosts?: string[]; + ca_sha256?: string; + is_default?: boolean; + is_default_monitoring?: boolean; config_yaml?: string; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx new file mode 100644 index 0000000000000..46601f3e74351 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.tsx @@ -0,0 +1,229 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiSelect, + EuiSwitch, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { HostsInput } from '../hosts_input'; +import type { Output } from '../../../../types'; +import { FLYOUT_MAX_WIDTH } from '../../constants'; + +import { YamlCodeEditorWithPlaceholder } from './yaml_code_editor_with_placeholder'; +import { useOutputForm } from './use_output_form'; + +export interface EditOutputFlyoutProps { + output?: Output; + onClose: () => void; +} + +export const EditOutputFlyout: React.FunctionComponent = ({ + onClose, + output, +}) => { + const form = useOutputForm(onClose, output); + const inputs = form.inputs; + + return ( + + + +

+ {!output ? ( + + ) : ( + + )} +

+
+
+ + {output?.is_preconfigured && ( + <> + + } + > + + + + + )} + + + } + {...inputs.nameInput.formRowProps} + > + + + + } + > + + + + + + + + + + + ), + }} + /> + } + /> + + + + + + ), + }} + /> + } + /> + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx new file mode 100644 index 0000000000000..1ad49dc091412 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 { validateHosts, validateYamlConfig } from './output_form_validators'; + +describe('Output form validation', () => { + describe('validateHosts', () => { + it('should work without any urls', () => { + const res = validateHosts([]); + + expect(res).toBeUndefined(); + }); + + it('should work with valid url', () => { + const res = validateHosts(['https://test.fr:9200']); + + expect(res).toBeUndefined(); + }); + + it('should return an error with invalid url', () => { + const res = validateHosts(['toto']); + + expect(res).toEqual([{ index: 0, message: 'Invalid URL' }]); + }); + + it('should return an error with url with invalid port', () => { + const res = validateHosts(['https://test.fr:qwerty9200']); + + expect(res).toEqual([{ index: 0, message: 'Invalid URL' }]); + }); + + it('should return an error with multiple invalid urls', () => { + const res = validateHosts(['toto', 'tata']); + + expect(res).toEqual([ + { index: 0, message: 'Invalid URL' }, + { index: 1, message: 'Invalid URL' }, + ]); + }); + it('should return an error with duplicate urls', () => { + const res = validateHosts(['http://test.fr', 'http://test.fr']); + + expect(res).toEqual([ + { index: 0, message: 'Duplicate URL' }, + { index: 1, message: 'Duplicate URL' }, + ]); + }); + }); + describe('validateYamlConfig', () => { + it('should work with an empty yaml', () => { + const res = validateYamlConfig(``); + + expect(res).toBeUndefined(); + }); + + it('should work with valid yaml', () => { + const res = validateYamlConfig(`test: 123`); + + expect(res).toBeUndefined(); + }); + + it('should return an error with invalid yaml', () => { + const res = validateYamlConfig(`{}}`); + + expect(res).toBeDefined(); + if (typeof res !== 'undefined') { + expect(res[0]).toContain('Invalid YAML: '); + } + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx new file mode 100644 index 0000000000000..5d4bebb3cb7d8 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/output_form_validators.tsx @@ -0,0 +1,73 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { safeLoad } from 'js-yaml'; + +export function validateHosts(value: string[]) { + const res: Array<{ message: string; index: number }> = []; + const urlIndexes: { [key: string]: number[] } = {}; + value.forEach((val, idx) => { + try { + const urlParsed = new URL(val); + if (!['http:', 'https:'].includes(urlParsed.protocol)) { + throw new Error('Invalid protocol'); + } + } catch (error) { + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.elasticHostError', { + defaultMessage: 'Invalid URL', + }), + index: idx, + }); + } + + const curIndexes = urlIndexes[val] || []; + urlIndexes[val] = [...curIndexes, idx]; + }); + + Object.values(urlIndexes) + .filter(({ length }) => length > 1) + .forEach((indexes) => { + indexes.forEach((index) => + res.push({ + message: i18n.translate('xpack.fleet.settings.outputForm.elasticHostDuplicateError', { + defaultMessage: 'Duplicate URL', + }), + index, + }) + ); + }); + + if (res.length) { + return res; + } +} + +export function validateYamlConfig(value: string) { + try { + safeLoad(value); + return; + } catch (error) { + return [ + i18n.translate('xpack.fleet.settings.outputForm.invalidYamlFormatErrorMessage', { + defaultMessage: 'Invalid YAML: {reason}', + values: { reason: error.message }, + }), + ]; + } +} + +export function validateName(value: string) { + if (!value || value === '') { + return [ + i18n.translate('xpack.fleet.settings.outputForm.nameIsRequiredErrorMessage', { + defaultMessage: 'Name is required', + }), + ]; + } +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx new file mode 100644 index 0000000000000..1e993595d5cd4 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx @@ -0,0 +1,210 @@ +/* + * 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, { useCallback, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { + sendPostOutput, + useComboInput, + useInput, + useSwitchInput, + useStartServices, + sendPutOutput, +} from '../../../../hooks'; +import type { Output, PostOutputRequest } from '../../../../types'; +import { useConfirmModal } from '../../hooks/use_confirm_modal'; +import { getAgentAndPolicyCountForOutput } from '../../services/agent_and_policies_count'; + +import { validateName, validateHosts, validateYamlConfig } from './output_form_validators'; + +const ConfirmTitle = () => ( + +); + +interface ConfirmDescriptionProps { + output: Output; + agentCount: number; + agentPolicyCount: number; +} + +const ConfirmDescription: React.FunctionComponent = ({ + output, + agentCount, + agentPolicyCount, +}) => ( + {output.name}, + agents: ( + + + + ), + policies: ( + + + + ), + }} + /> +); + +async function confirmUpdate( + output: Output, + confirm: ReturnType['confirm'] +) { + const { agentCount, agentPolicyCount } = await getAgentAndPolicyCountForOutput(output); + return confirm( + , + + ); +} + +export function useOutputForm(onSucess: () => void, output?: Output) { + const [isLoading, setIsloading] = useState(false); + const { notifications } = useStartServices(); + const { confirm } = useConfirmModal(); + + // preconfigured output do not allow edition + const isPreconfigured = output?.is_preconfigured ?? false; + + // Define inputs + const nameInput = useInput(output?.name ?? '', validateName, isPreconfigured); + const typeInput = useInput(output?.type ?? '', undefined, isPreconfigured); + const elasticsearchUrlInput = useComboInput( + 'esHostsComboxBox', + output?.hosts ?? [], + validateHosts, + isPreconfigured + ); + const additionalYamlConfigInput = useInput( + output?.config_yaml ?? '', + validateYamlConfig, + isPreconfigured + ); + + const defaultOutputInput = useSwitchInput( + output?.is_default ?? false, + isPreconfigured || output?.is_default + ); + const defaultMonitoringOutputInput = useSwitchInput( + output?.is_default_monitoring ?? false, + isPreconfigured || output?.is_default_monitoring + ); + + const inputs = { + nameInput, + typeInput, + elasticsearchUrlInput, + additionalYamlConfigInput, + defaultOutputInput, + defaultMonitoringOutputInput, + }; + + const hasChanged = Object.values(inputs).some((input) => input.hasChanged); + + const validate = useCallback(() => { + const nameInputValid = nameInput.validate(); + const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); + const additionalYamlConfigValid = additionalYamlConfigInput.validate(); + + if (!elasticsearchUrlsValid || !additionalYamlConfigValid || !nameInputValid) { + return false; + } + + return true; + }, [nameInput, elasticsearchUrlInput, additionalYamlConfigInput]); + + const submit = useCallback(async () => { + try { + if (!validate()) { + return; + } + setIsloading(true); + + const data: PostOutputRequest['body'] = { + name: nameInput.value, + type: 'elasticsearch', + hosts: elasticsearchUrlInput.value, + is_default: defaultOutputInput.value, + is_default_monitoring: defaultMonitoringOutputInput.value, + config_yaml: additionalYamlConfigInput.value, + }; + + if (output) { + // Update + if (!(await confirmUpdate(output, confirm))) { + setIsloading(false); + return; + } + + const res = await sendPutOutput(output.id, data); + if (res.error) { + throw res.error; + } + } else { + // Create + const res = await sendPostOutput(data); + if (res.error) { + throw res.error; + } + } + + onSucess(); + setIsloading(false); + } catch (err) { + setIsloading(false); + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.settings.outputForm.errorToastTitle', { + defaultMessage: 'Error while saving output', + }), + }); + } + }, [ + validate, + confirm, + additionalYamlConfigInput.value, + defaultMonitoringOutputInput.value, + defaultOutputInput.value, + elasticsearchUrlInput.value, + nameInput.value, + notifications.toasts, + onSucess, + output, + ]); + + return { + inputs, + submit, + isLoading, + isDisabled: isLoading || isPreconfigured || (output && !hasChanged), + }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/yaml_code_editor_with_placeholder.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/yaml_code_editor_with_placeholder.stories.tsx new file mode 100644 index 0000000000000..320d2bb7cbef8 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/yaml_code_editor_with_placeholder.stories.tsx @@ -0,0 +1,42 @@ +/* + * 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, { useState } from 'react'; + +import { YamlCodeEditorWithPlaceholder as Component } from './yaml_code_editor_with_placeholder'; + +export default { + component: Component, + title: 'Sections/Fleet/Settings/YamlCodeEditorWithPlaceholder', +}; + +interface Args { + width: number; + placeholder: string; +} + +const args: Args = { + width: 1200, + placeholder: '# Place holder example', +}; + +export const YamlCodeEditorWithPlaceholder = ({ width, placeholder }: Args) => { + const [value, setValue] = useState(''); + + // This component is not renderable in tests + if (typeof jest !== 'undefined') { + return null; + } + + return ( +
+ +
+ ); +}; + +YamlCodeEditorWithPlaceholder.args = args; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/yaml_code_editor_with_placeholder.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/yaml_code_editor_with_placeholder.tsx new file mode 100644 index 0000000000000..a59c61262e0b9 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/yaml_code_editor_with_placeholder.tsx @@ -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 React from 'react'; +import styled from 'styled-components'; + +import { i18n } from '@kbn/i18n'; +import { EuiCodeBlock, EuiTextColor } from '@elastic/eui'; + +import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import type { CodeEditorProps } from '../../../../../../../../../../src/plugins/kibana_react/public'; + +const CodeEditorContainer = styled.div` + min-height: 0; + position: relative; + height: 116px; +`; + +const CodeEditorPlaceholder = styled(EuiTextColor).attrs((props) => ({ + color: 'subdued', + size: 'xs', +}))` + position: absolute; + top: 0; + left: 0; + // Matches monaco editor + font-family: Menlo, Monaco, 'Courier New', monospace; + font-size: 12px; + line-height: 21px; + pointer-events: none; +`; + +const CODE_EDITOR_OPTIONS: CodeEditorProps['options'] = { + minimap: { + enabled: false, + }, + + ariaLabel: i18n.translate('xpack.fleet.settings.yamlCodeEditor', { + defaultMessage: 'YAML Code Editor', + }), + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + tabSize: 2, + // To avoid left margin + lineNumbers: 'off', + lineNumbersMinChars: 0, + glyphMargin: false, + folding: false, + lineDecorationsWidth: 0, +}; + +export type YamlCodeEditorWithPlaceholderProps = Pick & { + placeholder: string; + disabled?: boolean; +}; + +export const YamlCodeEditorWithPlaceholder: React.FunctionComponent = + (props) => { + const { placeholder, disabled, ...editorProps } = props; + + if (disabled) { + return ( + +
{editorProps.value}
+
+ ); + } + + return ( + + + {(!editorProps.value || editorProps.value === '') && ( + {placeholder} + )} + + ); + }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx index 6736b5a30d23e..c4ff6917867ca 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.stories.tsx @@ -5,19 +5,13 @@ * 2.0. */ -import { addParameters } from '@storybook/react'; import React from 'react'; import { FleetServerHostsFlyout as Component } from '.'; -addParameters({ - docs: { - inlineStories: false, - }, -}); export default { component: Component, - title: 'Sections/Fleet/Settings', + title: 'Sections/Fleet/Settings/FleetServerHostFlyout', }; interface Args { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx index 07593ffe3e6c5..5ec7735025096 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/fleet_server_hosts_flyout/index.tsx @@ -19,15 +19,15 @@ import { EuiFlexItem, EuiButtonEmpty, EuiButton, + EuiSpacer, } from '@elastic/eui'; import { HostsInput } from '../hosts_input'; import { useStartServices } from '../../../../hooks'; +import { FLYOUT_MAX_WIDTH } from '../../constants'; import { useFleetServerHostsForm } from './use_fleet_server_host_form'; -const FLYOUT_MAX_WIDTH = 800; - export interface FleetServerHostsFlyoutProps { onClose: () => void; fleetServerHosts: string[]; @@ -74,6 +74,7 @@ export const FleetServerHostsFlyout: React.FunctionComponent + @@ -90,7 +91,7 @@ export const FleetServerHostsFlyout: React.FunctionComponent ( + +); + +interface ConfirmDescriptionProps { + agentCount: number; + agentPolicyCount: number; +} + +const ConfirmDescription: React.FunctionComponent = ({ + agentCount, + agentPolicyCount, +}) => ( + + + + ), + policies: ( + + + + ), + }} + /> +); + function validateFleetServerHosts(value: string[]) { if (value.length === 0) { return [ @@ -76,7 +124,7 @@ export function useFleetServerHostsForm( fleetServerHostsDefaultValue: string[], onSuccess: () => void ) { - const [isLoading, setIsLoading] = React.useState(false); + const [isLoading, setIsLoading] = useState(false); const { notifications } = useStartServices(); const { confirm } = useConfirmModal(); @@ -97,15 +145,11 @@ export function useFleetServerHostsForm( if (!validate) { return; } + const { agentCount, agentPolicyCount } = await getAgentAndPolicyCount(); if ( !(await confirm( - i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalTitle', { - defaultMessage: 'Save and deploy changes?', - }), - i18n.translate('xpack.fleet.settings.fleetServerHostsFlyout.confirmModalDescription', { - defaultMessage: - 'This action will update all of your agent policies and all of your agents. Are you sure you wish to continue?', - }) + , + )) ) { return; @@ -136,6 +180,7 @@ export function useFleetServerHostsForm( return { isLoading, + isDisabled: isLoading || !fleetServerHostsInput.hasChanged, submit, fleetServerHostsInput, }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx index 64ac34c52d112..9b4674f3ce778 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.stories.tsx @@ -19,22 +19,24 @@ addParameters({ export default { component: Component, - title: 'Sections/Fleet/Settings', + title: 'Sections/Fleet/Settings/HostInput', }; interface Args { width: number; label: string; helpText: string; + disabled: boolean; } const args: Args = { width: 250, label: 'Demo label', helpText: 'Demo helpText', + disabled: false, }; -export const HostsInput = ({ width, label, helpText }: Args) => { +export const HostsInput = ({ width, label, helpText, disabled }: Args) => { const [value, setValue] = useState([]); return (
@@ -44,9 +46,31 @@ export const HostsInput = ({ width, label, helpText }: Args) => { value={value} onChange={setValue} label={label} + disabled={disabled} />
); }; - HostsInput.args = args; + +export const HostsInputDisabled = ({ value }: { value: string[] }) => { + return ( +
+ {}} + label={'Host input label'} + disabled={true} + /> +
+ ); +}; + +HostsInputDisabled.args = { value: ['http://test1.fr', 'http://test2.fr'] }; +HostsInputDisabled.argTypes = { + value: { + control: { type: 'object' }, + }, +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx index 6b169a207ea73..e50024a2aabae 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/hosts_input/index.tsx @@ -37,6 +37,7 @@ export interface HostInputProps { helpText?: ReactNode; errors?: Array<{ message: string; index?: number }>; isInvalid?: boolean; + disabled?: boolean; } interface SortableTextFieldProps { @@ -47,6 +48,7 @@ interface SortableTextFieldProps { onDelete: (index: number) => void; errors?: string[]; autoFocus?: boolean; + disabled?: boolean; } const DraggableDiv = sytled.div` @@ -62,7 +64,7 @@ function displayErrors(errors?: string[]) { } const SortableTextField: FunctionComponent = React.memo( - ({ id, index, value, onChange, onDelete, autoFocus, errors }) => { + ({ id, index, value, onChange, onDelete, autoFocus, errors, disabled }) => { const onDeleteHandler = useCallback(() => { onDelete(index); }, [onDelete, index]); @@ -75,6 +77,7 @@ const SortableTextField: FunctionComponent = React.memo( spacing="m" index={index} draggableId={id} + isDragDisabled={disabled} customDragHandle={true} style={{ paddingLeft: 0, @@ -109,6 +112,7 @@ const SortableTextField: FunctionComponent = React.memo( onChange={onChange} autoFocus={autoFocus} isInvalid={isInvalid} + disabled={disabled} placeholder={i18n.translate('xpack.fleet.hostsInput.placeholder', { defaultMessage: 'Specify host URL', })} @@ -120,6 +124,7 @@ const SortableTextField: FunctionComponent = React.memo( color="text" onClick={onDeleteHandler} iconType="cross" + disabled={disabled} aria-label={i18n.translate('xpack.fleet.settings.deleteHostButton', { defaultMessage: 'Delete host', })} @@ -140,6 +145,7 @@ export const HostsInput: FunctionComponent = ({ label, isInvalid, errors, + disabled, }) => { const [autoFocus, setAutoFocus] = useState(false); const value = useMemo(() => { @@ -214,7 +220,7 @@ export const HostsInput: FunctionComponent = ({ <> {helpText} - + {helpText && } {rows.map((row, idx) => ( @@ -228,6 +234,7 @@ export const HostsInput: FunctionComponent = ({ value={row.value} autoFocus={autoFocus} errors={indexedErrors[idx]} + disabled={disabled} /> ) : ( <> @@ -239,6 +246,7 @@ export const HostsInput: FunctionComponent = ({ placeholder={i18n.translate('xpack.fleet.hostsInput.placeholder', { defaultMessage: 'Specify host URL', })} + disabled={disabled} /> {displayErrors(indexedErrors[idx])} @@ -249,7 +257,13 @@ export const HostsInput: FunctionComponent = ({ {displayErrors(globalErrors)} - + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx deleted file mode 100644 index c4de3fe513214..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/confirm_modal.tsx +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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 { - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalFooter, - EuiModalBody, - EuiCallOut, - EuiButton, - EuiButtonEmpty, - EuiBasicTable, - EuiText, - EuiSpacer, -} from '@elastic/eui'; -import type { EuiBasicTableProps } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export interface SettingsConfirmModalProps { - changes: Array<{ - type: 'elasticsearch' | 'fleet_server'; - direction: 'removed' | 'added'; - urls: string[]; - }>; - onConfirm: () => void; - onClose: () => void; -} - -type Change = SettingsConfirmModalProps['changes'][0]; - -const TABLE_COLUMNS: EuiBasicTableProps['columns'] = [ - { - name: i18n.translate('xpack.fleet.settingsConfirmModal.fieldLabel', { - defaultMessage: 'Field', - }), - field: 'label', - render: (_, item) => getLabel(item), - width: '180px', - }, - { - field: 'urls', - name: i18n.translate('xpack.fleet.settingsConfirmModal.valueLabel', { - defaultMessage: 'Value', - }), - render: (_, item) => { - return ( - - {item.urls.map((url) => ( -
{url}
- ))} -
- ); - }, - }, -]; - -function getLabel(change: Change) { - if (change.type === 'elasticsearch' && change.direction === 'removed') { - return i18n.translate('xpack.fleet.settingsConfirmModal.elasticsearchRemovedLabel', { - defaultMessage: 'Elasticsearch hosts (old)', - }); - } - - if (change.type === 'elasticsearch' && change.direction === 'added') { - return i18n.translate('xpack.fleet.settingsConfirmModal.elasticsearchAddedLabel', { - defaultMessage: 'Elasticsearch hosts (new)', - }); - } - - if (change.type === 'fleet_server' && change.direction === 'removed') { - return i18n.translate('xpack.fleet.settingsConfirmModal.fleetServerRemovedLabel', { - defaultMessage: 'Fleet Server hosts (old)', - }); - } - - if (change.type === 'fleet_server' && change.direction === 'added') { - return i18n.translate('xpack.fleet.settingsConfirmModal.fleetServerAddedLabel', { - defaultMessage: 'Fleet Server hosts (new)', - }); - } - - return i18n.translate('xpack.fleet.settingsConfirmModal.defaultChangeLabel', { - defaultMessage: 'Unknown setting', - }); -} - -export const SettingsConfirmModal = React.memo( - ({ changes, onConfirm, onClose }) => { - const hasESChanges = changes.some((change) => change.type === 'elasticsearch'); - const hasFleetServerChanges = changes.some((change) => change.type === 'fleet_server'); - - return ( - - - - - - - - - - } - color="warning" - iconType="alert" - > - - {hasFleetServerChanges && ( -

- - - - ), - }} - /> -

- )} - - {hasESChanges && ( -

- - - - ), - }} - /> -

- )} -
-
- - {changes.length > 0 && ( - <> - - - - )} -
- - - - - - - - - -
- ); - } -); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx deleted file mode 100644 index 6c52475400bdc..0000000000000 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/legacy_settings_form/index.tsx +++ /dev/null @@ -1,370 +0,0 @@ -/* - * 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, { useEffect, useCallback } from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { - EuiPortal, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiButton, - EuiForm, - EuiFormRow, - EuiCode, - EuiPanel, - EuiTextColor, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText } from '@elastic/eui'; -import { safeLoad } from 'js-yaml'; - -import { - useComboInput, - useStartServices, - useGetSettings, - useInput, - useDefaultOutput, - sendPutOutput, -} from '../../../../../../hooks'; -import { normalizeHostsForAgents } from '../../../../../../../common'; -import { CodeEditor } from '../../../../../../../../../../src/plugins/kibana_react/public'; -import { HostsInput } from '../hosts_input'; - -import { SettingsConfirmModal } from './confirm_modal'; -import type { SettingsConfirmModalProps } from './confirm_modal'; - -const CodeEditorContainer = styled.div` - min-height: 0; - position: relative; - height: 250px; -`; - -const CodeEditorPlaceholder = styled(EuiTextColor).attrs((props) => ({ - color: 'subdued', - size: 'xs', -}))` - position: absolute; - top: 0; - left: 0; - // Matches monaco editor - font-family: Menlo, Monaco, 'Courier New', monospace; - font-size: 12px; - line-height: 21px; - pointer-events: none; -`; - -const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm; - -function normalizeHosts(hostsInput: string[]) { - return hostsInput.map((host) => { - try { - return normalizeHostsForAgents(host); - } catch (err) { - return host; - } - }); -} - -function isSameArrayValueWithNormalizedHosts(arrayA: string[] = [], arrayB: string[] = []) { - const hostsA = normalizeHosts(arrayA); - const hostsB = normalizeHosts(arrayB); - return hostsA.length === hostsB.length && hostsA.every((val, index) => val === hostsB[index]); -} - -function useSettingsForm(outputId: string | undefined) { - const [isLoading, setIsloading] = React.useState(false); - const { notifications } = useStartServices(); - - const elasticsearchUrlInput = useComboInput('esHostsComboxBox', [], (value) => { - const res: Array<{ message: string; index: number }> = []; - const urlIndexes: { [key: string]: number[] } = {}; - value.forEach((val, idx) => { - if (!val.match(URL_REGEX)) { - res.push({ - message: i18n.translate('xpack.fleet.settings.elasticHostError', { - defaultMessage: 'Invalid URL', - }), - index: idx, - }); - } - const curIndexes = urlIndexes[val] || []; - urlIndexes[val] = [...curIndexes, idx]; - }); - - Object.values(urlIndexes) - .filter(({ length }) => length > 1) - .forEach((indexes) => { - indexes.forEach((index) => - res.push({ - message: i18n.translate('xpack.fleet.settings.elasticHostDuplicateError', { - defaultMessage: 'Duplicate URL', - }), - index, - }) - ); - }); - - if (res.length) { - return res; - } - }); - - const additionalYamlConfigInput = useInput('', (value) => { - try { - safeLoad(value); - return; - } catch (error) { - return [ - i18n.translate('xpack.fleet.settings.invalidYamlFormatErrorMessage', { - defaultMessage: 'Invalid YAML: {reason}', - values: { reason: error.message }, - }), - ]; - } - }); - - const validate = useCallback(() => { - const elasticsearchUrlsValid = elasticsearchUrlInput.validate(); - const additionalYamlConfigValid = additionalYamlConfigInput.validate(); - - if (!elasticsearchUrlsValid || !additionalYamlConfigValid) { - return false; - } - - return true; - }, [elasticsearchUrlInput, additionalYamlConfigInput]); - - return { - isLoading, - validate, - submit: async () => { - try { - setIsloading(true); - if (!outputId) { - throw new Error('Unable to load outputs'); - } - const outputResponse = await sendPutOutput(outputId, { - hosts: elasticsearchUrlInput.value, - config_yaml: additionalYamlConfigInput.value, - }); - if (outputResponse.error) { - throw outputResponse.error; - } - - notifications.toasts.addSuccess( - i18n.translate('xpack.fleet.settings.success.message', { - defaultMessage: 'Output saved', - }) - ); - setIsloading(false); - } catch (error) { - setIsloading(false); - notifications.toasts.addError(error, { - title: 'Error', - }); - } - }, - inputs: { - elasticsearchUrl: elasticsearchUrlInput, - additionalYamlConfig: additionalYamlConfigInput, - }, - }; -} - -export const LegacySettingsForm: React.FunctionComponent = () => { - const settingsRequest = useGetSettings(); - const settings = settingsRequest?.data?.item; - const { output } = useDefaultOutput(); - const { inputs, submit, validate, isLoading } = useSettingsForm(output?.id); - - const [isConfirmModalVisible, setConfirmModalVisible] = React.useState(false); - - const onSubmit = useCallback(() => { - if (validate()) { - setConfirmModalVisible(true); - } - }, [validate, setConfirmModalVisible]); - - const onConfirm = useCallback(() => { - setConfirmModalVisible(false); - submit(); - }, [submit]); - - const onConfirmModalClose = useCallback(() => { - setConfirmModalVisible(false); - }, [setConfirmModalVisible]); - - useEffect(() => { - if (output) { - inputs.elasticsearchUrl.setValue(output.hosts || []); - inputs.additionalYamlConfig.setValue(output.config_yaml || ''); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [output]); - - const isUpdated = React.useMemo(() => { - if (!settings || !output) { - return false; - } - return ( - !isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value) || - (output.config_yaml || '') !== inputs.additionalYamlConfig.value - ); - }, [settings, inputs, output]); - - const changes = React.useMemo(() => { - if (!settings || !output || !isConfirmModalVisible) { - return []; - } - - const tmpChanges: SettingsConfirmModalProps['changes'] = []; - if (!isSameArrayValueWithNormalizedHosts(output.hosts, inputs.elasticsearchUrl.value)) { - tmpChanges.push( - { - type: 'elasticsearch', - direction: 'removed', - urls: normalizeHosts(output.hosts || []), - }, - { - type: 'elasticsearch', - direction: 'added', - urls: normalizeHosts(inputs.elasticsearchUrl.value), - } - ); - } - - return tmpChanges; - }, [settings, inputs, output, isConfirmModalVisible]); - - const body = settings && ( - - - outputs, - }} - /> - - - - - - - - - - - {(!inputs.additionalYamlConfig.value || inputs.additionalYamlConfig.value === '') && ( - - {`# YAML settings here will be added to the Elasticsearch output section of each policy`} - - )} - - - - - ); - - return ( - <> - {isConfirmModalVisible && ( - - - - )} - <> - <> - -

- -

-
- - <>{body} - <> - - - - - {isLoading ? ( - - ) : ( - - )} - - - - - - - ); -}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.stories.tsx new file mode 100644 index 0000000000000..fe29bd8d7192b --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.stories.tsx @@ -0,0 +1,39 @@ +/* + * 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 { + DefaultMonitoringOutputBadge as DefaultMonitoringOutputBadgeComponent, + DefaultOutputBadge as DefaultOutputBadgeComponent, + DefaultBadges as Component, +} from './badges'; + +export default { + component: Component, + subcomponents: { + DefaultMonitoringOutputBadge: DefaultMonitoringOutputBadgeComponent, + DefaultOutputBadge: DefaultOutputBadgeComponent, + }, + title: 'Sections/Fleet/Settings/OutputsTable/DefaultBadges', +}; + +export const DefaultBadges = () => { + return ( +
+ +
+ ); +}; + +export const DefaultMonitoringOutputBadge = () => { + return ; +}; + +export const DefaultOutputBadge = () => { + return ; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.tsx new file mode 100644 index 0000000000000..d9d2e87f562e7 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/badges.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiBadge, EuiBadgeGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { Output } from '../../../../types'; + +export const DefaultBadges = React.memo<{ + output: Pick; +}>(({ output }) => { + const badges = useMemo(() => { + const badgesArray = []; + if (output.is_default) { + badgesArray.push(); + } + if (output.is_default_monitoring) { + badgesArray.push(); + } + + return badgesArray; + }, [output]); + return {badges.map((badge, idx) => badge)}; +}); + +export const DefaultOutputBadge = () => ( + + + +); + +export const DefaultMonitoringOutputBadge = () => ( + + + +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.stories.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.stories.tsx new file mode 100644 index 0000000000000..70fb81b9f33db --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.stories.tsx @@ -0,0 +1,104 @@ +/* + * 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 type { Output } from '../../../../types'; + +import { OutputsTable as Component } from '.'; + +export default { + component: Component, + title: 'Sections/Fleet/Settings/OutputsTable', +}; + +interface Args { + width: number; +} + +const args: Args = { + width: 1200, +}; + +const outputs: Output[] = [ + { + id: 'output1', + name: 'Demo output 1', + is_default: true, + is_default_monitoring: false, + type: 'elasticsearch', + hosts: ['http://elasticsearch-host-test.fr:9200'], + }, + { + id: 'output2', + name: 'Demo output 2', + is_default: false, + is_default_monitoring: true, + type: 'elasticsearch', + hosts: ['http://test.fr:9200'], + }, + { + id: 'output3', + name: 'Demo output 3', + is_default: false, + is_default_monitoring: false, + type: 'elasticsearch', + hosts: ['http://elasticsearch-host-test.fr:9200'], + }, + { + id: 'output4', + name: 'Demo output 4 preconfigured', + is_default: false, + is_default_monitoring: false, + is_preconfigured: true, + type: 'elasticsearch', + hosts: ['http://elasticsearch-host-test.fr:9200'], + }, + { + id: 'output5', + name: 'Demo output 5 preconfigured with a long name really long name', + is_default: false, + is_default_monitoring: false, + is_preconfigured: true, + type: 'elasticsearch', + hosts: ['http://elasticsearch-host-test.fr:9200'], + }, + { + id: 'output6', + name: 'Demo output 6 multiple hosts', + is_default: false, + is_default_monitoring: false, + type: 'elasticsearch', + hosts: ['http://elasticsearch-host-test1.fr:443', 'http://elasticsearch-host-test2.fr:443'], + }, + { + id: 'output7', + name: 'Demo output 7 both default', + is_default: true, + is_default_monitoring: true, + type: 'elasticsearch', + hosts: ['http://test.fr:9200'], + }, + { + id: 'output8', + name: 'Demo output 8 long host name', + is_default: true, + is_default_monitoring: true, + type: 'elasticsearch', + hosts: ['http://elasticsearch-host-with-a-very-long-name-very-very-long.fr:9200'], + }, +]; + +export const OutputsTable = ({ width }: Args) => { + return ( +
+ {}} outputs={outputs} /> +
+ ); +}; + +OutputsTable.args = args; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx new file mode 100644 index 0000000000000..a2b85cbbc5ef2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/outputs_table/index.tsx @@ -0,0 +1,144 @@ +/* + * 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, { useMemo } from 'react'; +import styled from 'styled-components'; +import { EuiBasicTable, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { useLink } from '../../../../hooks'; +import type { Output } from '../../../../types'; + +import { DefaultBadges } from './badges'; + +export interface OutputsTableProps { + outputs: Output[]; + deleteOutput: (output: Output) => void; +} + +const NameFlexItemWithMaxWidth = styled(EuiFlexItem)` + max-width: 250px; +`; + +// Allow child to be truncated +const FlexGroupWithMinWidth = styled(EuiFlexGroup)` + min-width: 0px; +`; + +function displayOutputType(type: string) { + switch (type) { + case 'elasticsearch': + return i18n.translate('xpack.fleet.settings.outputsTable.elasticsearchTypeLabel', { + defaultMessage: 'Elasticsearch', + }); + default: + return type; + } +} + +export const OutputsTable: React.FunctionComponent = ({ + outputs, + deleteOutput, +}) => { + const { getHref } = useLink(); + + const columns = useMemo((): Array> => { + return [ + { + render: (output: Output) => ( + + +

+ {output.name} +

+
+ {output.is_preconfigured && ( + + + + )} +
+ ), + width: '288px', + name: i18n.translate('xpack.fleet.settings.outputsTable.nameColomnTitle', { + defaultMessage: 'Name', + }), + }, + { + width: '172px', + render: (output: Output) => displayOutputType(output.type), + name: i18n.translate('xpack.fleet.settings.outputsTable.typeColomnTitle', { + defaultMessage: 'Type', + }), + }, + { + truncateText: true, + render: (output: Output) => ( + + {(output.hosts || []).map((host) => ( + +

+ {host} +

+
+ ))} +
+ ), + name: i18n.translate('xpack.fleet.settings.outputsTable.hostColomnTitle', { + defaultMessage: 'Hosts', + }), + }, + { + render: (output: Output) => , + width: '200px', + name: i18n.translate('xpack.fleet.settings.outputSection.defaultColomnTitle', { + defaultMessage: 'Default', + }), + }, + { + width: '68px', + render: (output: Output) => { + const isDeleteVisible = + !output.is_default && !output.is_default_monitoring && !output.is_preconfigured; + + return ( + + + {isDeleteVisible && ( + deleteOutput(output)} + title={i18n.translate('xpack.fleet.settings.outputSection.deleteButtonTitle', { + defaultMessage: 'Delete', + })} + /> + )} + + + + + + ); + }, + name: i18n.translate('xpack.fleet.settings.outputSection.actionsColomnTitle', { + defaultMessage: 'Actions', + }), + }, + ]; + }, [deleteOutput, getHref]); + + return ; +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx index 66a95a7952c35..6aacaa5dc4038 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/index.tsx @@ -8,21 +8,28 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; -import type { Settings } from '../../../../types'; -import { LegacySettingsForm } from '../legacy_settings_form'; +import type { Output, Settings } from '../../../../types'; import { SettingsSection } from './settings_section'; +import { OutputSection } from './output_section'; export interface SettingsPageProps { settings: Settings; + outputs: Output[]; + deleteOutput: (output: Output) => void; } -export const SettingsPage: React.FunctionComponent = ({ settings }) => { +export const SettingsPage: React.FunctionComponent = ({ + settings, + outputs, + deleteOutput, +}) => { return ( <> - + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx new file mode 100644 index 0000000000000..7dfe44ac95f24 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/settings_page/output_section.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiTitle, EuiText, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { useLink } from '../../../../hooks'; +import type { Output } from '../../../../types'; +import { OutputsTable } from '../outputs_table'; +import { FEATURE_ADD_OUTPUT_ENABLED } from '../../constants'; + +export interface OutputSectionProps { + outputs: Output[]; + deleteOutput: (output: Output) => void; +} + +export const OutputSection: React.FunctionComponent = ({ + outputs, + deleteOutput, +}) => { + const { getHref } = useLink(); + + return ( + <> + +

+ +

+
+ + + + + + + + {FEATURE_ADD_OUTPUT_ENABLED && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx new file mode 100644 index 0000000000000..b609c4c25308f --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/constants/index.tsx @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export const FLYOUT_MAX_WIDTH = 670; + +export const FEATURE_ADD_OUTPUT_ENABLED = false; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx index b8663f8cb2977..542c9a1e1d154 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/hooks/use_confirm_modal.tsx @@ -6,16 +6,22 @@ */ import { EuiConfirmModal, EuiPortal } from '@elastic/eui'; +import type { EuiConfirmModalProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useCallback, useContext, useState } from 'react'; interface ModalState { title?: React.ReactNode; description?: React.ReactNode; + buttonColor?: EuiConfirmModalProps['buttonColor']; onConfirm: () => void; onCancel: () => void; } +interface ModalOptions { + buttonColor?: EuiConfirmModalProps['buttonColor']; +} + const ModalContext = React.createContext void; }>(null); @@ -24,7 +30,7 @@ export function useConfirmModal() { const context = useContext(ModalContext); const confirm = useCallback( - async (title: React.ReactNode, description: React.ReactNode) => { + async (title: React.ReactNode, description: React.ReactNode, options?: ModalOptions) => { if (context === null) { throw new Error('Context need to be provided to use useConfirmModal'); } @@ -34,6 +40,7 @@ export function useConfirmModal() { description, onConfirm: () => resolve(true), onCancel: () => resolve(false), + buttonColor: options?.buttonColor, }); }); }, @@ -45,6 +52,14 @@ export function useConfirmModal() { }; } +export function withConfirmModalProvider(WrappedComponent: React.FunctionComponent) { + return (props: T) => ( + + + + ); +} + export const ConfirmModalProvider: React.FunctionComponent = ({ children }) => { const [isVisible, setIsVisible] = useState(false); const [modal, setModal] = useState({ @@ -74,6 +89,7 @@ export const ConfirmModalProvider: React.FunctionComponent = ({ children }) => { ( + +); + +interface ConfirmDescriptionProps { + output: Output; + agentCount: number; + agentPolicyCount: number; +} + +const ConfirmDescription: React.FunctionComponent = ({ + output, + agentCount, + agentPolicyCount, +}) => ( + {output.name}, + agents: ( + + + + ), + policies: ( + + + + ), + }} + /> +); + +export function useDeleteOutput(onSuccess: () => void) { + const { confirm } = useConfirmModal(); + const { notifications } = useStartServices(); + const deleteOutput = useCallback( + async (output: Output) => { + try { + const { agentCount, agentPolicyCount } = await getAgentAndPolicyCountForOutput(output); + + const isConfirmed = await confirm( + , + , + { + buttonColor: 'danger', + } + ); + + if (!isConfirmed) { + return; + } + + const res = await sendDeleteOutput(output.id); + + if (res.error) { + throw res.error; + } + + onSuccess(); + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.settings.deleteOutputs.errorToastTitle', { + defaultMessage: 'Error deleting output', + }), + }); + } + }, + [confirm, notifications.toasts, onSuccess] + ); + + return { deleteOutput }; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx index 212b2d9191e24..5a393ee74ea7b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/index.tsx @@ -7,31 +7,44 @@ import React, { useCallback } from 'react'; import { EuiPortal } from '@elastic/eui'; -import { Router, Route, Switch, useHistory } from 'react-router-dom'; +import { Router, Route, Switch, useHistory, Redirect } from 'react-router-dom'; -import { useBreadcrumbs, useGetSettings } from '../../hooks'; +import { useBreadcrumbs, useGetOutputs, useGetSettings } from '../../hooks'; import { FLEET_ROUTING_PATHS, pagePathGetters } from '../../constants'; import { DefaultLayout } from '../../layouts'; import { Loading } from '../../components'; import { SettingsPage } from './components/settings_page'; -import { ConfirmModalProvider } from './hooks/use_confirm_modal'; +import { withConfirmModalProvider } from './hooks/use_confirm_modal'; import { FleetServerHostsFlyout } from './components/fleet_server_hosts_flyout'; +import { EditOutputFlyout } from './components/edit_output_flyout'; +import { useDeleteOutput } from './hooks/use_delete_output'; +import { FEATURE_ADD_OUTPUT_ENABLED } from './constants'; -export const SettingsApp = () => { +export const SettingsApp = withConfirmModalProvider(() => { useBreadcrumbs('settings'); const history = useHistory(); const settings = useGetSettings(); + const outputs = useGetOutputs(); + + const { deleteOutput } = useDeleteOutput(outputs.resendRequest); const resendSettingsRequest = settings.resendRequest; + const resendOutputRequest = outputs.resendRequest; const onCloseCallback = useCallback(() => { resendSettingsRequest(); + resendOutputRequest(); history.replace(pagePathGetters.settings()[1]); - }, [history, resendSettingsRequest]); + }, [history, resendSettingsRequest, resendOutputRequest]); - if (settings.isLoading || !settings.data?.item) { + if ( + (settings.isLoading && settings.isInitialRequest) || + !settings.data?.item || + (outputs.isLoading && outputs.isInitialRequest) || + !outputs.data?.items + ) { return ( @@ -41,21 +54,45 @@ export const SettingsApp = () => { return ( - - - - + + + + + + + + {FEATURE_ADD_OUTPUT_ENABLED && ( + - + - - - - + )} + + {(route: { match: { params: { outputId: string } } }) => { + const output = outputs.data?.items.find((o) => route.match.params.outputId === o.id); + + if (!output) { + return ; + } + + return ( + + + + ); + }} + + + + ); -}; +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/services/agent_and_policies_count.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/services/agent_and_policies_count.tsx new file mode 100644 index 0000000000000..be115af946799 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/services/agent_and_policies_count.tsx @@ -0,0 +1,68 @@ +/* + * 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 { sendGetAgentPolicies, sendGetAgents } from '../../../hooks'; +import type { Output } from '../../../types'; +import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../constants'; + +export async function getAgentAndPolicyCountForOutput(output: Output) { + let kuery = `${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:"${output.id}" or ${AGENT_POLICY_SAVED_OBJECT_TYPE}.monitoring_output_id:"${output.id}"`; + if (output.is_default) { + kuery += ` or (not ${AGENT_POLICY_SAVED_OBJECT_TYPE}.data_output_id:*)`; + } + const agentPolicies = await sendGetAgentPolicies({ + kuery, + }); + + if (agentPolicies.error) { + throw agentPolicies.error; + } + const agentPolicyCount = agentPolicies.data?.items?.length ?? 0; + + let agentCount = 0; + if (agentPolicyCount > 0) { + const agents = await sendGetAgents({ + page: 1, + perPage: 0, // We only need the count here + showInactive: false, + kuery: agentPolicies.data?.items.map((policy) => `policy_id:"${policy.id}"`).join(' or '), + }); + + if (agents.error) { + throw agents.error; + } + + agentCount = agents.data?.total ?? 0; + } + + return { agentPolicyCount, agentCount }; +} + +export async function getAgentAndPolicyCount() { + const agentPolicies = await sendGetAgentPolicies({ + perPage: 0, + }); + + if (agentPolicies.error) { + throw agentPolicies.error; + } + const agentPolicyCount = agentPolicies.data?.total ?? 0; + + const agents = await sendGetAgents({ + page: 1, + perPage: 0, // We only need the count here + showInactive: false, + }); + + if (agents.error) { + throw agents.error; + } + + const agentCount = agents.data?.total ?? 0; + + return { agentPolicyCount, agentCount }; +} diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index 39b6e4c6da075..3f90dcb65a151 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -16,7 +16,8 @@ export type StaticPage = | 'enrollment_tokens' | 'data_streams' | 'settings' - | 'settings_edit_fleet_server_hosts'; + | 'settings_edit_fleet_server_hosts' + | 'settings_create_outputs'; export type DynamicPage = | 'integrations_all' @@ -33,7 +34,8 @@ export type DynamicPage = | 'upgrade_package_policy' | 'agent_list' | 'agent_details' - | 'agent_details_logs'; + | 'agent_details_logs' + | 'settings_edit_outputs'; export type Page = StaticPage | DynamicPage; @@ -61,6 +63,8 @@ export const FLEET_ROUTING_PATHS = { data_streams: '/data-streams', settings: '/settings', settings_edit_fleet_server_hosts: '/settings/edit-fleet-server-hosts', + settings_create_outputs: '/settings/create-outputs', + settings_edit_outputs: '/settings/outputs/:outputId', // TODO: Move this to the integrations app add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?', @@ -154,4 +158,9 @@ export const pagePathGetters: { FLEET_BASE_PATH, FLEET_ROUTING_PATHS.settings_edit_fleet_server_hosts, ], + settings_edit_outputs: ({ outputId }) => [ + FLEET_BASE_PATH, + FLEET_ROUTING_PATHS.settings_edit_outputs.replace(':outputId', outputId), + ], + settings_create_outputs: () => [FLEET_BASE_PATH, FLEET_ROUTING_PATHS.settings_create_outputs], }; diff --git a/x-pack/plugins/fleet/public/hooks/use_input.ts b/x-pack/plugins/fleet/public/hooks/use_input.ts index 908c3f4f717ca..1c89fb232a66e 100644 --- a/x-pack/plugins/fleet/public/hooks/use_input.ts +++ b/x-pack/plugins/fleet/public/hooks/use_input.ts @@ -5,20 +5,38 @@ * 2.0. */ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import type React from 'react'; +import type { EuiSwitchEvent } from '@elastic/eui'; -export function useInput(defaultValue = '', validate?: (value: string) => string[] | undefined) { +export function useInput( + defaultValue = '', + validate?: (value: string) => string[] | undefined, + disabled: boolean = false +) { const [value, setValue] = useState(defaultValue); const [errors, setErrors] = useState(); + const [hasChanged, setHasChanged] = useState(false); - const onChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - setValue(newValue); - if (errors && validate && validate(newValue) === undefined) { - setErrors(undefined); + const onChange = useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value; + setValue(newValue); + if (errors && validate && validate(newValue) === undefined) { + setErrors(undefined); + } + }, + [errors, validate] + ); + + useEffect(() => { + if (hasChanged) { + return; } - }; + if (value !== defaultValue) { + setHasChanged(true); + } + }, [hasChanged, value, defaultValue]); const isInvalid = errors !== undefined; @@ -29,6 +47,7 @@ export function useInput(defaultValue = '', validate?: (value: string) => string onChange, value, isInvalid, + disabled, }, formRowProps: { error: errors, @@ -47,16 +66,62 @@ export function useInput(defaultValue = '', validate?: (value: string) => string return true; }, setValue, + hasChanged, + }; +} + +export function useSwitchInput(defaultValue = false, disabled = false) { + const [value, setValue] = useState(defaultValue); + const [hasChanged, setHasChanged] = useState(false); + + useEffect(() => { + if (hasChanged) { + return; + } + if (value !== defaultValue) { + setHasChanged(true); + } + }, [hasChanged, value, defaultValue]); + + const onChange = (e: EuiSwitchEvent) => { + const newValue = e.target.checked; + setValue(newValue); + }; + + return { + value, + props: { + onChange, + checked: value, + disabled, + }, + formRowProps: {}, + setValue, + hasChanged, }; } export function useComboInput( id: string, defaultValue: string[] = [], - validate?: (value: string[]) => Array<{ message: string; index?: number }> | undefined + validate?: (value: string[]) => Array<{ message: string; index?: number }> | undefined, + disabled = false ) { const [value, setValue] = useState(defaultValue); const [errors, setErrors] = useState | undefined>(); + const [hasChanged, setHasChanged] = useState(false); + + useEffect(() => { + if (hasChanged) { + return; + } + if ( + value.length !== defaultValue.length || + value.some((val, idx) => val !== defaultValue[idx]) + ) { + setHasChanged(true); + } + }, [hasChanged, value, defaultValue]); const isInvalid = errors !== undefined; @@ -77,6 +142,7 @@ export function useComboInput( onChange, errors, isInvalid, + disabled, }, value, clear: () => { @@ -93,5 +159,6 @@ export function useComboInput( return true; }, + hasChanged, }; } diff --git a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts index 214fa5f5ed142..8c56ee811e465 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/outputs.ts @@ -6,7 +6,7 @@ */ import { outputRoutesService } from '../../services'; -import type { PutOutputRequest, GetOutputsResponse } from '../../types'; +import type { PutOutputRequest, GetOutputsResponse, PostOutputRequest } from '../../types'; import { sendRequest, useRequest } from './use_request'; @@ -31,3 +31,18 @@ export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) body, }); } + +export function sendPostOutput(body: PostOutputRequest['body']) { + return sendRequest({ + method: 'post', + path: outputRoutesService.getCreatePath(), + body, + }); +} + +export function sendDeleteOutput(outputId: string) { + return sendRequest({ + method: 'delete', + path: outputRoutesService.getDeletePath(outputId), + }); +} diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index d61223f0cebc7..ead44a798cfc7 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -75,6 +75,7 @@ export type { GetOutputsResponse, PutOutputRequest, PutOutputResponse, + PostOutputRequest, GetSettingsResponse, PutSettingsRequest, PutSettingsResponse, diff --git a/x-pack/plugins/fleet/server/routes/output/handler.ts b/x-pack/plugins/fleet/server/routes/output/handler.ts index 98b42775ae223..21d337455b1ec 100644 --- a/x-pack/plugins/fleet/server/routes/output/handler.ts +++ b/x-pack/plugins/fleet/server/routes/output/handler.ts @@ -21,6 +21,7 @@ import type { } from '../../../common'; import { outputService } from '../../services/output'; import { defaultIngestErrorHandler } from '../../errors'; +import { agentPolicyService } from '../../services'; export const getOutputsHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; @@ -68,9 +69,15 @@ export const putOuputHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { await outputService.update(soClient, request.params.outputId, request.body); const output = await outputService.get(soClient, request.params.outputId); + if (output.is_default || output.is_default_monitoring) { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); + } else { + await agentPolicyService.bumpAllAgentPoliciesForOutput(soClient, esClient, output.id); + } const body: GetOneOutputResponse = { item: output, @@ -94,9 +101,13 @@ export const postOuputHandler: RequestHandler< TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; try { const { id, ...data } = request.body; const output = await outputService.create(soClient, data, { id }); + if (output.is_default || output.is_default_monitoring) { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); + } const body: GetOneOutputResponse = { item: output, diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 0626caa37df9a..511aba4e6a932 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -193,6 +193,8 @@ class OutputService { type: SAVED_OBJECT_TYPE, page: 1, perPage: SO_SEARCH_LIMIT, + sortField: 'is_default', + sortOrder: 'desc', }); return { diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index 840dbd0ccb607..2e17c4ffe898e 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -22,6 +22,8 @@ export const AgentPolicyBaseSchema = { schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)]) ) ), + data_output_id: schema.maybe(schema.string()), + data_monitoring_output_id: schema.maybe(schema.string()), }; export const NewAgentPolicySchema = schema.object({ diff --git a/x-pack/plugins/fleet/server/types/rest_spec/output.ts b/x-pack/plugins/fleet/server/types/rest_spec/output.ts index 05a307009d527..dc60b26087219 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/output.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/output.ts @@ -39,6 +39,7 @@ export const PutOutputRequestSchema = { outputId: schema.string(), }), body: schema.object({ + type: schema.maybe(schema.oneOf([schema.literal('elasticsearch')])), name: schema.maybe(schema.string()), is_default: schema.maybe(schema.boolean()), is_default_monitoring: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 958f36d227cc6..bb6d573653964 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -31,7 +31,10 @@ import { import { ACTION_VISUALIZE_LENS_FIELD } from '../../../../../src/plugins/ui_actions/public'; import { LensAttributeService } from '../lens_attribute_service'; import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, +} from '../../../../../src/plugins/kibana_react/public'; import { makeConfigureStore, navigateAway, @@ -254,24 +257,26 @@ export async function mountApp( const PresentationUtilContext = getPresentationUtilContext(); render( - - - - - - - } - /> - - - - - - - , + + + + + + + + } + /> + + + + + + + + , params.element ); return () => { diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 03691d56ee56a..b3d4f95faadd9 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -12,8 +12,9 @@ import { I18nProvider } from '@kbn/i18n/react'; import type { IAggType } from 'src/plugins/data/public'; import { PaletteRegistry } from 'src/plugins/charts/public'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, ThemeServiceStart } from 'kibana/public'; import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { DatatableComponent } from './components/table_basic'; import type { ILensInterpreterRenderHandlers } from '../types'; @@ -25,6 +26,7 @@ export const getDatatableRenderer = (dependencies: { getType: Promise<(name: string) => IAggType>; paletteService: PaletteRegistry; uiSettings: IUiSettingsClient; + theme: ThemeServiceStart; }): ExpressionRenderDefinition => ({ name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { @@ -69,18 +71,20 @@ export const getDatatableRenderer = (dependencies: { } ReactDOM.render( - - - , + + + + + , domNode, () => { handlers.done(); diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index 51f1f54cc03ce..5ab970e84d043 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -34,6 +34,7 @@ export class DatatableVisualization { expressions.registerRenderer(() => getDatatableRenderer({ formatFactory, + theme: core.theme, getType: core .getStartServices() .then(([_, { data: dataStart }]) => dataStart.search.aggs.types.get), @@ -42,7 +43,7 @@ export class DatatableVisualization { }) ); - return getDatatableVisualization({ paletteService: palettes }); + return getDatatableVisualization({ paletteService: palettes, theme: core.theme }); }); } } diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 4f27d33e0a94f..b3aabea46b874 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -18,6 +18,7 @@ import { } from '../types'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { layerTypes } from '../../common'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; function mockFrame(): FramePublicAPI { return { @@ -28,6 +29,7 @@ function mockFrame(): FramePublicAPI { const datatableVisualization = getDatatableVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + theme: themeServiceMock.createStartContract(), }); describe('Datatable Visualization', () => { diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 903dd7bcaa74a..cdfdb833f019a 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -11,6 +11,8 @@ import { Ast } from '@kbn/interpreter/common'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { SuggestionRequest, Visualization, @@ -40,8 +42,10 @@ const visualizationLabel = i18n.translate('xpack.lens.datatable.label', { export const getDatatableVisualization = ({ paletteService, + theme, }: { paletteService: PaletteRegistry; + theme: ThemeServiceStart; }): Visualization => ({ id: 'lnsDatatable', @@ -295,9 +299,11 @@ export const getDatatableVisualization = ({ }, renderDimensionEditor(domElement, props) { render( - - - , + + + + + , domElement ); }, @@ -405,9 +411,11 @@ export const getDatatableVisualization = ({ renderToolbar(domElement, props) { render( - - - , + + + + + , domElement ); }, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx index 59d6325e1c0ce..4300b67856d8a 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.test.tsx @@ -18,7 +18,7 @@ import { Query, TimeRange, Filter, IndexPatternsContract } from 'src/plugins/dat import { Document } from '../persistence'; import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public/embeddable'; -import { coreMock, httpServiceMock } from '../../../../../src/core/public/mocks'; +import { coreMock, httpServiceMock, themeServiceMock } from '../../../../../src/core/public/mocks'; import { IBasePath } from '../../../../../src/core/public'; import { AttributeService, ViewMode } from '../../../../../src/plugins/embeddable/public'; import { LensAttributeService } from '../lens_attribute_service'; @@ -126,6 +126,7 @@ describe('embeddable', () => { }, inspector: inspectorPluginMock.createStartContract(), getTrigger, + theme: themeServiceMock.createStartContract(), visualizationMap: {}, documentToExpression: () => Promise.resolve({ @@ -168,6 +169,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -213,6 +215,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -260,6 +263,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -303,6 +307,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -343,6 +348,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -386,6 +392,7 @@ describe('embeddable', () => { capabilities: { canSaveDashboards: true, canSaveVisualizations: true }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -436,6 +443,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -484,6 +492,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -539,6 +548,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -595,6 +605,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -654,6 +665,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -697,6 +709,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -740,6 +753,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -783,6 +797,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -841,6 +856,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -915,6 +931,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -964,6 +981,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -1013,6 +1031,7 @@ describe('embeddable', () => { }, getTrigger, visualizationMap: {}, + theme: themeServiceMock.createStartContract(), documentToExpression: () => Promise.resolve({ ast: { @@ -1082,6 +1101,7 @@ describe('embeddable', () => { canSaveVisualizations: true, }, getTrigger, + theme: themeServiceMock.createStartContract(), visualizationMap: { [visDocument.visualizationType as string]: { onEditAction: onEditActionMock, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 2960e74efe97f..da4a97049a519 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -27,6 +27,7 @@ import { map, distinctUntilChanged, skip } from 'rxjs/operators'; import fastIsEqual from 'fast-deep-equal'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { METRIC_TYPE } from '@kbn/analytics'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -58,7 +59,7 @@ import { import { IndexPatternsContract } from '../../../../../src/plugins/data/public'; import { getEditPath, DOC_TYPE, PLUGIN_ID } from '../../common'; -import { IBasePath } from '../../../../../src/core/public'; +import { IBasePath, ThemeServiceStart } from '../../../../../src/core/public'; import { LensAttributeService } from '../lens_attribute_service'; import type { ErrorMessage } from '../editor_frame_service/types'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; @@ -111,6 +112,7 @@ export interface LensEmbeddableDeps { capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean }; usageCollection?: UsageCollectionSetup; spaces?: SpacesPluginStart; + theme: ThemeServiceStart; } const getExpressionFromDocument = async ( @@ -399,29 +401,31 @@ export class Embeddable const input = this.getInput(); render( - { - this.logError('runtime'); - }} - />, + + { + this.logError('runtime'); + }} + /> + , domNode ); } diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 811f391e32f9a..63b07affcb9ed 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Capabilities, HttpSetup } from 'kibana/public'; +import type { Capabilities, HttpSetup, ThemeServiceStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/common'; @@ -42,6 +42,7 @@ export interface LensEmbeddableStartServices { ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; visualizationMap: VisualizationMap; spaces?: SpacesPluginStart; + theme: ThemeServiceStart; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -94,6 +95,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, capabilities, usageCollection, + theme, inspector, spaces, } = await this.getStartServices(); @@ -117,6 +119,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { canSaveVisualizations: Boolean(capabilities.visualize.save), }, usageCollection, + theme, spaces, }, input, diff --git a/x-pack/plugins/lens/public/heatmap_visualization/index.ts b/x-pack/plugins/lens/public/heatmap_visualization/index.ts index 017af3187acd3..fbf8f5e0510ac 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/index.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/index.ts @@ -19,7 +19,8 @@ export class HeatmapVisualization { editorFrame.registerVisualization(async () => { const { getHeatmapVisualization } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - return getHeatmapVisualization({ paletteService: palettes }); + + return getHeatmapVisualization({ paletteService: palettes, theme: core.theme }); }); } } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts index 767905077eb71..68039e79ea45c 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.test.ts @@ -23,6 +23,7 @@ import type { HeatmapVisualizationState } from './types'; import type { DatasourcePublicAPI, Operation } from '../types'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { layerTypes } from '../../common'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; function exampleState(): HeatmapVisualizationState { return { @@ -46,6 +47,7 @@ function exampleState(): HeatmapVisualizationState { } const paletteService = chartPluginMock.createPaletteRegistry(); +const theme = themeServiceMock.createStartContract(); describe('heatmap', () => { let frame: ReturnType; @@ -56,7 +58,7 @@ describe('heatmap', () => { describe('#intialize', () => { test('returns a default state', () => { - expect(getHeatmapVisualization({ paletteService }).initialize(() => 'l1')).toEqual({ + expect(getHeatmapVisualization({ paletteService, theme }).initialize(() => 'l1')).toEqual({ layerId: 'l1', layerType: layerTypes.DATA, title: 'Empty Heatmap chart', @@ -79,7 +81,10 @@ describe('heatmap', () => { test('returns persisted state', () => { expect( - getHeatmapVisualization({ paletteService }).initialize(() => 'test-layer', exampleState()) + getHeatmapVisualization({ paletteService, theme }).initialize( + () => 'test-layer', + exampleState() + ) ).toEqual(exampleState()); }); }); @@ -117,6 +122,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getConfiguration({ state, frame, layerId: 'first' }) ).toEqual({ groups: [ @@ -174,6 +180,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getConfiguration({ state, frame, layerId: 'first' }) ).toEqual({ groups: [ @@ -226,6 +233,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getConfiguration({ state, frame, layerId: 'first' }) ).toEqual({ groups: [ @@ -280,6 +288,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).setDimension({ prevState, layerId: 'first', @@ -304,6 +313,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).removeDimension({ prevState, layerId: 'first', @@ -322,6 +332,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getSupportedLayers() ).toHaveLength(1); }); @@ -336,6 +347,7 @@ describe('heatmap', () => { }; const instance = getHeatmapVisualization({ paletteService, + theme, }); expect(instance.getLayerType('test-layer', state)).toEqual(layerTypes.DATA); expect(instance.getLayerType('foo', state)).toBeUndefined(); @@ -369,6 +381,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).toExpression(state, datasourceLayers) ).toEqual({ type: 'expression', @@ -454,6 +467,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).toExpression(state, datasourceLayers, attributes) ).toEqual(null); }); @@ -485,6 +499,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).toPreviewExpression!(state, datasourceLayers) ).toEqual({ type: 'expression', @@ -561,6 +576,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getErrorMessages(mockState) ).toEqual(undefined); }); @@ -573,6 +589,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getErrorMessages(mockState) ).toEqual([ { @@ -605,6 +622,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getWarningMessages!(mockState, frame) ).toEqual(undefined); }); @@ -625,6 +643,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getWarningMessages!(mockState, frame) ).toEqual(undefined); }); @@ -650,6 +669,7 @@ describe('heatmap', () => { expect( getHeatmapVisualization({ paletteService, + theme, }).getWarningMessages!(mockState, frame) ).toHaveLength(1); }); diff --git a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx index cb4c5ffdc48ee..fb9c30e2abeff 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/visualization.tsx @@ -11,6 +11,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { Ast } from '@kbn/interpreter/common'; import { Position } from '@elastic/charts'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { PaletteRegistry } from '../../../../../src/plugins/charts/public'; import type { OperationMetadata, Visualization } from '../types'; import type { HeatmapVisualizationState } from './types'; @@ -39,6 +41,7 @@ const groupLabelForHeatmap = i18n.translate('xpack.lens.heatmapVisualization.hea interface HeatmapVisualizationDeps { paletteService: PaletteRegistry; + theme: ThemeServiceStart; } function getAxisName(axis: 'x' | 'y') { @@ -95,6 +98,7 @@ function computePaletteParams(params: CustomPaletteParams) { export const getHeatmapVisualization = ({ paletteService, + theme, }: HeatmapVisualizationDeps): Visualization => ({ id: LENS_HEATMAP_ID, @@ -258,18 +262,22 @@ export const getHeatmapVisualization = ({ renderDimensionEditor(domElement, props) { render( - - - , + + + + + , domElement ); }, renderToolbar(domElement, props) { render( - - - , + + + + + , domElement ); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 8924684621995..032bde0866306 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -46,7 +46,10 @@ import { isColumnInvalid, isDraggedField, normalizeOperationDataType } from './u import { LayerPanel } from './layerpanel'; import { GenericIndexPatternColumn, getErrorMessages, insertNewColumn } from './operations'; import { IndexPatternField, IndexPatternPrivateState, IndexPatternPersistedState } from './types'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + KibanaThemeProvider, +} from '../../../../../src/plugins/kibana_react/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../src/plugins/ui_actions/public'; import { mergeLayer } from './state_helpers'; @@ -224,18 +227,20 @@ export function getIndexPatternDatasource({ props: DatasourceDataPanelProps ) { render( - - - , + + + + + , domElement ); }, @@ -285,21 +290,26 @@ export function getIndexPatternDatasource({ const columnLabelMap = indexPatternDatasource.uniqueLabels(props.state); render( - - - - - , + + + + + + + , domElement ); }, @@ -311,30 +321,32 @@ export function getIndexPatternDatasource({ const columnLabelMap = indexPatternDatasource.uniqueLabels(props.state); render( - - - - - , + + + + + + + , domElement ); }, @@ -344,21 +356,23 @@ export function getIndexPatternDatasource({ props: DatasourceLayerPanelProps ) => { render( - { - changeLayerIndexPattern({ - indexPatternId, - setState: props.setState, - state: props.state, - layerId: props.layerId, - onError: onIndexPatternLoadError, - replaceIfPossible: true, - storage, - indexPatternsService, - }); - }} - {...props} - />, + + { + changeLayerIndexPattern({ + indexPatternId, + setState: props.setState, + state: props.state, + layerId: props.layerId, + onError: onIndexPatternLoadError, + replaceIfPossible: true, + storage, + indexPatternsService, + }); + }} + {...props} + /> + , domElement ); }, diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.tsx index fd4b4f20c5c50..2ec46cf28d87d 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.tsx @@ -9,7 +9,8 @@ import './expression.scss'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import ReactDOM from 'react-dom'; -import { IUiSettingsClient } from 'kibana/public'; +import { IUiSettingsClient, ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { ExpressionRenderDefinition, IInterpreterRenderHandlers, @@ -29,7 +30,8 @@ export type { MetricChartProps, MetricState, MetricConfig } from '../../common/e export const getMetricChartRenderer = ( formatFactory: FormatFactory, - uiSettings: IUiSettingsClient + uiSettings: IUiSettingsClient, + theme: ThemeServiceStart ): ExpressionRenderDefinition => ({ name: 'lens_metric_chart_renderer', displayName: 'Metric chart', @@ -38,9 +40,11 @@ export const getMetricChartRenderer = ( reuseDomNode: true, render: (domNode: Element, config: MetricChartProps, handlers: IInterpreterRenderHandlers) => { ReactDOM.render( - - - , + + + + + , domNode, () => { handlers.done(); diff --git a/x-pack/plugins/lens/public/metric_visualization/index.ts b/x-pack/plugins/lens/public/metric_visualization/index.ts index bff5f71f8ba1d..8740a3af5435c 100644 --- a/x-pack/plugins/lens/public/metric_visualization/index.ts +++ b/x-pack/plugins/lens/public/metric_visualization/index.ts @@ -27,8 +27,10 @@ export class MetricVisualization { const { getMetricVisualization, getMetricChartRenderer } = await import('../async_services'); const palettes = await charts.palettes.getPalettes(); - expressions.registerRenderer(() => getMetricChartRenderer(formatFactory, core.uiSettings)); - return getMetricVisualization({ paletteService: palettes }); + expressions.registerRenderer(() => + getMetricChartRenderer(formatFactory, core.uiSettings, core.theme) + ); + return getMetricVisualization({ paletteService: palettes, theme: core.theme }); }); } } diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts index 889b711739fb3..bba08c1aa2442 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.test.ts @@ -13,6 +13,7 @@ import { generateId } from '../id_generator'; import { DatasourcePublicAPI, FramePublicAPI } from '../types'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; import { ColorMode } from 'src/plugins/charts/common'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; jest.mock('../id_generator'); @@ -36,6 +37,7 @@ function mockFrame(): FramePublicAPI { const metricVisualization = getMetricVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + theme: themeServiceMock.createStartContract(), }); describe('metric_visualization', () => { diff --git a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx index 63da7c4b44317..d9abf512c6524 100644 --- a/x-pack/plugins/lens/public/metric_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/visualization.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { render } from 'react-dom'; import { Ast } from '@kbn/interpreter/common'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { ColorMode } from '../../../../../src/plugins/charts/common'; import { PaletteRegistry } from '../../../../../src/plugins/charts/public'; import { getSuggestions } from './metric_suggestions'; @@ -73,8 +75,10 @@ const toExpression = ( }; export const getMetricVisualization = ({ paletteService, + theme, }: { paletteService: PaletteRegistry; + theme: ThemeServiceStart; }): Visualization => ({ id: 'lnsMetric', @@ -191,9 +195,11 @@ export const getMetricVisualization = ({ renderDimensionEditor(domElement, props) { render( - - - , + + + + + , domElement ); }, diff --git a/x-pack/plugins/lens/public/pie_visualization/expression.tsx b/x-pack/plugins/lens/public/pie_visualization/expression.tsx index d26289450bd0f..453f011540a60 100644 --- a/x-pack/plugins/lens/public/pie_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/expression.tsx @@ -13,6 +13,8 @@ import type { IInterpreterRenderHandlers, ExpressionRenderDefinition, } from 'src/plugins/expressions/public'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { LensFilterEvent } from '../types'; import { PieComponent } from './render_function'; import type { FormatFactory } from '../../common'; @@ -23,6 +25,7 @@ export const getPieRenderer = (dependencies: { formatFactory: FormatFactory; chartsThemeService: ChartsPluginSetup['theme']; paletteService: PaletteRegistry; + kibanaTheme: ThemeServiceStart; }): ExpressionRenderDefinition => ({ name: 'lens_pie_renderer', displayName: i18n.translate('xpack.lens.pie.visualizationName', { @@ -37,18 +40,20 @@ export const getPieRenderer = (dependencies: { }; ReactDOM.render( - - - , + + + + + , domNode, () => { handlers.done(); diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts index b4670b3b9c9dd..ce54f53c1cc93 100644 --- a/x-pack/plugins/lens/public/pie_visualization/index.ts +++ b/x-pack/plugins/lens/public/pie_visualization/index.ts @@ -37,9 +37,10 @@ export class PieVisualization { formatFactory, chartsThemeService: charts.theme, paletteService: palettes, + kibanaTheme: core.theme, }) ); - return getPieVisualization({ paletteService: palettes }); + return getPieVisualization({ paletteService: palettes, kibanaTheme: core.theme }); }); } } diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts index cdbd480297627..86ac635e36068 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.test.ts @@ -11,6 +11,7 @@ import { layerTypes } from '../../common'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { FramePublicAPI } from '../types'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; jest.mock('../id_generator'); @@ -18,6 +19,7 @@ const LAYER_ID = 'l1'; const pieVisualization = getPieVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), + kibanaTheme: themeServiceMock.createStartContract(), }); function getExampleState(): PieVisualizationState { diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index f72a6e5bef11e..6ca405e8a5eed 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -10,6 +10,8 @@ import { render } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import type { PaletteRegistry } from 'src/plugins/charts/public'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { Visualization, OperationMetadata, @@ -60,8 +62,10 @@ const applyPaletteToColumnConfig = ( export const getPieVisualization = ({ paletteService, + kibanaTheme, }: { paletteService: PaletteRegistry; + kibanaTheme: ThemeServiceStart; }): Visualization => ({ id: 'lnsPie', @@ -232,9 +236,11 @@ export const getPieVisualization = ({ }, renderDimensionEditor(domElement, props) { render( - - - , + + + + + , domElement ); }, @@ -260,9 +266,11 @@ export const getPieVisualization = ({ renderToolbar(domElement, props) { render( - - - , + + + + + , domElement ); }, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 5724e98f9bba9..b89492d7e7588 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -216,6 +216,7 @@ export class LensPlugin { usageCollection, inspector: plugins.inspector, spaces: plugins.spaces, + theme: core.theme, }; }; diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 60d0fe85ed546..78bcd41464cd3 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -43,6 +43,8 @@ import type { import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { RenderMode } from 'src/plugins/expressions'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import type { ILensInterpreterRenderHandlers, LensFilterEvent, LensBrushEvent } from '../types'; import type { LensMultiTable, FormatFactory } from '../../common'; import { layerTypes } from '../../common'; @@ -134,6 +136,7 @@ export const getXyChartRenderer = (dependencies: { paletteService: PaletteRegistry; timeZone: string; useLegacyTimeAxis: boolean; + kibanaTheme: ThemeServiceStart; }): ExpressionRenderDefinition => ({ name: 'lens_xy_chart_renderer', displayName: 'XY chart', @@ -156,23 +159,25 @@ export const getXyChartRenderer = (dependencies: { }; ReactDOM.render( - - - , + + + + + , domNode, () => handlers.done() ); diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 8ac9076919dd9..9697ba149e16e 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -39,9 +39,15 @@ export class XyVisualization { paletteService: palettes, timeZone: getTimeZone(core.uiSettings), useLegacyTimeAxis, + kibanaTheme: core.theme, }) ); - return getXyVisualization({ paletteService: palettes, fieldFormats, useLegacyTimeAxis }); + return getXyVisualization({ + paletteService: palettes, + fieldFormats, + useLegacyTimeAxis, + kibanaTheme: core.theme, + }); }); } } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index 033e324a5d02d..ea4db04080f65 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -14,11 +14,13 @@ import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { defaultReferenceLineColor } from './color_assignment'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), fieldFormats: fieldFormatsServiceMock.createStartContract(), + kibanaTheme: themeServiceMock.createStartContract(), useLegacyTimeAxis: false, }); let mockDatasource: ReturnType; diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 029cfe8ecbe40..086e05b3e4622 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -16,6 +16,7 @@ import { LensIconChartBar } from '../assets/chart_bar'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; import { Datatable } from 'src/plugins/expressions'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; function exampleState(): State { return { @@ -41,6 +42,7 @@ const xyVisualization = getXyVisualization({ paletteService: paletteServiceMock, fieldFormats: fieldFormatsMock, useLegacyTimeAxis: false, + kibanaTheme: themeServiceMock.createStartContract(), }); describe('xy_visualization', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 0ba74f4b8bb3a..67a8179ce6df8 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -13,6 +13,8 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { PaletteRegistry } from 'src/plugins/charts/public'; import { FieldFormatsStart } from 'src/plugins/field_formats/public'; +import { ThemeServiceStart } from 'kibana/public'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; import { getSuggestions } from './xy_suggestions'; import { XyToolbar, DimensionEditor } from './xy_config_panel'; import { LayerHeader } from './xy_config_panel/layer_header'; @@ -99,10 +101,12 @@ export const getXyVisualization = ({ paletteService, fieldFormats, useLegacyTimeAxis, + kibanaTheme, }: { paletteService: PaletteRegistry; fieldFormats: FieldFormatsStart; useLegacyTimeAxis: boolean; + kibanaTheme: ThemeServiceStart; }): Visualization => ({ id: 'lnsXY', @@ -565,31 +569,37 @@ export const getXyVisualization = ({ renderLayerHeader(domElement, props) { render( - - - , + + + + + , domElement ); }, renderToolbar(domElement, props) { render( - - - , + + + + + , domElement ); }, renderDimensionEditor(domElement, props) { render( - - - , + + + + + , domElement ); }, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts index fb2dd7aa85f36..d7b48553ce73a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts @@ -14,6 +14,7 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks' import { PaletteOutput } from 'src/plugins/charts/public'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; +import { themeServiceMock } from '../../../../../src/core/public/mocks'; jest.mock('../id_generator'); @@ -21,6 +22,7 @@ const xyVisualization = getXyVisualization({ paletteService: chartPluginMock.createPaletteRegistry(), fieldFormats: fieldFormatsServiceMock.createStartContract(), useLegacyTimeAxis: false, + kibanaTheme: themeServiceMock.createStartContract(), }); describe('xy_suggestions', () => { diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts index 8ee9d30ecbf89..e2d93a6937675 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts @@ -108,6 +108,40 @@ describe('getESFilters', () => { ]); }); + test('Should return phrase filters when field value is an array', async () => { + const esTooltipProperty = new ESTooltipProperty( + new TooltipProperty(featurePropertyField.getName(), await featurePropertyField.getLabel(), [ + 'my value', + 'my other value', + ]), + indexPattern, + featurePropertyField, + APPLY_GLOBAL_QUERY + ); + expect(await esTooltipProperty.getESFilters()).toEqual([ + { + meta: { + index: 'indexPatternId', + }, + query: { + match_phrase: { + ['machine.os']: 'my value', + }, + }, + }, + { + meta: { + index: 'indexPatternId', + }, + query: { + match_phrase: { + ['machine.os']: 'my other value', + }, + }, + }, + ]); + }); + test('Should return NOT exists filter for null values', async () => { const esTooltipProperty = new ESTooltipProperty( new TooltipProperty( diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts index 8b08d3a195a34..1b677b196c997 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts @@ -96,13 +96,16 @@ export class ESTooltipProperty implements ITooltipProperty { return []; } - const value = this.getRawValue(); - if (value == null) { + const rawValue = this.getRawValue(); + if (rawValue == null) { const existsFilter = esFilters.buildExistsFilter(indexPatternField, this._indexPattern); existsFilter.meta.negate = true; return [existsFilter]; } else { - return [esFilters.buildPhraseFilter(indexPatternField, value as string, this._indexPattern)]; + const values = Array.isArray(rawValue) ? (rawValue as string[]) : [rawValue as string]; + return values.map((value) => { + return esFilters.buildPhraseFilter(indexPatternField, value, this._indexPattern); + }); } } } diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx index f8f8e0ce59a4f..c9e422001da3e 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_editor.tsx @@ -14,7 +14,7 @@ import { extractLayerDescriptorParams } from './utils'; import { RegionMapVisParams } from './types'; import { title } from './region_map_vis_type'; -export function RegionMapEditor(props: VisEditorOptionsProps) { +function RegionMapEditor(props: VisEditorOptionsProps) { const onClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -32,3 +32,7 @@ export function RegionMapEditor(props: VisEditorOptionsProps) { return ; } + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default RegionMapEditor; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx index 1d3531bfed82a..19066a618ee79 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_renderer.tsx @@ -5,13 +5,17 @@ * 2.0. */ -import React from 'react'; +import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import type { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { RegionMapVisRenderValue } from './region_map_fn'; -import { RegionMapVisualization } from './region_map_visualization'; +import type { RegionMapVisRenderValue } from './region_map_fn'; +import { LazyWrapper } from '../../lazy_wrapper'; import { REGION_MAP_RENDER } from './types'; +const getLazyComponent = () => { + return lazy(() => import('./region_map_visualization')); +}; + export const regionMapRenderer = { name: REGION_MAP_RENDER, reuseDomNode: true, @@ -20,17 +24,16 @@ export const regionMapRenderer = { unmountComponentAtNode(domNode); }); - render( - { - handlers.done(); - }} - filters={filters} - query={query} - timeRange={timeRange} - visConfig={visConfig} - />, - domNode - ); + const props = { + onInitialRenderComplete: () => { + handlers.done(); + }, + filters, + query, + timeRange, + visConfig, + }; + + render(, domNode); }, } as ExpressionRenderDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx similarity index 71% rename from x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.ts rename to x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx index 4c6e4b2150fb1..473f83a270871 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.ts +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_vis_type.tsx @@ -5,16 +5,25 @@ * 2.0. */ +import React, { lazy } from 'react'; import { i18n } from '@kbn/i18n'; +import type { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; import { VisTypeDefinition } from '../../../../../../src/plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; import { REGION_MAP_VIS_TYPE, RegionMapVisParams } from './types'; -import { RegionMapEditor } from './region_map_editor'; +import { LazyWrapper } from '../../lazy_wrapper'; export const title = i18n.translate('xpack.maps.regionMapMap.vis.title', { defaultMessage: 'Region Map', }); +const LazyRegionMapEditor = function (props: VisEditorOptionsProps) { + const getLazyComponent = () => { + return lazy(() => import('./region_map_editor')); + }; + return ; +}; + export const regionMapVisType = { name: REGION_MAP_VIS_TYPE, title, @@ -27,7 +36,7 @@ export const regionMapVisType = { { name: '', title: '', - editor: RegionMapEditor, + editor: LazyRegionMapEditor, }, ], }, diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx index 5bb75d781e79b..50294e69868c4 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_visualization.tsx @@ -19,7 +19,7 @@ interface Props { onInitialRenderComplete: () => void; } -export function RegionMapVisualization(props: Props) { +function RegionMapVisualization(props: Props) { const mapCenter = { lat: props.visConfig.mapCenter[0], lon: props.visConfig.mapCenter[1], @@ -45,3 +45,7 @@ export function RegionMapVisualization(props: Props) { /> ); } + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default RegionMapVisualization; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx index 60efb807795af..81f440eef233e 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_editor.tsx @@ -14,7 +14,7 @@ import { extractLayerDescriptorParams } from './utils'; import { TileMapVisParams } from './types'; import { title } from './tile_map_vis_type'; -export function TileMapEditor(props: VisEditorOptionsProps) { +function TileMapEditor(props: VisEditorOptionsProps) { const onClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -32,3 +32,7 @@ export function TileMapEditor(props: VisEditorOptionsProps) { return ; } + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default TileMapEditor; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx index 5e61a0e0cd368..75569da5020c7 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_renderer.tsx @@ -5,13 +5,17 @@ * 2.0. */ -import React from 'react'; +import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import type { ExpressionRenderDefinition } from 'src/plugins/expressions'; -import { TileMapVisRenderValue } from './tile_map_fn'; -import { TileMapVisualization } from './tile_map_visualization'; +import type { TileMapVisRenderValue } from './tile_map_fn'; +import { LazyWrapper } from '../../lazy_wrapper'; import { TILE_MAP_RENDER } from './types'; +const getLazyComponent = () => { + return lazy(() => import('./tile_map_visualization')); +}; + export const tileMapRenderer = { name: TILE_MAP_RENDER, reuseDomNode: true, @@ -20,17 +24,16 @@ export const tileMapRenderer = { unmountComponentAtNode(domNode); }); - render( - { - handlers.done(); - }} - filters={filters} - query={query} - timeRange={timeRange} - visConfig={visConfig} - />, - domNode - ); + const props = { + onInitialRenderComplete: () => { + handlers.done(); + }, + filters, + query, + timeRange, + visConfig, + }; + + render(, domNode); }, } as ExpressionRenderDefinition; diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx similarity index 72% rename from x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.ts rename to x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx index 458adcab8c8d1..3ecf8c24e200a 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.ts +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_vis_type.tsx @@ -5,16 +5,25 @@ * 2.0. */ +import React, { lazy } from 'react'; import { i18n } from '@kbn/i18n'; +import type { VisEditorOptionsProps } from 'src/plugins/visualizations/public'; import { VisTypeDefinition } from '../../../../../../src/plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; import { MapTypes, TileMapVisParams, TILE_MAP_VIS_TYPE } from './types'; -import { TileMapEditor } from './tile_map_editor'; +import { LazyWrapper } from '../../lazy_wrapper'; export const title = i18n.translate('xpack.maps.tileMap.vis.title', { defaultMessage: 'Coordinate Map', }); +const LazyTileMapEditor = function (props: VisEditorOptionsProps) { + const getLazyComponent = () => { + return lazy(() => import('./tile_map_editor')); + }; + return ; +}; + export const tileMapVisType = { name: TILE_MAP_VIS_TYPE, title, @@ -27,7 +36,7 @@ export const tileMapVisType = { { name: '', title: '', - editor: TileMapEditor, + editor: LazyTileMapEditor, }, ], }, diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx index 225b29de5652b..a6f3d7aa843fc 100644 --- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx +++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_visualization.tsx @@ -19,7 +19,7 @@ interface Props { onInitialRenderComplete: () => void; } -export function TileMapVisualization(props: Props) { +function TileMapVisualization(props: Props) { const mapCenter = { lat: props.visConfig.mapCenter[0], lon: props.visConfig.mapCenter[1], @@ -45,3 +45,7 @@ export function TileMapVisualization(props: Props) { /> ); } + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default TileMapVisualization; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx index 9ee583ba2d3e7..631b916202505 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/ml_job_editor/ml_job_editor.tsx @@ -37,7 +37,12 @@ export const MLJobEditor: FC = ({ onChange = () => {}, }) => { if (mode === ML_EDITOR_MODE.XJSON) { - value = expandLiteralStrings(value); + try { + value = expandLiteralStrings(value); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } } return ( diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index c2b98ab1b0c29..c8e31003cb2eb 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -471,7 +471,24 @@ export function getMlClient( }, async updateDatafeed(...p: Parameters) { await datafeedIdsCheck(p); - return mlClient.updateDatafeed(...p); + + // Temporary workaround for the incorrect updateDatafeed function in the esclient + if (p.length === 0 || p[0] === undefined) { + // Temporary generic error message. This should never be triggered + // but is added for type correctness below + throw new Error('Incorrect arguments supplied'); + } + const { datafeed_id: id, body } = p[0]; + + return client.asInternalUser.transport.request({ + method: 'POST', + path: `/_ml/datafeeds/${id}/_update`, + body, + }); + + // this should be reinstated once https://github.com/elastic/elasticsearch-js/issues/1601 + // is fixed + // return mlClient.updateDatafeed(...p); }, async updateFilter(...p: Parameters) { return mlClient.updateFilter(...p); diff --git a/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx b/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx index 1ec447ee2f84a..dec8ab7c0defc 100644 --- a/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx +++ b/x-pack/plugins/monitoring/public/components/apm/apm_metrics.tsx @@ -30,8 +30,13 @@ interface TitleType { title?: string; heading?: unknown; } + +interface Stats { + versions: string[]; + [key: string]: unknown; +} interface Props { - stats: { versions: string[]; [key: string]: unknown }; + stats: Stats; metrics: { [key: string]: unknown }; seriesToShow: unknown[]; title: string; @@ -41,6 +46,7 @@ interface Props { container: boolean; }; }; + StatusComponent: React.FC<{ stats: Stats }>; } const createCharts = (series: unknown[], props: Partial) => { @@ -76,7 +82,15 @@ const getHeading = (isFleetTypeMetric: boolean) => { return titles; }; -export const ApmMetrics = ({ stats, metrics, seriesToShow, title, summary, ...props }: Props) => { +export const ApmMetrics = ({ + stats, + metrics, + seriesToShow, + title, + summary, + StatusComponent, + ...props +}: Props) => { if (!metrics) { return null; } @@ -96,7 +110,7 @@ export const ApmMetrics = ({ stats, metrics, seriesToShow, title, summary, ...pr

{titles.heading as FormattedMessage}

- + diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js index f52ca7cf8ad49..19485c2b8a38c 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js @@ -8,6 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ApmMetrics } from '../apm_metrics'; +import { Status } from './status'; const title = i18n.translate('xpack.monitoring.apm.instance.panels.title', { defaultMessage: 'APM Server - Metrics', @@ -29,5 +30,5 @@ export function ApmServerInstance(props) { const stats = props.summary; const metricProps = { ...props, title, seriesToShow, stats }; - return ; + return ; } diff --git a/x-pack/plugins/monitoring/public/components/apm/overview/index.js b/x-pack/plugins/monitoring/public/components/apm/overview/index.js index ec622535444bc..b61a2c432276e 100644 --- a/x-pack/plugins/monitoring/public/components/apm/overview/index.js +++ b/x-pack/plugins/monitoring/public/components/apm/overview/index.js @@ -8,6 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { ApmMetrics } from '../apm_metrics'; +import { Status } from './status'; const title = i18n.translate('xpack.monitoring.apm.overview.panels.title', { defaultMessage: 'APM Server - Metrics', @@ -23,7 +24,6 @@ export function ApmOverview(props) { metrics.apm_requests, metrics.apm_transformations, ]; - const metricProps = { ...props, title, seriesToShow }; - return ; + return ; } diff --git a/x-pack/plugins/monitoring/public/components/apm/overview/status.js b/x-pack/plugins/monitoring/public/components/apm/overview/status.js new file mode 100644 index 0000000000000..455c4e81ea6bb --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/apm/overview/status.js @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Status as InstancesStatus } from '../instances/status'; + +export function Status({ stats }) { + return ; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx index 23500b63e900a..b5ccda3700318 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx @@ -8,18 +8,18 @@ import React from 'react'; import { ExpViewActionMenuContent } from './action_menu'; import HeaderMenuPortal from '../../../header_menu_portal'; -import { usePluginContext } from '../../../../../hooks/use_plugin_context'; import { TypedLensByValueInput } from '../../../../../../../lens/public'; +import { useExploratoryView } from '../../contexts/exploatory_view_config'; interface Props { timeRange?: { from: string; to: string }; lensAttributes: TypedLensByValueInput['attributes'] | null; } export function ExpViewActionMenu(props: Props) { - const { appMountParameters } = usePluginContext(); + const { setHeaderActionMenu } = useExploratoryView(); return ( - + ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 3f6551986527c..413d41acef5da 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -5,48 +5,32 @@ * 2.0. */ -import { AppDataType, ReportViewType } from '../types'; -import { getRumDistributionConfig } from './rum/data_distribution_config'; -import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config'; -import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; -import { getKPITrendsLensConfig } from './rum/kpi_over_time_config'; +import { AppDataType, ReportViewType, SeriesConfig } from '../types'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; -import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; -import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; -import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; -import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; -import { DataTypes, ReportTypes } from './constants'; +import { ReportConfigMap } from '../contexts/exploatory_view_config'; interface Props { reportType: ReportViewType; indexPattern: IndexPattern; dataType: AppDataType; + reportConfigMap: ReportConfigMap; } -export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { - switch (dataType) { - case DataTypes.UX: - if (reportType === ReportTypes.DISTRIBUTION) { - return getRumDistributionConfig({ indexPattern }); - } - if (reportType === ReportTypes.CORE_WEB_VITAL) { - return getCoreWebVitalsConfig({ indexPattern }); - } - return getKPITrendsLensConfig({ indexPattern }); - case DataTypes.SYNTHETICS: - if (reportType === ReportTypes.DISTRIBUTION) { - return getSyntheticsDistributionConfig({ indexPattern }); - } - return getSyntheticsKPIConfig({ indexPattern }); - case DataTypes.MOBILE: - if (reportType === ReportTypes.DISTRIBUTION) { - return getMobileKPIDistributionConfig({ indexPattern }); - } - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { - return getMobileDeviceDistributionConfig({ indexPattern }); - } - return getMobileKPIConfig({ indexPattern }); - default: - return getKPITrendsLensConfig({ indexPattern }); - } +export const getDefaultConfigs = ({ + reportType, + dataType, + indexPattern, + reportConfigMap, +}: Props): SeriesConfig => { + let configResult: SeriesConfig; + + reportConfigMap[dataType].some((fn) => { + const config = fn({ indexPattern }); + if (config.reportType === reportType) { + configResult = config; + } + return config.reportType === reportType; + }); + + return configResult!; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 135cf3c59a1ce..9dc27b84bef0e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -18,6 +18,7 @@ import { import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; import { RECORDS_FIELD, REPORT_METRIC_FIELD, PERCENTILE_RANKS, ReportTypes } from './constants'; +import { obsvReportConfigMap } from '../obsv_exploratory_view'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -26,6 +27,7 @@ describe('Lens Attribute', () => { reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, + reportConfigMap: obsvReportConfigMap, }); reportViewConfig.baseFilters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); @@ -57,6 +59,7 @@ describe('Lens Attribute', () => { reportType: ReportTypes.KPI, dataType: 'ux', indexPattern: mockIndexPattern, + reportConfigMap: obsvReportConfigMap, }); const lnsAttrKpi = new LensAttributes([ @@ -81,6 +84,7 @@ describe('Lens Attribute', () => { reportType: ReportTypes.KPI, dataType: 'ux', indexPattern: mockIndexPattern, + reportConfigMap: obsvReportConfigMap, }); const lnsAttrKpi = new LensAttributes([ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 35e094996f6f2..0602e37f61a20 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -10,6 +10,7 @@ import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { obsvReportConfigMap } from '../../obsv_exploratory_view'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -18,6 +19,7 @@ describe('Core web vital config test', function () { reportType: 'core-web-vitals', dataType: 'ux', indexPattern: mockIndexPattern, + reportConfigMap: obsvReportConfigMap, }); let lnsAttr: LensAttributes; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploatory_view_config.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploatory_view_config.tsx new file mode 100644 index 0000000000000..fe7ea3884880a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/contexts/exploatory_view_config.tsx @@ -0,0 +1,65 @@ +/* + * 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, { createContext, useContext } from 'react'; +import { AppMountParameters } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import type { AppDataType, ConfigProps, ReportViewType, SeriesConfig } from '../types'; + +export type ReportConfigMap = Record SeriesConfig>>; + +interface ExploratoryViewContextValue { + dataTypes: Array<{ id: AppDataType; label: string }>; + reportTypes: Array<{ + reportType: ReportViewType | typeof SELECT_REPORT_TYPE; + label: string; + }>; + indexPatterns: Record; + reportConfigMap: ReportConfigMap; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +} + +export const ExploratoryViewContext = createContext({ + indexPatterns: {}, +} as ExploratoryViewContextValue); + +export function ExploratoryViewContextProvider({ + children, + reportTypes, + dataTypes, + indexPatterns, + reportConfigMap, + setHeaderActionMenu, +}: { children: JSX.Element } & ExploratoryViewContextValue) { + const value = { + reportTypes, + dataTypes, + indexPatterns, + reportConfigMap, + setHeaderActionMenu, + }; + + return ( + {children} + ); +} + +export function useExploratoryView() { + const context = useContext(ExploratoryViewContext); + + if (context === undefined) { + throw new Error('useExploratoryView must be used within a ExploratoryViewContextProvider'); + } + return context; +} + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx index 497435ec69a50..ba03640669330 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -15,6 +15,8 @@ import { getLayerConfigs } from '../hooks/use_lens_attributes'; import { LensPublicStart, XYState } from '../../../../../../lens/public'; import { OperationTypeComponent } from '../series_editor/columns/operation_type_select'; import { IndexPatternState } from '../hooks/use_app_index_pattern'; +import { ReportConfigMap } from '../contexts/exploatory_view_config'; +import { obsvReportConfigMap } from '../obsv_exploratory_view'; export interface ExploratoryEmbeddableProps { reportType: ReportViewType; @@ -25,6 +27,7 @@ export interface ExploratoryEmbeddableProps { axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings']; legendIsVisible?: boolean; dataTypesIndexPatterns?: Record; + reportConfigMap?: ReportConfigMap; } export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps { @@ -42,6 +45,7 @@ export default function Embeddable({ lens, axisTitlesVisibility, legendIsVisible, + reportConfigMap = {}, showCalculationMethod = false, }: ExploratoryEmbeddableComponentProps) { const LensComponent = lens?.EmbeddableComponent; @@ -51,7 +55,13 @@ export default function Embeddable({ const [operationType, setOperationType] = useState(series?.operationType); const theme = useTheme(); - const layerConfigs: LayerConfig[] = getLayerConfigs(attributes, reportType, theme, indexPatterns); + const layerConfigs: LayerConfig[] = getLayerConfigs( + attributes, + reportType, + theme, + indexPatterns, + { ...reportConfigMap, ...obsvReportConfigMap } + ); if (layerConfigs.length < 1) { return null; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 9bd611c05e956..674a2b04d3520 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { ObservabilityIndexPatterns } from '../utils/observability_index_patterns'; import { getDataHandler } from '../../../../data_handler'; +import { useExploratoryView } from '../contexts/exploatory_view_config'; export interface IndexPatternContext { loading: boolean; @@ -28,7 +29,7 @@ interface ProviderProps { children: JSX.Element; } -type HasAppDataState = Record; +type HasAppDataState = Record; export type IndexPatternState = Record; export type IndexPatternErrors = Record; type LoadingState = Record; @@ -39,27 +40,26 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { const [indexPatternErrors, setIndexPatternErrors] = useState( {} as IndexPatternErrors ); - const [hasAppData, setHasAppData] = useState({ - infra_metrics: null, - infra_logs: null, - synthetics: null, - ux: null, - apm: null, - mobile: null, - } as HasAppDataState); + const [hasAppData, setHasAppData] = useState({} as HasAppDataState); const { services: { data }, } = useKibana(); + const { indexPatterns: indexPatternsList } = useExploratoryView(); + const loadIndexPattern: IndexPatternContext['loadIndexPattern'] = useCallback( async ({ dataType }) => { - if (hasAppData[dataType] === null && !loading[dataType]) { + if (typeof hasAppData[dataType] === 'undefined' && !loading[dataType]) { setLoading((prevState) => ({ ...prevState, [dataType]: true })); try { let hasDataT = false; let indices: string | undefined = ''; + if (indexPatternsList[dataType]) { + indices = indexPatternsList[dataType]; + hasDataT = true; + } switch (dataType) { case 'ux': case 'synthetics': @@ -91,7 +91,7 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { } } }, - [data, hasAppData, loading] + [data, hasAppData, indexPatternsList, loading] ); return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx index 3334b69e5becc..654421dfdf60a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx @@ -17,6 +17,8 @@ import { TRANSACTION_DURATION } from '../configurations/constants/elasticsearch_ import * as lensAttributes from '../configurations/lens_attributes'; import * as indexPattern from './use_app_index_pattern'; import * as theme from '../../../../hooks/use_theme'; +import { dataTypes, obsvReportConfigMap, reportTypesList } from '../obsv_exploratory_view'; +import { ExploratoryViewContextProvider } from '../contexts/exploatory_view_config'; const mockSingleSeries = [ { @@ -51,7 +53,17 @@ describe('useExpViewTimeRange', function () { const lensAttributesSpy = jest.spyOn(lensAttributes, 'LensAttributes'); function Wrapper({ children }: { children: JSX.Element }) { - return {children}; + return ( + + {children} + + ); } it('updates lens attributes with report type from storage', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index f81494e8f9ac7..b2b7a3b6ba537 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -24,6 +24,7 @@ import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_c import { useTheme } from '../../../../hooks/use_theme'; import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; import { LABEL_FIELDS_BREAKDOWN } from '../configurations/constants'; +import { ReportConfigMap, useExploratoryView } from '../contexts/exploatory_view_config'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -40,7 +41,8 @@ export function getLayerConfigs( allSeries: AllSeries, reportType: ReportViewType, theme: EuiTheme, - indexPatterns: IndexPatternState + indexPatterns: IndexPatternState, + reportConfigMap: ReportConfigMap ) { const layerConfigs: LayerConfig[] = []; @@ -57,6 +59,7 @@ export function getLayerConfigs( reportType, indexPattern, dataType: series.dataType, + reportConfigMap, }); const filters: UrlFilter[] = (series.filters ?? []).concat( @@ -89,6 +92,8 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const { indexPatterns } = useAppIndexPatternContext(); + const { reportConfigMap } = useExploratoryView(); + const theme = useTheme(); return useMemo(() => { @@ -99,7 +104,13 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportTypeT) { return null; } - const layerConfigs = getLayerConfigs(allSeriesT, reportTypeT, theme, indexPatterns); + const layerConfigs = getLayerConfigs( + allSeriesT, + reportTypeT, + theme, + indexPatterns, + reportConfigMap + ); if (layerConfigs.length < 1) { return null; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 1fc38ab79de7f..5dbe0c2a6c078 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -23,23 +23,34 @@ import { UrlStorageContextProvider } from './hooks/use_series_storage'; import { useTrackPageview } from '../../..'; import { TypedLensByValueInput } from '../../../../../lens/public'; +export interface ExploratoryViewPageProps { + useSessionStorage?: boolean; + saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; + app?: { id: string; label: string }; +} + export function ExploratoryViewPage({ + app, saveAttributes, useSessionStorage = false, -}: { - useSessionStorage?: boolean; - saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; -}) { +}: ExploratoryViewPageProps) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); - useTrackPageview({ app: 'observability-overview', path: 'exploratory-view', delay: 15000 }); + useTrackPageview({ + app: 'observability-overview', + path: 'exploratory-view', + delay: 15000, + }); - useBreadcrumbs([ - { - text: i18n.translate('xpack.observability.overview.exploratoryView', { - defaultMessage: 'Explore data', - }), - }, - ]); + useBreadcrumbs( + [ + { + text: i18n.translate('xpack.observability.overview.exploratoryView', { + defaultMessage: 'Explore data', + }), + }, + ], + app + ); const { services: { uiSettings, notifications }, @@ -69,3 +80,6 @@ export function ExploratoryViewPage({ const Wrapper = euiStyled.div` padding: ${(props) => props.theme.eui.paddingSizes.l}; `; + +// eslint-disable-next-line import/no-default-export +export default ExploratoryViewPage; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx new file mode 100644 index 0000000000000..04e291b1b6629 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx @@ -0,0 +1,99 @@ +/* + * 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 * as React from 'react'; +import { i18n } from '@kbn/i18n'; +import { ExploratoryViewPage } from './index'; +import { ExploratoryViewContextProvider } from './contexts/exploatory_view_config'; +import { AppDataType, ReportViewType } from './types'; + +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from './configurations/constants/labels'; +import { SELECT_REPORT_TYPE } from './series_editor/series_editor'; +import { DataTypes } from './configurations/constants'; +import { getRumDistributionConfig } from './configurations/rum/data_distribution_config'; +import { getKPITrendsLensConfig } from './configurations/rum/kpi_over_time_config'; +import { getCoreWebVitalsConfig } from './configurations/rum/core_web_vitals_config'; +import { getSyntheticsKPIConfig } from './configurations/synthetics/kpi_over_time_config'; +import { getSyntheticsDistributionConfig } from './configurations/synthetics/data_distribution_config'; +import { getMobileKPIDistributionConfig } from './configurations/mobile/distribution_config'; +import { getMobileKPIConfig } from './configurations/mobile/kpi_over_time_config'; +import { getMobileDeviceDistributionConfig } from './configurations/mobile/device_distribution_config'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; + +export const DataTypesLabels = { + [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { + defaultMessage: 'User experience (RUM)', + }), + + [DataTypes.SYNTHETICS]: i18n.translate( + 'xpack.observability.overview.exploratoryView.syntheticsLabel', + { + defaultMessage: 'Synthetics monitoring', + } + ), + + [DataTypes.MOBILE]: i18n.translate( + 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', + { + defaultMessage: 'Mobile experience', + } + ), +}; +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { + id: DataTypes.SYNTHETICS, + label: DataTypesLabels[DataTypes.SYNTHETICS], + }, + { + id: DataTypes.UX, + label: DataTypesLabels[DataTypes.UX], + }, + { + id: DataTypes.MOBILE, + label: DataTypesLabels[DataTypes.MOBILE], + }, +]; + +export const reportTypesList: Array<{ + reportType: ReportViewType | typeof SELECT_REPORT_TYPE; + label: string; +}> = [ + { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, +]; + +export const obsvReportConfigMap = { + [DataTypes.UX]: [getKPITrendsLensConfig, getRumDistributionConfig, getCoreWebVitalsConfig], + [DataTypes.SYNTHETICS]: [getSyntheticsKPIConfig, getSyntheticsDistributionConfig], + [DataTypes.MOBILE]: [ + getMobileKPIConfig, + getMobileKPIDistributionConfig, + getMobileDeviceDistributionConfig, + ], +}; + +export function ObservabilityExploratoryView() { + const { appMountParameters } = usePluginContext(); + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 04d74844beb83..86d353fa8712f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -24,7 +24,12 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContext, IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; +import { + AllSeries, + reportTypeKey, + SeriesContextValue, + UrlStorageContext, +} from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -42,6 +47,8 @@ import { dataPluginMock } from '../../../../../../../src/plugins/data/public/moc import { ListItem } from '../../../hooks/use_values_list'; import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; import { casesPluginMock } from '../../../../../cases/public/mocks'; +import { dataTypes, obsvReportConfigMap, reportTypesList } from './obsv_exploratory_view'; +import { ExploratoryViewContextProvider } from './contexts/exploatory_view_config'; interface KibanaProps { services?: KibanaServices; @@ -194,9 +201,17 @@ export function render( return { ...reactTestLibRender( - - {ui} - + + + {ui} + + , renderOptions ), @@ -301,7 +316,13 @@ function mockSeriesStorageContext({ firstSeries: mockDataSeries[0], allSeries: mockDataSeries, setReportType: jest.fn(), - storage: { get: jest.fn().mockReturnValue(mockDataSeries) } as any, + storage: { + get: jest + .fn() + .mockImplementation((key: string) => + key === reportTypeKey ? 'data-distribution' : mockDataSeries + ), + } as any, } as SeriesContextValue; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx index 8ed279ace28f6..e213a41238123 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx @@ -12,12 +12,14 @@ import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { RECORDS_FIELD } from '../../configurations/constants'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; +import { obsvReportConfigMap } from '../../obsv_exploratory_view'; describe('Breakdowns', function () { const dataViewSeries = getDefaultConfigs({ reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', + reportConfigMap: obsvReportConfigMap, }); it('should render properly', async function () { @@ -62,6 +64,7 @@ describe('Breakdowns', function () { reportType: 'kpi-over-time', indexPattern: mockIndexPattern, dataType: 'ux', + reportConfigMap: obsvReportConfigMap, }); render( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx index fc96ad0741ec5..1afe1f979b272 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx @@ -8,8 +8,9 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; -import { DataTypesLabels, DataTypesSelect } from './data_type_select'; +import { DataTypesSelect } from './data_type_select'; import { DataTypes } from '../../configurations/constants'; +import { DataTypesLabels } from '../../obsv_exploratory_view'; describe('DataTypeSelect', function () { const seriesId = 0; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx index 71fd147e8e264..4efee7615a8ac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx @@ -18,6 +18,7 @@ import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { AppDataType, SeriesUrl } from '../../types'; import { DataTypes, ReportTypes } from '../../configurations/constants'; +import { useExploratoryView } from '../../contexts/exploatory_view_config'; interface Props { seriesId: number; @@ -26,41 +27,6 @@ interface Props { }; } -export const DataTypesLabels = { - [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { - defaultMessage: 'User experience (RUM)', - }), - - [DataTypes.SYNTHETICS]: i18n.translate( - 'xpack.observability.overview.exploratoryView.syntheticsLabel', - { - defaultMessage: 'Synthetics monitoring', - } - ), - - [DataTypes.MOBILE]: i18n.translate( - 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', - { - defaultMessage: 'Mobile experience', - } - ), -}; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { - id: DataTypes.SYNTHETICS, - label: DataTypesLabels[DataTypes.SYNTHETICS], - }, - { - id: DataTypes.UX, - label: DataTypesLabels[DataTypes.UX], - }, - { - id: DataTypes.MOBILE, - label: DataTypesLabels[DataTypes.MOBILE], - }, -]; - const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; export function DataTypesSelect({ seriesId, series }: Props) { @@ -77,6 +43,8 @@ export function DataTypesSelect({ seriesId, series }: Props) { } }; + const { dataTypes } = useExploratoryView(); + const options = dataTypes .filter(({ id }) => { if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { @@ -92,6 +60,8 @@ export function DataTypesSelect({ seriesId, series }: Props) { inputDisplay: label, })); + const currDataType = dataTypes.find((dt) => dt.id === series.dataType); + return ( <> {!series.dataType && ( @@ -122,7 +92,7 @@ export function DataTypesSelect({ seriesId, series }: Props) { )} {series.dataType && ( - {DataTypesLabels[series.dataType as DataTypes]} + {currDataType?.label} )} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx index 544a294e021e2..3ec2af4a8c9d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx @@ -16,6 +16,7 @@ import { render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; +import { obsvReportConfigMap } from '../../obsv_exploratory_view'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); @@ -25,6 +26,7 @@ describe('Series Builder ReportDefinitionCol', function () { reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', + reportConfigMap: obsvReportConfigMap, }); mockUseValuesList([{ label: 'elastic-co', count: 10 }]); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx index ddabdf83323cf..8ca4241891d5a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx @@ -10,43 +10,32 @@ import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { ReportViewType } from '../../types'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../../configurations/constants/labels'; -const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; +import { useExploratoryView } from '../../contexts/exploatory_view_config'; -export const reportTypesList: Array<{ - reportType: ReportViewType | typeof SELECT_REPORT_TYPE; - label: string; -}> = [ - { - reportType: SELECT_REPORT_TYPE, - label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { - defaultMessage: 'Select report type', - }), - }, - { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, -]; +const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; interface Props { prepend: string; } +const SELECT_REPORT = { + reportType: SELECT_REPORT_TYPE, + label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { + defaultMessage: 'Select report type', + }), +}; + export function ReportTypesSelect({ prepend }: Props) { const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); + const { reportTypes } = useExploratoryView(); + const onReportTypeChange = (reportType: ReportViewType) => { setReportType(reportType); }; - const options = reportTypesList + const options = [SELECT_REPORT, ...reportTypes] .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) .map(({ reportType, label }) => ({ value: reportType, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx index 64291f84f7662..9fcf4b14353b9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.test.tsx @@ -11,6 +11,7 @@ import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../ import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; +import { obsvReportConfigMap } from '../../obsv_exploratory_view'; describe('SelectedFilters', function () { mockAppIndexPattern(); @@ -19,6 +20,7 @@ describe('SelectedFilters', function () { reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', + reportConfigMap: obsvReportConfigMap, }); it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx index 83958840f63d9..afb1043e9caab 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx @@ -11,9 +11,11 @@ import { ExpandedSeriesRow } from './expanded_series_row'; import { mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; import { getDefaultConfigs } from '../configurations/default_configs'; import { PERCENTILE } from '../configurations/constants'; +import { obsvReportConfigMap } from '../obsv_exploratory_view'; describe('ExpandedSeriesRow', function () { const dataViewSeries = getDefaultConfigs({ + reportConfigMap: obsvReportConfigMap, reportType: 'kpi-over-time', indexPattern: mockIndexPattern, dataType: 'ux', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.tsx index 767b765ba1f19..829727d739b9c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.test.tsx @@ -11,12 +11,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../ import { getDefaultConfigs } from '../configurations/default_configs'; import { PERCENTILE } from '../configurations/constants'; import { ReportMetricOptions } from './report_metric_options'; +import { obsvReportConfigMap } from '../obsv_exploratory_view'; describe('ReportMetricOptions', function () { const dataViewSeries = getDefaultConfigs({ + dataType: 'ux', reportType: 'kpi-over-time', indexPattern: mockIndexPattern, - dataType: 'ux', + reportConfigMap: obsvReportConfigMap, }); it('should render properly', async function () { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 15040592686e0..155eedf0af74c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -17,6 +17,7 @@ import { getDefaultConfigs } from '../configurations/default_configs'; import { ReportTypesSelect } from './columns/report_type_select'; import { ViewActions } from '../views/view_actions'; import { Series } from './series'; +import { ReportConfigMap, useExploratoryView } from '../contexts/exploatory_view_config'; export interface ReportTypeItem { id: string; @@ -30,23 +31,26 @@ export const getSeriesToEdit = ({ indexPatterns, allSeries, reportType, + reportConfigMap, }: { allSeries: SeriesContextValue['allSeries']; indexPatterns: IndexPatternState; reportType: ReportViewType; + reportConfigMap: ReportConfigMap; }): BuilderItem[] => { const getDataViewSeries = (dataType: AppDataType) => { if (indexPatterns?.[dataType]) { return getDefaultConfigs({ dataType, reportType, + reportConfigMap, indexPattern: indexPatterns[dataType], }); } }; return allSeries.map((series, seriesIndex) => { - const seriesConfig = getDataViewSeries(series.dataType)!; + const seriesConfig = getDataViewSeries(series.dataType); return { id: seriesIndex, series, seriesConfig }; }); @@ -59,6 +63,8 @@ export const SeriesEditor = React.memo(function () { const { loading, indexPatterns } = useAppIndexPatternContext(); + const { reportConfigMap } = useExploratoryView(); + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>({}); const [{ prevCount, curCount }, setSeriesCount] = useState<{ @@ -83,6 +89,7 @@ export const SeriesEditor = React.memo(function () { reportType, allSeries, indexPatterns, + reportConfigMap, }); newEditorItems.forEach(({ series, id }) => { @@ -101,7 +108,7 @@ export const SeriesEditor = React.memo(function () { setItemIdToExpandedRowMap((prevState) => { return { ...prevState, ...newExpandRows }; }); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); + }, [allSeries, getSeries, indexPatterns, loading, reportConfigMap, reportType]); const toggleDetails = (item: BuilderItem) => { const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index 1c14ffe3d13c9..b37f0a05e26cc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -50,14 +50,14 @@ const appToPatternMap: Record = { }; const getAppIndicesWithPattern = (app: AppDataType, indices: string) => { - return `${appToPatternMap[app]},${indices}`; + return `${appToPatternMap?.[app] ?? app},${indices}`; }; const getAppIndexPatternId = (app: AppDataType, indices: string) => { // Replace characters / ? , " < > | * with _ const postfix = indices.replace(/[^A-Z0-9]+/gi, '_').toLowerCase(); - return `${indexPatternList[app]}_${postfix}`; + return `${indexPatternList?.[app] ?? app}_${postfix}`; }; export function isParamsSame(param1: IFieldFormat['_params'], param2: FieldFormatParams) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx index 71b16c9c0e682..06a5b8609b0e4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/add_series_button.tsx @@ -14,6 +14,7 @@ import { getSeriesToEdit } from '../series_editor/series_editor'; import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; +import { useExploratoryView } from '../contexts/exploatory_view_config'; export function AddSeriesButton() { const [editorItems, setEditorItems] = useState([]); @@ -21,9 +22,11 @@ export function AddSeriesButton() { const { loading, indexPatterns } = useAppIndexPatternContext(); + const { reportConfigMap } = useExploratoryView(); + useEffect(() => { - setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); + setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType, reportConfigMap })); + }, [allSeries, getSeries, indexPatterns, loading, reportConfigMap, reportType]); const addSeries = () => { const prevSeries = allSeries?.[0]; diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index e73cab3e4fae5..03eac23062273 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -11,7 +11,7 @@ import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; import type { FilterValueLabelProps } from './filter_value_label/filter_value_label'; import type { SelectableUrlListProps } from './exploratory_view/components/url_search/selectable_url_list'; - +import type { ExploratoryViewPageProps } from './exploratory_view/index'; export { createLazyObservabilityPageTemplate } from './page_template'; export type { LazyObservabilityPageTemplateProps } from './page_template'; @@ -66,3 +66,13 @@ export function SelectableUrlList(props: SelectableUrlListProps) { ); } + +const ExploratoryViewLazy = lazy(() => import('./exploratory_view/index')); + +export function ExploratoryView(props: ExploratoryViewPageProps) { + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts index 241a978d36948..899308e6d73bf 100644 --- a/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts @@ -34,7 +34,10 @@ function getTitleFromBreadCrumbs(breadcrumbs: ChromeBreadcrumb[]) { return breadcrumbs.map(({ text }) => text?.toString() ?? '').reverse(); } -export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { +export const useBreadcrumbs = ( + extraCrumbs: ChromeBreadcrumb[], + app?: { id: string; label: string } +) => { const params = useQueryParams(); const { @@ -44,14 +47,16 @@ export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { }, } = useKibana(); const setTitle = docTitle.change; - const appPath = getUrlForApp('observability-overview') ?? ''; + const appPath = getUrlForApp(app?.id ?? 'observability-overview') ?? ''; useEffect(() => { const breadcrumbs = [ { - text: i18n.translate('xpack.observability.breadcrumbs.observabilityLinkText', { - defaultMessage: 'Observability', - }), + text: + app?.label ?? + i18n.translate('xpack.observability.breadcrumbs.observabilityLinkText', { + defaultMessage: 'Observability', + }), href: appPath + '/overview', }, ...extraCrumbs, @@ -62,5 +67,5 @@ export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { if (setTitle) { setTitle(getTitleFromBreadCrumbs(breadcrumbs)); } - }, [appPath, extraCrumbs, navigateToUrl, params, setBreadcrumbs, setTitle]); + }, [app?.label, appPath, extraCrumbs, navigateToUrl, params, setBreadcrumbs, setTitle]); }; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index a4ce62ddde0c7..0dab3e5135717 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -47,6 +47,7 @@ export { FieldValueSuggestions, FilterValueLabel, SelectableUrlList, + ExploratoryView, } from './components/shared/'; export type { LazyObservabilityPageTemplateProps } from './components/shared'; @@ -87,3 +88,9 @@ export { InspectorContextProvider } from './context/inspector/inspector_context' export { useInspectorContext } from './context/inspector/use_inspector_context'; export { enableComparisonByDefault } from '../common/ui_settings_keys'; +export type { SeriesConfig, ConfigProps } from './components/shared/exploratory_view/types'; +export { + ReportTypes, + REPORT_METRIC_FIELD, +} from './components/shared/exploratory_view/configurations/constants'; +export { ExploratoryViewContextProvider } from './components/shared/exploratory_view/contexts/exploatory_view_config'; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index a684feec535fc..169f4b5254c04 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -8,13 +8,13 @@ import * as t from 'io-ts'; import React from 'react'; import { casesPath } from '../../common'; -import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { AlertsPage } from '../pages/alerts'; import { CasesPage } from '../pages/cases'; import { HomePage } from '../pages/home'; import { LandingPage } from '../pages/landing'; import { OverviewPage } from '../pages/overview'; import { jsonRt } from './json_rt'; +import { ObservabilityExploratoryView } from '../components/shared/exploratory_view/obsv_exploratory_view'; export type RouteParams = DecodeParams; @@ -74,7 +74,7 @@ export const routes = { }, '/exploratory-view/': { handler: () => { - return ; + return ; }, params: { query: t.partial({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6e10b6b6cdc77..2fedbf91649b5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10702,40 +10702,14 @@ "xpack.fleet.serverError.enrollmentKeyDuplicate": "エージェントポリシーの{agentPolicyId}登録キー{providedKeyName}はすでに存在します", "xpack.fleet.serverError.returnedIncorrectKey": "find enrollmentKeyByIdで正しくないキーが返されました", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "登録APIキーを作成できません", - "xpack.fleet.settings.additionalYamlConfig": "Elasticsearch出力構成(YAML)", "xpack.fleet.settings.deleteHostButton": "ホストの削除", - "xpack.fleet.settings.elasticHostDuplicateError": "重複するURL", - "xpack.fleet.settings.elasticHostError": "無効なURL", - "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearchホスト", - "xpack.fleet.settings.elasticsearchUrlsHelpTect": "エージェントがデータを送信するElasticsearch URLを指定します。Elasticsearchはデフォルトで9200番ポートを使用します。", "xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError": "各URLのプロトコルとパスは同じでなければなりません", "xpack.fleet.settings.fleetServerHostsDuplicateError": "重複するURL", "xpack.fleet.settings.fleetServerHostsEmptyError": "1つ以上のURLが必要です。", "xpack.fleet.settings.fleetServerHostsError": "無効なURL", - "xpack.fleet.settings.flyoutTitle": "Fleet 設定", - "xpack.fleet.settings.globalOutputDescription": "これらの設定はグローバルにすべてのエージェントポリシーの{outputs}セクションに適用され、すべての登録されたエージェントに影響します。", - "xpack.fleet.settings.invalidYamlFormatErrorMessage": "無効なYAML形式:{reason}", - "xpack.fleet.settings.saveButtonLabel": "設定を保存して適用", - "xpack.fleet.settings.saveButtonLoadingLabel": "設定を適用しています...", "xpack.fleet.settings.sortHandle": "ホストハンドルの並べ替え", - "xpack.fleet.settings.success.message": "設定が保存されました", "xpack.fleet.settings.userGuideLink": "FleetおよびElasticエージェントガイド", "xpack.fleet.settings.yamlCodeEditor": "YAMLコードエディター", - "xpack.fleet.settingsConfirmModal.calloutTitle": "すべてのエージェントポリシーと登録されたエージェントが更新されます", - "xpack.fleet.settingsConfirmModal.cancelButton": "キャンセル", - "xpack.fleet.settingsConfirmModal.confirmButton": "設定を適用", - "xpack.fleet.settingsConfirmModal.defaultChangeLabel": "不明な設定", - "xpack.fleet.settingsConfirmModal.elasticsearchAddedLabel": "Elasticsearchホスト(新)", - "xpack.fleet.settingsConfirmModal.elasticsearchHosts": "Elasticsearchホスト", - "xpack.fleet.settingsConfirmModal.elasticsearchRemovedLabel": "Elasticsearchホスト(旧)", - "xpack.fleet.settingsConfirmModal.eserverChangedText": "新しい{elasticsearchHosts}で接続できないエージェントは、データを送信できない場合でも、正常ステータスです。FleetサーバーがElasticsearchに接続するために使用するURLを更新するには、Fleetサーバーを再登録する必要があります。", - "xpack.fleet.settingsConfirmModal.fieldLabel": "フィールド", - "xpack.fleet.settingsConfirmModal.fleetServerAddedLabel": "Fleetサーバーホスト(新)", - "xpack.fleet.settingsConfirmModal.fleetServerChangedText": "新しい{fleetServerHosts}に接続できないエージェントはエラーが記録されます。新しいURLで接続するまでは、エージェントは現在のポリシーを使用し、古いURLで更新を確認します。", - "xpack.fleet.settingsConfirmModal.fleetServerHosts": "Fleetサーバーホスト", - "xpack.fleet.settingsConfirmModal.fleetServerRemovedLabel": "Fleetサーバーホスト(旧)", - "xpack.fleet.settingsConfirmModal.title": "設定をすべてのエージェントポリシーに適用", - "xpack.fleet.settingsConfirmModal.valueLabel": "値", "xpack.fleet.setup.titleLabel": "Fleetを読み込んでいます...", "xpack.fleet.setup.uiPreconfigurationErrorTitle": "構成エラー", "xpack.fleet.setupPage.apiKeyServiceLink": "APIキーサービス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8e25e1415521a..83d480e7a1385 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10809,40 +10809,14 @@ "xpack.fleet.serverError.enrollmentKeyDuplicate": "称作 {providedKeyName} 的注册密钥对于代理策略 {agentPolicyId} 已存在", "xpack.fleet.serverError.returnedIncorrectKey": "find enrollmentKeyById 返回错误的密钥", "xpack.fleet.serverError.unableToCreateEnrollmentKey": "无法创建注册 api 密钥", - "xpack.fleet.settings.additionalYamlConfig": "Elasticsearch 输出配置 (YAML)", "xpack.fleet.settings.deleteHostButton": "删除主机", - "xpack.fleet.settings.elasticHostDuplicateError": "复制 URL", - "xpack.fleet.settings.elasticHostError": "URL 无效", - "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearch 主机", - "xpack.fleet.settings.elasticsearchUrlsHelpTect": "指定代理用于发送数据的 Elasticsearch URL。Elasticsearch 默认使用端口 9200。", "xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError": "对于每个 URL,协议和路径必须相同", "xpack.fleet.settings.fleetServerHostsDuplicateError": "复制 URL", "xpack.fleet.settings.fleetServerHostsEmptyError": "至少需要一个 URL", "xpack.fleet.settings.fleetServerHostsError": "URL 无效", - "xpack.fleet.settings.flyoutTitle": "Fleet 设置", - "xpack.fleet.settings.globalOutputDescription": "这些设置将全局应用到所有代理策略的 {outputs} 部分并影响所有注册的代理。", - "xpack.fleet.settings.invalidYamlFormatErrorMessage": "YAML 无效:{reason}", - "xpack.fleet.settings.saveButtonLabel": "保存并应用设置", - "xpack.fleet.settings.saveButtonLoadingLabel": "正在应用设置......", "xpack.fleet.settings.sortHandle": "排序主机手柄", - "xpack.fleet.settings.success.message": "设置已保存", "xpack.fleet.settings.userGuideLink": "Fleet 和 Elastic 代理指南", "xpack.fleet.settings.yamlCodeEditor": "YAML 代码编辑器", - "xpack.fleet.settingsConfirmModal.calloutTitle": "此操作更新所有代理策略和注册的代理", - "xpack.fleet.settingsConfirmModal.cancelButton": "取消", - "xpack.fleet.settingsConfirmModal.confirmButton": "应用设置", - "xpack.fleet.settingsConfirmModal.defaultChangeLabel": "未知设置", - "xpack.fleet.settingsConfirmModal.elasticsearchAddedLabel": "Elasticsearch 主机(新)", - "xpack.fleet.settingsConfirmModal.elasticsearchHosts": "Elasticsearch 主机", - "xpack.fleet.settingsConfirmModal.elasticsearchRemovedLabel": "Elasticsearch 主机(旧)", - "xpack.fleet.settingsConfirmModal.eserverChangedText": "无法连接新 {elasticsearchHosts} 的代理有运行正常状态,即使无法发送数据。要更新 Fleet 服务器用于连接 Elasticsearch 的 URL,必须重新注册 Fleet 服务器。", - "xpack.fleet.settingsConfirmModal.fieldLabel": "字段", - "xpack.fleet.settingsConfirmModal.fleetServerAddedLabel": "Fleet 服务器主机(新)", - "xpack.fleet.settingsConfirmModal.fleetServerChangedText": "无法连接到新 {fleetServerHosts}的代理会记录错误。代理仍基于当前策略,并检查位于旧 URL 的更新,直到连接到新 URL。", - "xpack.fleet.settingsConfirmModal.fleetServerHosts": "Fleet 服务器主机", - "xpack.fleet.settingsConfirmModal.fleetServerRemovedLabel": "Fleet 服务器主机(旧)", - "xpack.fleet.settingsConfirmModal.title": "将设置应用到所有代理策略", - "xpack.fleet.settingsConfirmModal.valueLabel": "值", "xpack.fleet.setup.titleLabel": "正在加载 Fleet......", "xpack.fleet.setup.uiPreconfigurationErrorTitle": "配置错误", "xpack.fleet.setupPage.apiKeyServiceLink": "API 密钥服务", diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 7a3ffc50a4821..229da6aa708f5 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,9 +14,10 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - // FLAKY: https://github.com/elastic/kibana/issues/100570 - describe.skip('Enter Space', function () { - this.tags('includeFirefox'); + describe('Enter Space', function () { + // FLAKY: https://github.com/elastic/kibana/issues/100570 + // These tests fail very intermittently in Firefox. Skip Firefox testing until resolved. + // this.tags('includeFirefox'); before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/spaces/enter_space'); await PageObjects.security.forceLogout();