From 2f1e32da2212562f361e84133350c5b3e2f6a82f Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 25 Aug 2024 14:42:22 +0300 Subject: [PATCH 1/8] chore: get detected destinations from gql --- frontend/graph/generated.go | 345 ++++++++++++++++++ frontend/graph/model/models_gen.go | 6 + frontend/graph/schema.graphqls | 7 + frontend/graph/schema.resolvers.go | 30 +- .../destination_finder.go | 79 ++++ .../destination_recognition/elasticsearch.go | 45 +++ .../destination_recognition/jaeger.go | 46 +++ frontend/services/destinations.go | 24 ++ frontend/services/utils.go | 15 + 9 files changed, 593 insertions(+), 4 deletions(-) create mode 100644 frontend/services/destination_recognition/destination_finder.go create mode 100644 frontend/services/destination_recognition/elasticsearch.go create mode 100644 frontend/services/destination_recognition/jaeger.go diff --git a/frontend/graph/generated.go b/frontend/graph/generated.go index 23247a13c..ec686160a 100644 --- a/frontend/graph/generated.go +++ b/frontend/graph/generated.go @@ -77,6 +77,12 @@ type ComplexityRoot struct { Type func(childComplexity int) int } + DestinationDetails struct { + Fields func(childComplexity int) int + Type func(childComplexity int) int + URLString func(childComplexity int) int + } + DestinationTypesCategoryItem struct { DisplayName func(childComplexity int) int ImageUrl func(childComplexity int) int @@ -156,6 +162,7 @@ type ComplexityRoot struct { Config func(childComplexity int) int DestinationTypeDetails func(childComplexity int, typeArg string) int DestinationTypes func(childComplexity int) int + PotentialDestinations func(childComplexity int) int } SourceContainerRuntimeDetails struct { @@ -204,6 +211,7 @@ type QueryResolver interface { Config(ctx context.Context) (*model.GetConfigResponse, error) DestinationTypes(ctx context.Context) (*model.GetDestinationTypesResponse, error) DestinationTypeDetails(ctx context.Context, typeArg string) (*model.GetDestinationDetailsResponse, error) + PotentialDestinations(ctx context.Context) ([]*model.DestinationDetails, error) } type executableSchema struct { @@ -361,6 +369,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Destination.Type(childComplexity), true + case "DestinationDetails.fields": + if e.complexity.DestinationDetails.Fields == nil { + break + } + + return e.complexity.DestinationDetails.Fields(childComplexity), true + + case "DestinationDetails.type": + if e.complexity.DestinationDetails.Type == nil { + break + } + + return e.complexity.DestinationDetails.Type(childComplexity), true + + case "DestinationDetails.urlString": + if e.complexity.DestinationDetails.URLString == nil { + break + } + + return e.complexity.DestinationDetails.URLString(childComplexity), true + case "DestinationTypesCategoryItem.displayName": if e.complexity.DestinationTypesCategoryItem.DisplayName == nil { break @@ -685,6 +714,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.DestinationTypes(childComplexity), true + case "Query.potentialDestinations": + if e.complexity.Query.PotentialDestinations == nil { + break + } + + return e.complexity.Query.PotentialDestinations(childComplexity), true + case "SourceContainerRuntimeDetails.containerName": if e.complexity.SourceContainerRuntimeDetails.ContainerName == nil { break @@ -1965,6 +2001,138 @@ func (ec *executionContext) fieldContext_Destination_conditions(_ context.Contex return fc, nil } +func (ec *executionContext) _DestinationDetails_type(ctx context.Context, field graphql.CollectedField, obj *model.DestinationDetails) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DestinationDetails_type(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Type, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DestinationDetails_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DestinationDetails", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DestinationDetails_urlString(ctx context.Context, field graphql.CollectedField, obj *model.DestinationDetails) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DestinationDetails_urlString(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.URLString, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DestinationDetails_urlString(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DestinationDetails", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _DestinationDetails_fields(ctx context.Context, field graphql.CollectedField, obj *model.DestinationDetails) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_DestinationDetails_fields(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Fields, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_DestinationDetails_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "DestinationDetails", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _DestinationTypesCategoryItem_type(ctx context.Context, field graphql.CollectedField, obj *model.DestinationTypesCategoryItem) (ret graphql.Marshaler) { fc, err := ec.fieldContext_DestinationTypesCategoryItem_type(ctx, field) if err != nil { @@ -3978,6 +4146,58 @@ func (ec *executionContext) fieldContext_Query_destinationTypeDetails(ctx contex return fc, nil } +func (ec *executionContext) _Query_potentialDestinations(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_potentialDestinations(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().PotentialDestinations(rctx) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*model.DestinationDetails) + fc.Result = res + return ec.marshalNDestinationDetails2ᚕᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestinationDetailsᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_potentialDestinations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "type": + return ec.fieldContext_DestinationDetails_type(ctx, field) + case "urlString": + return ec.fieldContext_DestinationDetails_urlString(ctx, field) + case "fields": + return ec.fieldContext_DestinationDetails_fields(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type DestinationDetails", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query___type(ctx, field) if err != nil { @@ -6984,6 +7204,55 @@ func (ec *executionContext) _Destination(ctx context.Context, sel ast.SelectionS return out } +var destinationDetailsImplementors = []string{"DestinationDetails"} + +func (ec *executionContext) _DestinationDetails(ctx context.Context, sel ast.SelectionSet, obj *model.DestinationDetails) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, destinationDetailsImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("DestinationDetails") + case "type": + out.Values[i] = ec._DestinationDetails_type(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "urlString": + out.Values[i] = ec._DestinationDetails_urlString(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "fields": + out.Values[i] = ec._DestinationDetails_fields(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var destinationTypesCategoryItemImplementors = []string{"DestinationTypesCategoryItem"} func (ec *executionContext) _DestinationTypesCategoryItem(ctx context.Context, sel ast.SelectionSet, obj *model.DestinationTypesCategoryItem) graphql.Marshaler { @@ -7696,6 +7965,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "potentialDestinations": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_potentialDestinations(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "__type": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { @@ -8256,6 +8547,60 @@ func (ec *executionContext) marshalNDestination2ᚖgithubᚗcomᚋodigosᚑioᚋ return ec._Destination(ctx, sel, v) } +func (ec *executionContext) marshalNDestinationDetails2ᚕᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestinationDetailsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.DestinationDetails) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNDestinationDetails2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestinationDetails(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + +func (ec *executionContext) marshalNDestinationDetails2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestinationDetails(ctx context.Context, sel ast.SelectionSet, v *model.DestinationDetails) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._DestinationDetails(ctx, sel, v) +} + func (ec *executionContext) unmarshalNDestinationInput2githubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestinationInput(ctx context.Context, v interface{}) (model.DestinationInput, error) { res, err := ec.unmarshalInputDestinationInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/frontend/graph/model/models_gen.go b/frontend/graph/model/models_gen.go index e71638825..52b137a14 100644 --- a/frontend/graph/model/models_gen.go +++ b/frontend/graph/model/models_gen.go @@ -25,6 +25,12 @@ type Condition struct { Message *string `json:"message,omitempty"` } +type DestinationDetails struct { + Type string `json:"type"` + URLString string `json:"urlString"` + Fields string `json:"fields"` +} + type DestinationInput struct { Name string `json:"name"` Type string `json:"type"` diff --git a/frontend/graph/schema.graphqls b/frontend/graph/schema.graphqls index 9cfed5381..ac7d0f7e1 100644 --- a/frontend/graph/schema.graphqls +++ b/frontend/graph/schema.graphqls @@ -209,11 +209,18 @@ type TestConnectionResponse { reason: String } +type DestinationDetails { + type: String! + urlString: String! + fields: String! +} + type Query { computePlatform: ComputePlatform config: GetConfigResponse destinationTypes: GetDestinationTypesResponse destinationTypeDetails(type: String!): GetDestinationDetailsResponse + potentialDestinations: [DestinationDetails!]! } type Mutation { diff --git a/frontend/graph/schema.resolvers.go b/frontend/graph/schema.resolvers.go index 983b6a218..372ae91fe 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -196,8 +196,8 @@ func (r *mutationResolver) PersistK8sSources(ctx context.Context, namespace stri } // TestConnectionForDestination is the resolver for the testConnectionForDestination field. -func (r *mutationResolver) TestConnectionForDestination(ctx context.Context, input model.DestinationInput) (*model.TestConnectionResponse, error) { - destType := common.DestinationType(input.Type) +func (r *mutationResolver) TestConnectionForDestination(ctx context.Context, destination model.DestinationInput) (*model.TestConnectionResponse, error) { + destType := common.DestinationType(destination.Type) destConfig, err := services.GetDestinationTypeConfig(destType) if err != nil { @@ -205,10 +205,10 @@ func (r *mutationResolver) TestConnectionForDestination(ctx context.Context, inp } if !destConfig.Spec.TestConnectionSupported { - return nil, fmt.Errorf("destination type %s does not support test connection", input.Type) + return nil, fmt.Errorf("destination type %s does not support test connection", destination.Type) } - configurer, err := testconnection.ConvertDestinationToConfigurer(input) + configurer, err := testconnection.ConvertDestinationToConfigurer(destination) if err != nil { return nil, err } @@ -303,6 +303,28 @@ func (r *queryResolver) DestinationTypeDetails(ctx context.Context, typeArg stri return &resp, nil } +// PotentialDestinations is the resolver for the potentialDestinations field. +func (r *queryResolver) PotentialDestinations(ctx context.Context) ([]*model.DestinationDetails, error) { + potentialDestinations := services.PotentialDestinations(ctx) + if potentialDestinations == nil { + return nil, fmt.Errorf("failed to fetch potential destinations") + } + + // Convert []destination_recognition.DestinationDetails to []*DestinationDetails + var result []*model.DestinationDetails + for _, dest := range potentialDestinations { + + fieldsString := services.ConvertFieldsToString(dest.Fields) + + result = append(result, &model.DestinationDetails{ + Type: string(dest.Type), + Fields: fieldsString, + }) + } + + return result, nil +} + // ComputePlatform returns ComputePlatformResolver implementation. func (r *Resolver) ComputePlatform() ComputePlatformResolver { return &computePlatformResolver{r} } diff --git a/frontend/services/destination_recognition/destination_finder.go b/frontend/services/destination_recognition/destination_finder.go new file mode 100644 index 000000000..20258eff7 --- /dev/null +++ b/frontend/services/destination_recognition/destination_finder.go @@ -0,0 +1,79 @@ +package destination_recognition + +import ( + "context" + + odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" + "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/frontend/kube" + "github.com/odigos-io/odigos/k8sutils/pkg/client" + k8s "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var SupportedDestinationType = []common.DestinationType{common.JaegerDestinationType, common.ElasticsearchDestinationType} + +type DestinationDetails struct { + Type common.DestinationType `json:"type"` + Fields map[string]string `json:"fields"` +} + +type IDestinationFinder interface { + isPotentialService(k8s.Service) bool + fetchDestinationDetails(k8s.Service) DestinationDetails + getServiceURL() string +} + +func GetAllPotentialDestinationDetails(ctx context.Context, namespaces []k8s.Namespace, dests *odigosv1.DestinationList) ([]DestinationDetails, error) { + var destinationFinder IDestinationFinder + var destinationDetails []DestinationDetails + var err error + + for _, ns := range namespaces { + err = client.ListWithPages(client.DefaultPageSize, kube.DefaultClient.CoreV1().Services(ns.Name).List, + ctx, metav1.ListOptions{}, func(services *k8s.ServiceList) error { + for _, service := range services.Items { + for _, destinationType := range SupportedDestinationType { + destinationFinder = getDestinationFinder(destinationType) + + if destinationFinder.isPotentialService(service) { + potentialDestination := destinationFinder.fetchDestinationDetails(service) + + if !destinationExist(dests, potentialDestination, destinationFinder) { + destinationDetails = append(destinationDetails, potentialDestination) + } + break + } + } + } + return nil + }) + } + + if err != nil { + return nil, err + } + + return destinationDetails, nil +} + +func getDestinationFinder(destinationType common.DestinationType) IDestinationFinder { + switch destinationType { + case common.JaegerDestinationType: + return &JaegerDestinationFinder{} + case common.ElasticsearchDestinationType: + return &ElasticSearchDestinationFinder{} + } + + return nil +} + +func destinationExist(dests *odigosv1.DestinationList, potentialDestination DestinationDetails, destinationFinder IDestinationFinder) bool { + for _, dest := range dests.Items { + if dest.Spec.Type == potentialDestination.Type && dest.GetConfig()[destinationFinder.getServiceURL()] == potentialDestination.Fields[destinationFinder.getServiceURL()] { + return true + } + } + + return false +} diff --git a/frontend/services/destination_recognition/elasticsearch.go b/frontend/services/destination_recognition/elasticsearch.go new file mode 100644 index 000000000..38c3e7093 --- /dev/null +++ b/frontend/services/destination_recognition/elasticsearch.go @@ -0,0 +1,45 @@ +package destination_recognition + +import ( + "fmt" + "strings" + + "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/config" + k8s "k8s.io/api/core/v1" +) + +type ElasticSearchDestinationFinder struct{} + +const ElasticSearchHttpPort int32 = 9200 +const ElasticSearchHttpUrlFormat = "https://%s.%s:%d" + +func (j *ElasticSearchDestinationFinder) isPotentialService(service k8s.Service) bool { + for _, port := range service.Spec.Ports { + if isElasticSearchService(port.Port, service.Name) { + return true + } + } + + return false +} + +func isElasticSearchService(portNumber int32, name string) bool { + return portNumber == ElasticSearchHttpPort && strings.Contains(name, string(common.ElasticsearchDestinationType)) +} + +func (j *ElasticSearchDestinationFinder) fetchDestinationDetails(service k8s.Service) DestinationDetails { + urlString := fmt.Sprintf(ElasticSearchHttpUrlFormat, service.Name, service.Namespace, ElasticSearchHttpPort) + elasticServiceURL := j.getServiceURL() + fields := make(map[string]string) + fields[elasticServiceURL] = urlString + + return DestinationDetails{ + Type: common.ElasticsearchDestinationType, + Fields: fields, + } +} + +func (j *ElasticSearchDestinationFinder) getServiceURL() string { + return config.ElasticsearchUrlKey +} diff --git a/frontend/services/destination_recognition/jaeger.go b/frontend/services/destination_recognition/jaeger.go new file mode 100644 index 000000000..f66fd9bdd --- /dev/null +++ b/frontend/services/destination_recognition/jaeger.go @@ -0,0 +1,46 @@ +package destination_recognition + +import ( + "fmt" + "strings" + + "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/config" + k8s "k8s.io/api/core/v1" +) + +type JaegerDestinationFinder struct{} + +const JaegerGrpcOtlpPort int32 = 4317 +const JaegerGrpcUrlFormat = "%s.%s:%d" + +func (j *JaegerDestinationFinder) isPotentialService(service k8s.Service) bool { + for _, port := range service.Spec.Ports { + if isJaegerService(port.Port, service.Name) { + return true + } + } + + return false +} + +func isJaegerService(portNumber int32, name string) bool { + return portNumber == JaegerGrpcOtlpPort && strings.Contains(name, string(common.JaegerDestinationType)) +} + +func (j *JaegerDestinationFinder) fetchDestinationDetails(service k8s.Service) DestinationDetails { + urlString := fmt.Sprintf(JaegerGrpcUrlFormat, service.Name, service.Namespace, JaegerGrpcOtlpPort) + + jaegerServiceURL := j.getServiceURL() + fields := make(map[string]string) + fields[jaegerServiceURL] = urlString + + return DestinationDetails{ + Type: common.JaegerDestinationType, + Fields: fields, + } +} + +func (j *JaegerDestinationFinder) getServiceURL() string { + return config.JaegerUrlKey +} diff --git a/frontend/services/destinations.go b/frontend/services/destinations.go index 30c1bc77a..8a76180d4 100644 --- a/frontend/services/destinations.go +++ b/frontend/services/destinations.go @@ -7,9 +7,12 @@ import ( "github.com/odigos-io/odigos/api/odigos/v1alpha1" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/consts" "github.com/odigos-io/odigos/destinations" "github.com/odigos-io/odigos/frontend/graph/model" "github.com/odigos-io/odigos/frontend/kube" + "github.com/odigos-io/odigos/frontend/services/destination_recognition" + "github.com/odigos-io/odigos/k8sutils/pkg/env" k8s "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -267,3 +270,24 @@ func AddDestinationOwnerReferenceToSecret(ctx context.Context, odigosns string, } return nil } + +func PotentialDestinations(ctx context.Context) []destination_recognition.DestinationDetails { + odigosns := consts.DefaultOdigosNamespace + relevantNamespaces, err := getRelevantNameSpaces(ctx, env.GetCurrentNamespace()) + if err != nil { + return nil + } + + // Existing Destinations + existingDestination, err := kube.DefaultClient.OdigosClient.Destinations(odigosns).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil + } + + destinationDetails, err := destination_recognition.GetAllPotentialDestinationDetails(ctx, relevantNamespaces, existingDestination) + if err != nil { + return nil + } + + return destinationDetails +} diff --git a/frontend/services/utils.go b/frontend/services/utils.go index 4464bb016..a85f6b8f3 100644 --- a/frontend/services/utils.go +++ b/frontend/services/utils.go @@ -3,7 +3,9 @@ package services import ( "context" "errors" + "fmt" "path" + "strings" "github.com/odigos-io/odigos/frontend/kube" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,3 +35,16 @@ func setWorkloadInstrumentationLabel(ctx context.Context, nsName string, workloa return errors.New("unsupported workload kind " + string(workloadKind)) } } + +func ConvertFieldsToString(fields map[string]string) string { + if len(fields) == 0 { + return "" + } + + var parts []string + for key, value := range fields { + parts = append(parts, fmt.Sprintf("%s: %s", key, value)) + } + + return strings.Join(parts, ", ") +} From 40ca78cd51315bc68abaf9b35db4f231c0df35b5 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 25 Aug 2024 14:53:32 +0300 Subject: [PATCH 2/8] chore: wip --- .vscode/launch.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1932951d2..a3ba143a6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -28,6 +28,14 @@ "cwd": "${workspaceFolder}/frontend", "args": ["--port", "8085", "--address", "0.0.0.0"] }, + { + "name": "gql-playground", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}/frontend/main.go", + "cwd": "${workspaceFolder}/frontend" + }, { "name": "autoscaler local", "type": "go", From 917c399e30d0f7da9ddfd5804ebcf14a0bc2c0f3 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 25 Aug 2024 16:43:33 +0300 Subject: [PATCH 3/8] chore: skeleton component --- frontend/graph/schema.resolvers.go | 7 +- .../add-destination-modal/index.tsx | 1 - .../choose-destination-modal-body/index.tsx | 7 +- .../destinations-list/index.tsx | 1 - .../potential-destinations-list/index.tsx | 159 ++++++++++++++++++ .../webapp/graphql/queries/destination.ts | 9 + frontend/webapp/hooks/destinations/index.ts | 1 + .../destinations/usePotentialDestinations.ts | 36 ++++ frontend/webapp/public/brand/odigos-icon.svg | 11 +- frontend/webapp/reuseable-components/index.ts | 1 + .../section-title/index.tsx | 34 +++- .../skeleton-loader/index.tsx | 66 ++++++++ 12 files changed, 313 insertions(+), 20 deletions(-) create mode 100644 frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx create mode 100644 frontend/webapp/hooks/destinations/usePotentialDestinations.ts create mode 100644 frontend/webapp/reuseable-components/skeleton-loader/index.tsx diff --git a/frontend/graph/schema.resolvers.go b/frontend/graph/schema.resolvers.go index 372ae91fe..b141a6dbf 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -314,11 +314,14 @@ func (r *queryResolver) PotentialDestinations(ctx context.Context) ([]*model.Des var result []*model.DestinationDetails for _, dest := range potentialDestinations { - fieldsString := services.ConvertFieldsToString(dest.Fields) + fieldsString, err := json.Marshal(dest.Fields) + if err != nil { + return nil, fmt.Errorf("error marshalling fields: %v", err) + } result = append(result, &model.DestinationDetails{ Type: string(dest.Type), - Fields: fieldsString, + Fields: string(fieldsString), }) } diff --git a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx index 29b8b3b51..3ed14e370 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx @@ -65,7 +65,6 @@ export function AddDestinationModal({ }, [data]); function buildDestinationTypeList() { - console.log({ data }); const destinationTypes = data?.destinationTypes?.categories || []; const destinationTypeList: DestinationTypeItem[] = destinationTypes.reduce( (acc: DestinationTypeItem[], category: DestinationCategory) => { diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx index 5f4fdd619..ffd30ba15 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx @@ -6,6 +6,7 @@ import { Body, Container, SideMenuWrapper } from '../styled'; import { Divider, SectionTitle } from '@/reuseable-components'; import { DestinationFilterComponent } from '../choose-destination-menu'; import { DestinationTypeItem, DropdownOption, StepProps } from '@/types'; +import { PotentialDestinationsList } from '../potential-destinations-list'; interface ChooseDestinationModalBodyProps { data: DestinationTypeItem[]; @@ -96,7 +97,11 @@ export function ChooseDestinationModalBody({ onMonitorSelect={onMonitorSelect} /> - + + {/* */} ); diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx index 05968c978..fd2f64388 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx @@ -89,7 +89,6 @@ const DestinationsList: React.FC = ({ items, setSelectedItems, }) => { - console.log({ items }); function renderSupportedSignals(item: DestinationTypeItem) { const supportedSignals = item.supportedSignals; const signals = Object.keys(supportedSignals); diff --git a/frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx new file mode 100644 index 000000000..4d044d2b3 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx @@ -0,0 +1,159 @@ +import React, { useEffect } from 'react'; +import Image from 'next/image'; +import styled from 'styled-components'; +import { DestinationTypeItem } from '@/types'; +import { + NoDataFound, + SectionTitle, + SkeletonLoader, + Text, +} from '@/reuseable-components'; +import { usePotentialDestinations } from '@/hooks'; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + align-self: stretch; + max-height: calc(100vh - 424px); + overflow-y: auto; + + @media (height < 800px) { + max-height: calc(100vh - 400px); + } +`; + +const ListItem = styled.div<{}>` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 16px 0px; + transition: background 0.3s; + border-radius: 16px; + + cursor: pointer; + background: rgba(249, 249, 249, 0.04); + + &:hover { + background: rgba(249, 249, 249, 0.08); + } + &:last-child { + margin-bottom: 32px; + } +`; + +const ListItemContent = styled.div` + margin-left: 16px; + display: flex; + gap: 12px; +`; + +const DestinationIconWrapper = styled.div` + display: flex; + width: 36px; + height: 36px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 8px; + background: linear-gradient( + 180deg, + rgba(249, 249, 249, 0.06) 0%, + rgba(249, 249, 249, 0.02) 100% + ); +`; + +const SignalsWrapper = styled.div` + display: flex; + gap: 4px; +`; + +const SignalText = styled(Text)` + color: rgba(249, 249, 249, 0.8); + font-size: 10px; + text-transform: capitalize; +`; + +const TextWrapper = styled.div` + display: flex; + flex-direction: column; + height: 36px; + justify-content: space-between; +`; +const NoDataFoundWrapper = styled(Container)` + margin-top: 80px; +`; + +interface PotentialDestinationsListProps { + items: DestinationTypeItem[]; + setSelectedItems: (item: DestinationTypeItem) => void; +} + +const PotentialDestinationsList: React.FC = ({ + items, + setSelectedItems, +}) => { + const { loading, error, data: pd } = usePotentialDestinations(); + + useEffect(() => { + if (pd) { + console.log({ pd }); + } + }, [pd]); + + function renderSupportedSignals(item: DestinationTypeItem) { + const supportedSignals = item.supportedSignals; + const signals = Object.keys(supportedSignals); + const supportedSignalsList = signals.filter( + (signal) => supportedSignals[signal].supported + ); + + return supportedSignalsList.map((signal, index) => ( + + {signal} + {index < supportedSignalsList.length - 1 && ·} + + )); + } + + if (!items.length) { + return ( + + + + ); + } + + return ( + + + + {items.map((item) => ( + setSelectedItems(item)}> + + + destination + + + {item.displayName} + {renderSupportedSignals(item)} + + + + ))} + + ); +}; + +export { PotentialDestinationsList }; diff --git a/frontend/webapp/graphql/queries/destination.ts b/frontend/webapp/graphql/queries/destination.ts index 993f302c4..1bc8a05d9 100644 --- a/frontend/webapp/graphql/queries/destination.ts +++ b/frontend/webapp/graphql/queries/destination.ts @@ -40,3 +40,12 @@ export const GET_DESTINATION_TYPE_DETAILS = gql` } } `; + +export const GET_POTENTIAL_DESTINATIONS = gql` + query GetPotentialDestinations { + potentialDestinations { + type + fields + } + } +`; diff --git a/frontend/webapp/hooks/destinations/index.ts b/frontend/webapp/hooks/destinations/index.ts index ccb9e9a9d..ee99a9db8 100644 --- a/frontend/webapp/hooks/destinations/index.ts +++ b/frontend/webapp/hooks/destinations/index.ts @@ -2,3 +2,4 @@ export * from './useDestinations'; export * from './useTestConnection'; export * from './useConnectDestinationForm'; export * from './useCreateDestination'; +export * from './usePotentialDestinations'; diff --git a/frontend/webapp/hooks/destinations/usePotentialDestinations.ts b/frontend/webapp/hooks/destinations/usePotentialDestinations.ts new file mode 100644 index 000000000..951285954 --- /dev/null +++ b/frontend/webapp/hooks/destinations/usePotentialDestinations.ts @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; +import { useQuery } from '@apollo/client'; +import { GET_POTENTIAL_DESTINATIONS } from '@/graphql'; +import { safeJsonParse } from '@/utils'; + +interface DestinationDetails { + type: string; + fields: string; +} + +interface GetPotentialDestinationsData { + potentialDestinations: DestinationDetails[]; +} + +// Custom hook +export const usePotentialDestinations = () => { + const { loading, error, data } = useQuery( + GET_POTENTIAL_DESTINATIONS + ); + + // Memoize the transformed data to avoid unnecessary recalculations + const transformedData = useMemo(() => { + if (!data) return null; + + return data.potentialDestinations.map((destination) => ({ + ...destination, + fields: safeJsonParse>(destination.fields, {}), + })); + }, [data]); + + return { + loading, + error, + data: transformedData, + }; +}; diff --git a/frontend/webapp/public/brand/odigos-icon.svg b/frontend/webapp/public/brand/odigos-icon.svg index 9247584f3..7690eb2ee 100644 --- a/frontend/webapp/public/brand/odigos-icon.svg +++ b/frontend/webapp/public/brand/odigos-icon.svg @@ -1,6 +1,5 @@ - - - - - - + + + + + \ No newline at end of file diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index ada40c8e4..c772ffb8c 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -18,3 +18,4 @@ export * from './textarea'; export * from './input-list'; export * from './key-value-input-list'; export * from './no-data-found'; +export * from './skeleton-loader'; diff --git a/frontend/webapp/reuseable-components/section-title/index.tsx b/frontend/webapp/reuseable-components/section-title/index.tsx index 055a87448..7bc506680 100644 --- a/frontend/webapp/reuseable-components/section-title/index.tsx +++ b/frontend/webapp/reuseable-components/section-title/index.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { Text } from '../text'; import styled from 'styled-components'; +import Image from 'next/image'; interface SectionTitleProps { title: string; description: string; - actionButton?: React.ReactNode; // Accept a React node as the action button + actionButton?: React.ReactNode; + size?: 'small' | 'medium' | 'large'; + icon?: string; } const Container = styled.div` @@ -15,12 +18,18 @@ const Container = styled.div` gap: 16px; `; -const TitleContainer = styled.div` +const HeaderWrapper = styled.div` display: flex; flex-direction: column; gap: 4px; `; +const TitleContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + const Title = styled(Text)``; const Description = styled(Text)``; @@ -28,18 +37,25 @@ const Description = styled(Text)``; const SectionTitle: React.FC = ({ title, description, - actionButton, // Use the custom action button + actionButton, + size = 'medium', + icon, }) => { + const titleSize = size === 'small' ? 16 : size === 'medium' ? 20 : 24; + const descriptionSize = size === 'small' ? 12 : size === 'medium' ? 14 : 16; return ( - - - {title} - - + + + {icon && icon} + + {title} + + + {description} - + {actionButton &&
{actionButton}
}
); diff --git a/frontend/webapp/reuseable-components/skeleton-loader/index.tsx b/frontend/webapp/reuseable-components/skeleton-loader/index.tsx new file mode 100644 index 000000000..0b99a5cf7 --- /dev/null +++ b/frontend/webapp/reuseable-components/skeleton-loader/index.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import styled, { keyframes } from 'styled-components'; + +const shimmer = keyframes` + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } +`; + +const SkeletonLoaderWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +const SkeletonItem = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; + +const SkeletonThumbnail = styled.div` + width: 50px; + height: 50px; + border-radius: 8px; + background: ${({ theme }) => + `linear-gradient(90deg, ${theme.colors.primary} 25%, ${theme.colors.primary} 50%, ${theme.colors.dark_grey} 75%)`}; + background-size: 200% 100%; + animation: ${shimmer} 10s infinite linear; +`; + +const SkeletonText = styled.div` + flex: 1; +`; + +const SkeletonLine = styled.div<{ width: string }>` + height: 16px; + margin-bottom: 0.5rem; + background: ${({ theme }) => + `linear-gradient(90deg, ${theme.colors.primary} 25%, ${theme.colors.primary} 50%, ${theme.colors.dark_grey} 75%)`}; + background-size: 200% 100%; + animation: ${shimmer} 1.5s infinite linear; + width: ${(props) => props.width}; + border-radius: 4px; +`; + +const SkeletonLoader: React.FC<{ size: number }> = ({ size = 5 }) => { + return ( + + {[...Array(size)].map((_, index) => ( + + + + + + + + ))} + + ); +}; + +export { SkeletonLoader }; From d84f1441855aea6c967935af81406ca2aca46aed Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 26 Aug 2024 12:10:34 +0300 Subject: [PATCH 4/8] chore: wip --- .../add-destination-modal/index.tsx | 4 +- .../choose-destination-modal-body/index.tsx | 107 +++++++----- .../destination-list-item/index.tsx | 102 +++++++++++ .../destinations-list/index.tsx | 134 ++++----------- .../potential-destinations-list/index.tsx | 50 ++++++ .../potential-destinations-list/index.tsx | 159 ------------------ .../destinations/usePotentialDestinations.ts | 48 ++++-- frontend/webapp/types/destinations.ts | 55 ++++-- 8 files changed, 336 insertions(+), 323 deletions(-) create mode 100644 frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx create mode 100644 frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx diff --git a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx index 3ed14e370..220283340 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { useQuery } from '@apollo/client'; -import { DestinationTypeItem } from '@/types'; +import { DestinationTypeItem, GetDestinationTypesResponse } from '@/types'; import { GET_DESTINATION_TYPE } from '@/graphql'; import { Modal, NavigationButtons } from '@/reuseable-components'; import { ConnectDestinationModalBody } from '../connect-destination-modal-body'; @@ -52,7 +52,7 @@ export function AddDestinationModal({ isModalOpen, handleCloseModal, }: AddDestinationModalProps) { - const { data } = useQuery(GET_DESTINATION_TYPE); + const { data } = useQuery(GET_DESTINATION_TYPE); const [selectedItem, setSelectedItem] = useState(); const [destinationTypeList, setDestinationTypeList] = useState< DestinationTypeItem[] diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx index ffd30ba15..b0031e186 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx @@ -1,12 +1,20 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { SideMenu } from '@/components'; import { DestinationsList } from '../destinations-list'; import { Body, Container, SideMenuWrapper } from '../styled'; import { Divider, SectionTitle } from '@/reuseable-components'; import { DestinationFilterComponent } from '../choose-destination-menu'; -import { DestinationTypeItem, DropdownOption, StepProps } from '@/types'; -import { PotentialDestinationsList } from '../potential-destinations-list'; +import { + DestinationsCategory, + DestinationTypeItem, + DropdownOption, + GetDestinationTypesResponse, + StepProps, +} from '@/types'; +import { PotentialDestinationsList } from '../destinations-list/potential-destinations-list'; +import { useQuery } from '@apollo/client'; +import { GET_DESTINATION_TYPE } from '@/graphql'; interface ChooseDestinationModalBodyProps { data: DestinationTypeItem[]; @@ -26,50 +34,75 @@ const SIDE_MENU_DATA: StepProps[] = [ }, ]; +const DEFAULT_MONITORS = ['logs', 'metrics', 'traces']; +const DEFAULT_DROPDOWN_VALUE = { id: 'all', value: 'All types' }; + +const CATEGORIES_DESCRIPTION = { + managed: 'Effortless Monitoring with Scalable Performance Management', + 'self hosted': + 'Full Control and Customization for Advanced Application Monitoring', +}; + +export interface IDestinationListItem extends DestinationsCategory { + description: string; +} + export function ChooseDestinationModalBody({ - data, onSelect, }: ChooseDestinationModalBodyProps) { + const { data } = useQuery(GET_DESTINATION_TYPE); const [searchValue, setSearchValue] = useState(''); - const [selectedMonitors, setSelectedMonitors] = useState([ - 'logs', - 'metrics', - 'traces', - ]); - const [dropdownValue, setDropdownValue] = useState({ - id: 'all', - value: 'All types', - }); + const [destinations, setDestinations] = useState([]); + const [selectedMonitors, setSelectedMonitors] = + useState(DEFAULT_MONITORS); + const [dropdownValue, setDropdownValue] = useState( + DEFAULT_DROPDOWN_VALUE + ); + + useEffect(() => { + if (data) { + const destinationsCategories = data.destinationTypes.categories.map( + (category) => { + return { + name: category.name, + description: CATEGORIES_DESCRIPTION[category.name], + items: category.items, + }; + } + ); + setDestinations(destinationsCategories); + } + }, [data]); function handleTagSelect(option: DropdownOption) { setDropdownValue(option); } - function filterData() { - let filteredData = data; + // function filterData() { + // let filteredData = data; - if (searchValue) { - filteredData = filteredData.filter((item) => - item.displayName.toLowerCase().includes(searchValue.toLowerCase()) - ); - } + // if (searchValue) { + // filteredData = filteredData.filter((item) => + // item.displayName.toLowerCase().includes(searchValue.toLowerCase()) + // ); + // } - if (dropdownValue.id !== 'all') { - filteredData = filteredData.filter( - (item) => item.category === dropdownValue.id - ); - } + // if (dropdownValue.id !== 'all') { + // filteredData = filteredData.filter( + // (item) => item.category === dropdownValue.id + // ); + // } - if (selectedMonitors.length) { - filteredData = filteredData.filter((item) => - selectedMonitors.some( - (monitor) => item.supportedSignals[monitor].supported - ) - ); - } + // if (selectedMonitors.length) { + // filteredData = filteredData.filter((item) => + // selectedMonitors.some( + // (monitor) => item.supportedSignals[monitor].supported + // ) + // ); + // } - return filteredData; - } + // return filteredData; + // } function onMonitorSelect(monitor: string) { if (selectedMonitors.includes(monitor)) { @@ -97,11 +130,7 @@ export function ChooseDestinationModalBody({ onMonitorSelect={onMonitorSelect} /> - - {/* */} + ); diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx new file mode 100644 index 000000000..e6caabc08 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import Image from 'next/image'; +import styled from 'styled-components'; +import { DestinationTypeItem } from '@/types'; +import { Text } from '@/reuseable-components'; + +const ListItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 16px 0px; + transition: background 0.3s; + border-radius: 16px; + cursor: pointer; + background: rgba(249, 249, 249, 0.04); + + &:hover { + background: rgba(249, 249, 249, 0.08); + } + &:last-child { + margin-bottom: 24px; + } +`; + +const ListItemContent = styled.div` + margin-left: 16px; + display: flex; + gap: 12px; +`; + +const DestinationIconWrapper = styled.div` + display: flex; + width: 36px; + height: 36px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 8px; + background: linear-gradient( + 180deg, + rgba(249, 249, 249, 0.06) 0%, + rgba(249, 249, 249, 0.02) 100% + ); +`; + +const SignalsWrapper = styled.div` + display: flex; + gap: 4px; +`; + +const SignalText = styled(Text)` + color: rgba(249, 249, 249, 0.8); + font-size: 10px; + text-transform: capitalize; +`; + +const TextWrapper = styled.div` + display: flex; + flex-direction: column; + height: 36px; + justify-content: space-between; +`; + +interface DestinationListItemProps { + item: DestinationTypeItem; + onSelect: (item: DestinationTypeItem) => void; +} + +const DestinationListItem: React.FC = ({ + item, + onSelect, +}) => { + const renderSupportedSignals = () => { + const signals = Object.keys(item.supportedSignals).filter( + (signal) => item.supportedSignals[signal].supported + ); + + return signals.map((signal, index) => ( + + {signal} + {index < signals.length - 1 && ·} + + )); + }; + + return ( + onSelect(item)}> + + + destination + + + {item.displayName} + {renderSupportedSignals()} + + + + ); +}; + +export { DestinationListItem }; diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx index fd2f64388..701ccf317 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx @@ -1,14 +1,15 @@ import React from 'react'; -import Image from 'next/image'; import styled from 'styled-components'; import { DestinationTypeItem } from '@/types'; -import { NoDataFound, Text } from '@/reuseable-components'; +import { capitalizeFirstLetter } from '@/utils'; +import { DestinationListItem } from './destination-list-item'; +import { NoDataFound, SectionTitle } from '@/reuseable-components'; +import { IDestinationListItem } from '../choose-destination-modal-body'; +import { PotentialDestinationsList } from './potential-destinations-list'; const Container = styled.div` display: flex; flex-direction: column; - align-items: center; - gap: 12px; align-self: stretch; max-height: calc(100vh - 424px); overflow-y: auto; @@ -18,70 +19,18 @@ const Container = styled.div` } `; -const ListItem = styled.div<{}>` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 16px 0px; - transition: background 0.3s; - border-radius: 16px; - - cursor: pointer; - background: rgba(249, 249, 249, 0.04); - - &:hover { - background: rgba(249, 249, 249, 0.08); - } - &:last-child { - margin-bottom: 32px; - } -`; - -const ListItemContent = styled.div` - margin-left: 16px; +const ListsWrapper = styled.div` display: flex; + flex-direction: column; gap: 12px; `; -const DestinationIconWrapper = styled.div` - display: flex; - width: 36px; - height: 36px; - justify-content: center; - align-items: center; - gap: 8px; - border-radius: 8px; - background: linear-gradient( - 180deg, - rgba(249, 249, 249, 0.06) 0%, - rgba(249, 249, 249, 0.02) 100% - ); -`; - -const SignalsWrapper = styled.div` - display: flex; - gap: 4px; -`; - -const SignalText = styled(Text)` - color: rgba(249, 249, 249, 0.8); - font-size: 10px; - text-transform: capitalize; -`; - -const TextWrapper = styled.div` - display: flex; - flex-direction: column; - height: 36px; - justify-content: space-between; -`; const NoDataFoundWrapper = styled(Container)` margin-top: 80px; `; interface DestinationsListProps { - items: DestinationTypeItem[]; + items: IDestinationListItem[]; setSelectedItems: (item: DestinationTypeItem) => void; } @@ -89,49 +38,38 @@ const DestinationsList: React.FC = ({ items, setSelectedItems, }) => { - function renderSupportedSignals(item: DestinationTypeItem) { - const supportedSignals = item.supportedSignals; - const signals = Object.keys(supportedSignals); - const supportedSignalsList = signals.filter( - (signal) => supportedSignals[signal].supported - ); - - return supportedSignalsList.map((signal, index) => ( - - {signal} - {index < supportedSignalsList.length - 1 && ·} - - )); - } - - if (!items.length) { - return ( - - - - ); + function renderCategoriesList() { + if (!items.length) { + return ( + + + + ); + } + + return items.map((item) => { + return ( + + + {item.items.map((categoryItem) => ( + + ))} + + ); + }); } return ( - {items.map((item) => ( - setSelectedItems(item)}> - - - destination - - - {item.displayName} - {renderSupportedSignals(item)} - - - - ))} + + {renderCategoriesList()} ); }; diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx new file mode 100644 index 000000000..8470f2e81 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import styled from 'styled-components'; +import { DestinationTypeItem } from '@/types'; +import { usePotentialDestinations } from '@/hooks'; +import { DestinationListItem } from '../destination-list-item'; +import { SectionTitle, SkeletonLoader } from '@/reuseable-components'; + +const ListsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 12px; +`; + +interface PotentialDestinationsListProps { + setSelectedItems: (item: DestinationTypeItem) => void; +} + +const PotentialDestinationsList: React.FC = ({ + setSelectedItems, +}) => { + const { loading, data } = usePotentialDestinations(); + + if (!data.length) { + return null; + } + + return ( + + + {loading ? ( + + ) : ( + data.map((item) => ( + + )) + )} + + ); +}; + +export { PotentialDestinationsList }; diff --git a/frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx deleted file mode 100644 index 4d044d2b3..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/potential-destinations-list/index.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import React, { useEffect } from 'react'; -import Image from 'next/image'; -import styled from 'styled-components'; -import { DestinationTypeItem } from '@/types'; -import { - NoDataFound, - SectionTitle, - SkeletonLoader, - Text, -} from '@/reuseable-components'; -import { usePotentialDestinations } from '@/hooks'; - -const Container = styled.div` - display: flex; - flex-direction: column; - gap: 12px; - align-self: stretch; - max-height: calc(100vh - 424px); - overflow-y: auto; - - @media (height < 800px) { - max-height: calc(100vh - 400px); - } -`; - -const ListItem = styled.div<{}>` - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: 16px 0px; - transition: background 0.3s; - border-radius: 16px; - - cursor: pointer; - background: rgba(249, 249, 249, 0.04); - - &:hover { - background: rgba(249, 249, 249, 0.08); - } - &:last-child { - margin-bottom: 32px; - } -`; - -const ListItemContent = styled.div` - margin-left: 16px; - display: flex; - gap: 12px; -`; - -const DestinationIconWrapper = styled.div` - display: flex; - width: 36px; - height: 36px; - justify-content: center; - align-items: center; - gap: 8px; - border-radius: 8px; - background: linear-gradient( - 180deg, - rgba(249, 249, 249, 0.06) 0%, - rgba(249, 249, 249, 0.02) 100% - ); -`; - -const SignalsWrapper = styled.div` - display: flex; - gap: 4px; -`; - -const SignalText = styled(Text)` - color: rgba(249, 249, 249, 0.8); - font-size: 10px; - text-transform: capitalize; -`; - -const TextWrapper = styled.div` - display: flex; - flex-direction: column; - height: 36px; - justify-content: space-between; -`; -const NoDataFoundWrapper = styled(Container)` - margin-top: 80px; -`; - -interface PotentialDestinationsListProps { - items: DestinationTypeItem[]; - setSelectedItems: (item: DestinationTypeItem) => void; -} - -const PotentialDestinationsList: React.FC = ({ - items, - setSelectedItems, -}) => { - const { loading, error, data: pd } = usePotentialDestinations(); - - useEffect(() => { - if (pd) { - console.log({ pd }); - } - }, [pd]); - - function renderSupportedSignals(item: DestinationTypeItem) { - const supportedSignals = item.supportedSignals; - const signals = Object.keys(supportedSignals); - const supportedSignalsList = signals.filter( - (signal) => supportedSignals[signal].supported - ); - - return supportedSignalsList.map((signal, index) => ( - - {signal} - {index < supportedSignalsList.length - 1 && ·} - - )); - } - - if (!items.length) { - return ( - - - - ); - } - - return ( - - - - {items.map((item) => ( - setSelectedItems(item)}> - - - destination - - - {item.displayName} - {renderSupportedSignals(item)} - - - - ))} - - ); -}; - -export { PotentialDestinationsList }; diff --git a/frontend/webapp/hooks/destinations/usePotentialDestinations.ts b/frontend/webapp/hooks/destinations/usePotentialDestinations.ts index 951285954..d4ab2d837 100644 --- a/frontend/webapp/hooks/destinations/usePotentialDestinations.ts +++ b/frontend/webapp/hooks/destinations/usePotentialDestinations.ts @@ -1,7 +1,8 @@ import { useMemo } from 'react'; -import { useQuery } from '@apollo/client'; -import { GET_POTENTIAL_DESTINATIONS } from '@/graphql'; import { safeJsonParse } from '@/utils'; +import { useQuery } from '@apollo/client'; +import { GetDestinationTypesResponse } from '@/types'; +import { GET_DESTINATION_TYPE, GET_POTENTIAL_DESTINATIONS } from '@/graphql'; interface DestinationDetails { type: string; @@ -12,25 +13,48 @@ interface GetPotentialDestinationsData { potentialDestinations: DestinationDetails[]; } -// Custom hook export const usePotentialDestinations = () => { + const { data: destinationTypesData } = + useQuery(GET_DESTINATION_TYPE); const { loading, error, data } = useQuery( GET_POTENTIAL_DESTINATIONS ); - // Memoize the transformed data to avoid unnecessary recalculations - const transformedData = useMemo(() => { - if (!data) return null; + const mappedPotentialDestinations = useMemo(() => { + if (!destinationTypesData || !data) return []; + + // Create a deep copy of destination types to manipulate + const destinationTypesCopy = JSON.parse( + JSON.stringify(destinationTypesData.destinationTypes.categories) + ); - return data.potentialDestinations.map((destination) => ({ - ...destination, - fields: safeJsonParse>(destination.fields, {}), - })); - }, [data]); + // Map over the potential destinations + return data.potentialDestinations.map((destination) => { + for (const category of destinationTypesCopy) { + const index = category.items.findIndex( + (item) => item.type === destination.type + ); + if (index !== -1) { + // Spread the matched destination type data into the potential destination + const matchedType = category.items[index]; + category.items.splice(index, 1); // Remove the matched item from destination types + return { + ...destination, + ...matchedType, + fields: safeJsonParse<{ [key: string]: string }>( + destination.fields, + {} + ), + }; + } + } + return destination; + }); + }, [destinationTypesData, data]); return { loading, error, - data: transformedData, + data: mappedPotentialDestinations, }; }; diff --git a/frontend/webapp/types/destinations.ts b/frontend/webapp/types/destinations.ts index 47d3b088e..6d92b87cd 100644 --- a/frontend/webapp/types/destinations.ts +++ b/frontend/webapp/types/destinations.ts @@ -5,22 +5,51 @@ export enum DestinationsSortType { TYPE = 'type', } +// export interface DestinationTypeItem { +// displayName: string; +// imageUrl: string; +// category: 'managed' | 'self-hosted'; +// type: string; +// testConnectionSupported: boolean; +// supportedSignals: { +// logs: { +// supported: boolean; +// }; +// metrics: { +// supported: boolean; +// }; +// traces: { +// supported: boolean; +// }; +// }; +// } + +interface ObservabilitySignalSupport { + supported: boolean; +} + +interface SupportedSignals { + logs: ObservabilitySignalSupport; + metrics: ObservabilitySignalSupport; + traces: ObservabilitySignalSupport; +} + export interface DestinationTypeItem { - displayName: string; - imageUrl: string; - category: 'managed' | 'self-hosted'; type: string; testConnectionSupported: boolean; - supportedSignals: { - logs: { - supported: boolean; - }; - metrics: { - supported: boolean; - }; - traces: { - supported: boolean; - }; + displayName: string; + imageUrl: string; + supportedSignals: SupportedSignals; +} + +export interface DestinationsCategory { + name: string; + items: DestinationTypeItem[]; +} + +export interface GetDestinationTypesResponse { + destinationTypes: { + categories: DestinationsCategory[]; }; } From 508ebc2cb64bc4d1ae912efd9c0e7cfbcc2cba31 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 26 Aug 2024 12:26:40 +0300 Subject: [PATCH 5/8] chore: wip --- .../add-destination-modal/index.tsx | 50 +++---------------- .../choose-destination-modal-body/index.tsx | 3 +- 2 files changed, 7 insertions(+), 46 deletions(-) diff --git a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx index 220283340..0844174d0 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx @@ -1,21 +1,14 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { useQuery } from '@apollo/client'; -import { DestinationTypeItem, GetDestinationTypesResponse } from '@/types'; -import { GET_DESTINATION_TYPE } from '@/graphql'; +import React, { useState, useRef } from 'react'; +import { DestinationTypeItem } from '@/types'; import { Modal, NavigationButtons } from '@/reuseable-components'; -import { ConnectDestinationModalBody } from '../connect-destination-modal-body'; import { ChooseDestinationModalBody } from '../choose-destination-modal-body'; +import { ConnectDestinationModalBody } from '../connect-destination-modal-body'; interface AddDestinationModalProps { isModalOpen: boolean; handleCloseModal: () => void; } -interface DestinationCategory { - name: string; - items: DestinationTypeItem[]; -} - function ModalActionComponent({ onNext, onBack, @@ -38,7 +31,7 @@ function ModalActionComponent({ }, { label: 'DONE', - onClick: onNext, // This will trigger handleSubmit + onClick: onNext, variant: 'primary', }, ] @@ -52,36 +45,8 @@ export function AddDestinationModal({ isModalOpen, handleCloseModal, }: AddDestinationModalProps) { - const { data } = useQuery(GET_DESTINATION_TYPE); - const [selectedItem, setSelectedItem] = useState(); - const [destinationTypeList, setDestinationTypeList] = useState< - DestinationTypeItem[] - >([]); - const submitRef = useRef<() => void | null>(null); - - useEffect(() => { - data && buildDestinationTypeList(); - }, [data]); - - function buildDestinationTypeList() { - const destinationTypes = data?.destinationTypes?.categories || []; - const destinationTypeList: DestinationTypeItem[] = destinationTypes.reduce( - (acc: DestinationTypeItem[], category: DestinationCategory) => { - const items = category.items.map((item: DestinationTypeItem) => ({ - category: category.name, - displayName: item.displayName, - imageUrl: item.imageUrl, - supportedSignals: item.supportedSignals, - testConnectionSupported: item.testConnectionSupported, - type: item.type, - })); - return [...acc, ...items]; - }, - [] - ); - setDestinationTypeList(destinationTypeList); - } + const [selectedItem, setSelectedItem] = useState(); function handleNextStep(item: DestinationTypeItem) { setSelectedItem(item); @@ -94,10 +59,7 @@ export function AddDestinationModal({ onSubmitRef={submitRef} /> ) : ( - + ); } diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx index b0031e186..8b6ba5a9f 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx @@ -12,12 +12,11 @@ import { GetDestinationTypesResponse, StepProps, } from '@/types'; -import { PotentialDestinationsList } from '../destinations-list/potential-destinations-list'; + import { useQuery } from '@apollo/client'; import { GET_DESTINATION_TYPE } from '@/graphql'; interface ChooseDestinationModalBodyProps { - data: DestinationTypeItem[]; onSelect: (item: DestinationTypeItem) => void; } From 0e2906945b42dcf774e7b0a9df99c541cc3ede61 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 26 Aug 2024 12:45:51 +0300 Subject: [PATCH 6/8] chore: wip --- .../choose-destination-modal-body/index.tsx | 12 +++++------- .../add-destination/destinations-list/index.tsx | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx index 8b6ba5a9f..5b409aba5 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx @@ -1,21 +1,20 @@ import React, { useEffect, useState } from 'react'; import { SideMenu } from '@/components'; +import { useQuery } from '@apollo/client'; +import { GET_DESTINATION_TYPE } from '@/graphql'; import { DestinationsList } from '../destinations-list'; import { Body, Container, SideMenuWrapper } from '../styled'; import { Divider, SectionTitle } from '@/reuseable-components'; import { DestinationFilterComponent } from '../choose-destination-menu'; import { - DestinationsCategory, - DestinationTypeItem, + StepProps, DropdownOption, + DestinationTypeItem, + DestinationsCategory, GetDestinationTypesResponse, - StepProps, } from '@/types'; -import { useQuery } from '@apollo/client'; -import { GET_DESTINATION_TYPE } from '@/graphql'; - interface ChooseDestinationModalBodyProps { onSelect: (item: DestinationTypeItem) => void; } @@ -35,7 +34,6 @@ const SIDE_MENU_DATA: StepProps[] = [ const DEFAULT_MONITORS = ['logs', 'metrics', 'traces']; const DEFAULT_DROPDOWN_VALUE = { id: 'all', value: 'All types' }; - const CATEGORIES_DESCRIPTION = { managed: 'Effortless Monitoring with Scalable Performance Management', 'self hosted': diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx index 701ccf317..1e2c67109 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { DestinationTypeItem } from '@/types'; import { capitalizeFirstLetter } from '@/utils'; import { DestinationListItem } from './destination-list-item'; -import { NoDataFound, SectionTitle } from '@/reuseable-components'; +import { Counter, NoDataFound, SectionTitle } from '@/reuseable-components'; import { IDestinationListItem } from '../choose-destination-modal-body'; import { PotentialDestinationsList } from './potential-destinations-list'; From 5ce5d5b2eb9e45d5aa12e32f6a1a7f6baa094c73 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 26 Aug 2024 15:04:56 +0300 Subject: [PATCH 7/8] chore: wip --- .../choose-destination-modal-body/index.tsx | 52 ++++++++++--------- .../destination-list-item/index.tsx | 19 +++++++ 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx index 5b409aba5..160e9b77e 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { SideMenu } from '@/components'; import { useQuery } from '@apollo/client'; @@ -47,7 +47,6 @@ export interface IDestinationListItem extends DestinationsCategory { export function ChooseDestinationModalBody({ onSelect, }: ChooseDestinationModalBodyProps) { - const { data } = useQuery(GET_DESTINATION_TYPE); const [searchValue, setSearchValue] = useState(''); const [destinations, setDestinations] = useState([]); const [selectedMonitors, setSelectedMonitors] = @@ -56,6 +55,7 @@ export function ChooseDestinationModalBody({ DEFAULT_DROPDOWN_VALUE ); + const { data } = useQuery(GET_DESTINATION_TYPE); useEffect(() => { if (data) { const destinationsCategories = data.destinationTypes.categories.map( @@ -75,31 +75,32 @@ export function ChooseDestinationModalBody({ setDropdownValue(option); } - // function filterData() { - // let filteredData = data; + const filteredDestinations = useMemo(() => { + return destinations + .map((category) => { + const filteredItems = category.items.filter((item) => { + const matchesSearch = searchValue + ? item.displayName.toLowerCase().includes(searchValue.toLowerCase()) + : true; - // if (searchValue) { - // filteredData = filteredData.filter((item) => - // item.displayName.toLowerCase().includes(searchValue.toLowerCase()) - // ); - // } + const matchesDropdown = + dropdownValue.id !== 'all' + ? category.name === dropdownValue.id + : true; - // if (dropdownValue.id !== 'all') { - // filteredData = filteredData.filter( - // (item) => item.category === dropdownValue.id - // ); - // } + const matchesMonitor = selectedMonitors.length + ? selectedMonitors.some( + (monitor) => item.supportedSignals[monitor]?.supported + ) + : true; - // if (selectedMonitors.length) { - // filteredData = filteredData.filter((item) => - // selectedMonitors.some( - // (monitor) => item.supportedSignals[monitor].supported - // ) - // ); - // } + return matchesSearch && matchesDropdown && matchesMonitor; + }); - // return filteredData; - // } + return { ...category, items: filteredItems }; + }) + .filter((category) => category.items.length > 0); // Filter out empty categories + }, [destinations, searchValue, dropdownValue, selectedMonitors]); function onMonitorSelect(monitor: string) { if (selectedMonitors.includes(monitor)) { @@ -127,7 +128,10 @@ export function ChooseDestinationModalBody({ onMonitorSelect={onMonitorSelect} /> - + ); diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx index e6caabc08..aa16aea36 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx @@ -4,6 +4,10 @@ import styled from 'styled-components'; import { DestinationTypeItem } from '@/types'; import { Text } from '@/reuseable-components'; +const HoverTextWrapper = styled.div` + visibility: hidden; +`; + const ListItem = styled.div` display: flex; align-items: center; @@ -21,6 +25,12 @@ const ListItem = styled.div` &:last-child { margin-bottom: 24px; } + + &:hover { + ${HoverTextWrapper} { + visibility: visible; + } + } `; const ListItemContent = styled.div` @@ -62,6 +72,12 @@ const TextWrapper = styled.div` justify-content: space-between; `; +const HoverText = styled(Text)` + font-family: ${({ theme }) => theme.font_family.secondary}; + text-transform: uppercase; + margin-right: 16px; +`; + interface DestinationListItemProps { item: DestinationTypeItem; onSelect: (item: DestinationTypeItem) => void; @@ -95,6 +111,9 @@ const DestinationListItem: React.FC = ({ {renderSupportedSignals()} + + {'Select'} + ); }; From 6d25e8d354d0c34cbc232d33395648269a3fa304 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 26 Aug 2024 15:48:55 +0300 Subject: [PATCH 8/8] chore: wip --- .../add-destination-modal/index.tsx | 6 ++++- .../connect-destination-modal-body/index.tsx | 26 ++++++++++++++++--- .../reuseable-components/input/index.tsx | 21 +++++++++++++-- .../notification-note/index.tsx | 6 ++--- frontend/webapp/types/destinations.ts | 22 +++------------- 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx index 0844174d0..afadab7f3 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx @@ -66,6 +66,7 @@ export function AddDestinationModal({ function handleNext() { if (submitRef.current) { submitRef.current(); + setSelectedItem(undefined); handleCloseModal(); } } @@ -81,7 +82,10 @@ export function AddDestinationModal({ /> } header={{ title: 'Add destination' }} - onClose={handleCloseModal} + onClose={() => { + setSelectedItem(undefined); + handleCloseModal(); + }} > {renderModalBody()} diff --git a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx index b9a325a09..3f59b33a1 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx @@ -111,11 +111,21 @@ export function ConnectDestinationModalBody({ }, [destination]); useEffect(() => { - if (data) { + if (data && destination) { const df = buildFormDynamicFields(data.destinationTypeDetails.fields); - setDynamicFields(df); + + const newDynamicFields = df.map((field) => { + if (destination.fields && field?.name in destination.fields) { + return { + ...field, + initialValue: destination.fields[field.name], + }; + } + return field; + }); + setDynamicFields(newDynamicFields); } - }, [data]); + }, [data, destination]); useEffect(() => { // Assign handleSubmit to the onSubmitRef so it can be triggered externally @@ -152,7 +162,7 @@ export function ConnectDestinationModalBody({ destinationTypeDetails, type: destination?.type || '', imageUrl: destination?.imageUrl || '', - category: destination?.category || '', + category: '', displayName: destination?.displayName || '', }; @@ -210,6 +220,14 @@ export function ConnectDestinationModalBody({ /> )} + {destination.fields && !showConnectionError && ( + + + + )} { title?: string; tooltip?: string; required?: boolean; + initialValue?: string; } const Container = styled.div` @@ -148,8 +149,19 @@ const Input: React.FC = ({ title, tooltip, required, + initialValue, + onChange, ...props }) => { + const [value, setValue] = useState(initialValue || ''); + + const handleInputChange = (e: React.ChangeEvent) => { + setValue(e.target.value); + if (onChange) { + onChange(e); + } + }; + return ( {title && ( @@ -184,7 +196,12 @@ const Input: React.FC = ({ )} - + {buttonLabel && onButtonClick && (