Skip to content

Commit

Permalink
[Synthetics] Make overview grid embeddable (elastic#160597)
Browse files Browse the repository at this point in the history
## Summary

Overview grid can be embedded as part of dashboard !!

Can be added by selecting type `Select Type -> Synthetics -> `

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/b13f7e06-5f35-4415-9001-4a4340a6ce55">

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/d9fb7b2a-c339-4a9c-85c7-f273db601e9e">
  • Loading branch information
shahzad31 authored Aug 13, 2024
1 parent a9c4d2f commit f47f853
Show file tree
Hide file tree
Showing 47 changed files with 1,401 additions and 260 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -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<MonitorFilters>;
placeholder: string;
tooltip?: ReactNode;
suggestions?: Suggestion[];
isLoading?: boolean;
required?: boolean;
}

export function FieldSelector({
dataTestSubj,
label,
name,
placeholder,
tooltip,
required,
}: Props) {
const { control, getFieldState } = useFormContext<MonitorFilters>();
const [search, setSearch] = useState<string>('');

const { suggestions = [], isLoading } = useFetchSyntheticsSuggestions({
search,
fieldName: name,
});

const debouncedSearch = debounce((value) => setSearch(value), 200);

return (
<EuiFlexItem>
<EuiFormRow
label={
!!tooltip ? (
<span>
{label} {tooltip}
</span>
) : (
label
)
}
isInvalid={getFieldState(name).invalid}
fullWidth
labelAppend={!required ? <OptionalText /> : undefined}
>
<Controller
defaultValue=""
name={name}
control={control}
rules={{ required }}
render={({ field, fieldState }) => {
const selectedOptions =
!!Array.isArray(field.value) && field.value.length
? createSelectedOptions(field.value, suggestions)
: [];

return (
<EuiComboBox
{...field}
aria-label={placeholder}
async
data-test-subj={dataTestSubj}
isClearable
fullWidth
isInvalid={fieldState.invalid}
isLoading={isLoading}
onChange={(selected: EuiComboBoxOptionOption[]) => {
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}
/>
);
}}
/>
</EuiFormRow>
</EuiFlexItem>
);
}

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 };
});
}
Original file line number Diff line number Diff line change
@@ -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<MonitorFilters>({
defaultValues: {
monitorIds: [],
projects: [],
tags: [],
monitorTypes: [],
locations: [],
},
values: initialInput?.filters,
mode: 'all',
});
const { getValues, formState } = methods;

const onConfirmClick = () => {
const newFilters = getValues();
onCreate({
filters: newFilters,
});
};

return (
<EuiFlyout data-test-subj="sloSingleOverviewConfiguration" onClose={onCancel}>
<EuiFlyoutHeader>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiTitle>
<h2>
{i18n.translate(
'xpack.synthetics.overviewEmbeddable.config.sloSelector.headerTitle',
{
defaultMessage: 'Overview configuration',
}
)}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutHeader>

<>
<EuiFlyoutBody>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem data-test-subj="singleSloSelector" grow>
<FormProvider {...methods}>
<MonitorFiltersForm />
</FormProvider>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiButtonEmpty
data-test-subj="syntheticsMonitorConfigurationCancelButton"
onClick={onCancel}
>
<FormattedMessage
id="xpack.synthetics.sloEmbeddable.config.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>

<EuiButton
data-test-subj="syntheticsMonitorConfigurationSaveButton"
isDisabled={!(formState.isDirty || !initialInput)}
onClick={onConfirmClick}
fill
>
<FormattedMessage
id="xpack.synthetics.overviewEmbeddableSlo.config.confirmButtonLabel"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
</EuiFlyout>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexGroup direction="column" gutterSize="l">
<FieldSelector
label={i18n.translate('xpack.synthetics.monitorEdit.syntheticsAvailability.monitor', {
defaultMessage: 'Monitor name',
})}
placeholder={i18n.translate(
'xpack.synthetics.monitorEdit.syntheticsAvailability.monitor.placeholder',
{ defaultMessage: 'Select the Synthetics monitor or choose all' }
)}
name="monitorIds"
dataTestSubj="syntheticsAvailabilityMonitorSelector"
tooltip={
<EuiIconTip
content={i18n.translate(
'xpack.synthetics.monitorEdit.syntheticsAvailability.monitor.tooltip',
{
defaultMessage:
'This is the monitor or monitors part of this monitor. Select "*" to group by project, tag, or location',
}
)}
position="top"
/>
}
/>
<FieldSelector
label={i18n.translate('xpack.synthetics.monitorEdit.syntheticsAvailability.tags', {
defaultMessage: 'Tags',
})}
placeholder={i18n.translate(
'xpack.synthetics.monitorEdit.syntheticsAvailability.tags.placeholder',
{
defaultMessage: 'Select tags',
}
)}
name="tags"
dataTestSubj="syntheticsAvailabilityTagsSelector"
/>
<FieldSelector
label={i18n.translate('xpack.synthetics.monitorEdit.syntheticsAvailability.location', {
defaultMessage: 'Locations',
})}
placeholder={i18n.translate(
'xpack.synthetics.monitorEdit.syntheticsAvailability.location.placeholder',
{
defaultMessage: 'Select the locations',
}
)}
name="locations"
dataTestSubj="syntheticsAvailabilityLocationSelector"
/>
<FieldSelector
label={i18n.translate('xpack.synthetics.monitorEdit.syntheticsAvailability.project', {
defaultMessage: 'Project',
})}
placeholder={i18n.translate(
'xpack.synthetics.monitorEdit.syntheticsAvailability.project.placeholder',
{
defaultMessage: 'Select the project',
}
)}
name="projects"
dataTestSubj="syntheticsAvailabilityProjectSelector"
/>
<FieldSelector
label={i18n.translate('xpack.synthetics.monitorEdit.syntheticsAvailability.type', {
defaultMessage: 'Monitor type',
})}
placeholder={i18n.translate(
'xpack.synthetics.monitorEdit.syntheticsAvailability.type.placeholder',
{
defaultMessage: 'Select the monitor type',
}
)}
name="monitorTypes"
dataTestSubj="syntheticsAvailabilityProjectSelector"
/>
</EuiFlexGroup>
</EuiFlexGroup>
);
}
Loading

0 comments on commit f47f853

Please sign in to comment.