Skip to content

Commit 87e30f0

Browse files
fix: only show empty inputs as invalid when validating
1 parent f6174c4 commit 87e30f0

File tree

6 files changed

+130
-27
lines changed

6 files changed

+130
-27
lines changed

x-pack/platform/packages/shared/kbn-classic-stream-flyout/src/components/create_classic_stream_flyout/steps/name_and_confirm/name_and_confirm_step.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import React from 'react';
99
import { EuiFlexGroup } from '@elastic/eui';
1010
import type { TemplateDeserialized } from '@kbn/index-management-plugin/common/types';
11-
import { NameStreamSection, type ValidationErrorType } from './name_stream_section';
11+
import type { ValidationErrorType } from '../../../../utils';
12+
import { NameStreamSection } from './name_stream_section';
1213

1314
export type { ValidationErrorType };
1415

x-pack/platform/packages/shared/kbn-classic-stream-flyout/src/components/create_classic_stream_flyout/steps/name_and_confirm/name_stream_section.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import { EuiFormRow, EuiPanel, EuiSpacer, EuiTitle, EuiSelect, useEuiTheme } fro
1010
import { FormattedMessage } from '@kbn/i18n-react';
1111
import { i18n } from '@kbn/i18n';
1212
import { css } from '@emotion/react';
13+
import type { ValidationErrorType } from '../../../../utils';
1314
import { StreamNameInput } from '../../../stream_name_input';
1415

15-
export type ValidationErrorType = 'empty' | 'duplicate' | 'higherPriority' | null;
16-
1716
const getValidationErrorMessage = (
1817
validationError: ValidationErrorType,
1918
conflictingIndexPattern?: string
@@ -135,7 +134,7 @@ export const NameStreamSection = ({
135134
<StreamNameInput
136135
indexPattern={currentPattern}
137136
onChange={onStreamNameChange}
138-
isInvalid={validationError !== null}
137+
validationError={validationError}
139138
data-test-subj="streamNameInput"
140139
/>
141140
</EuiFormRow>

x-pack/platform/packages/shared/kbn-classic-stream-flyout/src/components/stream_name_input/stream_name_input.stories.tsx

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Meta, StoryObj } from '@storybook/react';
1010
import { action } from '@storybook/addon-actions';
1111
import { EuiFormRow, EuiPanel, EuiSpacer, EuiText, EuiCode } from '@elastic/eui';
1212

13+
import type { ValidationErrorType } from '../../utils';
1314
import { StreamNameInput } from './stream_name_input';
1415

1516
const meta: Meta<typeof StreamNameInput> = {
@@ -32,10 +33,10 @@ type Story = StoryObj<typeof StreamNameInput>;
3233
*/
3334
const StreamNameInputWithPreview = ({
3435
indexPattern,
35-
isInvalid,
36+
validationError,
3637
}: {
3738
indexPattern: string;
38-
isInvalid?: boolean;
39+
validationError?: ValidationErrorType;
3940
}) => {
4041
const [streamName, setStreamName] = useState('');
4142

@@ -45,15 +46,15 @@ const StreamNameInputWithPreview = ({
4546
label="Stream name"
4647
helpText="Name your classic stream by filling in the wildcard (*) portions of the index pattern."
4748
fullWidth
48-
isInvalid={isInvalid}
49+
isInvalid={validationError !== null && validationError !== undefined}
4950
>
5051
<StreamNameInput
5152
indexPattern={indexPattern}
5253
onChange={(name) => {
5354
setStreamName(name);
5455
action('onChange')(name);
5556
}}
56-
isInvalid={isInvalid}
57+
validationError={validationError}
5758
/>
5859
</EuiFormRow>
5960
<EuiSpacer size="m" />
@@ -70,22 +71,21 @@ const StreamNameInputWithPreview = ({
7071
export const Default: Story = {
7172
args: {
7273
indexPattern: '*-logs-*-*',
73-
isInvalid: false,
74+
validationError: null,
7475
},
7576
argTypes: {
7677
indexPattern: {
7778
control: 'text',
78-
description: 'Index pattern with wildcards (*)',
7979
},
80-
isInvalid: {
81-
control: 'boolean',
82-
description: 'Whether the input is in an invalid state',
80+
validationError: {
81+
control: 'select',
82+
options: ['empty', 'duplicate', 'higherPriority', null],
8383
},
8484
},
8585
render: (args) => (
8686
<StreamNameInputWithPreview
8787
indexPattern={args.indexPattern ?? '*-logs-*-*'}
88-
isInvalid={args.isInvalid}
88+
validationError={args.validationError}
8989
/>
9090
),
9191
};
@@ -121,8 +121,29 @@ export const LongStaticSegments: Story = {
121121
};
122122

123123
/**
124-
* Invalid state - shows error styling on inputs
124+
* Empty validation error - only empty inputs are highlighted.
125+
* Fill in some inputs and leave others empty to see the difference.
125126
*/
126-
export const InvalidState: Story = {
127-
render: () => <StreamNameInputWithPreview indexPattern="logs-*-data-*" isInvalid />,
127+
export const EmptyValidationError: Story = {
128+
render: () => (
129+
<StreamNameInputWithPreview indexPattern="*-logs-*-data-*" validationError="empty" />
130+
),
131+
};
132+
133+
/**
134+
* Duplicate validation error - all inputs are highlighted
135+
*/
136+
export const DuplicateValidationError: Story = {
137+
render: () => (
138+
<StreamNameInputWithPreview indexPattern="logs-*-data-*" validationError="duplicate" />
139+
),
140+
};
141+
142+
/**
143+
* Higher priority validation error - all inputs are highlighted
144+
*/
145+
export const HigherPriorityValidationError: Story = {
146+
render: () => (
147+
<StreamNameInputWithPreview indexPattern="logs-*-data-*" validationError="higherPriority" />
148+
),
128149
};

x-pack/platform/packages/shared/kbn-classic-stream-flyout/src/components/stream_name_input/stream_name_input.test.tsx

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,37 @@ describe('StreamNameInput', () => {
195195
});
196196

197197
describe('validation state', () => {
198-
it('shows invalid state on all inputs when isInvalid is true', () => {
198+
it('does not show invalid state when validationError is null', () => {
199199
const { getByTestId } = render(
200-
<StreamNameInput {...defaultProps} indexPattern="*-logs-*" isInvalid />
200+
<StreamNameInput {...defaultProps} indexPattern="*-logs-*" validationError={null} />
201+
);
202+
203+
const input0 = getByTestId('streamNameInput-wildcard-0');
204+
const input1 = getByTestId('streamNameInput-wildcard-1');
205+
206+
expect(input0).not.toHaveAttribute('aria-invalid', 'true');
207+
expect(input1).not.toHaveAttribute('aria-invalid', 'true');
208+
});
209+
210+
it('shows invalid state on all inputs when validationError is duplicate', () => {
211+
const { getByTestId } = render(
212+
<StreamNameInput {...defaultProps} indexPattern="*-logs-*" validationError="duplicate" />
213+
);
214+
215+
const input0 = getByTestId('streamNameInput-wildcard-0');
216+
const input1 = getByTestId('streamNameInput-wildcard-1');
217+
218+
expect(input0).toHaveAttribute('aria-invalid', 'true');
219+
expect(input1).toHaveAttribute('aria-invalid', 'true');
220+
});
221+
222+
it('shows invalid state on all inputs when validationError is higherPriority', () => {
223+
const { getByTestId } = render(
224+
<StreamNameInput
225+
{...defaultProps}
226+
indexPattern="*-logs-*"
227+
validationError="higherPriority"
228+
/>
201229
);
202230

203231
const input0 = getByTestId('streamNameInput-wildcard-0');
@@ -207,16 +235,47 @@ describe('StreamNameInput', () => {
207235
expect(input1).toHaveAttribute('aria-invalid', 'true');
208236
});
209237

210-
it('does not show invalid state when isInvalid is false', () => {
238+
it('shows invalid state only on empty inputs when validationError is empty', () => {
211239
const { getByTestId } = render(
212-
<StreamNameInput {...defaultProps} indexPattern="*-logs-*" isInvalid={false} />
240+
<StreamNameInput {...defaultProps} indexPattern="*-logs-*" validationError="empty" />
213241
);
214242

243+
// Initially all inputs are empty, so all should be invalid
215244
const input0 = getByTestId('streamNameInput-wildcard-0');
216245
const input1 = getByTestId('streamNameInput-wildcard-1');
217246

247+
expect(input0).toHaveAttribute('aria-invalid', 'true');
248+
expect(input1).toHaveAttribute('aria-invalid', 'true');
249+
250+
// Fill in the first input
251+
fireEvent.change(input0, { target: { value: 'filled' } });
252+
253+
// Now only the second input should be invalid
218254
expect(input0).not.toHaveAttribute('aria-invalid', 'true');
255+
expect(input1).toHaveAttribute('aria-invalid', 'true');
256+
});
257+
258+
it('clears invalid state on input when filled (empty validation error)', () => {
259+
const { getByTestId } = render(
260+
<StreamNameInput {...defaultProps} indexPattern="*-logs-*-data-*" validationError="empty" />
261+
);
262+
263+
const input0 = getByTestId('streamNameInput-wildcard-0');
264+
const input1 = getByTestId('streamNameInput-wildcard-1');
265+
const input2 = getByTestId('streamNameInput-wildcard-2');
266+
267+
// All empty initially
268+
expect(input0).toHaveAttribute('aria-invalid', 'true');
269+
expect(input1).toHaveAttribute('aria-invalid', 'true');
270+
expect(input2).toHaveAttribute('aria-invalid', 'true');
271+
272+
// Fill in the middle input
273+
fireEvent.change(input1, { target: { value: 'filled' } });
274+
275+
// Only first and third should be invalid now
276+
expect(input0).toHaveAttribute('aria-invalid', 'true');
219277
expect(input1).not.toHaveAttribute('aria-invalid', 'true');
278+
expect(input2).toHaveAttribute('aria-invalid', 'true');
220279
});
221280
});
222281
});

x-pack/platform/packages/shared/kbn-classic-stream-flyout/src/components/stream_name_input/stream_name_input.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import React, { useState, useMemo, useCallback, useEffect } from 'react';
99
import { EuiFieldText, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui';
1010
import { css } from '@emotion/react';
11+
import type { ValidationErrorType } from '../../utils';
1112

1213
/*
1314
* PatternSegment is a segment of the index pattern that is either a static text or a wildcard.
@@ -116,16 +117,19 @@ export interface StreamNameInputProps {
116117
indexPattern: string;
117118
/** Callback when the stream name changes */
118119
onChange: (streamName: string) => void;
119-
/** Whether the input is in an invalid state */
120-
isInvalid?: boolean;
120+
/**
121+
* Validation error type. When 'empty', only empty inputs are highlighted.
122+
* For other error types, all inputs are highlighted.
123+
*/
124+
validationError?: ValidationErrorType;
121125
/** Test subject prefix for the inputs */
122126
'data-test-subj'?: string;
123127
}
124128

125129
export const StreamNameInput = ({
126130
indexPattern,
127131
onChange,
128-
isInvalid = false,
132+
validationError = null,
129133
'data-test-subj': dataTestSubj = 'streamNameInput',
130134
}: StreamNameInputProps) => {
131135
const { euiTheme } = useEuiTheme();
@@ -161,6 +165,20 @@ export const StreamNameInput = ({
161165

162166
const hasMultipleWildcards = wildcardCount > 1;
163167

168+
// Determine if a specific input should be marked as invalid
169+
const isInputInvalid = useCallback(
170+
(wildcardIndex: number): boolean => {
171+
if (!validationError) return false;
172+
if (validationError === 'empty') {
173+
// Only highlight empty inputs
174+
return !parts[wildcardIndex]?.trim();
175+
}
176+
// For other errors (duplicate, higherPriority), highlight all inputs
177+
return true;
178+
},
179+
[validationError, parts]
180+
);
181+
164182
const getConnectedInputStyles = (isFirst: boolean, isLast: boolean) => {
165183
return css`
166184
flex: 1 1 0%;
@@ -198,7 +216,7 @@ export const StreamNameInput = ({
198216
onChange={(e) => handleWildcardChange(group.wildcardIndex, e.target.value)}
199217
placeholder="*"
200218
fullWidth
201-
isInvalid={isInvalid}
219+
isInvalid={isInputInvalid(group.wildcardIndex)}
202220
prepend={group.prepend}
203221
append={group.append}
204222
data-test-subj={`${dataTestSubj}-wildcard-${group.wildcardIndex}`}
@@ -229,7 +247,7 @@ export const StreamNameInput = ({
229247
onChange={(e) => handleWildcardChange(group.wildcardIndex, e.target.value)}
230248
placeholder="*"
231249
fullWidth
232-
isInvalid={isInvalid}
250+
isInvalid={isInputInvalid(group.wildcardIndex)}
233251
prepend={group.prepend}
234252
append={group.append}
235253
data-test-subj={`${dataTestSubj}-wildcard-${group.wildcardIndex}`}

x-pack/platform/packages/shared/kbn-classic-stream-flyout/src/utils/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,17 @@ export const hasEmptyWildcards = (streamName: string): boolean => {
3232
return streamName.includes('*');
3333
};
3434

35+
/**
36+
* Validation error types for stream name validation
37+
*/
38+
export type ValidationErrorType = 'empty' | 'duplicate' | 'higherPriority' | null;
39+
3540
/**
3641
* Result from stream name validation
3742
*/
3843
export interface StreamNameValidationResult {
3944
/** The type of error, or null if valid */
40-
errorType: 'empty' | 'duplicate' | 'higherPriority' | null;
45+
errorType: ValidationErrorType;
4146
/** For higherPriority errors, the conflicting index pattern */
4247
conflictingIndexPattern?: string;
4348
}

0 commit comments

Comments
 (0)