Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { StandaloneRuleFormFlyout } from '../standalone_rule_form_flyout';
import { RuleFormFlyout } from '../rule_form_flyout';
import { DynamicRuleForm } from '../../form/dynamic_rule_form';
import { StandaloneRuleForm } from '../../form/standalone_rule_form';
import type { RuleFormServices } from '../../form/contexts/rule_form_services_context';
import type { RuleFormServices } from '../../form/contexts/rule_form_context';

const mockServices = {
http: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,10 @@ const DynamicRuleFormFlyoutInner: React.FC<DynamicRuleFormFlyoutProps> = ({
const { createRule, isLoading } = useCreateRule({
http: services.http,
notifications: services.notifications,
onSuccess: onClose,
});

const handleSubmit = (values: FormValues) => {
createRule(values);
createRule(values, { onSuccess: onClose });
};

return (
Expand All @@ -57,6 +56,7 @@ const DynamicRuleFormFlyoutInner: React.FC<DynamicRuleFormFlyoutProps> = ({
isSubmitting={isLoading}
query={query}
services={services}
layout="flyout"
/>
</RuleFormFlyout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const RuleFormFlyout: React.FC<RuleFormFlyoutProps> = ({
type={push ? 'push' : 'overlay'}
onClose={onClose || (() => {})}
aria-labelledby={FLYOUT_TITLE_ID}
size="m"
size="l"
maxWidth={600}
>
<EuiFlyoutHeader hasBorder>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,10 @@ const StandaloneRuleFormFlyoutInner: React.FC<StandaloneRuleFormFlyoutProps> = (
const { createRule, isLoading } = useCreateRule({
http: services.http,
notifications: services.notifications,
onSuccess: onClose ?? (() => {}),
});

const handleSubmit = (values: FormValues) => {
createRule(values);
createRule(values, { onSuccess: onClose });
};

return (
Expand All @@ -57,6 +56,7 @@ const StandaloneRuleFormFlyoutInner: React.FC<StandaloneRuleFormFlyoutProps> = (
isSubmitting={isLoading}
query={query}
services={services}
layout="flyout"
/>
</RuleFormFlyout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ const toggleButtons = [
label: i18n.translate('xpack.alertingV2.ruleForm.editMode.form', {
defaultMessage: 'Form',
}),
iconType: 'productDashboard',
'data-test-subj': 'ruleV2FormEditModeFormButton',
},
{
id: 'yaml',
label: i18n.translate('xpack.alertingV2.ruleForm.editMode.yaml', {
defaultMessage: 'YAML',
}),
iconType: 'code',
'data-test-subj': 'ruleV2FormEditModeYamlButton',
},
];
Expand All @@ -48,6 +50,7 @@ export const EditModeToggle: React.FC<EditModeToggleProps> = ({ editMode, onChan
idSelected={editMode}
onChange={handleChange}
buttonSize="compressed"
isIconOnly
isFullWidth={false}
isDisabled={disabled}
data-test-subj="ruleV2FormEditModeToggle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*/

export {
RuleFormServicesProvider,
RuleFormProvider,
useRuleFormServices,
useRuleFormMeta,
type RuleFormServices,
} from './rule_form_services_context';
type RuleFormMeta,
type RuleFormLayout,
} from './rule_form_context';
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { PropsWithChildren } from 'react';
import React, { createContext, useContext, useMemo } from 'react';
import type { ApplicationStart, HttpStart, NotificationsStart } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';

export interface RuleFormServices {
http: HttpStart;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
notifications: NotificationsStart;
application: ApplicationStart;
}

export type RuleFormLayout = 'page' | 'flyout';

export interface RuleFormMeta {
/** Whether the form is rendered on a full page or inside a flyout. */
layout: RuleFormLayout;
}

interface RuleFormContextValue {
services: RuleFormServices;
meta: RuleFormMeta;
}

const DEFAULT_META: RuleFormMeta = { layout: 'page' };

const RuleFormContext = createContext<RuleFormContextValue | undefined>(undefined);

/**
* Provides services and metadata to all rule form descendants.
*
* `meta` defaults to `{ layout: 'page' }` when omitted.
*/
export const RuleFormProvider: React.FC<
PropsWithChildren<{ services: RuleFormServices; meta?: RuleFormMeta }>
> = ({ children, services, meta = DEFAULT_META }) => {
const value = useMemo(() => ({ services, meta }), [services, meta]);
return <RuleFormContext.Provider value={value}>{children}</RuleFormContext.Provider>;
};

const useRuleFormContext = (): RuleFormContextValue => {
const context = useContext(RuleFormContext);
if (!context) {
throw new Error('useRuleFormContext must be used within RuleFormProvider');
}
return context;
};

/** Backward-compatible hook that returns only the services object. */
export const useRuleFormServices = (): RuleFormServices => {
const { services } = useRuleFormContext();
return services;
};

/** Returns the form metadata (layout, etc.). */
export const useRuleFormMeta = (): RuleFormMeta => {
const { meta } = useRuleFormContext();
return meta;
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ describe('DynamicRuleForm', () => {
);

// The form should render
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Rule details')).toBeInTheDocument();
expect(screen.getByText('Untitled rule')).toBeInTheDocument();
});

it('updates form state when query prop changes', async () => {
Expand All @@ -101,7 +100,7 @@ describe('DynamicRuleForm', () => {
// The form should update - we can verify by checking that no errors occurred
// and the component re-rendered successfully
await waitFor(() => {
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Untitled rule')).toBeInTheDocument();
});
});

Expand All @@ -115,12 +114,23 @@ describe('DynamicRuleForm', () => {
</Wrapper>
);

// User modifies the name field
const nameInput = screen.getByRole('textbox', { name: 'Name' });
await user.type(nameInput, 'My Custom Rule');
// User modifies the name field — click to enter edit mode, then type
const readModeButton = screen.getByTestId('euiInlineReadModeButton');
await user.click(readModeButton);

// Find the name input (not the combo box input) and replace content
const nameInput = screen.getByLabelText('Edit rule name');

// Select all text and replace with new value
await user.tripleClick(nameInput);
await user.keyboard('My Custom Rule');

expect(nameInput).toHaveValue('My Custom Rule');

// Save the edit
const saveButton = screen.getByTestId('euiInlineEditModeSaveButton');
await user.click(saveButton);

// Query prop changes (simulating Discover updating the query)
rerender(
<Wrapper>
Expand All @@ -130,7 +140,7 @@ describe('DynamicRuleForm', () => {

// User's input should be preserved (keepDirtyValues: true)
await waitFor(() => {
expect(nameInput).toHaveValue('My Custom Rule');
expect(screen.getByText('My Custom Rule')).toBeInTheDocument();
});
});

Expand Down Expand Up @@ -170,7 +180,7 @@ describe('DynamicRuleForm', () => {

// Form should have updated - component renders without errors
await waitFor(() => {
expect(screen.getByText('Rule details')).toBeInTheDocument();
expect(screen.getByText('Untitled rule')).toBeInTheDocument();
});
});

Expand All @@ -185,7 +195,7 @@ describe('DynamicRuleForm', () => {
);

// Form should still render
expect(screen.getByText('Rule details')).toBeInTheDocument();
expect(screen.getByText('Untitled rule')).toBeInTheDocument();
});

it('handles query prop changes from invalid to valid', () => {
Expand All @@ -204,7 +214,7 @@ describe('DynamicRuleForm', () => {
);

// Form should still render
expect(screen.getByText('Rule details')).toBeInTheDocument();
expect(screen.getByText('Untitled rule')).toBeInTheDocument();
});

it('calls onSubmit with form values when form is submitted', async () => {
Expand All @@ -222,9 +232,19 @@ describe('DynamicRuleForm', () => {
</Wrapper>
);

// Fill in required field
const nameInput = screen.getByRole('textbox', { name: 'Name' });
await user.type(nameInput, 'Test Rule');
// Fill in required field — click inline edit title, then type
const readModeButton = screen.getByTestId('euiInlineReadModeButton');
await user.click(readModeButton);

const nameInput = screen.getByLabelText('Edit rule name');

// Select all and replace with new value
await user.tripleClick(nameInput);
await user.keyboard('Test Rule');

// Save the edit
const saveButton = screen.getByTestId('euiInlineEditModeSaveButton');
await user.click(saveButton);

// Submit the form using the constant RULE_FORM_ID
const form = document.getElementById(RULE_FORM_ID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { i18n } from '@kbn/i18n';
import { validateEsqlQuery } from '@kbn/alerting-v2-schemas';
import type { FormValues } from './types';
import { RuleForm } from './rule_form';
import type { RuleFormServices } from './contexts';
import type { RuleFormServices, RuleFormLayout } from './contexts';
import { useFormDefaults } from './hooks/use_form_defaults';

export interface DynamicRuleFormProps {
/** The query that drives form values - changes will sync to form state */
query: string;
services: RuleFormServices;
/** Layout mode: 'page' renders the preview side-by-side; 'flyout' uses a nested flyout. Default: 'page'. */
layout?: RuleFormLayout;
/**
* External submit handler. When provided, form submission delegates to this callback.
* When omitted, the form uses `useCreateRule` internally.
Expand Down Expand Up @@ -53,6 +55,7 @@ export interface DynamicRuleFormProps {
export const DynamicRuleForm: React.FC<DynamicRuleFormProps> = ({
query,
services,
layout,
onSubmit,
onSuccess,
includeYaml = false,
Expand Down Expand Up @@ -90,6 +93,7 @@ export const DynamicRuleForm: React.FC<DynamicRuleFormProps> = ({
/>
<RuleForm
services={services}
layout={layout}
onSubmit={onSubmit}
onSuccess={onSuccess}
includeQueryEditor={false}
Expand Down
Loading
Loading