From c332365adfcac33d8f3c72099f61443e82cfbfa1 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 13 Oct 2024 11:10:25 +0300 Subject: [PATCH 01/20] feat: init edit destination --- .../overview/overview-data-flow/index.tsx | 15 ++++++++++++ .../main/overview/overview-drawer/index.tsx | 23 ++++++++++++++----- frontend/webapp/package.json | 2 +- .../nodes-data-flow/builder.ts | 18 +++++++++++---- frontend/webapp/store/useDrawerStore.tsx | 4 ++-- frontend/webapp/yarn.lock | 18 +++++++-------- 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx index 492ecb9383..a49c823c0a 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx @@ -18,6 +18,7 @@ export const OverviewDataFlowWrapper = styled.div` `; const TYPE_SOURCE = 'source'; +const TYPE_DESTINATION = 'destination'; export function OverviewDataFlowContainer() { const containerRef = useRef(null); @@ -79,6 +80,20 @@ export function OverviewDataFlowContainer() { type: TYPE_SOURCE, }); } + + if (object.data.type === TYPE_DESTINATION) { + const { id } = object.data; + + const selectedDrawerItem = destinations.find( + (destination) => destination.id === id + ); + + setSelectedItem({ + id, + item: selectedDrawerItem, + type: TYPE_DESTINATION, + }); + } } return ( diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 360787c22e..b33a30cdf0 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -8,7 +8,12 @@ import { SourceDrawer } from '../../sources'; import { Drawer } from '@/reuseable-components'; import { DeleteEntityModal } from '@/components'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; -import { K8sActualSource, PatchSourceRequestInput, WorkloadId } from '@/types'; +import { + WorkloadId, + K8sActualSource, + ActualDestination, + PatchSourceRequestInput, +} from '@/types'; const componentMap = { source: SourceDrawer, @@ -122,11 +127,7 @@ const OverviewDrawer = () => { title={title} onClose={isEditing ? handleCancel : handleClose} imageUri={ - selectedItem?.item - ? getMainContainerLanguageLogo( - selectedItem.item as K8sActualSource - ) - : '' + selectedItem?.item ? getItemImageByType(selectedItem.item) : '' } {...{ isEditing, setIsEditing }} /> @@ -155,6 +156,16 @@ const OverviewDrawer = () => { ) : null; }; +function getItemImageByType(item: K8sActualSource | ActualDestination): string { + if ('destinationType' in item) { + // item is of type ActualDestination + return item.destinationType.imageUrl; + } else { + // item is of type K8sActualSource + return getMainContainerLanguageLogo(item as K8sActualSource); + } +} + export default OverviewDrawer; const DrawerContent = styled.div` diff --git a/frontend/webapp/package.json b/frontend/webapp/package.json index c63d15009a..d797d3439c 100644 --- a/frontend/webapp/package.json +++ b/frontend/webapp/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@apollo/client": "^3.11.0-rc.2", - "@apollo/experimental-nextjs-app-support": "^0.11.2", + "@apollo/experimental-nextjs-app-support": "^0.11.3", "@focus-reactive/react-yaml": "^1.1.2", "@keyval-dev/design-system": "^2.3.1", "@next/font": "^13.4.7", diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts index d8282e968c..23d570ec8b 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts +++ b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts @@ -1,8 +1,12 @@ +import theme from '@/styles/theme'; import { Node, Edge } from 'react-flow-renderer'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; -import { ActionItem } from '@/types'; -import theme from '@/styles/theme'; -import { useDrawerStore } from '@/store'; +import { + ActionData, + ActionItem, + ActualDestination, + K8sActualSource, +} from '@/types'; // Constants const NODE_HEIGHT = 80; @@ -51,14 +55,18 @@ export const buildNodesAndEdges = ({ destinations, columnWidth, containerWidth, +}: { + sources: K8sActualSource[]; + actions: ActionData[]; + destinations: ActualDestination[]; + columnWidth: number; + containerWidth: number; }) => { // Calculate x positions for each column const leftColumnX = 0; const rightColumnX = containerWidth - columnWidth; const centerColumnX = (containerWidth - columnWidth) / 2; - const setSelectedItem = useDrawerStore((state) => state.setSelectedItem); - // Build Source Nodes const sourcesNode: Node[] = [ createNode('header-source', 'header', leftColumnX, 0, { diff --git a/frontend/webapp/store/useDrawerStore.tsx b/frontend/webapp/store/useDrawerStore.tsx index b69349fe21..863dda558c 100644 --- a/frontend/webapp/store/useDrawerStore.tsx +++ b/frontend/webapp/store/useDrawerStore.tsx @@ -1,12 +1,12 @@ // drawerStore.ts import { create } from 'zustand'; -import { Destination, K8sActualSource, WorkloadId } from '@/types'; +import { ActualDestination, K8sActualSource, WorkloadId } from '@/types'; type ItemType = 'source' | 'action' | 'destination'; interface BaseItem { id: string | WorkloadId; - item?: K8sActualSource | Destination; + item?: K8sActualSource | ActualDestination; type: ItemType; // Add common properties here } diff --git a/frontend/webapp/yarn.lock b/frontend/webapp/yarn.lock index 166a813708..fdc6854b61 100644 --- a/frontend/webapp/yarn.lock +++ b/frontend/webapp/yarn.lock @@ -10,10 +10,10 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@apollo/client-react-streaming@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@apollo/client-react-streaming/-/client-react-streaming-0.11.2.tgz#788a5b0254469b679f8abf5391e40c420fe61965" - integrity sha512-rRA/dIA09/Y6+jtGGBnXHQfPOv6BYYVZwQP8OzQtWrWbSgDEI6uAhqULssU5f0ZhQJVzKDuslqGE9QAX0gdfRQ== +"@apollo/client-react-streaming@0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@apollo/client-react-streaming/-/client-react-streaming-0.11.3.tgz#ca14d15241b60e9e5e86f075d14e7cf72e1485ba" + integrity sha512-bAyyD7iZQ8UIvYZv2ZY3i5FTNdCgM0kfWW/0St3sqJLAs4Ji6QB9uzGUTc5434vQo6Ddb17N+Q+Ikr7fj2yTxw== dependencies: ts-invariant "^0.10.3" @@ -37,12 +37,12 @@ tslib "^2.3.0" zen-observable-ts "^1.2.5" -"@apollo/experimental-nextjs-app-support@^0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@apollo/experimental-nextjs-app-support/-/experimental-nextjs-app-support-0.11.2.tgz#3df9253229afd6ec94bc5873f649f23c487c9dfb" - integrity sha512-HRQ8/Ux/tM2pezrhZeoHsJs55+nJvJZRV1B21QwEVtWhslQXjT5gqs5nKw86KURF0xR7gX18Nyy659NzJ09Pmw== +"@apollo/experimental-nextjs-app-support@^0.11.3": + version "0.11.3" + resolved "https://registry.yarnpkg.com/@apollo/experimental-nextjs-app-support/-/experimental-nextjs-app-support-0.11.3.tgz#a0910e4d6376d6ac8293e4718e17d8df96b69de8" + integrity sha512-eMfbEtHyQE9EceBn0sTBWcHVvjhd+dkMO5dBhoEglEm0ga2n87KKiTeaNNb/XZnvOX81/6y0iyc0U7cgITpvKw== dependencies: - "@apollo/client-react-streaming" "0.11.2" + "@apollo/client-react-streaming" "0.11.3" "@babel/cli@^7.21.0": version "7.24.8" From a2f82096edff05c361dbb62b873844a8b5619c4a Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 13 Oct 2024 11:43:23 +0300 Subject: [PATCH 02/20] wip: destination drawer --- .../destination-drawer-container/index.tsx | 29 +++++++++++++++++++ .../containers/main/destinations/index.tsx | 1 + .../main/overview/overview-drawer/index.tsx | 6 +++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx new file mode 100644 index 0000000000..c9f63809c6 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -0,0 +1,29 @@ +import React, { useMemo } from 'react'; +import { useDrawerStore } from '@/store'; +import { CardDetails } from '@/components'; +import { ActualDestination } from '@/types'; + +const DestinationDrawer: React.FC = () => { + const destination = useDrawerStore(({ selectedItem }) => selectedItem); + const cardData = useMemo(() => { + const { exportedSignals, destinationType } = + destination?.item as ActualDestination; + + const monitors = Object.keys(exportedSignals) + .map((key) => (exportedSignals[key] === true ? key : null)) + .filter(Boolean) + .join(', '); + + return [ + { title: 'Destination', value: destinationType.displayName || 'N/A' }, + { + title: 'Monitors', + value: monitors, + }, + ]; + }, [destination]); + + return ; +}; + +export { DestinationDrawer }; diff --git a/frontend/webapp/containers/main/destinations/index.tsx b/frontend/webapp/containers/main/destinations/index.tsx index a50795fd7f..4decd49fcb 100644 --- a/frontend/webapp/containers/main/destinations/index.tsx +++ b/frontend/webapp/containers/main/destinations/index.tsx @@ -1,2 +1,3 @@ export * from './managed'; export * from './add-destination'; +export * from './destination-drawer-container'; diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index b33a30cdf0..fa7d382ac6 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -7,6 +7,7 @@ import DrawerFooter from './drawer-footer'; import { SourceDrawer } from '../../sources'; import { Drawer } from '@/reuseable-components'; import { DeleteEntityModal } from '@/components'; +import { DestinationDrawer } from '../../destinations'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; import { WorkloadId, @@ -18,7 +19,7 @@ import { const componentMap = { source: SourceDrawer, action: () =>
Action
, - destination: () =>
Destination
, + destination: DestinationDrawer, }; const DRAWER_WIDTH = '560px'; @@ -42,6 +43,9 @@ const OverviewDrawer = () => { if (selectedItem?.type === 'source' && selectedItem.item) { const title = (selectedItem.item as K8sActualSource).reportedName; setTitle(title || ''); + } else if (selectedItem?.type === 'destination' && selectedItem.item) { + const title = (selectedItem.item as ActualDestination).name; + setTitle(title || ''); } else { setTitle(''); } From 1d4e5ffd1aa0afe7f03cbd26a54bf4a23cbf388f Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 13 Oct 2024 15:16:23 +0300 Subject: [PATCH 03/20] wip: destintion card details --- frontend/graph/generated.go | 81 ++----------------- frontend/graph/model/destination.go | 2 +- frontend/graph/schema.graphqls | 2 +- frontend/graph/schema.resolvers.go | 15 ++-- frontend/services/destinations.go | 9 ++- .../destination-drawer-container/index.tsx | 79 +++++++++++++++--- .../graphql/queries/compute-platform.ts | 2 + .../destinations/useActualDestinations.ts | 24 +++++- frontend/webapp/types/destinations.ts | 5 +- 9 files changed, 124 insertions(+), 95 deletions(-) diff --git a/frontend/graph/generated.go b/frontend/graph/generated.go index 9e2dc0ffca..2f64038af6 100644 --- a/frontend/graph/generated.go +++ b/frontend/graph/generated.go @@ -216,8 +216,6 @@ type ComputePlatformResolver interface { type DestinationResolver interface { Type(ctx context.Context, obj *model.Destination) (string, error) - Fields(ctx context.Context, obj *model.Destination) ([]string, error) - Conditions(ctx context.Context, obj *model.Destination) ([]*model.Condition, error) } type K8sActualNamespaceResolver interface { @@ -2068,7 +2066,7 @@ func (ec *executionContext) _Destination_fields(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Destination().Fields(rctx, obj) + return obj.Fields, nil }) if err != nil { ec.Error(ctx, err) @@ -2080,17 +2078,17 @@ func (ec *executionContext) _Destination_fields(ctx context.Context, field graph } return graphql.Null } - res := resTmp.([]string) + res := resTmp.(string) fc.Result = res - return ec.marshalNString2ᚕstringᚄ(ctx, field.Selections, res) + return ec.marshalNString2string(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Destination_fields(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Destination", Field: field, - IsMethod: true, - IsResolver: true, + 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") }, @@ -7939,41 +7937,10 @@ func (ec *executionContext) _Destination(ctx context.Context, sel ast.SelectionS atomic.AddUint32(&out.Invalids, 1) } case "fields": - 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._Destination_fields(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - if field.Deferrable != nil { - dfs, ok := deferred[field.Deferrable.Label] - di := 0 - if ok { - dfs.AddField(field) - di = len(dfs.Values) - 1 - } else { - dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) - deferred[field.Deferrable.Label] = dfs - } - dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { - return innerFunc(ctx, dfs) - }) - - // don't run the out.Concurrently() call below - out.Values[i] = graphql.Null - continue + out.Values[i] = ec._Destination_fields(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) case "destinationType": out.Values[i] = ec._Destination_destinationType(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -10177,38 +10144,6 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } -func (ec *executionContext) unmarshalNString2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) { - var vSlice []interface{} - if v != nil { - vSlice = graphql.CoerceList(v) - } - var err error - res := make([]string, len(vSlice)) - for i := range vSlice { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) - res[i], err = ec.unmarshalNString2string(ctx, vSlice[i]) - if err != nil { - return nil, err - } - } - return res, nil -} - -func (ec *executionContext) marshalNString2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - for i := range v { - ret[i] = ec.marshalNString2string(ctx, sel, v[i]) - } - - for _, e := range ret { - if e == graphql.Null { - return graphql.Null - } - } - - return ret -} - func (ec *executionContext) marshalNSupportedSignals2githubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐSupportedSignals(ctx context.Context, sel ast.SelectionSet, v model.SupportedSignals) graphql.Marshaler { return ec._SupportedSignals(ctx, sel, &v) } diff --git a/frontend/graph/model/destination.go b/frontend/graph/model/destination.go index 58a8bea118..5f2890a08e 100644 --- a/frontend/graph/model/destination.go +++ b/frontend/graph/model/destination.go @@ -42,7 +42,7 @@ type Destination struct { Name string `json:"name"` Type common.DestinationType `json:"type"` ExportedSignals ExportedSignals `json:"signals"` - Fields map[string]string `json:"fields"` + Fields string `json:"fields"` DestinationType DestinationTypesCategoryItem `json:"destination_type"` Conditions []metav1.Condition `json:"conditions,omitempty"` } diff --git a/frontend/graph/schema.graphqls b/frontend/graph/schema.graphqls index a153372cea..959457c4b5 100644 --- a/frontend/graph/schema.graphqls +++ b/frontend/graph/schema.graphqls @@ -162,7 +162,7 @@ type Destination { name: String! type: String! exportedSignals: ExportedSignals! - fields: [String!]! + fields: String! destinationType: DestinationTypesCategoryItem! conditions: [Condition!] } diff --git a/frontend/graph/schema.resolvers.go b/frontend/graph/schema.resolvers.go index 3a8208482e..12083d0260 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -272,11 +272,6 @@ func (r *destinationResolver) Type(ctx context.Context, obj *model.Destination) panic(fmt.Errorf("not implemented: Type - type")) } -// Fields is the resolver for the fields field. -func (r *destinationResolver) Fields(ctx context.Context, obj *model.Destination) ([]string, error) { - panic(fmt.Errorf("not implemented: Fields - fields")) -} - // Conditions is the resolver for the conditions field. func (r *destinationResolver) Conditions(ctx context.Context, obj *model.Destination) ([]*model.Condition, error) { panic(fmt.Errorf("not implemented: Conditions - conditions")) @@ -553,3 +548,13 @@ type destinationResolver struct{ *Resolver } type k8sActualNamespaceResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } + +// !!! WARNING !!! +// The code below was going to be deleted when updating resolvers. It has been copied here so you have +// one last chance to move it out of harms way if you want. There are two reasons this happens: +// - When renaming or deleting a resolver the old code will be put in here. You can safely delete +// it when you're done. +// - You have helper methods in this file. Move them out to keep these resolver files clean. +func (r *destinationResolver) Fields(ctx context.Context, obj *model.Destination) (string, error) { + panic(fmt.Errorf("not implemented: Fields - fields")) +} diff --git a/frontend/services/destinations.go b/frontend/services/destinations.go index 8a76180d4c..79147d89b3 100644 --- a/frontend/services/destinations.go +++ b/frontend/services/destinations.go @@ -160,6 +160,13 @@ func K8sDestinationToEndpointFormat(k8sDest v1alpha1.Destination, secretFields m mergedFields := mergeDataAndSecrets(k8sDest.Spec.Data, secretFields) destTypeConfig := DestinationTypeConfigToCategoryItem(destinations.GetDestinationByType(string(destType))) + fieldsJSON, err := json.Marshal(mergedFields) + if err != nil { + // Handle JSON encoding error + fmt.Printf("Error marshaling fields to JSON: %v\n", err) + fieldsJSON = []byte("{}") // Set to an empty JSON object in case of error + } + var conditions []metav1.Condition for _, condition := range k8sDest.Status.Conditions { conditions = append(conditions, metav1.Condition{ @@ -179,7 +186,7 @@ func K8sDestinationToEndpointFormat(k8sDest v1alpha1.Destination, secretFields m Metrics: isSignalExported(k8sDest, common.MetricsObservabilitySignal), Logs: isSignalExported(k8sDest, common.LogsObservabilitySignal), }, - Fields: mergedFields, + Fields: string(fieldsJSON), DestinationType: destTypeConfig, Conditions: conditions, } diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx index c9f63809c6..34b3a2a026 100644 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -1,29 +1,84 @@ import React, { useMemo } from 'react'; +import { safeJsonParse } from '@/utils'; import { useDrawerStore } from '@/store'; +import { useQuery } from '@apollo/client'; import { CardDetails } from '@/components'; -import { ActualDestination } from '@/types'; +import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; +import { + ActualDestination, + isActualDestination, + DestinationDetailsResponse, +} from '@/types'; const DestinationDrawer: React.FC = () => { const destination = useDrawerStore(({ selectedItem }) => selectedItem); + + const shouldSkip = !isActualDestination(destination?.item); + const destinationType = isActualDestination(destination?.item) + ? destination.item.destinationType.type + : null; + + const { data: destinationFields, error } = + useQuery(GET_DESTINATION_TYPE_DETAILS, { + variables: { type: destinationType }, + skip: shouldSkip, + }); + const cardData = useMemo(() => { - const { exportedSignals, destinationType } = - destination?.item as ActualDestination; + if (shouldSkip || !destination?.item || !destinationFields) { + return [ + { title: 'Error', value: 'No destination selected or data missing' }, + ]; + } + + const { exportedSignals, destinationType, fields } = + destination.item as ActualDestination; + const { destinationTypeDetails } = destinationFields; - const monitors = Object.keys(exportedSignals) - .map((key) => (exportedSignals[key] === true ? key : null)) - .filter(Boolean) - .join(', '); + const parsedFields = safeJsonParse>(fields, {}); + const destinationFieldData = buildDestinationFieldData( + parsedFields, + destinationTypeDetails?.fields + ); + + const monitors = buildMonitorsList(exportedSignals); return [ { title: 'Destination', value: destinationType.displayName || 'N/A' }, - { - title: 'Monitors', - value: monitors, - }, + { title: 'Monitors', value: monitors || 'None' }, + ...destinationFieldData, ]; - }, [destination]); + }, [shouldSkip, destination, destinationFields]); + + if (error) { + console.error('Error fetching destination details:', error); + return

Error loading destination details

; + } return ; }; export { DestinationDrawer }; + +// Helper function to build the destination field data array +function buildDestinationFieldData( + parsedFields: Record, + fieldDetails?: { name: string; displayName: string }[] +) { + return Object.entries(parsedFields).map(([key, value]) => { + const displayName = + fieldDetails?.find((field) => field.name === key)?.displayName || key; + return { title: displayName, value: value || 'N/A' }; + }); +} + +function buildMonitorsList( + exportedSignals: ActualDestination['exportedSignals'] +): string { + return ( + Object.entries(exportedSignals) + .filter(([key, isEnabled]) => isEnabled && key !== '__typename') + .map(([key]) => key) + .join(', ') || 'None' + ); +} diff --git a/frontend/webapp/graphql/queries/compute-platform.ts b/frontend/webapp/graphql/queries/compute-platform.ts index 6c666b2e4f..56284daf47 100644 --- a/frontend/webapp/graphql/queries/compute-platform.ts +++ b/frontend/webapp/graphql/queries/compute-platform.ts @@ -24,12 +24,14 @@ export const GET_COMPUTE_PLATFORM = gql` destinations { id name + fields exportedSignals { logs metrics traces } destinationType { + type imageUrl displayName } diff --git a/frontend/webapp/hooks/destinations/useActualDestinations.ts b/frontend/webapp/hooks/destinations/useActualDestinations.ts index 5a01d89b10..ba6ce6f442 100644 --- a/frontend/webapp/hooks/destinations/useActualDestinations.ts +++ b/frontend/webapp/hooks/destinations/useActualDestinations.ts @@ -1,9 +1,31 @@ import { useComputePlatform } from '../compute-platform'; +import { ActualDestination } from '@/types'; + +// Function to map raw data to the ActualDestination interface +const mapToActualDestination = (data: any): ActualDestination => ({ + id: data.id, + name: data.name, + type: data.type, + exportedSignals: data.exportedSignals, + fields: data.fields, + conditions: data.conditions, + destinationType: { + type: data.destinationType.type, + displayName: data.destinationType.displayName, + imageUrl: data.destinationType.imageUrl, + }, +}); export const useActualDestination = () => { const { data } = useComputePlatform(); + // Use the mapToActualDestination function to transform raw data + const destinations = + data?.computePlatform.destinations.map((destination: any) => + mapToActualDestination(destination) + ) || []; + return { - destinations: data?.computePlatform.destinations || [], + destinations, }; }; diff --git a/frontend/webapp/types/destinations.ts b/frontend/webapp/types/destinations.ts index 540995e713..7fffa97613 100644 --- a/frontend/webapp/types/destinations.ts +++ b/frontend/webapp/types/destinations.ts @@ -174,7 +174,7 @@ export interface ActualDestination { metrics: boolean; logs: boolean; }; - fields: Record; + fields: string; conditions: Condition[]; destinationType: { type: string; @@ -182,3 +182,6 @@ export interface ActualDestination { imageUrl: string; }; } + +export const isActualDestination = (item: any): item is ActualDestination => + item && 'destinationType' in item; From 9e5d25ee5e67423e2bad70eee18e1073fd4d8913 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 14 Oct 2024 11:52:00 +0300 Subject: [PATCH 04/20] chore: get updated destinatoin --- .../destinations/destination-form/index.tsx | 55 ++++++ .../webapp/components/destinations/index.ts | 1 + .../destination-drawer-container/index.tsx | 162 +++++++++++++++++- .../main/overview/overview-drawer/index.tsx | 76 +++++--- .../graphql/queries/compute-platform.ts | 11 ++ .../destinations/useActualDestinations.ts | 1 + frontend/webapp/types/destinations.ts | 19 +- 7 files changed, 279 insertions(+), 46 deletions(-) create mode 100644 frontend/webapp/components/destinations/destination-form/index.tsx diff --git a/frontend/webapp/components/destinations/destination-form/index.tsx b/frontend/webapp/components/destinations/destination-form/index.tsx new file mode 100644 index 0000000000..909592e25b --- /dev/null +++ b/frontend/webapp/components/destinations/destination-form/index.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { CheckboxList, Input } from '@/reuseable-components'; +import { + DynamicField, + ExportedSignals, + SupportedDestinationSignals, +} from '@/types'; +import { DynamicConnectDestinationFormFields } from '@/containers/main/destinations/add-destination/dynamic-form-fields'; + +interface DestinationFormProps { + destinationName: string; + dynamicFields: DynamicField[]; + exportedSignals: ExportedSignals; + supportedSignals: SupportedDestinationSignals; + setDestinationName: (name: string) => void; + handleDynamicFieldChange: (name: string, value: any) => void; + handleSignalChange: (signal: keyof ExportedSignals, value: boolean) => void; +} + +export const DestinationForm: React.FC = ({ + dynamicFields, + destinationName, + exportedSignals, + supportedSignals, + setDestinationName, + handleSignalChange, + handleDynamicFieldChange, +}) => { + const monitors = [ + supportedSignals.logs.supported && { id: 'logs', title: 'Logs' }, + supportedSignals.metrics.supported && { id: 'metrics', title: 'Metrics' }, + supportedSignals.traces.supported && { id: 'traces', title: 'Traces' }, + ].filter(Boolean); + + return ( + <> + setDestinationName(e.target.value)} + /> + + + + ); +}; diff --git a/frontend/webapp/components/destinations/index.ts b/frontend/webapp/components/destinations/index.ts index e7e4b7330a..4ed4159b2d 100644 --- a/frontend/webapp/components/destinations/index.ts +++ b/frontend/webapp/components/destinations/index.ts @@ -1,2 +1,3 @@ export * from './add-destination-button'; export * from './monitors-tap-list'; +export * from './destination-form'; diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx index 34b3a2a026..f9b45a95a1 100644 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -1,29 +1,93 @@ -import React, { useMemo } from 'react'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react'; import { safeJsonParse } from '@/utils'; import { useDrawerStore } from '@/store'; import { useQuery } from '@apollo/client'; -import { CardDetails } from '@/components'; +import { CardDetails, DestinationForm } from '@/components'; import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; import { ActualDestination, isActualDestination, DestinationDetailsResponse, + ExportedSignals, + DynamicField, + SupportedDestinationSignals, } from '@/types'; +import styled from 'styled-components'; +import { useConnectDestinationForm } from '@/hooks'; -const DestinationDrawer: React.FC = () => { - const destination = useDrawerStore(({ selectedItem }) => selectedItem); +export type DestinationDrawerHandle = { + getCurrentData: () => { + name: string; + type: string; + exportedSignals: ExportedSignals; + fields: { key: string; value: any }[]; + }; +}; + +interface DestinationDrawerProps { + isEditing: boolean; +} +const DEFAULT_SUPPORTED_SIGNALS = { + logs: { + supported: false, + }, + metrics: { + supported: false, + }, + traces: { + supported: false, + }, +}; + +const DestinationDrawer = forwardRef< + DestinationDrawerHandle, + DestinationDrawerProps +>(({ isEditing }, ref) => { + const [dynamicFields, setDynamicFields] = useState([]); + const destination = useDrawerStore(({ selectedItem }) => selectedItem); + const [destinationName, setDestinationName] = useState(''); + const [supportedSignals, setSupportedSignals] = + useState(DEFAULT_SUPPORTED_SIGNALS); + const [exportedSignals, setExportedSignals] = useState({ + logs: false, + metrics: false, + traces: false, + }); const shouldSkip = !isActualDestination(destination?.item); const destinationType = isActualDestination(destination?.item) ? destination.item.destinationType.type : null; + const { buildFormDynamicFields } = useConnectDestinationForm(); + const { data: destinationFields, error } = useQuery(GET_DESTINATION_TYPE_DETAILS, { variables: { type: destinationType }, skip: shouldSkip, }); + useImperativeHandle(ref, () => ({ + getCurrentData: () => { + const fields = processFormFields(dynamicFields); + const newDestination = { + name: destinationName, + type: destination?.type || '', + exportedSignals, + fields, + }; + return newDestination; + }, + })); + + useEffect(initDynamicFields, [destinationFields, destination]); + const cardData = useMemo(() => { if (shouldSkip || !destination?.item || !destinationFields) { return [ @@ -50,13 +114,68 @@ const DestinationDrawer: React.FC = () => { ]; }, [shouldSkip, destination, destinationFields]); - if (error) { - console.error('Error fetching destination details:', error); - return

Error loading destination details

; + function initDynamicFields() { + if (destinationFields && destination) { + const df = buildFormDynamicFields( + destinationFields.destinationTypeDetails.fields + ); + + const { fields, exportedSignals, name, destinationType } = + destination.item as ActualDestination; + const parsedFields = safeJsonParse>(fields, {}); + const newDynamicFields = df.map((field) => { + if (field?.name in parsedFields) { + return { + ...field, + value: + field.componentType === 'dropdown' + ? { + id: parsedFields[field.name], + value: parsedFields[field.name], + } + : parsedFields[field.name], + }; + } + return field; + }); + setDestinationName(name); + setExportedSignals(exportedSignals); + setDynamicFields(newDynamicFields); + setSupportedSignals(destinationType.supportedSignals); + } } - return ; -}; + function handleSignalChange(signal: string, value: boolean) { + setExportedSignals((prev) => ({ ...prev, [signal]: value })); + } + + function handleDynamicFieldChange(name: string, value: any) { + setDynamicFields((prev) => { + return prev.map((field) => { + if (field.name === name) { + return { ...field, value }; + } + return field; + }); + }); + } + + return isEditing ? ( + + + + ) : ( + + ); +}); export { DestinationDrawer }; @@ -82,3 +201,28 @@ function buildMonitorsList( .join(', ') || 'None' ); } + +function processFormFields(dynamicFields) { + function processFieldValue(field) { + return field.componentType === 'dropdown' ? field.value.value : field.value; + } + + // Prepare fields for the request body + return dynamicFields.map((field) => ({ + key: field.name, + value: processFieldValue(field), + })); +} + +const FormContainer = styled.div` + display: flex; + width: 100%; + flex-direction: column; + gap: 24px; + height: 100%; + overflow-y: auto; + padding-right: 16px; + box-sizing: border-box; + overflow: overlay; + max-height: calc(100vh - 220px); +`; diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index fa7d382ac6..1262b776cf 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -7,7 +7,7 @@ import DrawerFooter from './drawer-footer'; import { SourceDrawer } from '../../sources'; import { Drawer } from '@/reuseable-components'; import { DeleteEntityModal } from '@/components'; -import { DestinationDrawer } from '../../destinations'; +import { DestinationDrawer, DestinationDrawerHandle } from '../../destinations'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; import { WorkloadId, @@ -19,7 +19,9 @@ import { const componentMap = { source: SourceDrawer, action: () =>
Action
, - destination: DestinationDrawer, + destination: (props: { isEditing: boolean }) => ( + + ), }; const DRAWER_WIDTH = '560px'; @@ -36,7 +38,7 @@ const OverviewDrawer = () => { const { updateActualSource, deleteSourcesForNamespace } = useActualSources(); const titleRef = useRef(null); - + const destinationDrawerRef = useRef(null); useEffect(initialTitle, [selectedItem]); function initialTitle() { @@ -52,31 +54,52 @@ const OverviewDrawer = () => { } const handleSave = async () => { - if (titleRef.current) { - const newTitle = titleRef.current.value; - setTitle(newTitle); - if (selectedItem?.type === 'source' && selectedItem.item) { - const sourceItem = selectedItem.item as K8sActualSource; - - const sourceId: WorkloadId = { - namespace: sourceItem.namespace, - kind: sourceItem.kind, - name: sourceItem.name, - }; - - const patchRequest: PatchSourceRequestInput = { - reportedName: newTitle, + if (selectedItem?.type === 'destination') { + if (destinationDrawerRef.current && titleRef.current) { + const name = titleRef.current.value; + const destinationData = { + ...destinationDrawerRef.current.getCurrentData(), + name, }; + console.log({ id: selectedItem.id, destinationData }); try { - await updateActualSource(sourceId, patchRequest); + // Replace this with your actual save logic + // await updateDestination(destinationData); } catch (error) { - console.error('Error updating source:', error); + console.error('Error updating destination:', error); // Optionally show error message to user } } } - setIsEditing(false); + + if (selectedItem?.type === 'source') { + if (titleRef.current) { + const newTitle = titleRef.current.value; + setTitle(newTitle); + if (selectedItem?.type === 'source' && selectedItem.item) { + const sourceItem = selectedItem.item as K8sActualSource; + + const sourceId: WorkloadId = { + namespace: sourceItem.namespace, + kind: sourceItem.kind, + name: sourceItem.name, + }; + + const patchRequest: PatchSourceRequestInput = { + reportedName: newTitle, + }; + + try { + await updateActualSource(sourceId, patchRequest); + } catch (error) { + console.error('Error updating source:', error); + // Optionally show error message to user + } + } + } + setIsEditing(false); + } }; const handleCancel = () => { @@ -129,14 +152,21 @@ const OverviewDrawer = () => { - + {selectedItem.type === 'destination' ? ( + + ) : ( + + )} {isEditing && ( <> diff --git a/frontend/webapp/graphql/queries/compute-platform.ts b/frontend/webapp/graphql/queries/compute-platform.ts index 56284daf47..b833920f43 100644 --- a/frontend/webapp/graphql/queries/compute-platform.ts +++ b/frontend/webapp/graphql/queries/compute-platform.ts @@ -34,6 +34,17 @@ export const GET_COMPUTE_PLATFORM = gql` type imageUrl displayName + supportedSignals { + logs { + supported + } + metrics { + supported + } + traces { + supported + } + } } } actions { diff --git a/frontend/webapp/hooks/destinations/useActualDestinations.ts b/frontend/webapp/hooks/destinations/useActualDestinations.ts index ba6ce6f442..0cdf41d05e 100644 --- a/frontend/webapp/hooks/destinations/useActualDestinations.ts +++ b/frontend/webapp/hooks/destinations/useActualDestinations.ts @@ -13,6 +13,7 @@ const mapToActualDestination = (data: any): ActualDestination => ({ type: data.destinationType.type, displayName: data.destinationType.displayName, imageUrl: data.destinationType.imageUrl, + supportedSignals: data.destinationType.supportedSignals, }, }); diff --git a/frontend/webapp/types/destinations.ts b/frontend/webapp/types/destinations.ts index 7fffa97613..0d1f3dd97f 100644 --- a/frontend/webapp/types/destinations.ts +++ b/frontend/webapp/types/destinations.ts @@ -104,7 +104,7 @@ interface SupportedSignal { supported: boolean; } -interface SupportedSignals { +export interface SupportedDestinationSignals { traces: SupportedSignal; metrics: SupportedSignal; logs: SupportedSignal; @@ -114,7 +114,7 @@ export interface SelectedDestination { type: string; display_name: string; image_url: string; - supported_signals: SupportedSignals; + supported_signals: SupportedDestinationSignals; test_connection_supported: boolean; } @@ -133,17 +133,7 @@ export interface Destination { type: string; display_name: string; image_url: string; - supported_signals: { - traces: { - supported: boolean; - }; - metrics: { - supported: boolean; - }; - logs: { - supported: boolean; - }; - }; + supported_signals: SupportedDestinationSignals; }; } @@ -159,7 +149,7 @@ export interface Field { export interface DestinationConfig { type: string; name: string; - signals: SupportedSignals; + signals: SupportedDestinationSignals; fields: { [key: string]: string; }; @@ -180,6 +170,7 @@ export interface ActualDestination { type: string; displayName: string; imageUrl: string; + supportedSignals: SupportedDestinationSignals; }; } From 18be9971a61fa03da38b646626a7d29d80185c54 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 14 Oct 2024 13:41:31 +0300 Subject: [PATCH 05/20] wip : updated destinatoin --- frontend/endpoints/destinations.go | 3 +- frontend/graph/generated.go | 116 ++++++++++++++++++ frontend/graph/schema.graphqls | 1 + frontend/graph/schema.resolvers.go | 115 +++++++++++++++-- .../destination-drawer-container/index.tsx | 21 ++-- .../main/overview/overview-drawer/index.tsx | 15 ++- .../webapp/graphql/mutations/destination.ts | 31 +++++ frontend/webapp/hooks/destinations/index.ts | 1 + .../destinations/useUpdateDestination.ts | 26 ++++ 9 files changed, 306 insertions(+), 23 deletions(-) create mode 100644 frontend/webapp/hooks/destinations/useUpdateDestination.ts diff --git a/frontend/endpoints/destinations.go b/frontend/endpoints/destinations.go index 6eb900f2b4..0be797fb8f 100644 --- a/frontend/endpoints/destinations.go +++ b/frontend/endpoints/destinations.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" + "net/http" + "github.com/odigos-io/odigos/frontend/endpoints/destination_recognition" "github.com/odigos-io/odigos/k8sutils/pkg/env" - "net/http" "github.com/gin-gonic/gin" "github.com/odigos-io/odigos/api/odigos/v1alpha1" diff --git a/frontend/graph/generated.go b/frontend/graph/generated.go index 2f64038af6..024d989189 100644 --- a/frontend/graph/generated.go +++ b/frontend/graph/generated.go @@ -170,6 +170,7 @@ type ComplexityRoot struct { PersistK8sNamespace func(childComplexity int, namespace model.PersistNamespaceItemInput) int PersistK8sSources func(childComplexity int, namespace string, sources []*model.PersistNamespaceSourceInput) int TestConnectionForDestination func(childComplexity int, destination model.DestinationInput) int + UpdateDestination func(childComplexity int, id string, destination model.DestinationInput) int UpdateK8sActualSource func(childComplexity int, sourceID model.K8sSourceID, patchSourceRequest model.PatchSourceRequestInput) int } @@ -227,6 +228,7 @@ type MutationResolver interface { PersistK8sSources(ctx context.Context, namespace string, sources []*model.PersistNamespaceSourceInput) (bool, error) TestConnectionForDestination(ctx context.Context, destination model.DestinationInput) (*model.TestConnectionResponse, error) UpdateK8sActualSource(ctx context.Context, sourceID model.K8sSourceID, patchSourceRequest model.PatchSourceRequestInput) (bool, error) + UpdateDestination(ctx context.Context, id string, destination model.DestinationInput) (*model.Destination, error) } type QueryResolver interface { ComputePlatform(ctx context.Context) (*model.ComputePlatform, error) @@ -766,6 +768,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.TestConnectionForDestination(childComplexity, args["destination"].(model.DestinationInput)), true + case "Mutation.updateDestination": + if e.complexity.Mutation.UpdateDestination == nil { + break + } + + args, err := ec.field_Mutation_updateDestination_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateDestination(childComplexity, args["id"].(string), args["destination"].(model.DestinationInput)), true + case "Mutation.updateK8sActualSource": if e.complexity.Mutation.UpdateK8sActualSource == nil { break @@ -1161,6 +1175,30 @@ func (ec *executionContext) field_Mutation_testConnectionForDestination_args(ctx return args, nil } +func (ec *executionContext) field_Mutation_updateDestination_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + var arg1 model.DestinationInput + if tmp, ok := rawArgs["destination"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("destination")) + arg1, err = ec.unmarshalNDestinationInput2githubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestinationInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["destination"] = arg1 + return args, nil +} + func (ec *executionContext) field_Mutation_updateK8sActualSource_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -4569,6 +4607,77 @@ func (ec *executionContext) fieldContext_Mutation_updateK8sActualSource(ctx cont return fc, nil } +func (ec *executionContext) _Mutation_updateDestination(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_updateDestination(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.Mutation().UpdateDestination(rctx, fc.Args["id"].(string), fc.Args["destination"].(model.DestinationInput)) + }) + 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.Destination) + fc.Result = res + return ec.marshalNDestination2ᚖgithubᚗcomᚋodigosᚑioᚋodigosᚋfrontendᚋgraphᚋmodelᚐDestination(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_updateDestination(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Destination_id(ctx, field) + case "name": + return ec.fieldContext_Destination_name(ctx, field) + case "type": + return ec.fieldContext_Destination_type(ctx, field) + case "exportedSignals": + return ec.fieldContext_Destination_exportedSignals(ctx, field) + case "fields": + return ec.fieldContext_Destination_fields(ctx, field) + case "destinationType": + return ec.fieldContext_Destination_destinationType(ctx, field) + case "conditions": + return ec.fieldContext_Destination_conditions(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Destination", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_updateDestination_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _ObservabilitySignalSupport_supported(ctx context.Context, field graphql.CollectedField, obj *model.ObservabilitySignalSupport) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ObservabilitySignalSupport_supported(ctx, field) if err != nil { @@ -8758,6 +8867,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "updateDestination": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_updateDestination(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/frontend/graph/schema.graphqls b/frontend/graph/schema.graphqls index 959457c4b5..666582cb97 100644 --- a/frontend/graph/schema.graphqls +++ b/frontend/graph/schema.graphqls @@ -260,4 +260,5 @@ type Mutation { sourceId: K8sSourceId! patchSourceRequest: PatchSourceRequestInput! ): Boolean! + updateDestination(id: ID!, destination: DestinationInput!): Destination! } diff --git a/frontend/graph/schema.resolvers.go b/frontend/graph/schema.resolvers.go index 12083d0260..9fbfff5844 100644 --- a/frontend/graph/schema.resolvers.go +++ b/frontend/graph/schema.resolvers.go @@ -448,6 +448,111 @@ func (r *mutationResolver) UpdateK8sActualSource(ctx context.Context, sourceID m return true, nil } +// UpdateDestination is the resolver for the updateDestination field. +func (r *mutationResolver) UpdateDestination(ctx context.Context, id string, input model.DestinationInput) (*model.Destination, error) { + odigosns := consts.DefaultOdigosNamespace + + destType := common.DestinationType(input.Type) + destName := input.Name + + // Get the destination type configuration + destTypeConfig, err := services.GetDestinationTypeConfig(destType) + if err != nil { + return nil, fmt.Errorf("destination type %s not found: %v", destType, err) + } + + // Convert fields from input to map[string]string + fields := make(map[string]string) + for _, field := range input.Fields { + fields[field.Key] = field.Value + } + + // Validate the destination data schema + validationErrors := services.VerifyDestinationDataScheme(destType, destTypeConfig, fields) + if len(validationErrors) > 0 { + var errMsg string + for _, e := range validationErrors { + errMsg += e.Error() + "; " + } + return nil, fmt.Errorf("validation errors: %s", errMsg) + } + + // Separate data fields and secret fields + dataFields, secretFields := services.TransformFieldsToDataAndSecrets(destTypeConfig, fields) + + // Retrieve the existing destination + dest, err := kube.DefaultClient.OdigosClient.Destinations(odigosns).Get(ctx, id, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get destination: %v", err) + } + + // Handle secrets + destUpdateHasSecrets := len(secretFields) > 0 + destCurrentlyHasSecrets := dest.Spec.SecretRef != nil + + if !destUpdateHasSecrets && destCurrentlyHasSecrets { + // Delete the secret if it's not needed anymore + err := kube.DefaultClient.CoreV1().Secrets(odigosns).Delete(ctx, dest.Spec.SecretRef.Name, metav1.DeleteOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to delete secret: %v", err) + } + dest.Spec.SecretRef = nil + } else if destUpdateHasSecrets && !destCurrentlyHasSecrets { + // Create the secret if it was added in this update + secretRef, err := services.CreateDestinationSecret(ctx, destType, secretFields, odigosns) + if err != nil { + return nil, fmt.Errorf("failed to create secret: %v", err) + } + dest.Spec.SecretRef = secretRef + // Add owner reference to the secret + err = services.AddDestinationOwnerReferenceToSecret(ctx, odigosns, dest) + if err != nil { + return nil, fmt.Errorf("failed to add owner reference to secret: %v", err) + } + } else if destUpdateHasSecrets && destCurrentlyHasSecrets { + // Update the secret in case it is modified + secret, err := kube.DefaultClient.CoreV1().Secrets(odigosns).Get(ctx, dest.Spec.SecretRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to get secret: %v", err) + } + origSecret := secret.DeepCopy() + + secret.StringData = secretFields + _, err = kube.DefaultClient.CoreV1().Secrets(odigosns).Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + // Rollback secret if needed + _, rollbackErr := kube.DefaultClient.CoreV1().Secrets(odigosns).Update(ctx, origSecret, metav1.UpdateOptions{}) + if rollbackErr != nil { + fmt.Printf("Failed to rollback secret: %v\n", rollbackErr) + } + return nil, fmt.Errorf("failed to update secret: %v", err) + } + } + + // Update the destination specification + dest.Spec.Type = destType + dest.Spec.DestinationName = destName + dest.Spec.Data = dataFields + dest.Spec.Signals = services.ExportedSignalsObjectToSlice(input.ExportedSignals) + + // Update the destination in Kubernetes + updatedDest, err := kube.DefaultClient.OdigosClient.Destinations(odigosns).Update(ctx, dest, metav1.UpdateOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to update destination: %v", err) + } + + // Get the secret fields for the updated destination + secretFields, err = services.GetDestinationSecretFields(ctx, odigosns, updatedDest) + if err != nil { + return nil, fmt.Errorf("failed to get secret fields: %v", err) + } + + // Convert the updated destination to the GraphQL model + resp := services.K8sDestinationToEndpointFormat(*updatedDest, secretFields) + + return &resp, nil +} + // ComputePlatform is the resolver for the computePlatform field. func (r *queryResolver) ComputePlatform(ctx context.Context) (*model.ComputePlatform, error) { return &model.ComputePlatform{ @@ -548,13 +653,3 @@ type destinationResolver struct{ *Resolver } type k8sActualNamespaceResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver } - -// !!! WARNING !!! -// The code below was going to be deleted when updating resolvers. It has been copied here so you have -// one last chance to move it out of harms way if you want. There are two reasons this happens: -// - When renaming or deleting a resolver the old code will be put in here. You can safely delete -// it when you're done. -// - You have helper methods in this file. Move them out to keep these resolver files clean. -func (r *destinationResolver) Fields(ctx context.Context, obj *model.Destination) (string, error) { - panic(fmt.Errorf("not implemented: Fields - fields")) -} diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx index f9b45a95a1..24e5033445 100644 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -5,21 +5,21 @@ import React, { useMemo, useState, } from 'react'; +import styled from 'styled-components'; import { safeJsonParse } from '@/utils'; import { useDrawerStore } from '@/store'; import { useQuery } from '@apollo/client'; -import { CardDetails, DestinationForm } from '@/components'; +import { useConnectDestinationForm } from '@/hooks'; import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; +import { CardDetails, DestinationForm } from '@/components'; import { + DynamicField, + ExportedSignals, ActualDestination, isActualDestination, DestinationDetailsResponse, - ExportedSignals, - DynamicField, SupportedDestinationSignals, } from '@/types'; -import styled from 'styled-components'; -import { useConnectDestinationForm } from '@/hooks'; export type DestinationDrawerHandle = { getCurrentData: () => { @@ -67,18 +67,21 @@ const DestinationDrawer = forwardRef< const { buildFormDynamicFields } = useConnectDestinationForm(); - const { data: destinationFields, error } = - useQuery(GET_DESTINATION_TYPE_DETAILS, { + const { data: destinationFields } = useQuery( + GET_DESTINATION_TYPE_DETAILS, + { variables: { type: destinationType }, skip: shouldSkip, - }); + } + ); useImperativeHandle(ref, () => ({ getCurrentData: () => { const fields = processFormFields(dynamicFields); + const { destinationType } = destination?.item as ActualDestination; const newDestination = { name: destinationName, - type: destination?.type || '', + type: destinationType?.type || '', exportedSignals, fields, }; diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 1262b776cf..e422a0749a 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -1,12 +1,12 @@ import { useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { useDrawerStore } from '@/store'; -import { useActualSources } from '@/hooks'; import DrawerHeader from './drawer-header'; import DrawerFooter from './drawer-footer'; import { SourceDrawer } from '../../sources'; import { Drawer } from '@/reuseable-components'; import { DeleteEntityModal } from '@/components'; +import { useActualSources, useUpdateDestination } from '@/hooks'; import { DestinationDrawer, DestinationDrawerHandle } from '../../destinations'; import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; import { @@ -35,8 +35,8 @@ const OverviewDrawer = () => { const [title, setTitle] = useState(selectedItem?.item?.name || ''); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const { updateExistingDestination } = useUpdateDestination(); const { updateActualSource, deleteSourcesForNamespace } = useActualSources(); - const titleRef = useRef(null); const destinationDrawerRef = useRef(null); useEffect(initialTitle, [selectedItem]); @@ -61,8 +61,16 @@ const OverviewDrawer = () => { ...destinationDrawerRef.current.getCurrentData(), name, }; + try { + const res = await updateExistingDestination( + selectedItem.id as string, + destinationData + ); + console.log({ res }); + } catch (error) { + console.error('Error updating destination:', error); + } - console.log({ id: selectedItem.id, destinationData }); try { // Replace this with your actual save logic // await updateDestination(destinationData); @@ -70,6 +78,7 @@ const OverviewDrawer = () => { console.error('Error updating destination:', error); // Optionally show error message to user } + setIsEditing(false); } } diff --git a/frontend/webapp/graphql/mutations/destination.ts b/frontend/webapp/graphql/mutations/destination.ts index 1c0f4ae013..cdce52cfa9 100644 --- a/frontend/webapp/graphql/mutations/destination.ts +++ b/frontend/webapp/graphql/mutations/destination.ts @@ -19,3 +19,34 @@ export const TEST_CONNECTION_MUTATION = gql` } } `; + +export const UPDATE_DESTINATION = gql` + mutation UpdateDestination($id: ID!, $destination: DestinationInput!) { + updateDestination(id: $id, destination: $destination) { + id + name + exportedSignals { + traces + metrics + logs + } + fields + destinationType { + type + displayName + imageUrl + supportedSignals { + traces { + supported + } + metrics { + supported + } + logs { + supported + } + } + } + } + } +`; diff --git a/frontend/webapp/hooks/destinations/index.ts b/frontend/webapp/hooks/destinations/index.ts index 11b11a37ab..dba0d3a20a 100644 --- a/frontend/webapp/hooks/destinations/index.ts +++ b/frontend/webapp/hooks/destinations/index.ts @@ -4,3 +4,4 @@ export * from './useConnectDestinationForm'; export * from './useCreateDestination'; export * from './usePotentialDestinations'; export * from './useActualDestinations'; +export * from './useUpdateDestination'; diff --git a/frontend/webapp/hooks/destinations/useUpdateDestination.ts b/frontend/webapp/hooks/destinations/useUpdateDestination.ts new file mode 100644 index 0000000000..81b9100016 --- /dev/null +++ b/frontend/webapp/hooks/destinations/useUpdateDestination.ts @@ -0,0 +1,26 @@ +// src/hooks/useUpdateDestination.ts + +import { UPDATE_DESTINATION } from '@/graphql'; +import { DestinationInput } from '@/types'; +import { useMutation } from '@apollo/client'; + +export function useUpdateDestination() { + const [updateDestinationMutation] = useMutation(UPDATE_DESTINATION); + + async function updateExistingDestination( + id: string, + destination: DestinationInput + ) { + try { + const { data } = await updateDestinationMutation({ + variables: { id, destination }, + }); + return data?.updateDestination?.id; + } catch (error) { + console.error('Error updating destination:', error); + throw error; + } + } + + return { updateExistingDestination }; +} From 80135c1de4e90a8e2c0ca26d038cd9c7819b57e3 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 14 Oct 2024 13:46:10 +0300 Subject: [PATCH 06/20] chore: update current item --- .../containers/main/overview/overview-drawer/index.tsx | 2 +- frontend/webapp/hooks/destinations/useUpdateDestination.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index e422a0749a..416afb4ead 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -170,7 +170,7 @@ const OverviewDrawer = () => { {selectedItem.type === 'destination' ? ( ) : ( diff --git a/frontend/webapp/hooks/destinations/useUpdateDestination.ts b/frontend/webapp/hooks/destinations/useUpdateDestination.ts index 81b9100016..d060dc42eb 100644 --- a/frontend/webapp/hooks/destinations/useUpdateDestination.ts +++ b/frontend/webapp/hooks/destinations/useUpdateDestination.ts @@ -1,12 +1,17 @@ // src/hooks/useUpdateDestination.ts import { UPDATE_DESTINATION } from '@/graphql'; +import { useDrawerStore } from '@/store'; import { DestinationInput } from '@/types'; import { useMutation } from '@apollo/client'; export function useUpdateDestination() { const [updateDestinationMutation] = useMutation(UPDATE_DESTINATION); + const setDrawerItem = useDrawerStore( + ({ setSelectedItem }) => setSelectedItem + ); + async function updateExistingDestination( id: string, destination: DestinationInput @@ -15,6 +20,8 @@ export function useUpdateDestination() { const { data } = await updateDestinationMutation({ variables: { id, destination }, }); + setDrawerItem({ id, item: data?.updateDestination, type: 'destination' }); + console.log({ data }); return data?.updateDestination?.id; } catch (error) { console.error('Error updating destination:', error); From f39d37e0bd50119ff2dd7439955320898123f9b6 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 14 Oct 2024 15:21:19 +0300 Subject: [PATCH 07/20] wip: split code to chunks --- .../destinations/destination-form/index.tsx | 16 +- .../connection-notification.tsx | 30 +++ .../form-container.tsx | 50 +++++ .../connect-destination-modal-body/index.tsx | 123 +++-------- .../destination-drawer-container/index.tsx | 204 +++--------------- frontend/webapp/hooks/destinations/index.ts | 2 + .../destinations/useDestinationFormData.ts | 120 +++++++++++ .../useEditDestinationFormHandlers.ts | 22 ++ 8 files changed, 284 insertions(+), 283 deletions(-) create mode 100644 frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx create mode 100644 frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/form-container.tsx create mode 100644 frontend/webapp/hooks/destinations/useDestinationFormData.ts create mode 100644 frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts diff --git a/frontend/webapp/components/destinations/destination-form/index.tsx b/frontend/webapp/components/destinations/destination-form/index.tsx index 909592e25b..8388edb97b 100644 --- a/frontend/webapp/components/destinations/destination-form/index.tsx +++ b/frontend/webapp/components/destinations/destination-form/index.tsx @@ -1,28 +1,24 @@ import React from 'react'; -import { CheckboxList, Input } from '@/reuseable-components'; +import { CheckboxList } from '@/reuseable-components'; +import { DynamicConnectDestinationFormFields } from '@/containers/main/destinations/add-destination/dynamic-form-fields'; import { DynamicField, ExportedSignals, SupportedDestinationSignals, } from '@/types'; -import { DynamicConnectDestinationFormFields } from '@/containers/main/destinations/add-destination/dynamic-form-fields'; interface DestinationFormProps { - destinationName: string; dynamicFields: DynamicField[]; exportedSignals: ExportedSignals; supportedSignals: SupportedDestinationSignals; - setDestinationName: (name: string) => void; handleDynamicFieldChange: (name: string, value: any) => void; handleSignalChange: (signal: keyof ExportedSignals, value: boolean) => void; } -export const DestinationForm: React.FC = ({ +export const EditDestinationForm: React.FC = ({ dynamicFields, - destinationName, exportedSignals, supportedSignals, - setDestinationName, handleSignalChange, handleDynamicFieldChange, }) => { @@ -34,12 +30,6 @@ export const DestinationForm: React.FC = ({ return ( <> - setDestinationName(e.target.value)} - /> ( + <> + {showConnectionError && ( + + + + )} + {destination?.fields && !showConnectionError && ( + + + + )} + +); + +const NotificationNoteWrapper = styled.div` + margin-top: 24px; +`; diff --git a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/form-container.tsx b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/form-container.tsx new file mode 100644 index 0000000000..71bd10c1b1 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/form-container.tsx @@ -0,0 +1,50 @@ +import styled from 'styled-components'; +import { CheckboxList, Input } from '@/reuseable-components'; +import { DynamicConnectDestinationFormFields } from '../dynamic-form-fields'; + +export const FormContainer = ({ + monitors, + dynamicFields, + exportedSignals, + destinationName, + handleDynamicFieldChange, + handleSignalChange, + setDestinationName, +}) => ( + + + setDestinationName(e.target.value)} + /> + + +); + +const StyledFormContainer = styled.div` + display: flex; + width: 100%; + max-width: 500px; + flex-direction: column; + gap: 24px; + height: 443px; + overflow-y: auto; + padding-right: 16px; + box-sizing: border-box; + overflow: overlay; + max-height: calc(100vh - 410px); + + @media (height < 768px) { + max-height: calc(100vh - 350px); + } +`; 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 118c46744f..0989e71738 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 @@ -1,29 +1,26 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useAppStore } from '@/store'; -import styled from 'styled-components'; import { SideMenu } from '@/components'; import { useQuery } from '@apollo/client'; +import { FormContainer } from './form-container'; import { TestConnection } from '../test-connection'; import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; import { Body, Container, SideMenuWrapper } from '../styled'; -import { useConnectDestinationForm, useConnectEnv } from '@/hooks'; -import { DynamicConnectDestinationFormFields } from '../dynamic-form-fields'; +import { Divider, SectionTitle } from '@/reuseable-components'; +import { ConnectionNotification } from './connection-notification'; +import { + useConnectDestinationForm, + useConnectEnv, + useDestinationFormData, + useEditDestinationFormHandlers, +} from '@/hooks'; import { StepProps, - DynamicField, - ExportedSignals, DestinationInput, DestinationTypeItem, DestinationDetailsResponse, ConfiguredDestination, } from '@/types'; -import { - CheckboxList, - Divider, - Input, - NotificationNote, - SectionTitle, -} from '@/reuseable-components'; const SIDE_MENU_DATA: StepProps[] = [ { @@ -38,28 +35,6 @@ const SIDE_MENU_DATA: StepProps[] = [ }, ]; -const FormContainer = styled.div` - display: flex; - width: 100%; - max-width: 500px; - flex-direction: column; - gap: 24px; - height: 443px; - overflow-y: auto; - padding-right: 16px; - box-sizing: border-box; - overflow: overlay; - max-height: calc(100vh - 410px); - - @media (height < 768px) { - max-height: calc(100vh - 350px); - } -`; - -const NotificationNoteWrapper = styled.div` - margin-top: 24px; -`; - interface ConnectDestinationModalBodyProps { destination: DestinationTypeItem | undefined; onSubmitRef: React.MutableRefObject<(() => void) | null>; @@ -73,15 +48,18 @@ export function ConnectDestinationModalBody({ }: ConnectDestinationModalBodyProps) { const [destinationName, setDestinationName] = useState(''); const [showConnectionError, setShowConnectionError] = useState(false); - const [dynamicFields, setDynamicFields] = useState([]); - const [exportedSignals, setExportedSignals] = useState({ - logs: false, - metrics: false, - traces: false, - }); + + const { + dynamicFields, + exportedSignals, + setExportedSignals, + setDynamicFields, + } = useDestinationFormData(); const { connectEnv } = useConnectEnv(); const { buildFormDynamicFields } = useConnectDestinationForm(); + const { handleDynamicFieldChange, handleSignalChange } = + useEditDestinationFormHandlers(setExportedSignals, setDynamicFields); const addConfiguredDestination = useAppStore( ({ addConfiguredDestination }) => addConfiguredDestination ); @@ -96,7 +74,6 @@ export function ConnectDestinationModalBody({ const monitors = useMemo(() => { if (!destination) return []; - const { logs, metrics, traces } = destination.supportedSignals; setExportedSignals({ @@ -143,20 +120,9 @@ export function ConnectDestinationModalBody({ onFormValidChange(isFormValid); }, [dynamicFields]); - function handleDynamicFieldChange(name: string, value: any) { + function onDynamicFieldChange(name: string, value: any) { setShowConnectionError(false); - setDynamicFields((prev) => { - return prev.map((field) => { - if (field.name === name) { - return { ...field, value }; - } - return field; - }); - }); - } - - function handleSignalChange(signal: string, value: boolean) { - setExportedSignals((prev) => ({ ...prev, [signal]: value })); + handleDynamicFieldChange(name, value); } function processFormFields() { @@ -259,43 +225,20 @@ export function ConnectDestinationModalBody({ ) } /> - {showConnectionError && ( - - - - )} - {destination.fields && !showConnectionError && ( - - - - )} + - - - setDestinationName(e.target.value)} - /> - - + ); diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx index 24e5033445..914f40714f 100644 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -1,29 +1,14 @@ -import React, { - forwardRef, - useEffect, - useImperativeHandle, - useMemo, - useState, -} from 'react'; +import React, { forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; -import { safeJsonParse } from '@/utils'; -import { useDrawerStore } from '@/store'; -import { useQuery } from '@apollo/client'; -import { useConnectDestinationForm } from '@/hooks'; -import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; -import { CardDetails, DestinationForm } from '@/components'; +import { ExportedSignals } from '@/types'; +import { CardDetails, EditDestinationForm } from '@/components'; import { - DynamicField, - ExportedSignals, - ActualDestination, - isActualDestination, - DestinationDetailsResponse, - SupportedDestinationSignals, -} from '@/types'; + useDestinationFormData, + useEditDestinationFormHandlers, +} from '@/hooks'; export type DestinationDrawerHandle = { getCurrentData: () => { - name: string; type: string; exportedSignals: ExportedSignals; fields: { key: string; value: any }[]; @@ -34,143 +19,37 @@ interface DestinationDrawerProps { isEditing: boolean; } -const DEFAULT_SUPPORTED_SIGNALS = { - logs: { - supported: false, - }, - metrics: { - supported: false, - }, - traces: { - supported: false, - }, -}; - const DestinationDrawer = forwardRef< DestinationDrawerHandle, DestinationDrawerProps >(({ isEditing }, ref) => { - const [dynamicFields, setDynamicFields] = useState([]); - const destination = useDrawerStore(({ selectedItem }) => selectedItem); - const [destinationName, setDestinationName] = useState(''); - const [supportedSignals, setSupportedSignals] = - useState(DEFAULT_SUPPORTED_SIGNALS); - const [exportedSignals, setExportedSignals] = useState({ - logs: false, - metrics: false, - traces: false, - }); - const shouldSkip = !isActualDestination(destination?.item); - const destinationType = isActualDestination(destination?.item) - ? destination.item.destinationType.type - : null; - - const { buildFormDynamicFields } = useConnectDestinationForm(); - - const { data: destinationFields } = useQuery( - GET_DESTINATION_TYPE_DETAILS, - { - variables: { type: destinationType }, - skip: shouldSkip, - } - ); + const { + cardData, + dynamicFields, + exportedSignals, + supportedSignals, + destinationType, + setDynamicFields, + setExportedSignals, + } = useDestinationFormData(); + + const { handleSignalChange, handleDynamicFieldChange } = + useEditDestinationFormHandlers(setExportedSignals, setDynamicFields); useImperativeHandle(ref, () => ({ - getCurrentData: () => { - const fields = processFormFields(dynamicFields); - const { destinationType } = destination?.item as ActualDestination; - const newDestination = { - name: destinationName, - type: destinationType?.type || '', - exportedSignals, - fields, - }; - return newDestination; - }, + getCurrentData: () => ({ + type: destinationType, + exportedSignals, + fields: dynamicFields.map(({ name, value }) => ({ key: name, value })), + }), })); - useEffect(initDynamicFields, [destinationFields, destination]); - - const cardData = useMemo(() => { - if (shouldSkip || !destination?.item || !destinationFields) { - return [ - { title: 'Error', value: 'No destination selected or data missing' }, - ]; - } - - const { exportedSignals, destinationType, fields } = - destination.item as ActualDestination; - const { destinationTypeDetails } = destinationFields; - - const parsedFields = safeJsonParse>(fields, {}); - const destinationFieldData = buildDestinationFieldData( - parsedFields, - destinationTypeDetails?.fields - ); - - const monitors = buildMonitorsList(exportedSignals); - - return [ - { title: 'Destination', value: destinationType.displayName || 'N/A' }, - { title: 'Monitors', value: monitors || 'None' }, - ...destinationFieldData, - ]; - }, [shouldSkip, destination, destinationFields]); - - function initDynamicFields() { - if (destinationFields && destination) { - const df = buildFormDynamicFields( - destinationFields.destinationTypeDetails.fields - ); - - const { fields, exportedSignals, name, destinationType } = - destination.item as ActualDestination; - const parsedFields = safeJsonParse>(fields, {}); - const newDynamicFields = df.map((field) => { - if (field?.name in parsedFields) { - return { - ...field, - value: - field.componentType === 'dropdown' - ? { - id: parsedFields[field.name], - value: parsedFields[field.name], - } - : parsedFields[field.name], - }; - } - return field; - }); - setDestinationName(name); - setExportedSignals(exportedSignals); - setDynamicFields(newDynamicFields); - setSupportedSignals(destinationType.supportedSignals); - } - } - - function handleSignalChange(signal: string, value: boolean) { - setExportedSignals((prev) => ({ ...prev, [signal]: value })); - } - - function handleDynamicFieldChange(name: string, value: any) { - setDynamicFields((prev) => { - return prev.map((field) => { - if (field.name === name) { - return { ...field, value }; - } - return field; - }); - }); - } - return isEditing ? ( - @@ -182,41 +61,6 @@ const DestinationDrawer = forwardRef< export { DestinationDrawer }; -// Helper function to build the destination field data array -function buildDestinationFieldData( - parsedFields: Record, - fieldDetails?: { name: string; displayName: string }[] -) { - return Object.entries(parsedFields).map(([key, value]) => { - const displayName = - fieldDetails?.find((field) => field.name === key)?.displayName || key; - return { title: displayName, value: value || 'N/A' }; - }); -} - -function buildMonitorsList( - exportedSignals: ActualDestination['exportedSignals'] -): string { - return ( - Object.entries(exportedSignals) - .filter(([key, isEnabled]) => isEnabled && key !== '__typename') - .map(([key]) => key) - .join(', ') || 'None' - ); -} - -function processFormFields(dynamicFields) { - function processFieldValue(field) { - return field.componentType === 'dropdown' ? field.value.value : field.value; - } - - // Prepare fields for the request body - return dynamicFields.map((field) => ({ - key: field.name, - value: processFieldValue(field), - })); -} - const FormContainer = styled.div` display: flex; width: 100%; diff --git a/frontend/webapp/hooks/destinations/index.ts b/frontend/webapp/hooks/destinations/index.ts index dba0d3a20a..01743f6612 100644 --- a/frontend/webapp/hooks/destinations/index.ts +++ b/frontend/webapp/hooks/destinations/index.ts @@ -5,3 +5,5 @@ export * from './useCreateDestination'; export * from './usePotentialDestinations'; export * from './useActualDestinations'; export * from './useUpdateDestination'; +export * from './useDestinationFormData'; +export * from './useEditDestinationFormHandlers'; diff --git a/frontend/webapp/hooks/destinations/useDestinationFormData.ts b/frontend/webapp/hooks/destinations/useDestinationFormData.ts new file mode 100644 index 0000000000..5bc76b8ce2 --- /dev/null +++ b/frontend/webapp/hooks/destinations/useDestinationFormData.ts @@ -0,0 +1,120 @@ +import { useState, useEffect, useMemo } from 'react'; +import { safeJsonParse } from '@/utils'; +import { useDrawerStore } from '@/store'; +import { useQuery } from '@apollo/client'; +import { useConnectDestinationForm } from '@/hooks'; +import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; +import { + DynamicField, + ActualDestination, + isActualDestination, + DestinationDetailsResponse, + SupportedDestinationSignals, +} from '@/types'; + +const DEFAULT_SUPPORTED_SIGNALS: SupportedDestinationSignals = { + logs: { supported: false }, + metrics: { supported: false }, + traces: { supported: false }, +}; + +export function useDestinationFormData() { + const [dynamicFields, setDynamicFields] = useState([]); + const [exportedSignals, setExportedSignals] = useState({ + logs: false, + metrics: false, + traces: false, + }); + const [supportedSignals, setSupportedSignals] = + useState(DEFAULT_SUPPORTED_SIGNALS); + + const destination = useDrawerStore(({ selectedItem }) => selectedItem); + const shouldSkip = !isActualDestination(destination?.item); + const destinationType = isActualDestination(destination?.item) + ? destination.item.destinationType.type + : null; + const { buildFormDynamicFields } = useConnectDestinationForm(); + + const { data: destinationFields } = useQuery( + GET_DESTINATION_TYPE_DETAILS, + { variables: { type: destinationType }, skip: shouldSkip } + ); + + useEffect(() => { + if (destinationFields && isActualDestination(destination?.item)) { + const { fields, exportedSignals, destinationType } = destination.item; + const destinationTypeDetails = destinationFields.destinationTypeDetails; + const formFields = buildFormDynamicFields( + destinationTypeDetails?.fields || [] + ); + const parsedFields = safeJsonParse>(fields, {}); + + setDynamicFields( + formFields.map((field) => ({ + ...field, + value: parsedFields[field.name] || '', + })) + ); + + setExportedSignals(exportedSignals); + setSupportedSignals(destinationType.supportedSignals); + } + }, [destinationFields, destination, buildFormDynamicFields]); + + const cardData = useMemo(() => { + if ( + shouldSkip || + !isActualDestination(destination?.item) || + !destinationFields + ) { + return [ + { title: 'Error', value: 'No destination selected or data missing' }, + ]; + } + + const { exportedSignals, destinationType, fields } = destination.item; + const parsedFields = safeJsonParse>(fields, {}); + const destinationDetails = destinationFields.destinationTypeDetails?.fields; + const fieldsData = buildDestinationFieldData( + parsedFields, + destinationDetails + ); + + return [ + { title: 'Destination', value: destinationType.displayName || 'N/A' }, + { title: 'Monitors', value: buildMonitorsList(exportedSignals) }, + ...fieldsData, + ]; + }, [shouldSkip, destination, destinationFields]); + + return { + cardData, + dynamicFields, + destinationType: destinationType || '', + exportedSignals, + supportedSignals, + setExportedSignals, + setDynamicFields, + }; +} + +function buildDestinationFieldData( + parsedFields: Record, + fieldDetails?: { name: string; displayName: string }[] +) { + return Object.entries(parsedFields).map(([key, value]) => ({ + title: + fieldDetails?.find((field) => field.name === key)?.displayName || key, + value: value || 'N/A', + })); +} + +function buildMonitorsList( + exportedSignals: ActualDestination['exportedSignals'] +): string { + return ( + Object.keys(exportedSignals) + .filter((key) => exportedSignals[key] && key !== '__typename') + .join(', ') || 'None' + ); +} diff --git a/frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts b/frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts new file mode 100644 index 0000000000..330b1390ff --- /dev/null +++ b/frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts @@ -0,0 +1,22 @@ +import { Dispatch, SetStateAction } from 'react'; +import { DynamicField, ExportedSignals } from '@/types'; + +export function useEditDestinationFormHandlers( + setExportedSignals: Dispatch>, + setDynamicFields: Dispatch> +) { + const handleSignalChange = ( + signal: keyof ExportedSignals, + value: boolean + ) => { + setExportedSignals((prev) => ({ ...prev, [signal]: value })); + }; + + const handleDynamicFieldChange = (name: string, value: any) => { + setDynamicFields((prev) => + prev.map((field) => (field.name === name ? { ...field, value } : field)) + ); + }; + + return { handleSignalChange, handleDynamicFieldChange }; +} From 6f1ba85189baf756345e9599b53a5c1365580e8c Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 14 Oct 2024 15:29:35 +0300 Subject: [PATCH 08/20] chore: wip --- .../main/overview/overview-drawer/index.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 416afb4ead..5c9e0bad69 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -19,8 +19,8 @@ import { const componentMap = { source: SourceDrawer, action: () =>
Action
, - destination: (props: { isEditing: boolean }) => ( - + destination: ({ isEditing }: { isEditing: boolean }) => ( + ), }; @@ -70,14 +70,6 @@ const OverviewDrawer = () => { } catch (error) { console.error('Error updating destination:', error); } - - try { - // Replace this with your actual save logic - // await updateDestination(destinationData); - } catch (error) { - console.error('Error updating destination:', error); - // Optionally show error message to user - } setIsEditing(false); } } @@ -103,7 +95,6 @@ const OverviewDrawer = () => { await updateActualSource(sourceId, patchRequest); } catch (error) { console.error('Error updating source:', error); - // Optionally show error message to user } } } From 5a7009444cc7bfb171416f21f21fcf65a186393a Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Mon, 14 Oct 2024 15:30:29 +0300 Subject: [PATCH 09/20] chore: wip --- .../webapp/containers/main/overview/overview-drawer/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 5c9e0bad69..7ad7a14745 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -62,11 +62,10 @@ const OverviewDrawer = () => { name, }; try { - const res = await updateExistingDestination( + await updateExistingDestination( selectedItem.id as string, destinationData ); - console.log({ res }); } catch (error) { console.error('Error updating destination:', error); } From 45ed9f22b02f53e4a5f0dfe3dcac564c4a97a9dd Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Tue, 15 Oct 2024 09:58:35 +0300 Subject: [PATCH 10/20] chore: wip --- .../main/overview/overview-drawer/index.tsx | 2 +- .../hooks/destinations/useDestinationFormData.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 7ad7a14745..25ba2bb855 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -151,7 +151,7 @@ const OverviewDrawer = () => { ( @@ -40,11 +41,17 @@ export function useDestinationFormData() { { variables: { type: destinationType }, skip: shouldSkip } ); + // Memoize the buildFormDynamicFields to ensure it's stable across renders + const memoizedBuildFormDynamicFields = useCallback( + buildFormDynamicFields, + [] + ); + useEffect(() => { if (destinationFields && isActualDestination(destination?.item)) { const { fields, exportedSignals, destinationType } = destination.item; const destinationTypeDetails = destinationFields.destinationTypeDetails; - const formFields = buildFormDynamicFields( + const formFields = memoizedBuildFormDynamicFields( destinationTypeDetails?.fields || [] ); const parsedFields = safeJsonParse>(fields, {}); @@ -59,7 +66,7 @@ export function useDestinationFormData() { setExportedSignals(exportedSignals); setSupportedSignals(destinationType.supportedSignals); } - }, [destinationFields, destination, buildFormDynamicFields]); + }, [destinationFields, destination, memoizedBuildFormDynamicFields]); const cardData = useMemo(() => { if ( From eb3ad4a97673f897e49bf9c996758ae55aac5732 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Tue, 15 Oct 2024 14:50:55 +0300 Subject: [PATCH 11/20] chore: spilt code to hooks --- .../overview/overview-data-flow/index.tsx | 80 ++++--------------- frontend/webapp/hooks/common/index.ts | 1 + .../webapp/hooks/common/useContainerWidth.ts | 23 ++++++ frontend/webapp/hooks/index.tsx | 2 + frontend/webapp/hooks/overview/index.tsx | 1 + .../hooks/overview/useNodeDataFlowHandlers.ts | 55 +++++++++++++ 6 files changed, 97 insertions(+), 65 deletions(-) create mode 100644 frontend/webapp/hooks/common/index.ts create mode 100644 frontend/webapp/hooks/common/useContainerWidth.ts create mode 100644 frontend/webapp/hooks/overview/index.tsx create mode 100644 frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx index a49c823c0a..233bf67119 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx @@ -1,11 +1,16 @@ 'use client'; +import React, { useMemo } from 'react'; import dynamic from 'next/dynamic'; import styled from 'styled-components'; -import { useDrawerStore } from '@/store'; -import React, { useMemo, useRef, useEffect, useState } from 'react'; import { OverviewActionMenuContainer } from '../overview-actions-menu'; import { buildNodesAndEdges, NodeBaseDataFlow } from '@/reuseable-components'; -import { useActualDestination, useActualSources, useGetActions } from '@/hooks'; +import { + useGetActions, + useActualSources, + useContainerWidth, + useActualDestination, + useNodeDataFlowHandlers, +} from '@/hooks'; const OverviewDrawer = dynamic(() => import('../overview-drawer'), { ssr: false, @@ -17,38 +22,12 @@ export const OverviewDataFlowWrapper = styled.div` position: relative; `; -const TYPE_SOURCE = 'source'; -const TYPE_DESTINATION = 'destination'; - export function OverviewDataFlowContainer() { - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - const { actions } = useGetActions(); const { sources } = useActualSources(); const { destinations } = useActualDestination(); - const setSelectedItem = useDrawerStore( - ({ setSelectedItem }) => setSelectedItem - ); - // Get the width of the container dynamically - useEffect(() => { - if (containerRef.current) { - setContainerWidth( - containerRef.current.getBoundingClientRect().width - 64 - ); - } - - const handleResize = () => { - if (containerRef.current) { - setContainerWidth( - containerRef.current.getBoundingClientRect().width - 64 - ); - } - }; - - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); + const { containerRef, containerWidth } = useContainerWidth(); + const { handleNodeClick } = useNodeDataFlowHandlers(sources, destinations); const columnWidth = 296; @@ -63,44 +42,15 @@ export function OverviewDataFlowContainer() { }); }, [sources, actions, destinations, columnWidth, containerWidth]); - function onNodeClick(_, object: any) { - if (object.data.type === TYPE_SOURCE) { - const { id } = object.data; - const selectedDrawerItem = sources.find( - ({ kind, name, namespace }) => - kind === id.kind && name === id.name && namespace === id.namespace - ); - if (!selectedDrawerItem) return; - - const { kind, name, namespace } = selectedDrawerItem; - - setSelectedItem({ - id: { kind, name, namespace }, - item: selectedDrawerItem, - type: TYPE_SOURCE, - }); - } - - if (object.data.type === TYPE_DESTINATION) { - const { id } = object.data; - - const selectedDrawerItem = destinations.find( - (destination) => destination.id === id - ); - - setSelectedItem({ - id, - item: selectedDrawerItem, - type: TYPE_DESTINATION, - }); - } - } - return ( - + ); } diff --git a/frontend/webapp/hooks/common/index.ts b/frontend/webapp/hooks/common/index.ts new file mode 100644 index 0000000000..e536761bd3 --- /dev/null +++ b/frontend/webapp/hooks/common/index.ts @@ -0,0 +1 @@ +export * from './useContainerWidth'; diff --git a/frontend/webapp/hooks/common/useContainerWidth.ts b/frontend/webapp/hooks/common/useContainerWidth.ts new file mode 100644 index 0000000000..9ea9c601bd --- /dev/null +++ b/frontend/webapp/hooks/common/useContainerWidth.ts @@ -0,0 +1,23 @@ +import { useEffect, useState, useRef } from 'react'; + +export function useContainerWidth() { + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + setContainerWidth( + containerRef.current.getBoundingClientRect().width - 64 + ); + } + }; + + updateWidth(); + + window.addEventListener('resize', updateWidth); + return () => window.removeEventListener('resize', updateWidth); + }, []); + + return { containerRef, containerWidth }; +} diff --git a/frontend/webapp/hooks/index.tsx b/frontend/webapp/hooks/index.tsx index c2051a0372..b1c09191ba 100644 --- a/frontend/webapp/hooks/index.tsx +++ b/frontend/webapp/hooks/index.tsx @@ -10,3 +10,5 @@ export * from './useSSE'; export * from './new-config'; export * from './compute-platform'; export * from './useOverviewMetrics'; +export * from './overview'; +export * from './common'; diff --git a/frontend/webapp/hooks/overview/index.tsx b/frontend/webapp/hooks/overview/index.tsx new file mode 100644 index 0000000000..45e0d22587 --- /dev/null +++ b/frontend/webapp/hooks/overview/index.tsx @@ -0,0 +1 @@ +export * from './useNodeDataFlowHandlers'; diff --git a/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts b/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts new file mode 100644 index 0000000000..eb0291a821 --- /dev/null +++ b/frontend/webapp/hooks/overview/useNodeDataFlowHandlers.ts @@ -0,0 +1,55 @@ +// src/hooks/useNodeDataFlowHandlers.ts +import { useCallback } from 'react'; +import { useDrawerStore } from '@/store'; +import { K8sActualSource, ActualDestination } from '@/types'; + +const TYPE_SOURCE = 'source'; +const TYPE_DESTINATION = 'destination'; + +export function useNodeDataFlowHandlers( + sources: K8sActualSource[], + destinations: ActualDestination[] +) { + const setSelectedItem = useDrawerStore( + ({ setSelectedItem }) => setSelectedItem + ); + + const handleNodeClick = useCallback( + (_, object: any) => { + if (object.data.type === TYPE_SOURCE) { + const { id } = object.data; + const selectedDrawerItem = sources.find( + ({ kind, name, namespace }) => + kind === id.kind && name === id.name && namespace === id.namespace + ); + if (!selectedDrawerItem) return; + + const { kind, name, namespace } = selectedDrawerItem; + + setSelectedItem({ + id: { kind, name, namespace }, + item: selectedDrawerItem, + type: TYPE_SOURCE, + }); + } + + if (object.data.type === TYPE_DESTINATION) { + const { id } = object.data; + const selectedDrawerItem = destinations.find( + (destination) => destination.id === id + ); + + setSelectedItem({ + id, + item: selectedDrawerItem, + type: TYPE_DESTINATION, + }); + } + }, + [sources, destinations, setSelectedItem] + ); + + return { + handleNodeClick, + }; +} From 58f34d02535700e0831475cdb28f83432b2e3775 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Tue, 15 Oct 2024 15:12:09 +0300 Subject: [PATCH 12/20] chore: checkboxs list disabled --- .../webapp/containers/main/overview/overview-drawer/index.tsx | 2 ++ frontend/webapp/lib/gql/apollo-wrapper.tsx | 4 +++- frontend/webapp/reuseable-components/checkbox-list/index.tsx | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 25ba2bb855..de88f74b9b 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -41,6 +41,8 @@ const OverviewDrawer = () => { const destinationDrawerRef = useRef(null); useEffect(initialTitle, [selectedItem]); + //TODO: split file to separate components by type: source, destination, action + function initialTitle() { if (selectedItem?.type === 'source' && selectedItem.item) { const title = (selectedItem.item as K8sActualSource).reportedName; diff --git a/frontend/webapp/lib/gql/apollo-wrapper.tsx b/frontend/webapp/lib/gql/apollo-wrapper.tsx index 0bef52a519..debbe9ea47 100644 --- a/frontend/webapp/lib/gql/apollo-wrapper.tsx +++ b/frontend/webapp/lib/gql/apollo-wrapper.tsx @@ -26,7 +26,9 @@ function makeClient() { }); return new ApolloClient({ - cache: new InMemoryCache(), + cache: new InMemoryCache({ + addTypename: false, + }), devtools: { enabled: true, }, diff --git a/frontend/webapp/reuseable-components/checkbox-list/index.tsx b/frontend/webapp/reuseable-components/checkbox-list/index.tsx index 47e8f2e17a..0528fce7fc 100644 --- a/frontend/webapp/reuseable-components/checkbox-list/index.tsx +++ b/frontend/webapp/reuseable-components/checkbox-list/index.tsx @@ -37,8 +37,10 @@ const CheckboxList: React.FC = ({ (value) => value ); + const trueValues = Object.values(exportedSignals).filter(Boolean); + return ( - monitors.length === 1 || + (monitors.length === 1 && trueValues.length === 1) || (selectedItems.length === 1 && exportedSignals[item.id]) ); } From ee71844ab1abbbb804657f4e98304b9c5b4ff30e Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Tue, 15 Oct 2024 15:46:54 +0300 Subject: [PATCH 13/20] wip: reset state when cancel --- .../destination-drawer-container/index.tsx | 30 ++++++++++++++-- .../main/overview/overview-drawer/index.tsx | 2 ++ .../destinations/useDestinationFormData.ts | 36 ++++++++++++++----- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx index 914f40714f..fa354eeb9f 100644 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -1,4 +1,9 @@ -import React, { forwardRef, useImperativeHandle } from 'react'; +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useState, +} from 'react'; import styled from 'styled-components'; import { ExportedSignals } from '@/types'; import { CardDetails, EditDestinationForm } from '@/components'; @@ -23,12 +28,14 @@ const DestinationDrawer = forwardRef< DestinationDrawerHandle, DestinationDrawerProps >(({ isEditing }, ref) => { + const [isFormDirty, setIsFormDirty] = useState(false); const { cardData, dynamicFields, exportedSignals, supportedSignals, destinationType, + resetFormData, setDynamicFields, setExportedSignals, } = useDestinationFormData(); @@ -36,6 +43,23 @@ const DestinationDrawer = forwardRef< const { handleSignalChange, handleDynamicFieldChange } = useEditDestinationFormHandlers(setExportedSignals, setDynamicFields); + useEffect(() => { + if (!isEditing && isFormDirty) { + setIsFormDirty(false); + resetFormData(); + } + }, [isEditing]); + + const onDynamicFieldChange = (name: string, value: any) => { + handleDynamicFieldChange(name, value); + setIsFormDirty(true); + }; + + const onSignalChange = (signal: keyof ExportedSignals, value: boolean) => { + handleSignalChange(signal, value); + setIsFormDirty(true); + }; + useImperativeHandle(ref, () => ({ getCurrentData: () => ({ type: destinationType, @@ -50,8 +74,8 @@ const DestinationDrawer = forwardRef< dynamicFields={dynamicFields} exportedSignals={exportedSignals} supportedSignals={supportedSignals} - handleSignalChange={handleSignalChange} - handleDynamicFieldChange={handleDynamicFieldChange} + handleSignalChange={onSignalChange} + handleDynamicFieldChange={onDynamicFieldChange} />
) : ( diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index de88f74b9b..28b0ff7570 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -37,8 +37,10 @@ const OverviewDrawer = () => { const { updateExistingDestination } = useUpdateDestination(); const { updateActualSource, deleteSourcesForNamespace } = useActualSources(); + const titleRef = useRef(null); const destinationDrawerRef = useRef(null); + useEffect(initialTitle, [selectedItem]); //TODO: split file to separate components by type: source, destination, action diff --git a/frontend/webapp/hooks/destinations/useDestinationFormData.ts b/frontend/webapp/hooks/destinations/useDestinationFormData.ts index b6f3e231a5..dd9b4b2aec 100644 --- a/frontend/webapp/hooks/destinations/useDestinationFormData.ts +++ b/frontend/webapp/hooks/destinations/useDestinationFormData.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { safeJsonParse } from '@/utils'; import { useDrawerStore } from '@/store'; import { useQuery } from '@apollo/client'; @@ -47,6 +47,16 @@ export function useDestinationFormData() { [] ); + const initialDynamicFieldsRef = useRef([]); + const initialExportedSignalsRef = useRef({ + logs: false, + metrics: false, + traces: false, + }); + const initialSupportedSignalsRef = useRef( + DEFAULT_SUPPORTED_SIGNALS + ); + useEffect(() => { if (destinationFields && isActualDestination(destination?.item)) { const { fields, exportedSignals, destinationType } = destination.item; @@ -55,16 +65,18 @@ export function useDestinationFormData() { destinationTypeDetails?.fields || [] ); const parsedFields = safeJsonParse>(fields, {}); + const df = formFields.map((field) => ({ + ...field, + value: parsedFields[field.name] || '', + })); - setDynamicFields( - formFields.map((field) => ({ - ...field, - value: parsedFields[field.name] || '', - })) - ); - + setDynamicFields(df); setExportedSignals(exportedSignals); setSupportedSignals(destinationType.supportedSignals); + + initialDynamicFieldsRef.current = df; + initialExportedSignalsRef.current = exportedSignals; + initialSupportedSignalsRef.current = destinationType.supportedSignals; } }, [destinationFields, destination, memoizedBuildFormDynamicFields]); @@ -94,6 +106,13 @@ export function useDestinationFormData() { ]; }, [shouldSkip, destination, destinationFields]); + // Reset function using initial values from refs + const resetFormData = useCallback(() => { + setDynamicFields(initialDynamicFieldsRef.current); + setExportedSignals(initialExportedSignalsRef.current); + setSupportedSignals(initialSupportedSignalsRef.current); + }, []); + return { cardData, dynamicFields, @@ -102,6 +121,7 @@ export function useDestinationFormData() { supportedSignals, setExportedSignals, setDynamicFields, + resetFormData, }; } From f77b5c35626f55dc00ff1d8a51c4e879804864cb Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Tue, 15 Oct 2024 15:49:12 +0300 Subject: [PATCH 14/20] chore: change file name --- .../{destination-form => edit-destination-form}/index.tsx | 0 frontend/webapp/components/destinations/index.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename frontend/webapp/components/destinations/{destination-form => edit-destination-form}/index.tsx (100%) diff --git a/frontend/webapp/components/destinations/destination-form/index.tsx b/frontend/webapp/components/destinations/edit-destination-form/index.tsx similarity index 100% rename from frontend/webapp/components/destinations/destination-form/index.tsx rename to frontend/webapp/components/destinations/edit-destination-form/index.tsx diff --git a/frontend/webapp/components/destinations/index.ts b/frontend/webapp/components/destinations/index.ts index 4ed4159b2d..e852a65cca 100644 --- a/frontend/webapp/components/destinations/index.ts +++ b/frontend/webapp/components/destinations/index.ts @@ -1,3 +1,3 @@ export * from './add-destination-button'; export * from './monitors-tap-list'; -export * from './destination-form'; +export * from './edit-destination-form'; From 1035692805d1ed0400817e99ce7315bd0f2492cc Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Tue, 15 Oct 2024 17:33:55 +0300 Subject: [PATCH 15/20] wip: show inputs values on edit mode --- .../destinations/useDestinationFormData.ts | 28 +++++++++++++++---- .../reuseable-components/input-list/index.tsx | 28 ++++++++++--------- .../key-value-input-list/index.tsx | 7 +++-- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/frontend/webapp/hooks/destinations/useDestinationFormData.ts b/frontend/webapp/hooks/destinations/useDestinationFormData.ts index dd9b4b2aec..bba30a780c 100644 --- a/frontend/webapp/hooks/destinations/useDestinationFormData.ts +++ b/frontend/webapp/hooks/destinations/useDestinationFormData.ts @@ -61,14 +61,32 @@ export function useDestinationFormData() { if (destinationFields && isActualDestination(destination?.item)) { const { fields, exportedSignals, destinationType } = destination.item; const destinationTypeDetails = destinationFields.destinationTypeDetails; + + const parsedFields = safeJsonParse>(fields, {}); const formFields = memoizedBuildFormDynamicFields( destinationTypeDetails?.fields || [] ); - const parsedFields = safeJsonParse>(fields, {}); - const df = formFields.map((field) => ({ - ...field, - value: parsedFields[field.name] || '', - })); + + const df = formFields.map((field) => { + let fieldValue: any = parsedFields[field.name] || ''; + + // Check if fieldValue is a JSON string that needs stringifying + try { + const parsedValue = JSON.parse(fieldValue); + console.log({ parsedValue }); + if (Array.isArray(parsedValue)) { + // If it's an array, stringify it for setting the value + fieldValue = parsedValue; + } + } catch (e) { + // If parsing fails, it's not JSON, so we keep it as is + } + + return { + ...field, + value: fieldValue, + }; + }); setDynamicFields(df); setExportedSignals(exportedSignals); diff --git a/frontend/webapp/reuseable-components/input-list/index.tsx b/frontend/webapp/reuseable-components/input-list/index.tsx index 43d6fffe5b..c3a3c2743e 100644 --- a/frontend/webapp/reuseable-components/input-list/index.tsx +++ b/frontend/webapp/reuseable-components/input-list/index.tsx @@ -11,6 +11,7 @@ interface InputListProps { title?: string; tooltip?: string; required?: boolean; + value?: string[]; onChange: (values: string[]) => void; } @@ -71,8 +72,9 @@ const InputList: React.FC = ({ tooltip, required, onChange, + value = [''], }) => { - const [inputs, setInputs] = useState(initialValues); + const [inputs, setInputs] = useState(value || initialValues); useEffect(() => { if (initialValues.length > 0) { @@ -101,15 +103,15 @@ const InputList: React.FC = ({ return ( {title && ( - - - {title} - {!required && ( - - (optional) - - )} - {tooltip && ( + + {title} + {!required && ( + + (optional) + + )} + {tooltip && ( + = ({ height={16} style={{ marginBottom: 4 }} /> - )} - - + + )} + )} {inputs.map((value, index) => ( diff --git a/frontend/webapp/reuseable-components/key-value-input-list/index.tsx b/frontend/webapp/reuseable-components/key-value-input-list/index.tsx index 1b25a0fbe7..cbd7904ea8 100644 --- a/frontend/webapp/reuseable-components/key-value-input-list/index.tsx +++ b/frontend/webapp/reuseable-components/key-value-input-list/index.tsx @@ -8,6 +8,7 @@ import { Tooltip } from '../tooltip'; interface KeyValueInputsListProps { initialKeyValuePairs?: { key: string; value: string }[]; + value?: { key: string; value: string }[]; title?: string; tooltip?: string; required?: boolean; @@ -67,13 +68,15 @@ const Title = styled(Text)` export const KeyValueInputsList: React.FC = ({ initialKeyValuePairs = [{ key: '', value: '' }], + value = [{ key: '', value: '' }], title, tooltip, required, onChange, }) => { - const [keyValuePairs, setKeyValuePairs] = - useState<{ key: string; value: string }[]>(initialKeyValuePairs); + const [keyValuePairs, setKeyValuePairs] = useState< + { key: string; value: string }[] + >(value || initialKeyValuePairs); const validPairsRef = useRef<{ key: string; value: string }[]>([]); From 6fedc1c551f25b5eb0c50c4124835e91dbf3fd01 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 20 Oct 2024 14:59:22 +0300 Subject: [PATCH 16/20] feat: show override name in overview --- .../webapp/reuseable-components/nodes-data-flow/builder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts index 23d570ec8b..7237229b94 100644 --- a/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts +++ b/frontend/webapp/reuseable-components/nodes-data-flow/builder.ts @@ -82,7 +82,9 @@ export const buildNodesAndEdges = ({ NODE_HEIGHT * (index + 1), { type: 'source', - title: source.name, + title: + source.name + + (source.reportedName ? ` (${source.reportedName})` : ''), subTitle: source.kind, imageUri: getMainContainerLanguageLogo(source), status: 'healthy', From 243f7a9a641e42b20d385422dd63df2023abe13b Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 20 Oct 2024 15:38:12 +0300 Subject: [PATCH 17/20] chore: wip --- .../webapp/hooks/destinations/useConnectDestinationForm.ts | 1 + frontend/webapp/reuseable-components/input-list/index.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts b/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts index 7f95908c57..1cae02d2a4 100644 --- a/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts +++ b/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts @@ -66,6 +66,7 @@ export function useConnectDestinationForm() { componentType, title: displayName, initialValues: initialValuesJson, + value: initialValuesJson, ...componentPropertiesJson, }; case 'keyValuePairs': diff --git a/frontend/webapp/reuseable-components/input-list/index.tsx b/frontend/webapp/reuseable-components/input-list/index.tsx index c3a3c2743e..67b935d7a3 100644 --- a/frontend/webapp/reuseable-components/input-list/index.tsx +++ b/frontend/webapp/reuseable-components/input-list/index.tsx @@ -87,7 +87,9 @@ const InputList: React.FC = ({ }; const handleDeleteInput = (index: number) => { - setInputs(inputs.filter((_, i) => i !== index)); + const newInputs = inputs.filter((_, i) => i !== index); + setInputs(newInputs); + onChange(newInputs); }; const handleInputChange = (value: string, index: number) => { From 4a4abd4863e5ad0b94a8b6119037a018eec82520 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 20 Oct 2024 15:56:15 +0300 Subject: [PATCH 18/20] chore: types --- .../dynamic-form-fields/index.tsx | 3 +-- .../main/overview/overview-drawer/index.tsx | 27 ++++++++++++++----- .../destinations/useConnectDestinationForm.ts | 12 ++++----- frontend/webapp/types/common.ts | 6 +++++ 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx index b935a558cf..5c6ae04ca1 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx @@ -1,6 +1,5 @@ import React from 'react'; - -import { INPUT_TYPES } from '@/utils/constants/string'; +import { INPUT_TYPES } from '@/utils'; import { Dropdown, Input, diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 28b0ff7570..5056f64111 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -13,6 +13,7 @@ import { WorkloadId, K8sActualSource, ActualDestination, + OVERVIEW_ENTITY_TYPES, PatchSourceRequestInput, } from '@/types'; @@ -46,10 +47,16 @@ const OverviewDrawer = () => { //TODO: split file to separate components by type: source, destination, action function initialTitle() { - if (selectedItem?.type === 'source' && selectedItem.item) { + if ( + selectedItem?.type === OVERVIEW_ENTITY_TYPES.SOURCE && + selectedItem.item + ) { const title = (selectedItem.item as K8sActualSource).reportedName; setTitle(title || ''); - } else if (selectedItem?.type === 'destination' && selectedItem.item) { + } else if ( + selectedItem?.type === OVERVIEW_ENTITY_TYPES.DESTINATION && + selectedItem.item + ) { const title = (selectedItem.item as ActualDestination).name; setTitle(title || ''); } else { @@ -58,7 +65,7 @@ const OverviewDrawer = () => { } const handleSave = async () => { - if (selectedItem?.type === 'destination') { + if (selectedItem?.type === OVERVIEW_ENTITY_TYPES.DESTINATION) { if (destinationDrawerRef.current && titleRef.current) { const name = titleRef.current.value; const destinationData = { @@ -77,11 +84,14 @@ const OverviewDrawer = () => { } } - if (selectedItem?.type === 'source') { + if (selectedItem?.type === OVERVIEW_ENTITY_TYPES.SOURCE) { if (titleRef.current) { const newTitle = titleRef.current.value; setTitle(newTitle); - if (selectedItem?.type === 'source' && selectedItem.item) { + if ( + selectedItem?.type === OVERVIEW_ENTITY_TYPES.SOURCE && + selectedItem.item + ) { const sourceItem = selectedItem.item as K8sActualSource; const sourceId: WorkloadId = { @@ -111,7 +121,10 @@ const OverviewDrawer = () => { }; const handleDelete = async () => { - if (selectedItem?.type === 'source' && selectedItem.item) { + if ( + selectedItem?.type === OVERVIEW_ENTITY_TYPES.SOURCE && + selectedItem.item + ) { const sourceItem = selectedItem.item as K8sActualSource; try { @@ -162,7 +175,7 @@ const OverviewDrawer = () => { {...{ isEditing, setIsEditing }} /> - {selectedItem.type === 'destination' ? ( + {selectedItem.type === OVERVIEW_ENTITY_TYPES.DESTINATION ? ( ( componentProperties, {} @@ -41,8 +41,8 @@ export function useConnectDestinationForm() { ...componentPropertiesJson, }; - case 'input': - case 'textarea': + case INPUT_TYPES.INPUT: + case INPUT_TYPES.TEXTAREA: componentPropertiesJson = safeJsonParse( componentProperties, [] @@ -54,7 +54,7 @@ export function useConnectDestinationForm() { ...componentPropertiesJson, }; - case 'multiInput': + case INPUT_TYPES.MULTI_INPUT: componentPropertiesJson = safeJsonParse( componentProperties, [] @@ -69,7 +69,7 @@ export function useConnectDestinationForm() { value: initialValuesJson, ...componentPropertiesJson, }; - case 'keyValuePairs': + case INPUT_TYPES.KEY_VALUE_PAIR: return { name, componentType, diff --git a/frontend/webapp/types/common.ts b/frontend/webapp/types/common.ts index 83e80afdc7..c90203c9ef 100644 --- a/frontend/webapp/types/common.ts +++ b/frontend/webapp/types/common.ts @@ -35,3 +35,9 @@ export interface StepProps { state: 'finish' | 'active' | 'disabled'; stepNumber: number; } + +export enum OVERVIEW_ENTITY_TYPES { + SOURCE = 'source', + DESTINATION = 'destination', + ACTION = 'action', +} From d5b4f9bc1e69a8a1ae6e0ec914a34ab6acc21114 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 20 Oct 2024 16:00:58 +0300 Subject: [PATCH 19/20] wip: reset after delete --- .../main/overview/overview-drawer/index.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 5056f64111..13eee904aa 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -120,6 +120,12 @@ const OverviewDrawer = () => { initialTitle(); }; + const handleClose = () => { + setIsEditing(false); + setDrawerItem(null); + setIsDeleteModalOpen(false); + }; + const handleDelete = async () => { if ( selectedItem?.type === OVERVIEW_ENTITY_TYPES.SOURCE && @@ -135,6 +141,7 @@ const OverviewDrawer = () => { selected: false, }, ]); + handleClose(); } catch (error) { console.error('Error deleting source:', error); } @@ -142,12 +149,6 @@ const OverviewDrawer = () => { setDrawerItem(null); // Close the drawer on delete }; - const handleClose = () => { - setIsEditing(false); - setDrawerItem(null); - setIsDeleteModalOpen(false); - }; - const handleCloseDeleteModal = () => { setIsDeleteModalOpen(false); }; From 09fe7af965265140c457e3479f15d66b5dae45b8 Mon Sep 17 00:00:00 2001 From: alonkeyval Date: Sun, 20 Oct 2024 16:04:34 +0300 Subject: [PATCH 20/20] wip: use file from guards --- .../webapp/containers/main/overview/overview-drawer/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 13eee904aa..64d6bbda88 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -13,6 +13,7 @@ import { WorkloadId, K8sActualSource, ActualDestination, + isActualDestination, OVERVIEW_ENTITY_TYPES, PatchSourceRequestInput, } from '@/types'; @@ -208,7 +209,7 @@ const OverviewDrawer = () => { }; function getItemImageByType(item: K8sActualSource | ActualDestination): string { - if ('destinationType' in item) { + if (isActualDestination(item)) { // item is of type ActualDestination return item.destinationType.imageUrl; } else {