@@ -38,6 +38,7 @@ import {
38
38
useCreateSource ,
39
39
useDeleteSource ,
40
40
useSource ,
41
+ useSources ,
41
42
useUpdateSource ,
42
43
} from '@/source' ;
43
44
@@ -56,6 +57,39 @@ const OTEL_CLICKHOUSE_EXPRESSIONS = {
56
57
resourceAttributesExpression : 'ResourceAttributes' ,
57
58
} ;
58
59
60
+ const CORRELATION_FIELD_MAP : Record <
61
+ SourceKind ,
62
+ Record < string , { targetKind : SourceKind ; targetField : keyof TSource } [ ] >
63
+ > = {
64
+ [ SourceKind . Log ] : {
65
+ metricSourceId : [
66
+ { targetKind : SourceKind . Metric , targetField : 'logSourceId' } ,
67
+ ] ,
68
+ traceSourceId : [
69
+ { targetKind : SourceKind . Trace , targetField : 'logSourceId' } ,
70
+ ] ,
71
+ } ,
72
+ [ SourceKind . Trace ] : {
73
+ logSourceId : [ { targetKind : SourceKind . Log , targetField : 'traceSourceId' } ] ,
74
+ sessionSourceId : [
75
+ { targetKind : SourceKind . Session , targetField : 'traceSourceId' } ,
76
+ ] ,
77
+ metricSourceId : [
78
+ { targetKind : SourceKind . Metric , targetField : 'logSourceId' } ,
79
+ ] ,
80
+ } ,
81
+ [ SourceKind . Session ] : {
82
+ traceSourceId : [
83
+ { targetKind : SourceKind . Trace , targetField : 'sessionSourceId' } ,
84
+ ] ,
85
+ } ,
86
+ [ SourceKind . Metric ] : {
87
+ logSourceId : [
88
+ { targetKind : SourceKind . Log , targetField : 'metricSourceId' } ,
89
+ ] ,
90
+ } ,
91
+ } ;
92
+
59
93
function FormRow ( {
60
94
label,
61
95
children,
@@ -906,6 +940,62 @@ export function TableSourceForm({
906
940
const updateSource = useUpdateSource ( ) ;
907
941
const deleteSource = useDeleteSource ( ) ;
908
942
943
+ // Bidirectional source linking
944
+ const { data : sources } = useSources ( ) ;
945
+ const currentSourceId = watch ( 'id' ) ;
946
+
947
+ useEffect ( ( ) => {
948
+ const { unsubscribe } = watch ( async ( _value , { name, type } ) => {
949
+ const value = _value as TSourceUnion ;
950
+ if ( ! currentSourceId || ! sources || type !== 'change' ) return ;
951
+
952
+ const correlationFields = CORRELATION_FIELD_MAP [ kind ] ;
953
+ if ( ! correlationFields || ! name || ! ( name in correlationFields ) ) return ;
954
+
955
+ const fieldName = name as keyof TSourceUnion ;
956
+ const newTargetSourceId = value [ fieldName ] as string | undefined ;
957
+ const targetConfigs = correlationFields [ fieldName ] ;
958
+
959
+ for ( const { targetKind, targetField } of targetConfigs ) {
960
+ // Find the previously linked source if any
961
+ const previouslyLinkedSource = sources . find (
962
+ s => s . kind === targetKind && s [ targetField ] === currentSourceId ,
963
+ ) ;
964
+
965
+ // If there was a previously linked source and it's different from the new one, unlink it
966
+ if (
967
+ previouslyLinkedSource &&
968
+ previouslyLinkedSource . id !== newTargetSourceId
969
+ ) {
970
+ await updateSource . mutateAsync ( {
971
+ source : {
972
+ ...previouslyLinkedSource ,
973
+ [ targetField ] : undefined ,
974
+ } as TSource ,
975
+ } ) ;
976
+ }
977
+
978
+ // If a new source is selected, link it back
979
+ if ( newTargetSourceId ) {
980
+ const targetSource = sources . find ( s => s . id === newTargetSourceId ) ;
981
+ if ( targetSource && targetSource . kind === targetKind ) {
982
+ // Only update if the target field is empty to avoid overwriting existing correlations
983
+ if ( ! targetSource [ targetField ] ) {
984
+ await updateSource . mutateAsync ( {
985
+ source : {
986
+ ...targetSource ,
987
+ [ targetField ] : currentSourceId ,
988
+ } as TSource ,
989
+ } ) ;
990
+ }
991
+ }
992
+ }
993
+ }
994
+ } ) ;
995
+
996
+ return ( ) => unsubscribe ( ) ;
997
+ } , [ watch , kind , currentSourceId , sources , updateSource ] ) ;
998
+
909
999
const sourceFormSchema = sourceSchemaWithout ( { id : true } ) ;
910
1000
const handleError = ( error : z . ZodError < TSourceUnion > ) => {
911
1001
const errors = error . errors ;
@@ -933,18 +1023,47 @@ export function TableSourceForm({
933
1023
934
1024
const _onCreate = useCallback ( ( ) => {
935
1025
clearErrors ( ) ;
936
- handleSubmit ( data => {
1026
+ handleSubmit ( async data => {
937
1027
const parseResult = sourceFormSchema . safeParse ( data ) ;
938
1028
if ( parseResult . error ) {
939
1029
handleError ( parseResult . error ) ;
940
1030
return ;
941
1031
}
1032
+
942
1033
createSource . mutate (
943
1034
// TODO: HDX-1768 get rid of this type assertion
944
1035
{ source : data as TSource } ,
945
1036
{
946
- onSuccess : data => {
947
- onCreate ?.( data ) ;
1037
+ onSuccess : async newSource => {
1038
+ // Handle bidirectional linking for new sources
1039
+ const correlationFields = CORRELATION_FIELD_MAP [ newSource . kind ] ;
1040
+ if ( correlationFields && sources ) {
1041
+ for ( const [ fieldName , targetConfigs ] of Object . entries (
1042
+ correlationFields ,
1043
+ ) ) {
1044
+ const targetSourceId = ( newSource as any ) [ fieldName ] ;
1045
+ if ( targetSourceId ) {
1046
+ for ( const { targetKind, targetField } of targetConfigs ) {
1047
+ const targetSource = sources . find (
1048
+ s => s . id === targetSourceId ,
1049
+ ) ;
1050
+ if ( targetSource && targetSource . kind === targetKind ) {
1051
+ // Only update if the target field is empty to avoid overwriting existing correlations
1052
+ if ( ! targetSource [ targetField ] ) {
1053
+ await updateSource . mutateAsync ( {
1054
+ source : {
1055
+ ...targetSource ,
1056
+ [ targetField ] : newSource . id ,
1057
+ } as TSource ,
1058
+ } ) ;
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+
1066
+ onCreate ?.( newSource ) ;
948
1067
notifications . show ( {
949
1068
color : 'green' ,
950
1069
message : 'Source created' ,
@@ -959,7 +1078,15 @@ export function TableSourceForm({
959
1078
} ,
960
1079
) ;
961
1080
} ) ( ) ;
962
- } , [ handleSubmit , createSource , onCreate , kind , formState ] ) ;
1081
+ } , [
1082
+ handleSubmit ,
1083
+ createSource ,
1084
+ onCreate ,
1085
+ kind ,
1086
+ formState ,
1087
+ sources ,
1088
+ updateSource ,
1089
+ ] ) ;
963
1090
964
1091
const _onSave = useCallback ( ( ) => {
965
1092
clearErrors ( ) ;
0 commit comments