diff --git a/frontend/services/destination_recognition/destination_finder.go b/frontend/services/destination_recognition/destination_finder.go index 20258eff75..6810272660 100644 --- a/frontend/services/destination_recognition/destination_finder.go +++ b/frontend/services/destination_recognition/destination_finder.go @@ -2,6 +2,7 @@ package destination_recognition import ( "context" + "strings" odigosv1 "github.com/odigos-io/odigos/api/odigos/v1alpha1" "github.com/odigos-io/odigos/common" @@ -11,8 +12,6 @@ import ( 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"` @@ -25,43 +24,42 @@ type IDestinationFinder interface { } 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 + err := client.ListWithPages(client.DefaultPageSize, kube.DefaultClient.CoreV1().Services(ns.Name).List, ctx, metav1.ListOptions{}, + func(svc *k8s.ServiceList) error { + for _, service := range svc.Items { + df := getDestinationFinder(service.Name) + + if df != nil && df.isPotentialService(service) { + pd := df.fetchDestinationDetails(service) + + if !destinationExist(dests, pd, df) { + destinationDetails = append(destinationDetails, pd) } + break } } + return nil - }) - } + }, + ) - if err != nil { - return nil, err + if err != nil { + return nil, err + } } return destinationDetails, nil } -func getDestinationFinder(destinationType common.DestinationType) IDestinationFinder { - switch destinationType { - case common.JaegerDestinationType: +func getDestinationFinder(serviceName string) IDestinationFinder { + if strings.Contains(serviceName, string(common.JaegerDestinationType)) { return &JaegerDestinationFinder{} - case common.ElasticsearchDestinationType: + } + + if strings.Contains(serviceName, string(common.ElasticsearchDestinationType)) { return &ElasticSearchDestinationFinder{} } diff --git a/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx index 12724ab7bc..68750cb58e 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx @@ -19,7 +19,7 @@ const Container = styled.div` overflow-y: scroll; `; -const ListItem: React.FC<{ item: ConfiguredDestination; isLastItem: boolean }> = ({ item, isLastItem }) => { +const ListItem: React.FC<{ item: ConfiguredDestination; isLastItem: boolean }> = ({ item, isLastItem, ...props }) => { const { removeConfiguredDestination } = useAppStore((state) => state); const [deleteWarning, setDeleteWarning] = useState(false); @@ -37,6 +37,7 @@ const ListItem: React.FC<{ item: ConfiguredDestination; isLastItem: boolean }> = )} + {...props} /> = export const ConfiguredDestinationsList: React.FC<{ data: IAppState['configuredDestinations'] }> = ({ data }) => { return ( - {data.map(({ stored }) => ( - + {data.map(({ stored }, idx) => ( + ))} ); diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx index ae906d20da..a9bab6cd78 100644 --- a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx @@ -48,16 +48,16 @@ export const DestinationsList: React.FC = ({ items, setSe return ( - {categoryItem.items.map((destinationItem) => ( + {categoryItem.items.map((item, idx) => ( destinationItem.supportedSignals[signal].supported)} + monitors={Object.keys(item.supportedSignals).filter((signal) => item.supportedSignals[signal].supported)} monitorsWithLabels - onClick={() => setSelectedItems(destinationItem)} + onClick={() => setSelectedItems(item)} /> ))} diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx index 1712500748..98730dd928 100644 --- a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { OdigosLogo } from '@/assets'; import styled from 'styled-components'; -import { DestinationTypeItem } from '@/types'; +import type { DestinationTypeItem } from '@/types'; import { usePotentialDestinations } from '@/hooks'; import { DataTab, SectionTitle, SkeletonLoader } from '@/reuseable-components'; @@ -31,10 +31,10 @@ export const PotentialDestinationsList: React.FC = ({ setSelectedItems }) {loading ? ( ) : ( - data.map((item) => ( + data.map((item, idx) => ( { cy.visit(ROUTES.CHOOSE_DESTINATION); cy.contains('button', BUTTONS.ADD_DESTINATION).click(); cy.wait('@gql').then(() => { - cy.get(DATA_IDS.SELECT_DESTINATION).contains(SELECTED_ENTITIES.DESTINATION).should('exist').click(); - expect(DATA_IDS.SELECT_DESTINATION_AUTOFILL_FIELD).to.not.be.empty; + cy.get(DATA_IDS.SELECT_DESTINATION).contains(SELECTED_ENTITIES.DESTINATION_DISPLAY_NAME).should('exist').click(); + cy.get(DATA_IDS.SELECT_DESTINATION_AUTOFILL_FIELD).should('have.value', SELECTED_ENTITIES.DESTINATION_AUTOFILL_VALUE); }); }); diff --git a/frontend/webapp/cypress/e2e/04-destinations.cy.ts b/frontend/webapp/cypress/e2e/04-destinations.cy.ts index a510d03c74..02a20544e1 100644 --- a/frontend/webapp/cypress/e2e/04-destinations.cy.ts +++ b/frontend/webapp/cypress/e2e/04-destinations.cy.ts @@ -18,8 +18,8 @@ describe('Destinations CRUD', () => { cy.get(DATA_IDS.ADD_ENTITY).click(); cy.get(DATA_IDS.ADD_DESTINATION).click(); cy.get(DATA_IDS.MODAL_ADD_DESTINATION).should('exist'); - cy.get(DATA_IDS.SELECT_DESTINATION).contains(SELECTED_ENTITIES.DESTINATION).click(); - expect(DATA_IDS.SELECT_DESTINATION_AUTOFILL_FIELD).to.not.be.empty; + cy.get(DATA_IDS.SELECT_DESTINATION).contains(SELECTED_ENTITIES.DESTINATION_DISPLAY_NAME).should('exist').click(); + cy.get(DATA_IDS.SELECT_DESTINATION_AUTOFILL_FIELD).should('have.value', SELECTED_ENTITIES.DESTINATION_AUTOFILL_VALUE); cy.get('button').contains(BUTTONS.DONE).click(); cy.wait('@gql').then(() => { @@ -35,7 +35,7 @@ describe('Destinations CRUD', () => { updateEntity( { nodeId: DATA_IDS.DESTINATION_NODE, - nodeContains: SELECTED_ENTITIES.DESTINATION, + nodeContains: SELECTED_ENTITIES.DESTINATION_DISPLAY_NAME, fieldKey: DATA_IDS.TITLE, fieldValue: TEXTS.UPDATED_NAME, }, @@ -58,7 +58,7 @@ describe('Destinations CRUD', () => { deleteEntity( { nodeId: DATA_IDS.DESTINATION_NODE, - nodeContains: SELECTED_ENTITIES.DESTINATION, + nodeContains: SELECTED_ENTITIES.DESTINATION_DISPLAY_NAME, warnModalTitle: TEXTS.DESTINATION_WARN_MODAL_TITLE, warnModalNote: TEXTS.DESTINATION_WARN_MODAL_NOTE, }, diff --git a/frontend/webapp/hooks/destinations/usePotentialDestinations.ts b/frontend/webapp/hooks/destinations/usePotentialDestinations.ts index d4ab2d837a..fe2efa35c8 100644 --- a/frontend/webapp/hooks/destinations/usePotentialDestinations.ts +++ b/frontend/webapp/hooks/destinations/usePotentialDestinations.ts @@ -1,56 +1,74 @@ import { useMemo } from 'react'; import { safeJsonParse } from '@/utils'; import { useQuery } from '@apollo/client'; +import { IAppState, useAppStore } from '@/store'; import { GetDestinationTypesResponse } from '@/types'; import { GET_DESTINATION_TYPE, GET_POTENTIAL_DESTINATIONS } from '@/graphql'; -interface DestinationDetails { +interface PotentialDestination { type: string; fields: string; } interface GetPotentialDestinationsData { - potentialDestinations: DestinationDetails[]; + potentialDestinations: PotentialDestination[]; } +const checkIfConfigured = (configuredDest: IAppState['configuredDestinations'][0], potentialDest: PotentialDestination, autoFilledFields: Record) => { + const typesMatch = configuredDest.stored.type === potentialDest.type; + if (!typesMatch) return false; + + let fieldsMatch = false; + + for (const { key, value } of configuredDest.form.fields) { + if (Object.hasOwn(autoFilledFields, key)) { + if (autoFilledFields[key] === value) { + fieldsMatch = true; + } else { + fieldsMatch = false; + break; + } + } + } + + return fieldsMatch; +}; + export const usePotentialDestinations = () => { - const { data: destinationTypesData } = - useQuery(GET_DESTINATION_TYPE); - const { loading, error, data } = useQuery( - GET_POTENTIAL_DESTINATIONS - ); + const { configuredDestinations } = useAppStore(); + const { data: { destinationTypes } = {} } = useQuery(GET_DESTINATION_TYPE); + const { loading, error, data: { potentialDestinations } = {} } = useQuery(GET_POTENTIAL_DESTINATIONS); const mappedPotentialDestinations = useMemo(() => { - if (!destinationTypesData || !data) return []; + if (!destinationTypes || !potentialDestinations) return []; // Create a deep copy of destination types to manipulate - const destinationTypesCopy = JSON.parse( - JSON.stringify(destinationTypesData.destinationTypes.categories) - ); + const categories: GetDestinationTypesResponse['destinationTypes']['categories'] = JSON.parse(JSON.stringify(destinationTypes.categories)); // 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 potentialDestinations + .map((pd) => { + for (const category of categories) { + const autoFilledFields = safeJsonParse<{ [key: string]: string }>(pd.fields, {}); + const alreadyConfigured = !!configuredDestinations.find((cd) => checkIfConfigured(cd, pd, autoFilledFields)); + + if (!alreadyConfigured) { + const idx = category.items.findIndex((item) => item.type === pd.type); + + if (idx !== -1) { + return { + // Spread the matched destination type data into the potential destination + ...category.items[idx], + fields: autoFilledFields, + }; + } + } } - } - return destination; - }); - }, [destinationTypesData, data]); + + return null; + }) + .filter((pd) => !!pd); + }, [configuredDestinations, destinationTypes, potentialDestinations]); return { loading,