Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import type { TemplateDeserialized } from '@kbn/index-management-plugin/common/types';

import { CreateClassicStreamFlyout } from './create_classic_stream_flyout';
import type { StreamNameValidator } from '../../utils';

const MOCK_TEMPLATES: TemplateDeserialized[] = [
{
Expand Down Expand Up @@ -52,6 +53,8 @@ const MOCK_TEMPLATES: TemplateDeserialized[] = [
ilmPolicy: { name: '.alerts-ilm-policy' },
indexPatterns: ['logs-apache.access-*'],
allowAutoCreate: 'NO_OVERWRITE',
indexMode: 'standard',
composedOf: ['logs@mappings', 'logs@settings', 'ecs@mappings'],
_kbnMeta: { type: 'default', hasDatastream: true },
},
{
Expand Down Expand Up @@ -136,6 +139,14 @@ const MOCK_TEMPLATES: TemplateDeserialized[] = [
ilmPolicy: { name: 'profiling-60-days' },
indexPatterns: ['logs-elastic_agent.apm_server-*'],
allowAutoCreate: 'NO_OVERWRITE',
indexMode: 'standard',
composedOf: [
'.fleet_agent_id_verification-1',
'.fleet_globals-1',
'logs@mappings',
'logs@settings',
'ecs@mappings',
],
_kbnMeta: { type: 'managed', hasDatastream: true },
},
{
Expand Down Expand Up @@ -163,9 +174,51 @@ const MOCK_TEMPLATES: TemplateDeserialized[] = [
name: 'logs-infinite-retention',
indexPatterns: ['logs-infinite-*'],
allowAutoCreate: 'NO_OVERWRITE',
indexMode: 'logsdb',
lifecycle: { enabled: true, infiniteDataRetention: true },
composedOf: ['logs@mappings'],
_kbnMeta: { type: 'default', hasDatastream: true },
},
{
name: 'multi-pattern-template',
ilmPolicy: { name: 'logs' },
indexPatterns: ['*-logs-*-*', 'logs-*-data-*', 'metrics-*'],
allowAutoCreate: 'NO_OVERWRITE',
indexMode: 'lookup',
version: 12,
composedOf: [
'.fleet_agent_id_verification-1',
'.fleet_globals-1',
'ecs@mappings',
'elastic_agent@custom',
'logs@custom',
'logs@mappings',
'logs@settings',
'logs-elastic_agent.apm_server@custom',
'logs-elastic_agent.apm_server@package',
],
_kbnMeta: { type: 'managed', hasDatastream: true },
},
{
name: 'very-long-pattern-template',
ilmPolicy: { name: 'logs' },
indexPatterns: ['*-reallllllllllllllllllly-*-loooooooooooong-*-index-*-name-*', 'short-*'],
allowAutoCreate: 'NO_OVERWRITE',
indexMode: 'lookup',
version: 12,
composedOf: [
'.fleet_agent_id_verification-1',
'.fleet_globals-1',
'ecs@mappings',
'elastic_agent@custom',
'logs@custom',
'logs@mappings',
'logs@settings',
'logs-elastic_agent.apm_server@custom',
'logs-elastic_agent.apm_server@package',
],
_kbnMeta: { type: 'managed', hasDatastream: true },
},
];

const meta: Meta<typeof CreateClassicStreamFlyout> = {
Expand All @@ -176,95 +229,111 @@ const meta: Meta<typeof CreateClassicStreamFlyout> = {
export default meta;
type Story = StoryObj<typeof CreateClassicStreamFlyout>;

const PrimaryStory = () => {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);
// Mock validation data for stories
const EXISTING_STREAM_NAMES = [
'foo-logs-bar-baz', // Matches *-logs-*-* pattern with foo, bar, baz
'test-logs-data-stream',
'logs-myapp-data-production',
];

const HIGHER_PRIORITY_PATTERNS = [
'foo-logs-bar-*', // Matches foo-logs-bar-anything
'test-logs-*-stream',
];

/**
* Creates a mock validator that wraps validation logic with Storybook action logging.
* You'll see "onValidate" events in the Actions panel whenever validation is triggered.
*/
const createMockValidator = (
existingNames: string[],
higherPriorityPatterns: string[]
): StreamNameValidator => {
const onValidateAction = action('onValidate');

return async (streamName: string) => {
// Log the validation call to Storybook actions panel
onValidateAction(streamName);

// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 500));

// Check for duplicate
if (existingNames.includes(streamName)) {
const result = { errorType: 'duplicate' as const, conflictingIndexPattern: undefined };
action('onValidate:result')(result);
return result;
}

// Check for higher priority pattern conflict
for (const pattern of higherPriorityPatterns) {
const regexPattern = pattern.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`);
if (regex.test(streamName)) {
const result = { errorType: 'higherPriority' as const, conflictingIndexPattern: pattern };
action('onValidate:result')(result);
return result;
}
}

return (
const result = { errorType: null, conflictingIndexPattern: undefined };
action('onValidate:result')(result);
return result;
};
};

export const Default: Story = {
render: () => (
<CreateClassicStreamFlyout
onClose={action('onClose')}
onCreate={action('onCreate')}
onCreateTemplate={action('onCreateTemplate')}
onRetryLoadTemplates={action('onRetryLoadTemplates')}
templates={MOCK_TEMPLATES}
selectedTemplate={selectedTemplate}
onTemplateSelect={(template) => {
action('onTemplateSelect')(template);
setSelectedTemplate(template);
}}
/>
);
),
};

export const Primary: Story = {
render: () => <PrimaryStory />,
};

const WithPreselectedTemplateStory = () => {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>('logs-apache.access');

return (
/**
* Tests all validation scenarios. Watch the Actions panel to see when `onValidate` is triggered:
* - Empty fields: Leave any wildcard empty and click Create
* - Duplicate: Select "multi-pattern-template", enter "foo", "bar", "baz" to trigger duplicate validation
* - Higher priority: Enter "foo", "bar", then anything (e.g., "test") to trigger higher priority conflict
*/
export const WithValidation: Story = {
render: () => (
<CreateClassicStreamFlyout
onClose={action('onClose')}
onCreate={action('onCreate')}
onCreateTemplate={action('onCreateTemplate')}
onRetryLoadTemplates={action('onRetryLoadTemplates')}
templates={MOCK_TEMPLATES}
selectedTemplate={selectedTemplate}
onTemplateSelect={(template) => {
action('onTemplateSelect')(template);
setSelectedTemplate(template);
}}
onValidate={createMockValidator(EXISTING_STREAM_NAMES, HIGHER_PRIORITY_PATTERNS)}
/>
);
};

export const WithPreselectedTemplate: Story = {
render: () => <WithPreselectedTemplateStory />,
),
};

const EmptyStateStory = () => {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);

return (
export const EmptyState: Story = {
render: () => (
<CreateClassicStreamFlyout
onClose={action('onClose')}
onCreate={action('onCreate')}
onCreateTemplate={action('onCreateTemplate')}
onRetryLoadTemplates={action('onRetryLoadTemplates')}
templates={[]}
selectedTemplate={selectedTemplate}
onTemplateSelect={(template) => {
action('onTemplateSelect')(template);
setSelectedTemplate(template);
}}
/>
);
),
};

export const EmptyState: Story = {
render: () => <EmptyStateStory />,
};

const ErrorStateStory = () => {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null);

return (
export const ErrorState: Story = {
render: () => (
<CreateClassicStreamFlyout
onClose={action('onClose')}
onCreate={action('onCreate')}
onCreateTemplate={action('onCreateTemplate')}
onRetryLoadTemplates={action('onRetryLoadTemplates')}
templates={[]}
selectedTemplate={selectedTemplate}
onTemplateSelect={(template) => {
action('onTemplateSelect')(template);
setSelectedTemplate(template);
}}
hasErrorLoadingTemplates={true}
/>
);
};

export const ErrorState: Story = {
render: () => <ErrorStateStory />,
),
};
Loading