Skip to content

Commit

Permalink
SLO: Creation Form: Add Query Builder component (#149880)
Browse files Browse the repository at this point in the history
Resolves #149867
  • Loading branch information
CoenWarmer authored Jan 31, 2023
1 parent 7693b2f commit a706a11
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 81 deletions.
42 changes: 42 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_create_data_view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useEffect, useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '../utils/kibana_react';

interface UseCreateDataViewProps {
indexPatternString: string | undefined;
}

export function useCreateDataView({ indexPatternString }: UseCreateDataViewProps) {
const { dataViews } = useKibana().services;

const [stateDataView, setStateDataView] = useState<DataView | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(false);

useEffect(() => {
const createDataView = () =>
dataViews.create({
title: indexPatternString,
allowNoIndex: true,
});

if (indexPatternString) {
setIsLoading(true);
createDataView()
.then((value) => {
setStateDataView(value);
})
.finally(() => {
setIsLoading(false);
});
}
}, [indexPatternString, dataViews]);

return { dataView: stateDataView, loading: isLoading };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { ComponentStory } from '@storybook/react';

import { FormProvider, useForm } from 'react-hook-form';
import { KibanaReactStorybookDecorator } from '../../../../utils/kibana_react.storybook_decorator';
import { QueryBuilder as Component, Props } from './query_builder';
import { SLO_EDIT_FORM_DEFAULT_VALUES } from '../../constants';

export default {
component: Component,
title: 'app/SLO/EditPage/CustomKQL/QueryBuilder',
decorators: [KibanaReactStorybookDecorator],
};

const Template: ComponentStory<typeof Component> = (props: Props) => {
const methods = useForm({ defaultValues: SLO_EDIT_FORM_DEFAULT_VALUES });
return (
<FormProvider {...methods}>
<Component {...props} control={methods.control} />
</FormProvider>
);
};

const defaultProps = {
dataTestSubj: 'dataTestSubj',
indexPatternString: 'log*',
name: 'name' as const,
placeholder: 'Enter something if you dare',
};

export const QueryBuilder = Template.bind({});
QueryBuilder.args = defaultProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 { Control, Controller, FieldPath } from 'react-hook-form';
import { EuiFormRow } from '@elastic/eui';
import { CreateSLOInput } from '@kbn/slo-schema';
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
import { useKibana } from '../../../../utils/kibana_react';
import { useCreateDataView } from '../../../../hooks/use_create_data_view';

export interface Props {
control: Control<CreateSLOInput>;
dataTestSubj: string;
indexPatternString: string | undefined;
label: string;
name: FieldPath<CreateSLOInput>;
placeholder: string;
}

export function QueryBuilder({
control,
dataTestSubj,
indexPatternString,
label,
name,
placeholder,
}: Props) {
const { data, dataViews, docLinks, http, notifications, storage, uiSettings, unifiedSearch } =
useKibana().services;

const { dataView } = useCreateDataView({ indexPatternString });

return (
<EuiFormRow label={label} fullWidth>
<Controller
name={name}
control={control}
render={({ field }) => (
<QueryStringInput
appName="Observability"
bubbleSubmitEvent={false}
dataTestSubj={dataTestSubj}
deps={{
data,
dataViews,
docLinks,
http,
notifications,
storage,
uiSettings,
unifiedSearch,
}}
disableAutoFocus
disableLanguageSwitcher
indexPatterns={dataView ? [dataView] : []}
isDisabled={!indexPatternString}
languageSwitcherPopoverAnchorPosition="rightDown"
placeholder={placeholder}
query={{ query: String(field.value), language: 'kuery' }}
size="s"
onChange={(value) => {
field.onChange(value.query);
}}
/>
)}
/>
</EuiFormRow>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function SloEditForm({ slo }: Props) {
<EuiSpacer size="xxl" />

{watch('indicator.type') === 'sli.kql.custom' ? (
<SloEditFormDefinitionCustomKql control={control} />
<SloEditFormDefinitionCustomKql control={control} watch={watch} />
) : null}

<EuiSpacer size="m" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,107 +6,85 @@
*/

import React from 'react';
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiSuggest } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Control, Controller } from 'react-hook-form';
import { Control, UseFormWatch } from 'react-hook-form';
import type { CreateSLOInput } from '@kbn/slo-schema';

import { IndexSelection } from './custom_kql/index_selection';
import { QueryBuilder } from './custom_kql/query_builder';

export interface Props {
control: Control<CreateSLOInput>;
watch: UseFormWatch<CreateSLOInput>;
}

export function SloEditFormDefinitionCustomKql({ control }: Props) {
export function SloEditFormDefinitionCustomKql({ control, watch }: Props) {
return (
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexItem>
<IndexSelection control={control} />
</EuiFlexItem>

<EuiFlexItem>
<EuiFormLabel>
{i18n.translate('xpack.observability.slos.sloEdit.sloDefinition.customKql.queryFilter', {
defaultMessage: 'Query filter',
})}
</EuiFormLabel>
<Controller
name="indicator.params.filter"
<QueryBuilder
control={control}
rules={{ required: true }}
render={({ field }) => (
<EuiSuggest
append={<EuiButtonEmpty>KQL</EuiButtonEmpty>}
status="unchanged"
aria-label="Filter query"
data-test-subj="sloFormCustomKqlFilterQueryInput"
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.customFilter',
{
defaultMessage: 'Custom filter to apply on the index',
}
)}
suggestions={[]}
{...field}
/>
dataTestSubj="sloFormCustomKqlFilterQueryInput"
indexPatternString={watch('indicator.params.index')}
label={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.queryFilter',
{
defaultMessage: 'Query filter',
}
)}
name="indicator.params.filter"
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.customFilter',
{
defaultMessage: 'Custom filter to apply on the index',
}
)}
/>
</EuiFlexItem>

<EuiFlexItem>
<EuiFormLabel>
{i18n.translate('xpack.observability.slos.sloEdit.sloDefinition.customKql.goodQuery', {
defaultMessage: 'Good query',
})}
</EuiFormLabel>
<Controller
name="indicator.params.good"
<QueryBuilder
control={control}
rules={{ required: true }}
render={({ field }) => (
<EuiSuggest
append={<EuiButtonEmpty>KQL</EuiButtonEmpty>}
status="unchanged"
aria-label="Good filter"
data-test-subj="sloFormCustomKqlGoodQueryInput"
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.goodQueryPlaceholder',
{
defaultMessage: 'Define the good events',
}
)}
suggestions={[]}
{...field}
/>
dataTestSubj="sloFormCustomKqlGoodQueryInput"
indexPatternString={watch('indicator.params.index')}
label={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.goodQuery',
{
defaultMessage: 'Good query',
}
)}
name="indicator.params.good"
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.goodQueryPlaceholder',
{
defaultMessage: 'Define the good events',
}
)}
/>
</EuiFlexItem>

<EuiFlexItem>
<EuiFormLabel>
{i18n.translate('xpack.observability.slos.sloEdit.sloDefinition.customKql.totalQuery', {
defaultMessage: 'Total query',
})}
</EuiFormLabel>
<Controller
name="indicator.params.total"
<QueryBuilder
control={control}
rules={{ required: true }}
render={({ field }) => (
<EuiSuggest
append={<EuiButtonEmpty>KQL</EuiButtonEmpty>}
status="unchanged"
aria-label="Total filter"
data-test-subj="sloFormCustomKqlTotalQueryInput"
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.totalQueryPlaceholder',
{
defaultMessage: 'Define the total events',
}
)}
suggestions={[]}
{...field}
/>
dataTestSubj="sloFormCustomKqlTotalQueryInput"
indexPatternString={watch('indicator.params.index')}
label={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.totalQuery',
{
defaultMessage: 'Total query',
}
)}
name="indicator.params.total"
placeholder={i18n.translate(
'xpack.observability.slos.sloEdit.sloDefinition.customKql.totalQueryPlaceholder',
{
defaultMessage: 'Define the total events',
}
)}
/>
</EuiFlexItem>
Expand Down
31 changes: 29 additions & 2 deletions x-pack/plugins/observability/public/pages/slo_edit/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,43 @@ const mockBasePathPrepend = jest.fn();
const mockKibana = () => {
useKibanaMock.mockReturnValue({
services: {
application: { navigateToUrl: mockNavigate },
application: {
navigateToUrl: mockNavigate,
},
data: {
dataViews: {
find: jest.fn().mockReturnValue([]),
get: jest.fn().mockReturnValue([]),
},
},
dataViews: {
create: jest.fn().mockResolvedValue(42),
},
docLinks: {
links: {
query: {},
},
},
http: {
basePath: {
prepend: mockBasePathPrepend,
},
},
notifications: {
toasts: {
addSuccess: mockAddSuccess,
addError: mockAddError,
addSuccess: mockAddSuccess,
},
},
storage: {
get: () => {},
},
uiSettings: {
get: () => {},
},
unifiedSearch: {
autocomplete: {
hasQuerySuggestions: () => {},
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/observability/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { SecurityPluginStart } from '@kbn/security-plugin/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { RuleDetailsLocatorDefinition } from './locators/rule_details';
import { observabilityAppId, observabilityFeatureId, casesPath } from '../common';
import { createLazyObservabilityPageTemplate } from './components/shared';
Expand Down Expand Up @@ -107,6 +108,7 @@ export interface ObservabilityPublicPluginsStart {
spaces: SpacesPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
usageCollection: UsageCollectionSetup;
unifiedSearch: UnifiedSearchPublicPluginStart;
home?: HomePublicPluginStart;
}

Expand Down
Loading

0 comments on commit a706a11

Please sign in to comment.