66 EuiSpacer ,
77 EuiHorizontalRule ,
88 EuiText ,
9+ EuiCallOut ,
910} from "@elastic/eui" ;
1011import { feast } from "../protos" ;
1112import FormModal from "./forms/FormModal" ;
@@ -75,6 +76,8 @@ interface DataSourceFormModalProps {
7576 onSubmit : ( data : DataSourceFormData ) => void ;
7677 initialData ?: DataSourceFormData ;
7778 isEdit ?: boolean ;
79+ isSubmitting ?: boolean ;
80+ submitError ?: string | null ;
7881}
7982
8083const EMPTY_FORM : DataSourceFormData = {
@@ -100,11 +103,21 @@ const EMPTY_FORM: DataSourceFormData = {
100103 sparkPath : "" ,
101104} ;
102105
106+ const BATCH_SOURCE_TYPES = new Set ( [
107+ String ( feast . core . DataSource . SourceType . BATCH_FILE ) ,
108+ String ( feast . core . DataSource . SourceType . BATCH_BIGQUERY ) ,
109+ String ( feast . core . DataSource . SourceType . BATCH_SNOWFLAKE ) ,
110+ String ( feast . core . DataSource . SourceType . BATCH_REDSHIFT ) ,
111+ String ( feast . core . DataSource . SourceType . BATCH_SPARK ) ,
112+ ] ) ;
113+
103114const DataSourceFormModal : React . FC < DataSourceFormModalProps > = ( {
104115 onClose,
105116 onSubmit,
106117 initialData,
107118 isEdit = false ,
119+ isSubmitting = false ,
120+ submitError,
108121} ) => {
109122 const [ formData , setFormData ] = useState < DataSourceFormData > (
110123 initialData || EMPTY_FORM ,
@@ -118,8 +131,11 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
118131 }
119132 } , [ initialData ] ) ;
120133
134+ const isBatchSource = BATCH_SOURCE_TYPES . has ( formData . sourceType ) ;
135+
121136 const validate = ( ) : boolean => {
122137 const newErrors : Record < string , string > = { } ;
138+ const st = formData . sourceType ;
123139
124140 if ( ! formData . name . trim ( ) ) {
125141 newErrors . name = "Data source name is required." ;
@@ -128,30 +144,70 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
128144 "Must start with a letter or underscore, and contain only letters, numbers, and underscores." ;
129145 }
130146
131- const st = formData . sourceType ;
147+ // Source-type-specific required fields
132148 if ( st === String ( feast . core . DataSource . SourceType . BATCH_FILE ) ) {
133149 if ( ! formData . fileUri . trim ( ) ) {
134- newErrors . fileUri = "File URI is required for file sources." ;
150+ newErrors . fileUri = "File URI is required." ;
151+ } else if (
152+ ! / ^ ( s 3 | g s | g c s | h d f s | a b f s | f i l e ) : \/ \/ \S + $ / . test ( formData . fileUri . trim ( ) )
153+ ) {
154+ newErrors . fileUri =
155+ "Must be a valid URI (e.g. s3://bucket/path, gs://bucket/path, file:///local/path)." ;
135156 }
136157 } else if ( st === String ( feast . core . DataSource . SourceType . BATCH_BIGQUERY ) ) {
137158 if ( ! formData . bigqueryTable . trim ( ) && ! formData . bigqueryQuery . trim ( ) ) {
138- newErrors . bigqueryTable = "Either table or query is required." ;
159+ newErrors . bigqueryTable =
160+ "Either a table reference or a query is required." ;
139161 }
140162 } else if (
141163 st === String ( feast . core . DataSource . SourceType . BATCH_SNOWFLAKE )
142164 ) {
143165 if ( ! formData . snowflakeTable . trim ( ) ) {
144166 newErrors . snowflakeTable = "Table name is required for Snowflake." ;
145167 }
168+ if ( ! formData . snowflakeDatabase . trim ( ) ) {
169+ newErrors . snowflakeDatabase = "Database is required for Snowflake." ;
170+ }
171+ } else if ( st === String ( feast . core . DataSource . SourceType . BATCH_REDSHIFT ) ) {
172+ if ( ! formData . redshiftTable . trim ( ) ) {
173+ newErrors . redshiftTable = "Table name is required for Redshift." ;
174+ }
175+ if ( ! formData . redshiftDatabase . trim ( ) ) {
176+ newErrors . redshiftDatabase = "Database is required for Redshift." ;
177+ }
178+ } else if ( st === String ( feast . core . DataSource . SourceType . BATCH_SPARK ) ) {
179+ if ( ! formData . sparkTable . trim ( ) && ! formData . sparkPath . trim ( ) ) {
180+ newErrors . sparkTable =
181+ "Either a table reference or a path is required for Spark." ;
182+ }
146183 } else if ( st === String ( feast . core . DataSource . SourceType . STREAM_KAFKA ) ) {
147184 if ( ! formData . kafkaBootstrapServers . trim ( ) ) {
148185 newErrors . kafkaBootstrapServers = "Bootstrap servers are required." ;
186+ } else if (
187+ ! / ^ [ \w . - ] + : \d + ( , [ \w . - ] + : \d + ) * $ / . test (
188+ formData . kafkaBootstrapServers . trim ( ) ,
189+ )
190+ ) {
191+ newErrors . kafkaBootstrapServers =
192+ "Must be in host:port format (e.g. localhost:9092)." ;
149193 }
150194 if ( ! formData . kafkaTopic . trim ( ) ) {
151195 newErrors . kafkaTopic = "Topic is required." ;
152196 }
153197 }
154198
199+ // Timestamp field required for batch sources (needed for point-in-time correctness)
200+ if ( isBatchSource && ! formData . timestampField . trim ( ) ) {
201+ newErrors . timestampField =
202+ "Timestamp field is required for batch sources. It is used for point-in-time correct feature retrieval." ;
203+ } else if (
204+ formData . timestampField . trim ( ) &&
205+ ! / ^ [ a - z A - Z _ ] [ a - z A - Z 0 - 9 _ ] * $ / . test ( formData . timestampField . trim ( ) )
206+ ) {
207+ newErrors . timestampField =
208+ "Must be a valid column name (letters, numbers, underscores)." ;
209+ }
210+
155211 const tagKeys = formData . tags . map ( ( t ) => t . key ) . filter ( ( k ) => k . trim ( ) ) ;
156212 if ( new Set ( tagKeys ) . size !== tagKeys . length ) {
157213 newErrors . tags = "Tag keys must be unique." ;
@@ -195,7 +251,7 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
195251 label = "File URI"
196252 isInvalid = { ! ! errors . fileUri }
197253 error = { errors . fileUri }
198- helpText = "s3://path/to/file , gs://path/to/file, or file:///local/path "
254+ helpText = "Path to parquet or CSV file(s). Supports s3://, gs://, and file:// schemes. "
199255 >
200256 < EuiFieldText
201257 value = { formData . fileUri }
@@ -225,12 +281,12 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
225281 </ EuiFormRow >
226282 < EuiFormRow
227283 label = "Query"
228- helpText = "Alternative to table: a SQL query "
284+ helpText = "Optional SQL query — use instead of a fixed table reference. "
229285 >
230286 < EuiFieldText
231287 value = { formData . bigqueryQuery }
232288 onChange = { ( e ) => updateField ( "bigqueryQuery" , e . target . value ) }
233- placeholder = "SELECT * FROM ... "
289+ placeholder = "SELECT * FROM `project.dataset.table` "
234290 />
235291 </ EuiFormRow >
236292 </ >
@@ -241,21 +297,14 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
241297 return (
242298 < >
243299 < EuiFormRow
244- label = "Table "
245- isInvalid = { ! ! errors . snowflakeTable }
246- error = { errors . snowflakeTable }
300+ label = "Database "
301+ isInvalid = { ! ! errors . snowflakeDatabase }
302+ error = { errors . snowflakeDatabase }
247303 >
248- < EuiFieldText
249- value = { formData . snowflakeTable }
250- onChange = { ( e ) => updateField ( "snowflakeTable" , e . target . value ) }
251- isInvalid = { ! ! errors . snowflakeTable }
252- placeholder = "MY_TABLE"
253- />
254- </ EuiFormRow >
255- < EuiFormRow label = "Database" >
256304 < EuiFieldText
257305 value = { formData . snowflakeDatabase }
258306 onChange = { ( e ) => updateField ( "snowflakeDatabase" , e . target . value ) }
307+ isInvalid = { ! ! errors . snowflakeDatabase }
259308 placeholder = "MY_DATABASE"
260309 />
261310 </ EuiFormRow >
@@ -266,24 +315,34 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
266315 placeholder = "PUBLIC"
267316 />
268317 </ EuiFormRow >
318+ < EuiFormRow
319+ label = "Table"
320+ isInvalid = { ! ! errors . snowflakeTable }
321+ error = { errors . snowflakeTable }
322+ >
323+ < EuiFieldText
324+ value = { formData . snowflakeTable }
325+ onChange = { ( e ) => updateField ( "snowflakeTable" , e . target . value ) }
326+ isInvalid = { ! ! errors . snowflakeTable }
327+ placeholder = "MY_TABLE"
328+ />
329+ </ EuiFormRow >
269330 </ >
270331 ) ;
271332 }
272333
273334 if ( st === String ( feast . core . DataSource . SourceType . BATCH_REDSHIFT ) ) {
274335 return (
275336 < >
276- < EuiFormRow label = "Table" >
277- < EuiFieldText
278- value = { formData . redshiftTable }
279- onChange = { ( e ) => updateField ( "redshiftTable" , e . target . value ) }
280- placeholder = "my_table"
281- />
282- </ EuiFormRow >
283- < EuiFormRow label = "Database" >
337+ < EuiFormRow
338+ label = "Database"
339+ isInvalid = { ! ! errors . redshiftDatabase }
340+ error = { errors . redshiftDatabase }
341+ >
284342 < EuiFieldText
285343 value = { formData . redshiftDatabase }
286344 onChange = { ( e ) => updateField ( "redshiftDatabase" , e . target . value ) }
345+ isInvalid = { ! ! errors . redshiftDatabase }
287346 placeholder = "my_database"
288347 />
289348 </ EuiFormRow >
@@ -294,6 +353,18 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
294353 placeholder = "public"
295354 />
296355 </ EuiFormRow >
356+ < EuiFormRow
357+ label = "Table"
358+ isInvalid = { ! ! errors . redshiftTable }
359+ error = { errors . redshiftTable }
360+ >
361+ < EuiFieldText
362+ value = { formData . redshiftTable }
363+ onChange = { ( e ) => updateField ( "redshiftTable" , e . target . value ) }
364+ isInvalid = { ! ! errors . redshiftTable }
365+ placeholder = "my_table"
366+ />
367+ </ EuiFormRow >
297368 </ >
298369 ) ;
299370 }
@@ -305,14 +376,15 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
305376 label = "Bootstrap Servers"
306377 isInvalid = { ! ! errors . kafkaBootstrapServers }
307378 error = { errors . kafkaBootstrapServers }
379+ helpText = "Comma-separated list of broker host:port pairs."
308380 >
309381 < EuiFieldText
310382 value = { formData . kafkaBootstrapServers }
311383 onChange = { ( e ) =>
312384 updateField ( "kafkaBootstrapServers" , e . target . value )
313385 }
314386 isInvalid = { ! ! errors . kafkaBootstrapServers }
315- placeholder = "localhost :9092"
387+ placeholder = "broker1:9092,broker2 :9092"
316388 />
317389 </ EuiFormRow >
318390 < EuiFormRow
@@ -334,14 +406,23 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
334406 if ( st === String ( feast . core . DataSource . SourceType . BATCH_SPARK ) ) {
335407 return (
336408 < >
337- < EuiFormRow label = "Table" >
409+ < EuiFormRow
410+ label = "Table"
411+ isInvalid = { ! ! errors . sparkTable }
412+ error = { errors . sparkTable }
413+ helpText = "Spark catalog table reference (catalog.database.table). Provide either table or path."
414+ >
338415 < EuiFieldText
339416 value = { formData . sparkTable }
340417 onChange = { ( e ) => updateField ( "sparkTable" , e . target . value ) }
418+ isInvalid = { ! ! errors . sparkTable }
341419 placeholder = "catalog.database.table"
342420 />
343421 </ EuiFormRow >
344- < EuiFormRow label = "Path" helpText = "Alternative to table" >
422+ < EuiFormRow
423+ label = "Path"
424+ helpText = "Alternative to table: path to data files (s3://, gs://, hdfs://)."
425+ >
345426 < EuiFieldText
346427 value = { formData . sparkPath }
347428 onChange = { ( e ) => updateField ( "sparkPath" , e . target . value ) }
@@ -366,22 +447,33 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
366447 return null ;
367448 } ;
368449
369- const isBatchSource =
370- formData . sourceType !==
371- String ( feast . core . DataSource . SourceType . STREAM_KAFKA ) &&
372- formData . sourceType !==
373- String ( feast . core . DataSource . SourceType . REQUEST_SOURCE ) &&
374- formData . sourceType !==
375- String ( feast . core . DataSource . SourceType . PUSH_SOURCE ) ;
376-
377450 return (
378451 < FormModal
379452 title = { isEdit ? "Edit Data Source" : "Create Data Source" }
380453 submitLabel = { isEdit ? "Update Data Source" : "Create Data Source" }
381454 onClose = { onClose }
382455 onSubmit = { handleSubmit }
383456 width = { 650 }
457+ isSubmitting = { isSubmitting }
384458 >
459+ { submitError && (
460+ < >
461+ < EuiCallOut
462+ title = {
463+ isEdit
464+ ? "Unable to update data source"
465+ : "Unable to create data source"
466+ }
467+ color = "danger"
468+ iconType = "alert"
469+ size = "s"
470+ >
471+ < p > { submitError } </ p >
472+ </ EuiCallOut >
473+ < EuiSpacer size = "m" />
474+ </ >
475+ ) }
476+
385477 < NameDescriptionOwnerFields
386478 name = { formData . name }
387479 description = { formData . description }
@@ -396,11 +488,31 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
396488 descriptionPlaceholder = "Describe this data source..."
397489 />
398490
399- < EuiFormRow label = "Source Type" helpText = "Type of the data source." >
491+ < EuiFormRow
492+ label = "Source Type"
493+ helpText = "The type of underlying storage system for this data source."
494+ >
400495 < EuiSelect
401496 options = { SOURCE_TYPE_OPTIONS }
402497 value = { formData . sourceType }
403- onChange = { ( e ) => updateField ( "sourceType" , e . target . value ) }
498+ onChange = { ( e ) => {
499+ updateField ( "sourceType" , e . target . value ) ;
500+ // Clear source-specific errors when type changes
501+ setErrors ( ( prev ) => {
502+ const next = { ...prev } ;
503+ delete next . fileUri ;
504+ delete next . bigqueryTable ;
505+ delete next . snowflakeTable ;
506+ delete next . snowflakeDatabase ;
507+ delete next . redshiftTable ;
508+ delete next . redshiftDatabase ;
509+ delete next . kafkaBootstrapServers ;
510+ delete next . kafkaTopic ;
511+ delete next . sparkTable ;
512+ delete next . timestampField ;
513+ return next ;
514+ } ) ;
515+ } }
404516 disabled = { isEdit }
405517 />
406518 </ EuiFormRow >
@@ -419,17 +531,20 @@ const DataSourceFormModal: React.FC<DataSourceFormModalProps> = ({
419531 < EuiSpacer size = "m" />
420532 < EuiFormRow
421533 label = "Timestamp Field"
422- helpText = "Event timestamp column name."
534+ isInvalid = { ! ! errors . timestampField }
535+ error = { errors . timestampField }
536+ helpText = "Column containing the event timestamp. Required for point-in-time correct feature retrieval."
423537 >
424538 < EuiFieldText
425539 value = { formData . timestampField }
426540 onChange = { ( e ) => updateField ( "timestampField" , e . target . value ) }
541+ isInvalid = { ! ! errors . timestampField }
427542 placeholder = "event_timestamp"
428543 />
429544 </ EuiFormRow >
430545 < EuiFormRow
431546 label = "Created Timestamp Column"
432- helpText = "Column tracking when the row was created ."
547+ helpText = "Optional: column tracking when the row was written (used for deduplication) ."
433548 >
434549 < EuiFieldText
435550 value = { formData . createdTimestampColumn }
0 commit comments