diff --git a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics_alerts.ts b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics_alerts.ts index 620c5c88033a7..4f525bda17564 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics_alerts.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/constants/synthetics_alerts.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ActionGroup } from '@kbn/alerting-plugin/common'; +import type { ActionGroup } from '@kbn/alerting-plugin/common'; import { i18n } from '@kbn/i18n'; export type MonitorStatusActionGroup = diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts index 9fbda10dfba56..0c546eb431dea 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/monitor_management/state.ts @@ -34,6 +34,7 @@ export const FetchMonitorOverviewQueryArgsCodec = t.partial({ projects: t.array(t.string), schedules: t.array(t.string), monitorTypes: t.array(t.string), + monitorQueryIds: t.array(t.string), sortField: t.string, sortOrder: t.string, }); diff --git a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts index 5d481038b0869..2e9e36460c7be 100644 --- a/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts +++ b/x-pack/plugins/observability_solution/synthetics/common/runtime_types/ping/ping.ts @@ -16,7 +16,7 @@ export const PingErrorType = t.intersection([ t.partial({ code: t.string, id: t.string, - stack_trace: t.string, + stack_trace: t.union([t.string, t.null]), type: t.string, }), t.type({ diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/field_selector.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/field_selector.tsx new file mode 100644 index 0000000000000..0bb37e6c4e736 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/field_selector.tsx @@ -0,0 +1,128 @@ +/* + * 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, { ReactNode, useState } from 'react'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { debounce } from 'lodash'; +import { Controller, FieldPath, useFormContext } from 'react-hook-form'; +import { + Suggestion, + useFetchSyntheticsSuggestions, +} from '../hooks/use_fetch_synthetics_suggestions'; +import { OptionalText } from './optional_text'; +import { MonitorFilters } from '../monitors_overview/types'; + +interface Option { + label: string; + value: string; +} + +export interface Props { + dataTestSubj: string; + label: string; + name: FieldPath; + placeholder: string; + tooltip?: ReactNode; + suggestions?: Suggestion[]; + isLoading?: boolean; + required?: boolean; +} + +export function FieldSelector({ + dataTestSubj, + label, + name, + placeholder, + tooltip, + required, +}: Props) { + const { control, getFieldState } = useFormContext(); + const [search, setSearch] = useState(''); + + const { suggestions = [], isLoading } = useFetchSyntheticsSuggestions({ + search, + fieldName: name, + }); + + const debouncedSearch = debounce((value) => setSearch(value), 200); + + return ( + + + {label} {tooltip} + + ) : ( + label + ) + } + isInvalid={getFieldState(name).invalid} + fullWidth + labelAppend={!required ? : undefined} + > + { + const selectedOptions = + !!Array.isArray(field.value) && field.value.length + ? createSelectedOptions(field.value, suggestions) + : []; + + return ( + { + if (selected.length) { + field.onChange( + selected.map((option) => ({ + label: option.label, + value: option.value, + })) + ); + return; + } + field.onChange([]); + }} + onSearchChange={(value: string) => debouncedSearch(value)} + options={createOptions(suggestions)} + placeholder={placeholder} + selectedOptions={selectedOptions} + /> + ); + }} + /> + + + ); +} + +function createOptions(suggestions: Suggestion[] = []): Option[] { + return suggestions + .map((suggestion) => ({ label: suggestion.label, value: suggestion.value })) + .sort((a, b) => String(a.label).localeCompare(b.label)); +} + +function createSelectedOptions(selected: Option[] = [], suggestions: Suggestion[] = []): Option[] { + return selected.map((value) => { + const suggestion = suggestions.find((s) => s.value === value.value); + if (!suggestion) { + return { label: value.value, value: value.value }; + } + return { label: suggestion.label, value: suggestion.value }; + }); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx new file mode 100644 index 0000000000000..24995e898b3a1 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_configuration.tsx @@ -0,0 +1,120 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { FormProvider, useForm } from 'react-hook-form'; +import { MonitorFilters } from '../monitors_overview/types'; +import { MonitorFiltersForm } from './monitor_filters_form'; + +interface MonitorConfigurationProps { + initialInput?: { + filters: MonitorFilters; + }; + onCreate: (props: { filters: MonitorFilters }) => void; + onCancel: () => void; +} + +export function MonitorConfiguration({ + initialInput, + onCreate, + onCancel, +}: MonitorConfigurationProps) { + const methods = useForm({ + defaultValues: { + monitorIds: [], + projects: [], + tags: [], + monitorTypes: [], + locations: [], + }, + values: initialInput?.filters, + mode: 'all', + }); + const { getValues, formState } = methods; + + const onConfirmClick = () => { + const newFilters = getValues(); + onCreate({ + filters: newFilters, + }); + }; + + return ( + + + + + +

+ {i18n.translate( + 'xpack.synthetics.overviewEmbeddable.config.sloSelector.headerTitle', + { + defaultMessage: 'Overview configuration', + } + )} +

+
+
+
+
+ + <> + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.tsx new file mode 100644 index 0000000000000..80d2f3aa072cd --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitor_filters_form.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 { EuiFlexGroup, EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { FieldSelector } from './field_selector'; + +export function MonitorFiltersForm() { + return ( + + + + } + /> + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitors_open_configuration.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitors_open_configuration.tsx new file mode 100644 index 0000000000000..48dbcb085b806 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/monitors_open_configuration.tsx @@ -0,0 +1,59 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { toMountPoint } from '@kbn/react-kibana-mount'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { MonitorFilters } from '../monitors_overview/types'; +import { ClientPluginsStart } from '../../../plugin'; +import { MonitorConfiguration } from './monitor_configuration'; + +export async function openMonitorConfiguration({ + coreStart, + pluginStart, + initialState, +}: { + coreStart: CoreStart; + pluginStart: ClientPluginsStart; + initialState?: { filters: MonitorFilters }; +}): Promise<{ filters: MonitorFilters }> { + const { overlays } = coreStart; + const queryClient = new QueryClient(); + return new Promise(async (resolve, reject) => { + try { + const flyoutSession = overlays.openFlyout( + toMountPoint( + + + { + flyoutSession.close(); + resolve(update); + }} + onCancel={() => { + flyoutSession.close(); + reject(); + }} + /> + + , + coreStart + ) + ); + } catch (error) { + reject(error); + } + }); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/optional_text.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/optional_text.tsx new file mode 100644 index 0000000000000..28bd67301af16 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/optional_text.tsx @@ -0,0 +1,20 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export function OptionalText() { + return ( + + {i18n.translate('xpack.synthetics.formEdit.optionalLabel', { + defaultMessage: 'Optional', + })} + + ); +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/show_selected_filters.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/show_selected_filters.tsx new file mode 100644 index 0000000000000..0a6f6ab515d94 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/common/show_selected_filters.tsx @@ -0,0 +1,53 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBadgeGroup, EuiBadge } from '@elastic/eui'; +import { MonitorFilters } from '../monitors_overview/types'; + +export const ShowSelectedFilters = ({ filters }: { filters: MonitorFilters }) => { + return ( + + {Object.entries(filters).map(([key, filter]) => { + if (!filter || !filter.length) { + return null; + } + const values = filter + .map((f: { label: string; value: string }) => f.label || f.value) + .join(', '); + + return ( + + {i18n.translate('xpack.synthetics.showSelectedFilters.monitorBadgeLabel', { + defaultMessage: '{label}: {value}', + values: { label: labels[key], value: values }, + })} + + ); + })} + + ); +}; + +const labels: Record = { + monitorIds: i18n.translate('xpack.synthetics.showSelectedFilters.monitorIdLabel', { + defaultMessage: 'Monitor', + }), + tags: i18n.translate('xpack.synthetics.showSelectedFilters.tagsLabel', { + defaultMessage: 'Tags', + }), + locations: i18n.translate('xpack.synthetics.showSelectedFilters.locationsLabel', { + defaultMessage: 'Location', + }), + monitorTypes: i18n.translate('xpack.synthetics.showSelectedFilters.monitorTypeLabel', { + defaultMessage: 'Type', + }), + projects: i18n.translate('xpack.synthetics.showSelectedFilters.presetLabel', { + defaultMessage: 'Project', + }), +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/constants.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/constants.ts index b471d46ac3832..5afcf2d3026b4 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/constants.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/constants.ts @@ -5,4 +5,22 @@ * 2.0. */ -export const SYNTHETICS_OVERVIEW_EMBEDDABLE = 'SYNTHETICS_OVERVIEW_EMBEDDABLE'; +import { i18n } from '@kbn/i18n'; + +export const SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE = 'SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE'; +export const SYNTHETICS_MONITORS_EMBEDDABLE = 'SYNTHETICS_MONITORS_EMBEDDABLE'; + +export const COMMON_SYNTHETICS_GROUPING = [ + { + id: 'synthetics', + getDisplayName: () => + i18n.translate('xpack.synthetics.common.constants.grouping.legacy', { + defaultMessage: 'Synthetics', + }), + getIconType: () => { + return 'online'; + }, + }, +]; + +export const ALL_VALUE = '*'; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/hooks/use_fetch_synthetics_suggestions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/hooks/use_fetch_synthetics_suggestions.ts new file mode 100644 index 0000000000000..b9f838675bce4 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/hooks/use_fetch_synthetics_suggestions.ts @@ -0,0 +1,51 @@ +/* + * 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 { useFetcher } from '@kbn/observability-shared-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ClientPluginsStart } from '../../../plugin'; + +export interface Suggestion { + label: string; + value: string; + count: number; +} + +export interface Params { + fieldName: string; + filters?: { + locations?: string[]; + monitorIds?: string[]; + tags?: string[]; + projects?: string[]; + }; + search: string; +} + +type ApiResponse = Record; + +export function useFetchSyntheticsSuggestions({ filters, fieldName, search }: Params) { + const { http } = useKibana().services; + const { locations, monitorIds, tags, projects } = filters || {}; + + const { loading, data } = useFetcher(async () => { + return await http.get('/internal/synthetics/suggestions', { + query: { + locations: locations || [], + monitorQueryIds: monitorIds || [], + tags: tags || [], + projects: projects || [], + query: search, + }, + }); + }, [http, locations, monitorIds, projects, search, tags]); + + return { + suggestions: data?.[fieldName] ?? [], + isLoading: Boolean(loading), + }; +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/status_overview/status_overview_embeddable_factory.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx similarity index 57% rename from x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/status_overview/status_overview_embeddable_factory.tsx rename to x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx index 3f7b3fcf13699..e23de3cc67a14 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/status_overview/status_overview_embeddable_factory.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_embeddable_factory.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; import { DefaultEmbeddableApi, ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { initializeTitles, @@ -16,25 +15,30 @@ import { PublishesWritablePanelTitle, PublishesPanelTitle, SerializedTitles, + HasEditCapabilities, } from '@kbn/presentation-publishing'; import { BehaviorSubject, Subject } from 'rxjs'; import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; -import { SYNTHETICS_OVERVIEW_EMBEDDABLE } from '../constants'; +import { MonitorFilters } from './types'; +import { StatusGridComponent } from './monitors_grid_component'; +import { SYNTHETICS_MONITORS_EMBEDDABLE } from '../constants'; import { ClientPluginsStart } from '../../../plugin'; -import { StatusOverviewComponent } from './status_overview_component'; export const getOverviewPanelTitle = () => - i18n.translate('xpack.synthetics.statusOverview.displayName', { - defaultMessage: 'Synthetics Status Overview', + i18n.translate('xpack.synthetics.monitors.displayName', { + defaultMessage: 'Synthetics Monitors', }); -export type OverviewEmbeddableState = SerializedTitles; +export type OverviewEmbeddableState = SerializedTitles & { + filters: MonitorFilters; +}; export type StatusOverviewApi = DefaultEmbeddableApi & PublishesWritablePanelTitle & - PublishesPanelTitle; + PublishesPanelTitle & + HasEditCapabilities; -export const getStatusOverviewEmbeddableFactory = ( +export const getMonitorsEmbeddableFactory = ( getStartServices: StartServicesAccessor ) => { const factory: ReactEmbeddableFactory< @@ -42,29 +46,57 @@ export const getStatusOverviewEmbeddableFactory = ( OverviewEmbeddableState, StatusOverviewApi > = { - type: SYNTHETICS_OVERVIEW_EMBEDDABLE, + type: SYNTHETICS_MONITORS_EMBEDDABLE, deserializeState: (state) => { return state.rawState as OverviewEmbeddableState; }, buildEmbeddable: async (state, buildApi, uuid, parentApi) => { + const [coreStart, pluginStart] = await getStartServices(); + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); const defaultTitle$ = new BehaviorSubject(getOverviewPanelTitle()); const reload$ = new Subject(); + const filters$ = new BehaviorSubject(state.filters); const api = buildApi( { ...titlesApi, defaultPanelTitle: defaultTitle$, + getTypeDisplayName: () => + i18n.translate('xpack.synthetics.editSloOverviewEmbeddableTitle.typeDisplayName', { + defaultMessage: 'filters', + }), + isEditingEnabled: () => true, serializeState: () => { return { rawState: { ...serializeTitles(), + filters: filters$.getValue(), }, }; }, + onEdit: async () => { + try { + const { openMonitorConfiguration } = await import( + '../common/monitors_open_configuration' + ); + + const result = await openMonitorConfiguration({ + coreStart, + pluginStart, + initialState: { + filters: filters$.getValue(), + }, + }); + filters$.next(result.filters); + } catch (e) { + return Promise.reject(); + } + }, }, { ...titleComparators, + filters: [filters$, (value) => filters$.next(value)], } ); @@ -77,7 +109,7 @@ export const getStatusOverviewEmbeddableFactory = ( return { api, Component: () => { - const [] = useBatchedPublishingSubjects(); + const [filters] = useBatchedPublishingSubjects(filters$); useEffect(() => { return () => { @@ -86,9 +118,14 @@ export const getStatusOverviewEmbeddableFactory = ( }, []); return (
- +
); }, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx new file mode 100644 index 0000000000000..c3801bb98ca9a --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/monitors_grid_component.tsx @@ -0,0 +1,60 @@ +/* + * 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, useRef } from 'react'; +import { Subject } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { useDispatch } from 'react-redux'; +import { getOverviewStore } from './redux_store'; +import { ShowSelectedFilters } from '../common/show_selected_filters'; +import { setOverviewPageStateAction } from '../../synthetics/state'; +import { MonitorFilters } from './types'; +import { EmbeddablePanelWrapper } from '../../synthetics/components/common/components/embeddable_panel_wrapper'; +import { SyntheticsEmbeddableContext } from '../synthetics_embeddable_context'; +import { OverviewGrid } from '../../synthetics/components/monitors_page/overview/overview/overview_grid'; + +export const StatusGridComponent = ({ + reload$, + filters, +}: { + reload$: Subject; + filters: MonitorFilters; +}) => { + const overviewStore = useRef(getOverviewStore()); + + return ( + } + > + + + + + ); +}; + +const MonitorsOverviewList = ({ filters }: { filters: MonitorFilters }) => { + const dispatch = useDispatch(); + useEffect(() => { + if (!filters) return; + dispatch( + setOverviewPageStateAction({ + tags: filters.tags.map((tag) => tag.value), + locations: filters.locations.map((location) => location.value), + monitorTypes: filters.monitorTypes.map((monitorType) => monitorType.value), + monitorQueryIds: filters.monitorIds.map((monitorId) => monitorId.value), + projects: filters.projects.map((project) => project.value), + }) + ); + }, [dispatch, filters]); + + return ; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/redux_store.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/redux_store.ts new file mode 100644 index 0000000000000..86bdd93311674 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/redux_store.ts @@ -0,0 +1,25 @@ +/* + * 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 { configureStore } from '@reduxjs/toolkit'; +import createSagaMiddleware from 'redux-saga'; +import { rootReducer } from '../../synthetics/state/root_reducer'; +import { rootEffect } from '../../synthetics/state/root_effect'; + +export const getOverviewStore = () => { + const sagaMW = createSagaMiddleware(); + + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(sagaMW), + devTools: process.env.NODE_ENV !== 'production', + preloadedState: {}, + enhancers: [], + }); + sagaMW.run(rootEffect); + return store; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/types.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/types.ts new file mode 100644 index 0000000000000..e8d1c71ee7d11 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/monitors_overview/types.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +interface Option { + label: string; + value: string; +} + +export interface MonitorFilters { + projects: Option[]; + tags: Option[]; + monitorIds: Option[]; + monitorTypes: Option[]; + locations: Option[]; +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/register_embeddables.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/register_embeddables.ts index fbc516a6f611b..3491df4dfc96f 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/register_embeddables.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/register_embeddables.ts @@ -7,42 +7,54 @@ import { CoreSetup } from '@kbn/core-lifecycle-browser'; -import { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-browser/src'; -import { createStatusOverviewPanelAction } from './ui_actions/create_overview_panel_action'; import { ClientPluginsSetup, ClientPluginsStart } from '../../plugin'; -import { SYNTHETICS_OVERVIEW_EMBEDDABLE } from './constants'; +import { SYNTHETICS_MONITORS_EMBEDDABLE, SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE } from './constants'; export const registerSyntheticsEmbeddables = ( core: CoreSetup, pluginsSetup: ClientPluginsSetup ) => { pluginsSetup.embeddable.registerReactEmbeddableFactory( - SYNTHETICS_OVERVIEW_EMBEDDABLE, + SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE, async () => { - const { getStatusOverviewEmbeddableFactory } = await import( - './status_overview/status_overview_embeddable_factory' + const { getStatsOverviewEmbeddableFactory } = await import( + './stats_overview/stats_overview_embeddable_factory' ); - return getStatusOverviewEmbeddableFactory(core.getStartServices); + return getStatsOverviewEmbeddableFactory(core.getStartServices); } ); - const { uiActions, cloud, serverless } = pluginsSetup; - - // Initialize actions - const addOverviewPanelAction = createStatusOverviewPanelAction(); + pluginsSetup.embeddable.registerReactEmbeddableFactory( + SYNTHETICS_MONITORS_EMBEDDABLE, + async () => { + const { getMonitorsEmbeddableFactory } = await import( + './monitors_overview/monitors_embeddable_factory' + ); + return getMonitorsEmbeddableFactory(core.getStartServices); + } + ); core.getStartServices().then(([_, pluginsStart]) => { pluginsStart.dashboard.registerDashboardPanelPlacementSetting( - SYNTHETICS_OVERVIEW_EMBEDDABLE, + SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE, () => { return { width: 10, height: 8 }; } ); + pluginsStart.dashboard.registerDashboardPanelPlacementSetting( + SYNTHETICS_MONITORS_EMBEDDABLE, + () => { + return { width: 30, height: 12 }; + } + ); }); - // Assign triggers - // Only register these actions in stateful kibana, and the serverless observability project - if (Boolean((serverless && cloud?.serverless.projectType === 'observability') || !serverless)) { - uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addOverviewPanelAction); - } + const registerAsyncUiActions = async () => { + if (pluginsSetup.uiActions) { + const { registerSyntheticsUiActions } = await import('./ui_actions/register_ui_actions'); + registerSyntheticsUiActions(core, pluginsSetup); + } + }; + // can be done async + registerAsyncUiActions(); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/redux_store.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/redux_store.ts new file mode 100644 index 0000000000000..8192b9a787ac7 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/redux_store.ts @@ -0,0 +1,26 @@ +/* + * 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 { configureStore } from '@reduxjs/toolkit'; +import createSagaMiddleware from 'redux-saga'; +import { rootReducer } from '../../synthetics/state/root_reducer'; +import { rootEffect } from '../../synthetics/state/root_effect'; + +export const getStatsOverviewStore = () => { + const sagaMW = createSagaMiddleware(); + + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(sagaMW), + devTools: process.env.NODE_ENV !== 'production', + preloadedState: {}, + enhancers: [], + }); + sagaMW.run(rootEffect); + + return store; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx new file mode 100644 index 0000000000000..579596615ae72 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_component.tsx @@ -0,0 +1,49 @@ +/* + * 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, useRef } from 'react'; +import { Subject } from 'rxjs'; +import { useDispatch } from 'react-redux'; +import { getStatsOverviewStore } from './redux_store'; +import { ShowSelectedFilters } from '../common/show_selected_filters'; +import { MonitorFilters } from '../monitors_overview/types'; +import { setOverviewPageStateAction } from '../../synthetics/state'; +import { SyntheticsEmbeddableContext } from '../synthetics_embeddable_context'; +import { OverviewStatus } from '../../synthetics/components/monitors_page/overview/overview/overview_status'; + +export const StatsOverviewComponent = ({ + reload$, + filters, +}: { + reload$: Subject; + filters: MonitorFilters; +}) => { + const statsOverviewStore = useRef(getStatsOverviewStore()); + + return ( + + + + ); +}; + +const WithFiltersComponent = ({ filters }: { filters: MonitorFilters }) => { + const dispatch = useDispatch(); + useEffect(() => { + dispatch( + setOverviewPageStateAction({ + tags: filters.tags.map((tag) => tag.value), + locations: filters.locations.map((location) => location.value), + monitorTypes: filters.monitorTypes.map((monitorType) => monitorType.value), + monitorQueryIds: filters.monitorIds.map((monitorId) => monitorId.value), + projects: filters.projects.map((project) => project.value), + }) + ); + }, [dispatch, filters]); + + return } />; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx new file mode 100644 index 0000000000000..07f1e72fa1e98 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/stats_overview/stats_overview_embeddable_factory.tsx @@ -0,0 +1,135 @@ +/* + * 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 React, { useEffect } from 'react'; +import { DefaultEmbeddableApi, ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { + initializeTitles, + useBatchedPublishingSubjects, + fetch$, + PublishesWritablePanelTitle, + PublishesPanelTitle, + SerializedTitles, + HasEditCapabilities, +} from '@kbn/presentation-publishing'; +import { BehaviorSubject, Subject } from 'rxjs'; +import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; +import { MonitorFilters } from '../monitors_overview/types'; +import { SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE } from '../constants'; +import { ClientPluginsStart } from '../../../plugin'; +import { StatsOverviewComponent } from './stats_overview_component'; + +export const getOverviewPanelTitle = () => + i18n.translate('xpack.synthetics.statusOverview.list.displayName', { + defaultMessage: 'Synthetics Stats Overview', + }); + +export type OverviewEmbeddableState = SerializedTitles & { + filters: MonitorFilters; +}; + +export type StatsOverviewApi = DefaultEmbeddableApi & + PublishesWritablePanelTitle & + PublishesPanelTitle & + HasEditCapabilities; + +export const getStatsOverviewEmbeddableFactory = ( + getStartServices: StartServicesAccessor +) => { + const factory: ReactEmbeddableFactory< + OverviewEmbeddableState, + OverviewEmbeddableState, + StatsOverviewApi + > = { + type: SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE, + deserializeState: (state) => { + return state.rawState as OverviewEmbeddableState; + }, + buildEmbeddable: async (state, buildApi, uuid, parentApi) => { + const [coreStart, pluginStart] = await getStartServices(); + + const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state); + const defaultTitle$ = new BehaviorSubject(getOverviewPanelTitle()); + const reload$ = new Subject(); + const filters$ = new BehaviorSubject(state.filters); + + const api = buildApi( + { + ...titlesApi, + defaultPanelTitle: defaultTitle$, + getTypeDisplayName: () => + i18n.translate('xpack.synthetics.editSloOverviewEmbeddableTitle.typeDisplayName', { + defaultMessage: 'filters', + }), + isEditingEnabled: () => true, + onEdit: async () => { + try { + const { openMonitorConfiguration } = await import( + '../common/monitors_open_configuration' + ); + + const result = await openMonitorConfiguration({ + coreStart, + pluginStart, + initialState: { + filters: filters$.getValue(), + }, + }); + filters$.next(result.filters); + } catch (e) { + return Promise.reject(); + } + }, + serializeState: () => { + return { + rawState: { + ...serializeTitles(), + filters: filters$.getValue(), + }, + }; + }, + }, + { + ...titleComparators, + filters: [filters$, (value) => filters$.next(value)], + } + ); + + const fetchSubscription = fetch$(api) + .pipe() + .subscribe((next) => { + reload$.next(next.isReload); + }); + + return { + api, + Component: () => { + const [filters] = useBatchedPublishingSubjects(filters$); + + useEffect(() => { + return () => { + fetchSubscription.unsubscribe(); + }; + }, []); + return ( +
+ +
+ ); + }, + }; + }, + }; + return factory; +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/status_overview/status_overview_component.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/status_overview/status_overview_component.tsx deleted file mode 100644 index 1034f9ea959ec..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/status_overview/status_overview_component.tsx +++ /dev/null @@ -1,19 +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 { Subject } from 'rxjs'; -import { OverviewStatus } from '../../synthetics/components/monitors_page/overview/overview/overview_status'; -import { SyntheticsEmbeddableContext } from '../synthetics_embeddable_context'; - -export const StatusOverviewComponent = ({ reload$ }: { reload$: Subject }) => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx index 0953fb79961b1..6e4cbd4748e95 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/synthetics_embeddable_context.tsx @@ -9,23 +9,32 @@ import React from 'react'; import { createBrowserHistory } from 'history'; import { EuiPanel } from '@elastic/eui'; import { Router } from '@kbn/shared-ux-router'; +import { Subject } from 'rxjs'; +import { Store } from 'redux'; import { SyntheticsSharedContext } from '../synthetics/contexts/synthetics_shared_context'; import { SyntheticsEmbeddableStateContextProvider } from '../synthetics/contexts/synthetics_embeddable_context'; import { getSyntheticsAppProps } from '../synthetics/render_app'; import { SyntheticsSettingsContextProvider } from '../synthetics/contexts'; -export const SyntheticsEmbeddableContext: React.FC<{ search?: string }> = ({ - search, - children, -}) => { +export const SyntheticsEmbeddableContext: React.FC<{ + reload$: Subject; + reduxStore?: Store; +}> = ({ reload$, children, reduxStore }) => { const props = getSyntheticsAppProps(); return ( - + - {children} + + {children} + diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.tsx new file mode 100644 index 0000000000000..aa7355c4e1fec --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_monitors_overview_panel_action.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 { i18n } from '@kbn/i18n'; +import { apiIsPresentationContainer } from '@kbn/presentation-containers'; +import { + IncompatibleActionError, + type UiActionsActionDefinition, +} from '@kbn/ui-actions-plugin/public'; +import { EmbeddableApiContext } from '@kbn/presentation-publishing'; +import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; +import { ClientPluginsStart } from '../../../plugin'; +import { COMMON_SYNTHETICS_GROUPING, SYNTHETICS_MONITORS_EMBEDDABLE } from '../constants'; + +export const ADD_SYNTHETICS_MONITORS_OVERVIEW_ACTION_ID = + 'CREATE_SYNTHETICS_MONITORS_OVERVIEW_EMBEDDABLE'; + +export function createMonitorsOverviewPanelAction( + getStartServices: StartServicesAccessor +): UiActionsActionDefinition { + return { + id: ADD_SYNTHETICS_MONITORS_OVERVIEW_ACTION_ID, + grouping: COMMON_SYNTHETICS_GROUPING, + order: 30, + getIconType: () => 'play', + isCompatible: async ({ embeddable }) => { + return apiIsPresentationContainer(embeddable); + }, + execute: async ({ embeddable }) => { + if (!apiIsPresentationContainer(embeddable)) throw new IncompatibleActionError(); + const [coreStart, pluginStart] = await getStartServices(); + const { openMonitorConfiguration } = await import('../common/monitors_open_configuration'); + + const initialState = await openMonitorConfiguration({ + coreStart, + pluginStart, + }); + try { + embeddable.addNewPanel({ + panelType: SYNTHETICS_MONITORS_EMBEDDABLE, + initialState, + }); + } catch (e) { + return Promise.reject(); + } + }, + getDisplayName: () => + i18n.translate('xpack.synthetics.syntheticsEmbeddable.monitors.ariaLabel', { + defaultMessage: 'Monitors overview', + }), + }; +} diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx similarity index 57% rename from x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx rename to x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx index 79c6a6c1195a9..a3b2c67be88fe 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_overview_panel_action.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/create_stats_overview_panel_action.tsx @@ -10,23 +10,15 @@ import { type UiActionsActionDefinition, } from '@kbn/ui-actions-plugin/public'; import { EmbeddableApiContext } from '@kbn/presentation-publishing'; -import { SYNTHETICS_OVERVIEW_EMBEDDABLE } from '../constants'; +import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser'; +import { ClientPluginsStart } from '../../../plugin'; +import { COMMON_SYNTHETICS_GROUPING, SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE } from '../constants'; -export const COMMON_SYNTHETICS_GROUPING = [ - { - id: 'synthetics', - getDisplayName: () => - i18n.translate('xpack.synthetics.common.constants.grouping.legacy', { - defaultMessage: 'Synthetics', - }), - getIconType: () => { - return 'online'; - }, - }, -]; -export const ADD_SYNTHETICS_OVERVIEW_ACTION_ID = 'CREATE_SYNTHETICS_OVERVIEW_EMBEDDABLE'; +export const ADD_SYNTHETICS_OVERVIEW_ACTION_ID = 'CREATE_SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE'; -export function createStatusOverviewPanelAction(): UiActionsActionDefinition { +export function createStatusOverviewPanelAction( + getStartServices: StartServicesAccessor +): UiActionsActionDefinition { return { id: ADD_SYNTHETICS_OVERVIEW_ACTION_ID, grouping: COMMON_SYNTHETICS_GROUPING, @@ -40,16 +32,24 @@ export function createStatusOverviewPanelAction(): UiActionsActionDefinition - i18n.translate('xpack.synthetics.syntheticsEmbeddable.ariaLabel', { - defaultMessage: 'Synthetics Overview', + i18n.translate('xpack.synthetics.syntheticsEmbeddable.stats.ariaLabel', { + defaultMessage: 'Monitors stats', }), }; } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/register_ui_actions.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/register_ui_actions.ts new file mode 100644 index 0000000000000..6edc4bb40a028 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/embeddables/ui_actions/register_ui_actions.ts @@ -0,0 +1,30 @@ +/* + * 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 { ADD_PANEL_TRIGGER } from '@kbn/ui-actions-browser/src'; +import { CoreSetup } from '@kbn/core-lifecycle-browser'; +import { createStatusOverviewPanelAction } from './create_stats_overview_panel_action'; +import { createMonitorsOverviewPanelAction } from './create_monitors_overview_panel_action'; +import { ClientPluginsSetup, ClientPluginsStart } from '../../../plugin'; + +export const registerSyntheticsUiActions = async ( + core: CoreSetup, + pluginsSetup: ClientPluginsSetup +) => { + const { uiActions, cloud, serverless } = pluginsSetup; + + // Initialize actions + const addStatsOverviewPanelAction = createStatusOverviewPanelAction(core.getStartServices); + const addMonitorsOverviewPanelAction = createMonitorsOverviewPanelAction(core.getStartServices); + + // Assign triggers + // Only register these actions in stateful kibana, and the serverless observability project + if (Boolean((serverless && cloud?.serverless.projectType === 'observability') || !serverless)) { + uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addStatsOverviewPanelAction); + uiActions.addTriggerAction(ADD_PANEL_TRIGGER, addMonitorsOverviewPanelAction); + } +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/add_to_dashboard.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/add_to_dashboard.tsx new file mode 100644 index 0000000000000..2a910855aa245 --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/add_to_dashboard.tsx @@ -0,0 +1,142 @@ +/* + * 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 { + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useCallback } from 'react'; +import { + LazySavedObjectSaveModalDashboard, + SaveModalDashboardProps, + withSuspense, +} from '@kbn/presentation-util-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ClientPluginsStart } from '../../../../../plugin'; +import { + SYNTHETICS_MONITORS_EMBEDDABLE, + SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE, +} from '../../../../embeddables/constants'; + +const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); + +export const AddToDashboard = ({ + type, + asButton = false, +}: { + type: typeof SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE | typeof SYNTHETICS_MONITORS_EMBEDDABLE; + asButton?: boolean; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const [isDashboardAttachmentReady, setDashboardAttachmentReady] = React.useState(false); + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const { embeddable } = useKibana().services; + + const handleAttachToDashboardSave: SaveModalDashboardProps['onSave'] = useCallback( + ({ dashboardId, newTitle, newDescription }) => { + const stateTransfer = embeddable.getStateTransfer(); + const embeddableInput = {}; + + const state = { + input: embeddableInput, + type, + }; + + const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; + + stateTransfer.navigateToWithEmbeddablePackage('dashboards', { + state, + path, + }); + }, + [embeddable, type] + ); + const isSyntheticsApp = window.location.pathname.includes('/app/synthetics'); + + if (!isSyntheticsApp) { + return null; + } + + return ( + <> + {asButton ? ( + setDashboardAttachmentReady(true)} + > + {i18n.translate('xpack.synthetics.embeddablePanelWrapper.shareButtonLabel', { + defaultMessage: 'Add to dashboard', + })} + + ) : ( + setIsPopoverOpen(!isPopoverOpen)} + /> + } + isOpen={isPopoverOpen} + closePopover={closePopover} + > + { + setDashboardAttachmentReady(true); + closePopover(); + }} + > + {i18n.translate( + 'xpack.synthetics.embeddablePanelWrapper.shareContextMenuItemLabel', + { + defaultMessage: 'Add to dashboard', + } + )} + , + ]} + /> + + )} + {isDashboardAttachmentReady ? ( + { + setDashboardAttachmentReady(false); + }} + onSave={handleAttachToDashboardSave} + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx index cd73097c956a6..bb8ec04105167 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/common/components/embeddable_panel_wrapper.tsx @@ -5,66 +5,18 @@ * 2.0. */ -import React, { FC, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiPopover, - EuiProgress, - EuiTitle, -} from '@elastic/eui'; -import { - LazySavedObjectSaveModalDashboard, - SaveModalDashboardProps, - withSuspense, -} from '@kbn/presentation-util-plugin/public'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { SYNTHETICS_OVERVIEW_EMBEDDABLE } from '../../../../embeddables/constants'; -import { ClientPluginsStart } from '../../../../../plugin'; - -const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard); +import React, { FC } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress, EuiTitle } from '@elastic/eui'; +import { AddToDashboard } from './add_to_dashboard'; +import { SYNTHETICS_STATS_OVERVIEW_EMBEDDABLE } from '../../../../embeddables/constants'; export const EmbeddablePanelWrapper: FC<{ title: string; loading?: boolean; -}> = ({ children, title, loading }) => { - const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); - - const [isDashboardAttachmentReady, setDashboardAttachmentReady] = React.useState(false); - - const closePopover = () => { - setIsPopoverOpen(false); - }; - - const { embeddable } = useKibana().services; - + titleAppend?: React.ReactNode; +}> = ({ children, title, loading, titleAppend }) => { const isSyntheticsApp = window.location.pathname.includes('/app/synthetics'); - const handleAttachToDashboardSave: SaveModalDashboardProps['onSave'] = useCallback( - ({ dashboardId, newTitle, newDescription }) => { - const stateTransfer = embeddable.getStateTransfer(); - const embeddableInput = {}; - - const state = { - input: embeddableInput, - type: SYNTHETICS_OVERVIEW_EMBEDDABLE, - }; - - const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`; - - stateTransfer.navigateToWithEmbeddablePackage('dashboards', { - state, - path, - }); - }, - [embeddable] - ); - return ( <> @@ -77,63 +29,14 @@ export const EmbeddablePanelWrapper: FC<{ {isSyntheticsApp && ( - setIsPopoverOpen(!isPopoverOpen)} - /> - } - isOpen={isPopoverOpen} - closePopover={closePopover} - > - { - setDashboardAttachmentReady(true); - closePopover(); - }} - > - {i18n.translate( - 'xpack.synthetics.embeddablePanelWrapper.shareContextMenuItemLabel', - { defaultMessage: 'Add to dashboard' } - )} - , - ]} - /> - + )} + {titleAppend && {titleAppend}} {children} - {isDashboardAttachmentReady ? ( - { - setDashboardAttachmentReady(false); - }} - onSave={handleAttachToDashboardSave} - /> - ) : null} ); }; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index 7e24a0726a829..a55a93853344b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -30,6 +30,7 @@ import { EuiBadge, EuiToolTip, } from '@elastic/eui'; +import { kibanaService } from '../../../../../utils/kibana_service'; import { PROFILE_OPTIONS, ThrottlingConfigFieldProps, @@ -60,7 +61,6 @@ import { TextArea, ThrottlingWrapper, } from './field_wrappers'; -import { getDocLinks } from '../../../../../kibana_services'; import { useMonitorName } from '../../../hooks/use_monitor_name'; import { ConfigKey, @@ -1332,7 +1332,7 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({ })} {i18n.translate('xpack.synthetics.monitorConfig.playwrightOptions.learnMore', { diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx index 8f92887da6ba8..7fea62b348edd 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/grid_by_group/grid_group_item.tsx @@ -17,6 +17,7 @@ import { EuiTablePagination, } from '@elastic/eui'; import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; import { useKey } from 'react-use'; import { OverviewLoader } from '../overview_loader'; @@ -49,7 +50,7 @@ export const GroupGridItem = ({ const downMonitors = groupMonitors.filter((monitor) => { const downConfigs = overviewStatus?.downConfigs; if (downConfigs) { - return downConfigs[`${monitor.configId}-${monitor.location?.label}`]?.status === 'down'; + return downConfigs[`${monitor.configId}-${monitor.location?.id}`]?.status === 'down'; } }); @@ -96,14 +97,17 @@ export const GroupGridItem = ({ } extraAction={ - + { if (fullScreenGroup) { setFullScreenGroup(''); @@ -117,7 +121,30 @@ export const GroupGridItem = ({ - {groupMonitors.length} Monitors + + {i18n.translate('xpack.synthetics.groupGridItem.monitorsBadgeLabel.downCount', { + defaultMessage: '{downCount} Down', + values: { downCount: downMonitorsCount }, + })} + + + + + + {i18n.translate('xpack.synthetics.groupGridItem.monitorsBadgeLabel.upCount', { + defaultMessage: '{upCount} Up', + values: { upCount: groupMonitors.length - downMonitorsCount }, + })} + + + + + + {i18n.translate('xpack.synthetics.groupGridItem.monitorsBadgeLabel.count', { + defaultMessage: '{count, number} {count, plural, one {monitor} other {monitors}}', + values: { count: groupMonitors.length }, + })} + } @@ -145,7 +172,10 @@ export const GroupGridItem = ({ )} { if (!urlGroupField && groupField !== 'none' && !isUrlHydratedFromRedux.current) { // Hydrate url only during initialization updateUrlParams({ groupBy: groupField, groupOrderBy: groupOrder }); - } else { - dispatch( - setOverviewGroupByAction({ - field: urlGroupField ?? 'none', - order: urlGroupOrderBy ?? 'asc', - }) - ); } } isUrlHydratedFromRedux.current = true; @@ -48,6 +41,21 @@ export const GroupFields = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, groupField, groupOrder, urlGroupField, urlGroupOrderBy]); + const isUReduxHydratedFromUrl = useRef(false); + + useEffect(() => { + if (urlGroupField && urlGroupField !== groupField && !isUReduxHydratedFromUrl.current) { + dispatch( + setOverviewGroupByAction({ + field: urlGroupField ?? 'none', + order: urlGroupOrderBy ?? 'asc', + }) + ); + } + isUReduxHydratedFromUrl.current = true; + // Only depend on the serialized snapshot + }, [dispatch, groupField, groupOrder, urlGroupField, urlGroupOrderBy]); + const handleChange = (groupByState: GroupByState) => { dispatch(setOverviewGroupByAction(groupByState)); updateUrlParams({ groupBy: groupByState.field, groupOrderBy: groupByState.order }); diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item_icon.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item_icon.tsx index 7dacc511c2e70..8945726bc443b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item_icon.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/metric_item_icon.tsx @@ -18,6 +18,8 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem, + EuiLink, + EuiSpacer, } from '@elastic/eui'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; @@ -148,6 +150,21 @@ export const MetricItemIcon = ({
+ {ping?.url?.full && ( + <> + {i18n.translate('xpack.synthetics.metricItemIcon.div.urlLabel', { + defaultMessage: 'URL: ', + })} + + {ping.url.full} + + + + )}
@@ -164,6 +181,23 @@ export const MetricItemIcon = ({ ); } else { + if (ping?.url) { + return ( + + + + ); + } return null; } }; @@ -184,8 +218,10 @@ const StyledIcon = euiStyled.div<{ boxShadow: string }>` gap: 10px; width: 32px; height: 32px; - background: #ffffff; - border: 1px solid #d3dae6; + background: ${({ theme }) => + theme.darkMode ? theme.eui.euiColorDarkestShade : theme.eui.euiColorLightestShade}; + border: 1px solid ${({ theme }) => + theme.darkMode ? theme.eui.euiColorDarkShade : theme.eui.euiColorLightShade}; ${({ boxShadow }) => boxShadow} border-radius: 16px; flex: none; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx index 49f21acc20f10..3da7b03f3ad9e 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_grid.tsx @@ -14,7 +14,10 @@ import { EuiSpacer, EuiButtonEmpty, EuiText, + EuiProgress, } from '@elastic/eui'; +import { SYNTHETICS_MONITORS_EMBEDDABLE } from '../../../../../embeddables/constants'; +import { AddToDashboard } from '../../../common/components/add_to_dashboard'; import { useOverviewStatus } from '../../hooks/use_overview_status'; import { useInfiniteScroll } from './use_infinite_scroll'; import { GridItemsByGroup } from './grid_by_group/grid_items_by_group'; @@ -40,6 +43,7 @@ export const OverviewGrid = memo(() => { data: { monitors }, flyoutConfig, loaded, + loading, pageState, groupBy: { field: groupField }, } = useSelector(selectOverviewState); @@ -76,7 +80,7 @@ export const OverviewGrid = memo(() => { <> @@ -87,6 +91,10 @@ export const OverviewGrid = memo(() => { total={status ? monitorsSortedByStatus.length : undefined} /> + + + + setPage(1)} /> @@ -94,7 +102,9 @@ export const OverviewGrid = memo(() => { - + + {loading && } + <> {groupField === 'none' ? ( loaded && currentMonitors.length ? ( diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx index 28eaf97a37c5f..4f089b2464ed9 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/components/monitors_page/overview/overview/overview_status.tsx @@ -9,8 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiStat } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Subject } from 'rxjs'; -import { useSyntheticsRefreshContext } from '../../../../contexts'; import { EmbeddablePanelWrapper } from '../../../common/components/embeddable_panel_wrapper'; import { clearOverviewStatusErrorAction } from '../../../../state/overview_status'; import { kibanaService } from '../../../../../../utils/kibana_service'; @@ -21,11 +19,9 @@ function title(t?: number) { return t ?? '-'; } -export function OverviewStatus({ reload$ }: { reload$?: Subject }) { +export function OverviewStatus({ titleAppend }: { titleAppend?: React.ReactNode }) { const { statusFilter } = useGetUrlParams(); - const { refreshApp } = useSyntheticsRefreshContext(); - const { status, error: statusError, @@ -39,14 +35,6 @@ export function OverviewStatus({ reload$ }: { reload$?: Subject }) { disabledCount: status?.disabledCount, }); - useEffect(() => { - const sub = reload$?.subscribe(() => { - refreshApp(); - }); - - return () => sub?.unsubscribe(); - }, [refreshApp, reload$]); - useEffect(() => { if (statusError) { dispatch(clearOverviewStatusErrorAction()); @@ -104,9 +92,9 @@ export function OverviewStatus({ reload$ }: { reload$?: Subject }) { }, [status, statusFilter]); return ( - + - + > = ({ children }) => { +export const SyntheticsRefreshContextProvider: FC<{ + reload$?: Subject; +}> = ({ children, reload$ }) => { const [lastRefresh, setLastRefresh] = useState(Date.now()); const refreshPaused = useSelector(selectRefreshPaused); @@ -51,6 +53,13 @@ export const SyntheticsRefreshContextProvider: FC> = } }, [refreshApp, refreshPaused]); + useEffect(() => { + const subscription = reload$?.subscribe(() => { + refreshApp(); + }); + return () => subscription?.unsubscribe(); + }, [reload$, refreshApp]); + const value = useMemo(() => { return { lastRefresh, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx index b4a2702626285..da35fb89dbf3b 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/contexts/synthetics_shared_context.tsx @@ -10,18 +10,16 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common'; import { Provider as ReduxProvider } from 'react-redux'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { Subject } from 'rxjs'; +import { Store } from 'redux'; import { SyntheticsRefreshContextProvider } from './synthetics_refresh_context'; import { SyntheticsDataViewContextProvider } from './synthetics_data_view_context'; import { SyntheticsAppProps } from './synthetics_settings_context'; import { storage, store } from '../state'; -export const SyntheticsSharedContext: React.FC = ({ - coreStart, - setupPlugins, - startPlugins, - children, - darkMode, -}) => { +export const SyntheticsSharedContext: React.FC< + SyntheticsAppProps & { reload$?: Subject; reduxStore?: Store } +> = ({ reduxStore, coreStart, setupPlugins, startPlugins, children, darkMode, reload$ }) => { return ( = ({ }} > - - + + = (dependencies: { - core: CoreStart; - plugins: ClientPluginsStart; -}) => TAlertTypeModel; +import type { AlertTypeInitializer } from './types'; export const syntheticsAlertTypeInitializers: AlertTypeInitializer[] = [ initMonitorStatusAlertType, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx index ba86407859408..5f63d6ac298c7 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/monitor_status.tsx @@ -16,7 +16,8 @@ import { STATE_ID } from '../../../../../common/field_names'; import { SyntheticsMonitorStatusTranslations } from '../../../../../common/rules/synthetics/translations'; import type { StatusRuleParams } from '../../../../../common/rules/status_rule'; import { SYNTHETICS_ALERT_RULE_TYPES } from '../../../../../common/constants/synthetics_alerts'; -import type { AlertTypeInitializer } from '.'; +import type { AlertTypeInitializer } from './types'; + const { defaultActionMessage, defaultRecoveryMessage, description } = SyntheticsMonitorStatusTranslations; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx index 15c0fa90ec605..896a84cd1688c 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx @@ -14,12 +14,14 @@ import { TlsTranslations } from '../../../../../common/rules/synthetics/translat import { CERTIFICATES_ROUTE } from '../../../../../common/constants/ui'; import { SYNTHETICS_ALERT_RULE_TYPES } from '../../../../../common/constants/synthetics_alerts'; import type { TLSParams } from '../../../../../common/runtime_types/alerts/tls'; -import { AlertTypeInitializer } from '.'; + +import type { AlertTypeInitializer } from './types'; let validateFunc: (ruleParams: any) => ValidationResult; const { defaultActionMessage, defaultRecoveryMessage, description } = TlsTranslations; const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert')); + export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins, diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/types.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/types.ts new file mode 100644 index 0000000000000..baf1f6c75801c --- /dev/null +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/lib/alert_types/types.ts @@ -0,0 +1,15 @@ +/* + * 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 { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import type { ClientPluginsStart } from '../../../../plugin'; + +export type AlertTypeInitializer = (dependencies: { + core: CoreStart; + plugins: ClientPluginsStart; +}) => TAlertTypeModel; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts index 7244efc4dc655..cc496074332b0 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/api.ts @@ -36,6 +36,7 @@ export function toStatusOverviewQueryArgs( projects: pageState.projects, schedules: pageState.schedules, monitorTypes: pageState.monitorTypes, + monitorQueryIds: pageState.monitorQueryIds, searchFields: [], }; } diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts index 3f8038ffc84d3..665a92aeaf773 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/overview/index.ts @@ -11,6 +11,7 @@ import { MonitorOverviewState } from './models'; import { fetchMonitorOverviewAction, + quietFetchOverviewAction, setFlyoutConfig, setOverviewGroupByAction, setOverviewPageStateAction, @@ -44,6 +45,9 @@ export const monitorOverviewReducer = createReducer(initialState, (builder) => { state.loading = true; state.loaded = false; }) + .addCase(quietFetchOverviewAction.get, (state, action) => { + state.loading = true; + }) .addCase(fetchMonitorOverviewAction.success, (state, action) => { state.data = action.payload; state.loading = false; diff --git a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/store.ts b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/store.ts index 19320f0102a99..c88f126edeb52 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/store.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/apps/synthetics/state/store.ts @@ -20,7 +20,6 @@ export const store = configureStore({ preloadedState: {}, enhancers: [], }); - sagaMW.run(rootEffect); export const storage = new Storage(window.localStorage); diff --git a/x-pack/plugins/observability_solution/synthetics/public/kibana_services.ts b/x-pack/plugins/observability_solution/synthetics/public/kibana_services.ts deleted file mode 100644 index 9c3f641c29a80..0000000000000 --- a/x-pack/plugins/observability_solution/synthetics/public/kibana_services.ts +++ /dev/null @@ -1,19 +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 type { CoreStart } from '@kbn/core/public'; - -type StartServices = Pick; - -let coreStart: CoreStart; -export function setStartServices(core: CoreStart) { - coreStart = core; -} - -export const getStartServices = (): StartServices => coreStart; - -export const getDocLinks = () => coreStart?.docLinks; diff --git a/x-pack/plugins/observability_solution/synthetics/public/plugin.ts b/x-pack/plugins/observability_solution/synthetics/public/plugin.ts index 4489d418830e5..15de2de6d5eee 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/plugin.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/plugin.ts @@ -66,7 +66,6 @@ import { kibanaService } from './utils/kibana_service'; import { PLUGIN } from '../common/constants/plugin'; import { OVERVIEW_ROUTE } from '../common/constants/ui'; import { locators } from './apps/locators'; -import { setStartServices } from './kibana_services'; import { syntheticsAlertTypeInitializers } from './apps/synthetics/lib/alert_types'; export interface ClientPluginsSetup { @@ -216,9 +215,6 @@ export class SyntheticsPlugin public start(coreStart: CoreStart, pluginsStart: ClientPluginsStart): void { const { triggersActionsUi } = pluginsStart; - setStartServices(coreStart); - - setStartServices(coreStart); syntheticsAlertTypeInitializers.forEach((init) => { const { observabilityRuleTypeRegistry } = pluginsStart.observability; diff --git a/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts b/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts index 682b3be6a700a..9a0b887a7d6eb 100644 --- a/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts +++ b/x-pack/plugins/observability_solution/synthetics/public/utils/api_service/api_service.ts @@ -9,8 +9,10 @@ import { isRight } from 'fp-ts/lib/Either'; import { formatErrors } from '@kbn/securitysolution-io-ts-utils'; import { HttpFetchQuery, HttpHeadersInit, HttpSetup } from '@kbn/core/public'; import { FETCH_STATUS, AddInspectorRequest } from '@kbn/observability-shared-plugin/public'; -import { InspectorRequestProps } from '@kbn/observability-shared-plugin/public/contexts/inspector/inspector_context'; +import type { InspectorRequestProps } from '@kbn/observability-shared-plugin/public/contexts/inspector/inspector_context'; + type Params = HttpFetchQuery & { version?: string }; + class ApiService { private static instance: ApiService; private _http!: HttpSetup; diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts index b29e3578aba87..491b67160677e 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/common.ts @@ -44,6 +44,7 @@ export const OverviewStatusSchema = schema.object({ monitorTypes: StringOrArraySchema, locations: StringOrArraySchema, projects: StringOrArraySchema, + monitorQueryIds: StringOrArraySchema, schedules: StringOrArraySchema, status: StringOrArraySchema, scopeStatusByLocation: schema.maybe(schema.boolean()), diff --git a/x-pack/plugins/observability_solution/synthetics/server/routes/suggestions/route.ts b/x-pack/plugins/observability_solution/synthetics/server/routes/suggestions/route.ts index ae729e7361a4d..3a9c3b064bc0f 100644 --- a/x-pack/plugins/observability_solution/synthetics/server/routes/suggestions/route.ts +++ b/x-pack/plugins/observability_solution/synthetics/server/routes/suggestions/route.ts @@ -30,6 +30,9 @@ interface AggsResponse { projectsAggs: { buckets: Buckets; }; + monitorTypeAggs: { + buckets: Buckets; + }; monitorIdsAggs: { buckets: Array<{ key: string; @@ -82,7 +85,7 @@ export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory< searchFields: SEARCH_FIELDS, }); - const { tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } = + const { monitorTypeAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } = (data?.aggregations as AggsResponse) ?? {}; const allLocationsMap = new Map(allLocations.map((obj) => [obj.id, obj.label])); @@ -110,6 +113,12 @@ export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory< value: key, count, })) ?? [], + monitorTypes: + monitorTypeAggs?.buckets?.map(({ key, doc_count: count }) => ({ + label: key, + value: key, + count, + })) ?? [], }; } catch (error) { logger.error(`Failed to fetch synthetics suggestions: ${error}`); @@ -125,6 +134,13 @@ const aggs = { exclude: [''], }, }, + monitorTypeAggs: { + terms: { + field: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_TYPE}.keyword`, + size: 10000, + exclude: [''], + }, + }, locationsAggs: { terms: { field: `${syntheticsMonitorType}.attributes.${ConfigKey.LOCATIONS}.id`, diff --git a/x-pack/test/api_integration/apis/synthetics/suggestions.ts b/x-pack/test/api_integration/apis/synthetics/suggestions.ts index bbdac399714de..dab822f832c08 100644 --- a/x-pack/test/api_integration/apis/synthetics/suggestions.ts +++ b/x-pack/test/api_integration/apis/synthetics/suggestions.ts @@ -161,6 +161,18 @@ export default function ({ getService }: FtrProviderContext) { value: project, }, ], + monitorTypes: [ + { + count: 20, + label: 'http', + value: 'http', + }, + { + count: 2, + label: 'icmp', + value: 'icmp', + }, + ], tags: expect.arrayContaining([ { count: 21, @@ -237,6 +249,18 @@ export default function ({ getService }: FtrProviderContext) { value: project, }, ], + monitorTypes: [ + // { + // count: 20, + // label: 'http', + // value: 'http', + // }, + { + count: 2, + label: 'icmp', + value: 'icmp', + }, + ], tags: expect.arrayContaining([ { count: 1,