serviceFlowFields.ts749 lines · main
1import { BlockFieldConfig } from '../types'
2import { getStorageMetadata } from '../utils/storageUtils'
3import { formatBytes } from '@/lib/helpers'
4
5// Helper functions that avoid duplication with existing storage utilities
6const getFileName = (path: string): string => {
7 if (!path) return ''
8 // Remove query parameters and hash
9 const cleanPath = path.split('?')[0].split('#')[0]
10 // Get the last part after the last slash
11 const segments = cleanPath.split('/')
12 return segments[segments.length - 1] || ''
13}
14
15const formatStorageDate = (dateString: string): string => {
16 try {
17 return new Date(dateString).toLocaleString()
18 } catch {
19 return dateString
20 }
21}
22
23// =============================================================================
24// NETWORK FIELDS
25// =============================================================================
26
27// Field configurations - using filterable field IDs where possible
28export const originFields: BlockFieldConfig[] = [
29 {
30 id: 'date', // Matches filterFields 'date' (timerange) - FILTERABLE
31 label: 'Time',
32 getValue: (data) => {
33 if (!data?.timestamp && !data?.date) return null
34 try {
35 const timestamp = data?.timestamp || data?.date
36 return new Date(timestamp).toLocaleString()
37 } catch {
38 return 'Invalid date'
39 }
40 },
41 },
42]
43
44// Primary Network Fields (Always Visible) - FILTERABLE
45export const networkPrimaryFields: BlockFieldConfig[] = [
46 {
47 id: 'host', // Matches filterFields 'host' (input) - FILTERABLE
48 label: 'Host',
49 getValue: (data, enrichedData) =>
50 enrichedData?.request_host || enrichedData?.host || data?.host,
51 },
52 {
53 id: 'method', // Matches filterFields 'method' (checkbox) - FILTERABLE
54 label: 'Method',
55 getValue: (data, enrichedData) =>
56 enrichedData?.request_method || enrichedData?.method || data?.method,
57 },
58 {
59 id: 'pathname', // Matches filterFields 'pathname' (input) - FILTERABLE
60 label: 'Path',
61 getValue: (data, enrichedData) =>
62 enrichedData?.request_path || enrichedData?.pathname || data?.pathname,
63 },
64 {
65 id: 'user_agent',
66 label: 'Client',
67 getValue: (_data, enrichedData) => {
68 const userAgent = enrichedData?.headers_user_agent
69 if (!userAgent) return null
70 // TODO: Parse user agent for nice display with icons
71 return userAgent.length > 50 ? userAgent.substring(0, 50) + '...' : userAgent
72 },
73 requiresEnrichedData: true,
74 },
75]
76
77// Primary API Key Field (Always Visible) - Shows API key type only
78export const apiKeyPrimaryField: BlockFieldConfig = {
79 id: 'api_key_role',
80 label: 'API Key',
81 getValue: (_data, enrichedData) => {
82 const prefix = enrichedData?.api_key_prefix
83 const jwtRole = enrichedData?.jwt_key_role
84 const authRole = enrichedData?.authorization_role
85
86 if (prefix) {
87 // Extract key type from prefix
88 if (prefix.includes('publishable')) return 'publishable'
89 else if (prefix.includes('secret')) return 'secret'
90 return 'unknown'
91 }
92
93 // Fallback to JWT apikey postgres role if no API key prefix
94 if (jwtRole) {
95 return jwtRole
96 }
97
98 // Fallback to authorization postgres role if no apikey JWT
99 if (authRole) {
100 return authRole
101 }
102
103 return null
104 },
105 requiresEnrichedData: true,
106}
107
108// Additional API Key Fields (Collapsible)
109export const apiKeyAdditionalFields: BlockFieldConfig[] = [
110 {
111 id: 'api_key_prefix_full',
112 label: 'API Key Prefix',
113 getValue: (_data, enrichedData) => {
114 const prefix = enrichedData?.api_key_prefix
115 return prefix || null
116 },
117 requiresEnrichedData: true,
118 },
119 {
120 id: 'postgres_role',
121 label: 'Postgres Role',
122 getValue: (_data, enrichedData) => enrichedData?.jwt_key_role,
123 requiresEnrichedData: true,
124 },
125 {
126 id: 'api_key_error',
127 label: 'API Key Error',
128 getValue: (_data, enrichedData) => enrichedData?.api_key_error,
129 requiresEnrichedData: true,
130 },
131 {
132 id: 'api_key_hash',
133 label: 'API Key Hash',
134 getValue: (_data, enrichedData) => {
135 const hash = enrichedData?.api_key_hash
136 return hash ? `${hash.substring(0, 12)}...` : null
137 },
138 requiresEnrichedData: true,
139 },
140 {
141 id: 'authorization_role',
142 label: 'Postgres Role',
143 getValue: (_data, enrichedData) => enrichedData?.authorization_role,
144 requiresEnrichedData: true,
145 },
146]
147
148// Primary User Field (Always Visible)
149export const userPrimaryField: BlockFieldConfig = {
150 id: 'user_id',
151 label: 'User',
152 getValue: (_data, enrichedData) => {
153 const userId = enrichedData?.user_id
154 return userId ? `${userId.substring(0, 8)}...` : null
155 },
156 requiresEnrichedData: true,
157}
158
159// Additional User Fields (Collapsible)
160export const userAdditionalFields: BlockFieldConfig[] = [
161 {
162 id: 'auth_user', // Matches filterFields 'auth_user' (input) - FILTERABLE
163 label: 'Auth User',
164 getValue: (data, enrichedData) => enrichedData?.auth_user || data?.auth_user,
165 },
166 {
167 id: 'user_email',
168 label: 'User Email',
169 getValue: (_data, enrichedData) => enrichedData?.user_email,
170 requiresEnrichedData: true,
171 },
172]
173
174// Primary Location Field (Always Visible)
175export const locationPrimaryField: BlockFieldConfig = {
176 id: 'client_country',
177 label: 'Location',
178 getValue: (_data, enrichedData) => {
179 const country = enrichedData?.client_country || enrichedData?.cf_country
180 const city = enrichedData?.client_city
181 if (country && city) return `${city}, ${country}`
182 if (country) return country
183 if (city) return city
184 return null
185 },
186 requiresEnrichedData: true,
187}
188
189// Additional Location Fields (Collapsible) - includes IP addresses
190export const locationAdditionalFields: BlockFieldConfig[] = [
191 {
192 id: 'client_continent',
193 label: 'Continent',
194 getValue: (_data, enrichedData) => enrichedData?.client_continent,
195 requiresEnrichedData: true,
196 },
197 {
198 id: 'client_region',
199 label: 'Region',
200 getValue: (_data, enrichedData) => enrichedData?.client_region,
201 requiresEnrichedData: true,
202 },
203 {
204 id: 'client_timezone',
205 label: 'Timezone',
206 getValue: (_data, enrichedData) => enrichedData?.client_timezone,
207 requiresEnrichedData: true,
208 },
209 {
210 id: 'x_real_ip',
211 label: 'Real IP',
212 getValue: (_data, enrichedData) => enrichedData?.headers_x_real_ip,
213 requiresEnrichedData: true,
214 },
215 {
216 id: 'client_ip',
217 label: 'Client IP',
218 getValue: (_data, enrichedData) => enrichedData?.client_ip,
219 requiresEnrichedData: true,
220 },
221]
222
223// Authorization Fields (Collapsible) - JWT authorization details
224export const authorizationFields: BlockFieldConfig[] = [
225 {
226 id: 'jwt_auth_key_id',
227 label: 'Key ID',
228 getValue: (_data, enrichedData) =>
229 enrichedData?.jwt_auth_key_id || enrichedData?.jwt_apikey_key_id,
230 requiresEnrichedData: true,
231 },
232 {
233 id: 'jwt_auth_session_id',
234 label: 'Session ID',
235 getValue: (_data, enrichedData) =>
236 enrichedData?.jwt_auth_session_id || enrichedData?.jwt_apikey_session_id,
237 requiresEnrichedData: true,
238 },
239 {
240 id: 'jwt_auth_subject',
241 label: 'Subject',
242 getValue: (_data, enrichedData) =>
243 enrichedData?.jwt_auth_subject || enrichedData?.jwt_apikey_subject,
244 requiresEnrichedData: true,
245 },
246 {
247 id: 'jwt_auth_issuer',
248 label: 'Issuer',
249 getValue: (_data, enrichedData) =>
250 enrichedData?.jwt_auth_issuer || enrichedData?.jwt_apikey_issuer,
251 requiresEnrichedData: true,
252 },
253 {
254 id: 'jwt_auth_algorithm',
255 label: 'Algorithm',
256 getValue: (_data, enrichedData) =>
257 enrichedData?.jwt_auth_algorithm || enrichedData?.jwt_apikey_algorithm,
258 requiresEnrichedData: true,
259 },
260 {
261 id: 'jwt_auth_expires_at',
262 label: 'Expires At',
263 getValue: (_data, enrichedData) => {
264 const expiresAt = enrichedData?.jwt_auth_expires_at || enrichedData?.jwt_apikey_expires_at
265 if (!expiresAt) return null
266 try {
267 return new Date(expiresAt * 1000).toLocaleString()
268 } catch {
269 return expiresAt
270 }
271 },
272 requiresEnrichedData: true,
273 },
274]
275
276// Tech Details Fields (Collapsible)
277export const techDetailsFields: BlockFieldConfig[] = [
278 {
279 id: 'network_protocol',
280 label: 'Protocol',
281 getValue: (_data, enrichedData) => enrichedData?.network_protocol,
282 requiresEnrichedData: true,
283 },
284 {
285 id: 'cf_datacenter',
286 label: 'Datacenter',
287 getValue: (_data, enrichedData) =>
288 enrichedData?.network_datacenter || enrichedData?.cf_datacenter,
289 requiresEnrichedData: true,
290 },
291 {
292 id: 'cache_status',
293 label: 'Cache Status',
294 getValue: (_data, enrichedData) => enrichedData?.response_cache_status,
295 requiresEnrichedData: true,
296 },
297 {
298 id: 'cf_ray',
299 label: 'CF-Ray',
300 getValue: (_data, enrichedData) => enrichedData?.cf_ray,
301 requiresEnrichedData: true,
302 },
303 {
304 id: 'x_client_info',
305 label: 'SDK',
306 getValue: (_data, enrichedData) => enrichedData?.headers_x_client_info,
307 requiresEnrichedData: true,
308 },
309 {
310 id: 'x_forwarded_proto',
311 label: 'Forwarded Proto',
312 getValue: (_data, enrichedData) => enrichedData?.headers_x_forwarded_proto,
313 requiresEnrichedData: true,
314 },
315]
316
317// =============================================================================
318// POSTGREST FIELDS
319// =============================================================================
320
321// Primary PostgREST Fields (Always Visible) - FILTERABLE
322export const postgrestPrimaryFields: BlockFieldConfig[] = [
323 {
324 id: 'status', // Matches filterFields 'status' (checkbox) - FILTERABLE
325 label: 'Status',
326 getValue: (data, enrichedData) => enrichedData?.status || data?.status,
327 },
328 {
329 id: 'postgres_role',
330 label: 'Postgres Role',
331 getValue: (_data, enrichedData) => enrichedData?.api_role,
332 requiresEnrichedData: true,
333 },
334 {
335 id: 'response_time',
336 label: 'Response Time',
337 getValue: (data, enrichedData) => {
338 const time = enrichedData?.response_origin_time || data?.response_time_ms
339 return time ? `${time}ms` : null
340 },
341 requiresEnrichedData: true,
342 },
343]
344
345// PostgREST Response Details (Collapsible)
346export const postgrestResponseFields: BlockFieldConfig[] = [
347 {
348 id: 'query_params',
349 label: 'Query',
350 getValue: (_data, enrichedData) => enrichedData?.request_search,
351 requiresEnrichedData: true,
352 },
353 {
354 id: 'content_type',
355 label: 'Content Type',
356 getValue: (_data, enrichedData) => enrichedData?.response_content_type,
357 requiresEnrichedData: true,
358 },
359 {
360 id: 'message',
361 label: 'Message',
362 getValue: (_data, enrichedData) => enrichedData?.message,
363 requiresEnrichedData: true,
364 },
365]
366
367// =============================================================================
368// AUTH FIELDS
369// =============================================================================
370
371// Primary GoTrue/Auth Fields (Always Visible)
372export const authPrimaryFields: BlockFieldConfig[] = [
373 {
374 id: 'auth_path',
375 label: 'Auth Path',
376 getValue: (data, enrichedData) => {
377 return enrichedData?.path || enrichedData?.request_path || data?.path
378 },
379 requiresEnrichedData: true,
380 },
381 {
382 id: 'log_id',
383 label: 'Log ID',
384 getValue: (data, enrichedData) => {
385 const logId = data?.id || enrichedData?.id
386 return logId ? `${logId.substring(0, 8)}...` : null
387 },
388 },
389 {
390 id: 'referer',
391 label: 'Referer',
392 getValue: (_data, enrichedData) => {
393 return enrichedData?.headers_referer || null
394 },
395 requiresEnrichedData: true,
396 },
397]
398
399// =============================================================================
400// EDGE FUNCTION FIELDS
401// =============================================================================
402
403// Primary Edge Function Fields (Always Visible)
404export const edgeFunctionPrimaryFields: BlockFieldConfig[] = [
405 {
406 id: 'status',
407 label: 'Status',
408 getValue: (data, enrichedData) => enrichedData?.status || data?.status,
409 },
410 {
411 id: 'function_path',
412 label: 'Function Path',
413 getValue: (data, enrichedData) => {
414 return (
415 enrichedData?.path || enrichedData?.request_path || data?.path || enrichedData?.request_url
416 )
417 },
418 requiresEnrichedData: false,
419 },
420 {
421 id: 'execution_time',
422 label: 'Execution Time',
423 getValue: (data, enrichedData) => {
424 const time = enrichedData?.execution_time_ms || data?.execution_time_ms
425 return time ? `${time}ms` : null
426 },
427 requiresEnrichedData: false,
428 },
429 {
430 id: 'execution_id',
431 label: 'Execution ID',
432 getValue: (data, enrichedData) => {
433 const execId = enrichedData?.execution_id || data?.execution_id
434 return execId ? `${execId.substring(0, 8)}...` : null
435 },
436 requiresEnrichedData: false,
437 },
438]
439
440// Edge Function Details (Collapsible)
441export const edgeFunctionDetailsFields: BlockFieldConfig[] = [
442 {
443 id: 'function_id',
444 label: 'Function ID',
445 getValue: (data, enrichedData) => {
446 const funcId = enrichedData?.function_id || data?.function_id
447 return funcId ? `${funcId.substring(0, 8)}...` : null
448 },
449 requiresEnrichedData: false,
450 },
451 {
452 id: 'execution_region',
453 label: 'Execution Region',
454 getValue: (data, enrichedData) => {
455 return enrichedData?.execution_region || data?.execution_region || null
456 },
457 requiresEnrichedData: false,
458 },
459 {
460 id: 'function_log_count',
461 label: 'Function Logs',
462 getValue: (data, enrichedData) => {
463 const count = enrichedData?.function_log_count || data?.function_log_count || data?.log_count
464 return count ? `${count} logs` : 'No logs'
465 },
466 requiresEnrichedData: false,
467 },
468 {
469 id: 'method',
470 label: 'Method',
471 getValue: (data, enrichedData) => enrichedData?.method || data?.method,
472 requiresEnrichedData: false,
473 },
474 {
475 id: 'user_agent',
476 label: 'User Agent',
477 getValue: (data, enrichedData) => {
478 const ua = enrichedData?.headers_user_agent || data?.headers_user_agent
479 return ua ? (ua.length > 30 ? `${ua.substring(0, 30)}...` : ua) : null
480 },
481 requiresEnrichedData: false,
482 },
483]
484
485// =============================================================================
486// STORAGE FIELDS
487// =============================================================================
488
489// Primary Storage Fields (Always Visible)
490export const storagePrimaryFields: BlockFieldConfig[] = [
491 {
492 id: 'status',
493 label: 'Status',
494 getValue: (data, enrichedData) => enrichedData?.status || data?.status,
495 },
496 {
497 id: 'filename',
498 label: 'File Name',
499 getValue: (data, enrichedData) => {
500 const path = enrichedData?.path || enrichedData?.request_path || data?.path
501 return path ? getFileName(path) : null
502 },
503 requiresEnrichedData: false,
504 },
505 {
506 id: 'content_type_size',
507 label: 'Type & Size',
508 getValue: (data, enrichedData) => {
509 const status = enrichedData?.status || data?.status
510 const isObjectDeleted = status === 404 || status === '404'
511 const hasError = status && Number(status) >= 400
512
513 // For deleted objects, show status message
514 if (isObjectDeleted) {
515 return 'Object deleted'
516 } else if (hasError) {
517 return `Error ${status}`
518 }
519
520 // Try to get metadata like PreviewPane does
521 const metadata = getStorageMetadata(data, enrichedData)
522 const mimeType = metadata?.mimetype
523 const size = metadata?.size
524
525 // Fallback to headers if metadata not available
526 const contentType =
527 mimeType ||
528 enrichedData?.response_content_type ||
529 enrichedData?.storage_request_content_type
530 const contentLength =
531 size ||
532 (enrichedData?.storage_content_length
533 ? parseInt(enrichedData.storage_content_length)
534 : null)
535
536 if (contentType && contentLength) {
537 return `${contentType} - ${formatBytes(contentLength)}`
538 } else if (contentType) {
539 return contentType
540 } else if (contentLength) {
541 return formatBytes(contentLength)
542 }
543 return 'Unknown type'
544 },
545 requiresEnrichedData: true,
546 },
547 {
548 id: 'response_time',
549 label: 'Response Time',
550 getValue: (data, enrichedData) => {
551 const time = enrichedData?.response_origin_time || data?.response_time_ms
552 return time ? `${time}ms` : null
553 },
554 requiresEnrichedData: true,
555 },
556]
557
558// Storage Details (Collapsible)
559export const storageDetailsFields: BlockFieldConfig[] = [
560 {
561 id: 'storage_path',
562 label: 'Storage Path',
563 getValue: (data, enrichedData) => {
564 const path = enrichedData?.path || enrichedData?.request_path || data?.path
565 // Extract just the object path from the full storage path
566 const match = path?.match(/\/storage\/v1\/object\/([^\/]+)\/(.+)/)
567 if (match) {
568 const [, bucketName, objectPath] = match
569 return `${bucketName}/${objectPath}`
570 }
571 return path
572 },
573 requiresEnrichedData: false,
574 },
575 {
576 id: 'last_modified',
577 label: 'Last Modified',
578 getValue: (data, enrichedData) => {
579 const status = enrichedData?.status || data?.status
580 const isObjectDeleted = status === 404 || status === '404'
581 const hasError = status && Number(status) >= 400
582
583 if (isObjectDeleted) {
584 return 'Object deleted'
585 } else if (hasError) {
586 return 'Unavailable'
587 }
588
589 // Try to get dates from metadata like PreviewPane does
590 const metadata = getStorageMetadata(data, enrichedData)
591 const updatedAt = metadata?.updated_at || data?.updated_at || enrichedData?.updated_at
592 const lastModified = enrichedData?.storage_last_modified || updatedAt
593
594 return lastModified ? formatStorageDate(lastModified) : null
595 },
596 requiresEnrichedData: true,
597 },
598 {
599 id: 'etag',
600 label: 'ETag',
601 getValue: (data, enrichedData) => {
602 const status = enrichedData?.status || data?.status
603 const isObjectDeleted = status === 404 || status === '404'
604 const hasError = status && Number(status) >= 400
605
606 if (isObjectDeleted) {
607 return 'Object deleted'
608 } else if (hasError) {
609 return 'Unavailable'
610 }
611
612 const etag = enrichedData?.storage_etag
613 return etag ? `${etag.substring(0, 12)}...` : null
614 },
615 requiresEnrichedData: true,
616 },
617 {
618 id: 'content_disposition',
619 label: 'Content Disposition',
620 getValue: (data, enrichedData) => {
621 const status = enrichedData?.status || data?.status
622 const isObjectDeleted = status === 404 || status === '404'
623 const hasError = status && Number(status) >= 400
624
625 if (isObjectDeleted) {
626 return 'Object deleted'
627 } else if (hasError) {
628 return 'Unavailable'
629 }
630
631 return enrichedData?.storage_content_disposition || null
632 },
633 requiresEnrichedData: true,
634 },
635 {
636 id: 'method',
637 label: 'Method',
638 getValue: (data, enrichedData) => enrichedData?.method || data?.method,
639 requiresEnrichedData: false,
640 },
641 {
642 id: 'user_agent',
643 label: 'User Agent',
644 getValue: (data, enrichedData) => {
645 const ua = enrichedData?.headers_user_agent || data?.headers_user_agent
646 return ua ? (ua.length > 30 ? `${ua.substring(0, 30)}...` : ua) : null
647 },
648 requiresEnrichedData: false,
649 },
650]
651
652// =============================================================================
653// POSTGRES FIELDS
654// =============================================================================
655
656// Primary Postgres Fields (Always Visible)
657export const postgresPrimaryFields: BlockFieldConfig[] = [
658 {
659 id: 'event_message',
660 label: 'Message',
661 getValue: (data, enrichedData) => enrichedData?.event_message || data?.event_message,
662 wrap: true,
663 },
664 {
665 id: 'status',
666 label: 'Status',
667 getValue: (data, enrichedData) => enrichedData?.status || data?.status,
668 },
669 {
670 id: 'command_tag',
671 label: 'Command',
672 getValue: (data, enrichedData) => enrichedData?.command_tag || data?.command_tag,
673 requiresEnrichedData: true,
674 },
675 {
676 id: 'database_name',
677 label: 'Database',
678 getValue: (data, enrichedData) => enrichedData?.database_name || data?.database_name,
679 requiresEnrichedData: true,
680 },
681 {
682 id: 'database_user',
683 label: 'User',
684 getValue: (data, enrichedData) => enrichedData?.database_user || data?.database_user,
685 requiresEnrichedData: true,
686 },
687]
688
689// Postgres Details (Collapsible)
690export const postgresDetailsFields: BlockFieldConfig[] = [
691 {
692 id: 'backend_type',
693 label: 'Backend Type',
694 getValue: (data, enrichedData) => enrichedData?.backend_type || data?.backend_type,
695 requiresEnrichedData: true,
696 },
697 {
698 id: 'connection_from',
699 label: 'Connection From',
700 getValue: (data, enrichedData) => enrichedData?.connection_from || data?.connection_from,
701 requiresEnrichedData: true,
702 },
703 {
704 id: 'session_id',
705 label: 'Session ID',
706 getValue: (data, enrichedData) => enrichedData?.session_id || data?.session_id,
707 requiresEnrichedData: true,
708 },
709 {
710 id: 'process_id',
711 label: 'Process ID',
712 getValue: (data, enrichedData) => enrichedData?.process_id || data?.process_id,
713 requiresEnrichedData: true,
714 },
715 {
716 id: 'query_id',
717 label: 'Query ID',
718 getValue: (data, enrichedData) => {
719 const queryId = enrichedData?.query_id || data?.query_id
720 return queryId ? String(queryId) : null
721 },
722 requiresEnrichedData: true,
723 },
724 {
725 id: 'transaction_id',
726 label: 'Transaction ID',
727 getValue: (data, enrichedData) => {
728 const txId = enrichedData?.transaction_id || data?.transaction_id
729 return txId ? String(txId) : null
730 },
731 requiresEnrichedData: true,
732 },
733 {
734 id: 'virtual_transaction_id',
735 label: 'Virtual TX ID',
736 getValue: (data, enrichedData) =>
737 enrichedData?.virtual_transaction_id || data?.virtual_transaction_id,
738 requiresEnrichedData: true,
739 },
740 {
741 id: 'session_start_time',
742 label: 'Session Started',
743 getValue: (data, enrichedData) => {
744 const startTime = enrichedData?.session_start_time || data?.session_start_time
745 return startTime ? new Date(startTime).toLocaleString() : null
746 },
747 requiresEnrichedData: true,
748 },
749]