diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 2226c89ab90f6..68b72986a43e7 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -28,7 +28,8 @@ "kibanaUtils", "kibanaReact", "ml", - "embeddable" + "embeddable", + "controls" ], "owner": { "name": "Logs and Metrics UI", diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx new file mode 100644 index 0000000000000..c0441a7597352 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx @@ -0,0 +1,89 @@ +/* + * 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, useState } from 'react'; +import { ControlGroupContainer, CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/public'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; +import { Filter, TimeRange, compareFilters } from '@kbn/es-query'; +import { isEqual } from 'lodash'; +import { LazyControlsRenderer } from './lazy_controls_renderer'; +import { useControlPanels } from '../hooks/use_control_panels_url_state'; + +interface Props { + timeRange: TimeRange; + dataViewId: string; + filters: Filter[]; + query: { + language: string; + query: string; + }; + setPanelFilters: React.Dispatch>; +} + +// Disable refresh, allow our timerange changes to refresh the embeddable. +const REFRESH_CONFIG = { + pause: true, + value: 0, +}; + +export const ControlsContent: React.FC = ({ + timeRange, + dataViewId, + query, + filters, + setPanelFilters, +}) => { + const [controlPanel, setControlPanels] = useControlPanels(dataViewId); + const [controlGroup, setControlGroup] = useState(); + + useEffect(() => { + if (!controlGroup) { + return; + } + if ( + !isEqual(controlGroup.getInput().timeRange, timeRange) || + !compareFilters(controlGroup.getInput().filters ?? [], filters) || + !isEqual(controlGroup.getInput().query, query) + ) { + controlGroup.updateInput({ + timeRange, + query, + filters, + }); + } + }, [query, filters, controlGroup, timeRange]); + + return ( + ({ + id: dataViewId, + type: CONTROL_GROUP_TYPE, + timeRange, + refreshConfig: REFRESH_CONFIG, + viewMode: ViewMode.VIEW, + filters: [...filters], + query, + chainingSystem: 'HIERARCHICAL', + controlStyle: 'oneLine', + defaultControlWidth: 'small', + panels: controlPanel, + })} + onEmbeddableLoad={(newControlGroup) => { + setControlGroup(newControlGroup); + newControlGroup.onFiltersPublished$.subscribe((newFilters) => { + setPanelFilters([...newFilters]); + }); + newControlGroup.getInput$().subscribe(({ panels, filters: currentFilters }) => { + setControlPanels(panels); + if (currentFilters?.length === 0) { + setPanelFilters([]); + } + }); + }} + /> + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index 14d229b838e56..a15b1a3016ddb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -29,7 +29,7 @@ const HOST_METRICS: Array<{ type: SnapshotMetricType }> = [ export const HostsTable = () => { const { sourceId } = useSourceContext(); - const { buildQuery, dateRangeTimestamp } = useUnifiedSearchContext(); + const { buildQuery, dateRangeTimestamp, panelFilters } = useUnifiedSearchContext(); const timeRange: InfraTimerangeInput = { from: dateRangeTimestamp.from, @@ -61,7 +61,7 @@ export const HostsTable = () => { return ( <> - {loading ? ( + {loading || !panelFilters ? ( +) => ( + + }> + + + +); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx index 87715dabe9604..937420585556b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx @@ -12,6 +12,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { SavedQuery } from '@kbn/data-plugin/public'; import type { InfraClientStartDeps } from '../../../../types'; import { useUnifiedSearchContext } from '../hooks/use_unified_search'; +import { ControlsContent } from './controls_content'; interface Props { dataView: DataView; @@ -28,6 +29,7 @@ export const UnifiedSearchBar = ({ dataView }: Props) => { onSubmit, saveQuery, clearSavedQuery, + setPanelFilters, } = useUnifiedSearchContext(); const { SearchBar } = unifiedSearch.ui; @@ -59,20 +61,29 @@ export const UnifiedSearchBar = ({ dataView }: Props) => { }; return ( - + <> + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_control_panels_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_control_panels_url_state.ts new file mode 100644 index 0000000000000..adad16c930b27 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_control_panels_url_state.ts @@ -0,0 +1,76 @@ +/* + * 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 rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import { ControlGroupInput } from '@kbn/controls-plugin/common'; +import { useUrlState } from '../../../../utils/use_url_state'; + +export const getDefaultPanels = (dataViewId: string): ControlGroupInput['panels'] => + ({ + osPanel: { + order: 0, + width: 'medium', + grow: false, + type: 'optionsListControl', + explicitInput: { + id: 'osPanel', + dataViewId, + fieldName: 'host.os.name', + title: 'Operating System', + }, + }, + cloudProviderPanel: { + order: 1, + width: 'medium', + grow: false, + type: 'optionsListControl', + explicitInput: { + id: 'cloudProviderPanel', + dataViewId, + fieldName: 'cloud.provider', + title: 'Cloud Provider', + }, + }, + } as unknown as ControlGroupInput['panels']); +const HOST_FILTERS_URL_STATE_KEY = 'controlPanels'; + +export const useControlPanels = (dataViewId: string) => { + return useUrlState({ + defaultState: getDefaultPanels(dataViewId), + decodeUrlState, + encodeUrlState, + urlStateKey: HOST_FILTERS_URL_STATE_KEY, + }); +}; + +const PanelRT = rt.type({ + order: rt.number, + width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]), + grow: rt.boolean, + type: rt.string, + explicitInput: rt.intersection([ + rt.type({ id: rt.string }), + rt.partial({ + dataViewId: rt.string, + fieldName: rt.string, + title: rt.union([rt.string, rt.undefined]), + }), + ]), +}); + +const ControlPanelRT = rt.record(rt.string, PanelRT); + +type ControlPanels = rt.TypeOf; +const encodeUrlState = ControlPanelRT.encode; +const decodeUrlState = (value: unknown) => { + if (value) { + return pipe(ControlPanelRT.decode(value), fold(constant({}), identity)); + } +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts index 0bd7211cf1dc2..7ac6cec98eb9c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts @@ -6,7 +6,7 @@ */ import { useKibana } from '@kbn/kibana-react-plugin/public'; import createContainer from 'constate'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query'; import type { SavedQuery } from '@kbn/data-plugin/public'; import { debounce } from 'lodash'; @@ -16,6 +16,8 @@ import { useSyncKibanaTimeFilterTime } from '../../../../hooks/use_kibana_timefi import { useHostsUrlState, INITIAL_DATE_RANGE } from './use_hosts_url_state'; export const useUnifiedSearch = () => { + const [panelFilters, setPanelFilters] = useState(null); + const { state, dispatch, getRangeInTimestamp, getTime } = useHostsUrlState(); const { metricsDataView } = useMetricsDataViewContext(); const { services } = useKibana(); @@ -49,7 +51,7 @@ export const useUnifiedSearch = () => { }); } }, - [filterManager, getRangeInTimestamp, getTime, dispatch] + [getTime, dispatch, filterManager, getRangeInTimestamp] ); // This won't prevent onSubmit from being fired twice when `clear filters` is clicked, @@ -87,8 +89,11 @@ export const useUnifiedSearch = () => { if (!metricsDataView) { return null; } - return buildEsQuery(metricsDataView, state.query, state.filters); - }, [metricsDataView, state.filters, state.query]); + if (Array.isArray(panelFilters) && panelFilters.length > 0) { + return buildEsQuery(metricsDataView, state.query, [...state.filters, ...panelFilters]); + } + return buildEsQuery(metricsDataView, state.query, [...state.filters]); + }, [metricsDataView, panelFilters, state.filters, state.query]); return { dateRangeTimestamp: state.dateRangeTimestamp, @@ -99,6 +104,8 @@ export const useUnifiedSearch = () => { unifiedSearchQuery: state.query, unifiedSearchDateRange: getTime(), unifiedSearchFilters: state.filters, + setPanelFilters, + panelFilters, }; };