diff --git a/cli/cmd/resources/ui.go b/cli/cmd/resources/ui.go index 7af7877bf..23e34ee09 100644 --- a/cli/cmd/resources/ui.go +++ b/cli/cmd/resources/ui.go @@ -233,6 +233,11 @@ func NewUIClusterRole() *rbacv1.ClusterRole { Resources: []string{"namespaces"}, Verbs: []string{"get", "list", "watch", "patch"}, }, + { + APIGroups: []string{""}, + Resources: []string{"services"}, + Verbs: []string{"list"}, + }, { APIGroups: []string{""}, Resources: []string{"configmaps"}, diff --git a/common/config/root.go b/common/config/root.go index 52ef40531..b619cda4e 100644 --- a/common/config/root.go +++ b/common/config/root.go @@ -7,6 +7,7 @@ import ( "github.com/goccy/go-yaml" "github.com/odigos-io/odigos/common" + "github.com/odigos-io/odigos/common/consts" ) const ( @@ -151,6 +152,13 @@ func getBasicConfig(memoryLimiterConfig GenericMap) (*Config, []string) { // setting it to a large value to avoid dropping batches. "max_recv_msg_size_mib": 128, "endpoint": "0.0.0.0:4317", + // The Node Collector opens a gRPC stream to send data. This ensures that the Node Collector establishes a new connection when the Gateway scales up to include additional instances. + "keepalive": GenericMap{ + "server_parameters": GenericMap{ + "max_connection_age": consts.GatewayMaxConnectionAge, + "max_connection_age_grace": consts.GatewayMaxConnectionAgeGrace, + }, + }, }, // Node collectors send in gRPC, so this is probably not needed "http": GenericMap{ diff --git a/common/config/testdata/debugexporter.yaml b/common/config/testdata/debugexporter.yaml index b84e8e67a..5a0985d6b 100644 --- a/common/config/testdata/debugexporter.yaml +++ b/common/config/testdata/debugexporter.yaml @@ -3,6 +3,10 @@ receivers: protocols: grpc: endpoint: 0.0.0.0:4317 + keepalive: + server_parameters: + max_connection_age: 15s + max_connection_age_grace: 2s max_recv_msg_size_mib: 128 http: endpoint: 0.0.0.0:4318 diff --git a/common/config/testdata/minimal.yaml b/common/config/testdata/minimal.yaml index c96436796..2dc86edc7 100644 --- a/common/config/testdata/minimal.yaml +++ b/common/config/testdata/minimal.yaml @@ -3,6 +3,10 @@ receivers: protocols: grpc: endpoint: 0.0.0.0:4317 + keepalive: + server_parameters: + max_connection_age: 15s + max_connection_age_grace: 2s max_recv_msg_size_mib: 128 http: endpoint: 0.0.0.0:4318 diff --git a/common/consts/consts.go b/common/consts/consts.go index cb22ea132..e84a8c08d 100644 --- a/common/consts/consts.go +++ b/common/consts/consts.go @@ -17,6 +17,10 @@ const ( InstrumentationDisabled = "disabled" OdigosReportedNameAnnotation = "odigos.io/reported-name" + // GatewayMaxConnectionAge and GatewayMaxConnectionAgeGrace are the default values for the gateway collector. + GatewayMaxConnectionAge = "15s" + GatewayMaxConnectionAgeGrace = "2s" + // Used to store the original value of the environment variable in the pod manifest. // This is used to restore the original value when an instrumentation is removed // or odigos is uninstalled. diff --git a/frontend/main.go b/frontend/main.go index ded2f2000..eddbb7f10 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -128,7 +128,12 @@ func httpFileServerWith404(fs http.FileSystem) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := fs.Open(r.URL.Path) if err != nil { - // Redirect to root path + // If file not found, serve .html of it (example: /choose-sources -> /choose-sources.html) + r.URL.Path = r.URL.Path + ".html" + } + _, err = fs.Open(r.URL.Path) + if err != nil { + // If .html file not found, this route does not exist at all (404) so we should redirect to default r.URL.Path = "/" } http.FileServer(fs).ServeHTTP(w, r) diff --git a/frontend/webapp/app/(setup)/choose-destination/page.tsx b/frontend/webapp/app/(setup)/choose-destination/page.tsx index 07187db2a..eae2863c9 100644 --- a/frontend/webapp/app/(setup)/choose-destination/page.tsx +++ b/frontend/webapp/app/(setup)/choose-destination/page.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SideMenu } from '@/components'; import { SideMenuWrapper } from '../styled'; -import { ChooseDestinationContainer } from '@/containers/main'; +import { AddDestinationContainer } from '@/containers/main'; export default function ChooseDestinationPage() { return ( @@ -10,7 +10,7 @@ export default function ChooseDestinationPage() { - + ); } diff --git a/frontend/webapp/app/globals.css b/frontend/webapp/app/globals.css index 6bbdf7e7c..a7d610642 100644 --- a/frontend/webapp/app/globals.css +++ b/frontend/webapp/app/globals.css @@ -2,3 +2,8 @@ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Kode+Mono:wght@100;200;300;400;500;600;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter+Tight:wght@400;700&display=swap'); + +* { + scrollbar-color: black transparent; + scrollbar-width: thin; +} \ No newline at end of file diff --git a/frontend/webapp/app/layout.tsx b/frontend/webapp/app/layout.tsx index 2d44f2bd9..62d808b5d 100644 --- a/frontend/webapp/app/layout.tsx +++ b/frontend/webapp/app/layout.tsx @@ -7,11 +7,10 @@ import { ApolloWrapper } from '@/lib'; import { ThemeProviderWrapper } from '@/styles'; const LAYOUT_STYLE: React.CSSProperties = { - margin: 0, position: 'fixed', - scrollbarWidth: 'none', width: '100vw', height: '100vh', + margin: 0, backgroundColor: '#111111', }; diff --git a/frontend/webapp/components/common/configured-fields/index.tsx b/frontend/webapp/components/common/configured-fields/index.tsx index 97df981b2..0070c5a44 100644 --- a/frontend/webapp/components/common/configured-fields/index.tsx +++ b/frontend/webapp/components/common/configured-fields/index.tsx @@ -82,9 +82,8 @@ export const ConfiguredFields: React.FC = ({ details }) = {details.map((detail, index) => ( - + {detail.title} - {detail.tooltip && Info} {detail.title === 'Status' ? : {parseValue(detail.value)}} diff --git a/frontend/webapp/components/destinations/add-destination-button/index.tsx b/frontend/webapp/components/destinations/add-destination-button/index.tsx deleted file mode 100644 index 281d403c7..000000000 --- a/frontend/webapp/components/destinations/add-destination-button/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; -import Image from 'next/image'; -import theme from '@/styles/theme'; -import styled from 'styled-components'; -import { Button, Text } from '@/reuseable-components'; - -const StyledAddDestinationButton = styled(Button)` - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 100%; -`; - -interface ModalActionComponentProps { - onClick: () => void; -} - -export function AddDestinationButton({ onClick }: ModalActionComponentProps) { - return ( - - back - - ADD DESTINATION - - - ); -} diff --git a/frontend/webapp/components/destinations/edit-destination-form/index.tsx b/frontend/webapp/components/destinations/edit-destination-form/index.tsx deleted file mode 100644 index 674de1f67..000000000 --- a/frontend/webapp/components/destinations/edit-destination-form/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { CheckboxList } from '@/reuseable-components'; -import type { DynamicField, ExportedSignals, SupportedDestinationSignals } from '@/types'; -import { DynamicConnectDestinationFormFields } from '@/containers/main/destinations/add-destination/dynamic-form-fields'; - -interface DestinationFormProps { - exportedSignals: ExportedSignals; - supportedSignals: SupportedDestinationSignals; - dynamicFields: DynamicField[]; - handleDynamicFieldChange: (name: string, value: any) => void; - handleSignalChange: (signal: keyof ExportedSignals, value: boolean) => void; -} - -const Container = styled.div` - display: flex; - flex-direction: column; - gap: 24px; - padding: 4px; -`; - -export const EditDestinationForm: React.FC = ({ exportedSignals, supportedSignals, dynamicFields, 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 ( - - - - - ); -}; diff --git a/frontend/webapp/components/destinations/index.ts b/frontend/webapp/components/destinations/index.ts deleted file mode 100644 index e852a65cc..000000000 --- a/frontend/webapp/components/destinations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './add-destination-button'; -export * from './monitors-tap-list'; -export * from './edit-destination-form'; diff --git a/frontend/webapp/components/destinations/monitors-tap-list/index.tsx b/frontend/webapp/components/destinations/monitors-tap-list/index.tsx deleted file mode 100644 index fbc4685b4..000000000 --- a/frontend/webapp/components/destinations/monitors-tap-list/index.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { Text, Tag } from '@/reuseable-components'; -import { MONITORS_OPTIONS } from '@/utils'; -import Image from 'next/image'; - -interface MonitorButtonsProps { - selectedMonitors: string[]; - onMonitorSelect: (monitor: string) => void; -} - -const MonitorButtonsContainer = styled.div` - display: flex; - gap: 8px; - margin-left: 12px; -`; - -const MonitorsTitle = styled(Text)` - opacity: 0.8; - font-size: 14px; - margin-left: 32px; -`; - -const MonitorsTapList: React.FC = ({ - selectedMonitors, - onMonitorSelect, -}) => { - return ( - <> - Monitor by: - - {MONITORS_OPTIONS.map((monitor) => ( - onMonitorSelect(monitor.id)} - > - monitor - {monitor.value} - - ))} - - - ); -}; - -export { MonitorsTapList }; diff --git a/frontend/webapp/components/index.ts b/frontend/webapp/components/index.ts index aa76f1ec5..73363b759 100644 --- a/frontend/webapp/components/index.ts +++ b/frontend/webapp/components/index.ts @@ -1,7 +1,6 @@ export * from './setup'; export * from './overview'; export * from './common'; -export * from './destinations'; export * from './main'; export * from './modals'; export * from './notification'; diff --git a/frontend/webapp/containers/main/actions/action-drawer-container/build-card-from-action-spec.ts b/frontend/webapp/containers/main/actions/action-drawer/build-card-from-action-spec.ts similarity index 100% rename from frontend/webapp/containers/main/actions/action-drawer-container/build-card-from-action-spec.ts rename to frontend/webapp/containers/main/actions/action-drawer/build-card-from-action-spec.ts diff --git a/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx b/frontend/webapp/containers/main/actions/action-drawer/index.tsx similarity index 87% rename from frontend/webapp/containers/main/actions/action-drawer-container/index.tsx rename to frontend/webapp/containers/main/actions/action-drawer/index.tsx index d4fb5d123..e64aefc18 100644 --- a/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/actions/action-drawer/index.tsx @@ -1,14 +1,14 @@ import React, { useMemo, useState } from 'react'; +import { ActionFormBody } from '../'; import styled from 'styled-components'; import { getActionIcon } from '@/utils'; import { useDrawerStore } from '@/store'; import { CardDetails } from '@/components'; import type { ActionDataParsed } from '@/types'; -import { ChooseActionBody } from '../choose-action-body'; import { useActionCRUD, useActionFormData } from '@/hooks'; import OverviewDrawer from '../../overview/overview-drawer'; +import { ACTION_OPTIONS } from '../action-modal/action-options'; import buildCardFromActionSpec from './build-card-from-action-spec'; -import { ACTION_OPTIONS } from '../choose-action-modal/action-options'; interface Props {} @@ -36,7 +36,10 @@ const ActionDrawer: React.FC = () => { } const { item } = selectedItem as { item: ActionDataParsed }; - const found = ACTION_OPTIONS.find(({ type }) => type === item.type) || ACTION_OPTIONS.find(({ id }) => id === 'sampler')?.items?.find(({ type }) => type === item.type); + const found = + ACTION_OPTIONS.find(({ type }) => type === item.type) || + ACTION_OPTIONS.find(({ id }) => id === 'attributes')?.items?.find(({ type }) => type === item.type) || + ACTION_OPTIONS.find(({ id }) => id === 'sampler')?.items?.find(({ type }) => type === item.type); if (!found) return undefined; @@ -86,7 +89,7 @@ const ActionDrawer: React.FC = () => { > {isEditing && thisAction ? ( - = ({ value, setValue }) => { key: obj.attributeName, value: obj.attributeStringValue, })), - [value] + [value], ); - const handleChange = ( - arr: { - key: string; - value: string; - }[] - ) => { + const handleChange = (arr: { key: string; value: string }[]) => { const payload: Parsed = { clusterAttributes: arr.map((obj) => ({ attributeName: obj.key, @@ -38,7 +33,7 @@ const AddClusterInfo: React.FC = ({ value, setValue }) => { setValue(str); }; - return ; + return ; }; export default AddClusterInfo; diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/delete-attributes.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/delete-attributes.tsx similarity index 100% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/delete-attributes.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/delete-attributes.tsx diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/error-sampler.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx similarity index 90% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/error-sampler.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx index 692fa30aa..e089dcf90 100644 --- a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/error-sampler.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx @@ -17,11 +17,7 @@ const ErrorSampler: React.FC = ({ value, setValue }) => { const mappedValue = useMemo(() => safeJsonParse(value, { fallback_sampling_ratio: 0 }).fallback_sampling_ratio, [value]); const handleChange = (val: string) => { - let num = Number(val); - - if (Number.isNaN(num) || num < MIN || num > MAX) { - num = MIN; - } + const num = Math.max(MIN, Math.min(Number(val), MAX)) || MIN; const payload: Parsed = { fallback_sampling_ratio: num, diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/index.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/index.tsx similarity index 100% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/index.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/index.tsx diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/latency-sampler.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/latency-sampler.tsx similarity index 100% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/latency-sampler.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/latency-sampler.tsx diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/pii-masking.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/pii-masking.tsx similarity index 100% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/pii-masking.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/pii-masking.tsx diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/probabilistic-sampler.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/probabilistic-sampler.tsx similarity index 100% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/probabilistic-sampler.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/probabilistic-sampler.tsx diff --git a/frontend/webapp/containers/main/actions/choose-action-body/custom-fields/rename-attributes.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/rename-attributes.tsx similarity index 100% rename from frontend/webapp/containers/main/actions/choose-action-body/custom-fields/rename-attributes.tsx rename to frontend/webapp/containers/main/actions/action-form-body/custom-fields/rename-attributes.tsx diff --git a/frontend/webapp/containers/main/actions/choose-action-body/index.tsx b/frontend/webapp/containers/main/actions/action-form-body/index.tsx similarity index 86% rename from frontend/webapp/containers/main/actions/choose-action-body/index.tsx rename to frontend/webapp/containers/main/actions/action-form-body/index.tsx index 8421a32c0..cc55afe26 100644 --- a/frontend/webapp/containers/main/actions/choose-action-body/index.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/index.tsx @@ -2,10 +2,10 @@ import React from 'react'; import styled from 'styled-components'; import { type ActionInput } from '@/types'; import ActionCustomFields from './custom-fields'; -import { type ActionOption } from '../choose-action-modal/action-options'; +import { type ActionOption } from '../action-modal/action-options'; import { DocsButton, Input, Text, TextArea, MonitoringCheckboxes, SectionTitle, ToggleButtons } from '@/reuseable-components'; -interface ChooseActionContentProps { +interface Props { isUpdate?: boolean; action: ActionOption; formData: ActionInput; @@ -23,7 +23,7 @@ const FieldTitle = styled(Text)` margin-bottom: 12px; `; -const ChooseActionBody: React.FC = ({ isUpdate, action, formData, handleFormChange }) => { +export const ActionFormBody: React.FC = ({ isUpdate, action, formData, handleFormChange }) => { return ( {isUpdate && ( @@ -45,5 +45,3 @@ const ChooseActionBody: React.FC = ({ isUpdate, action ); }; - -export { ChooseActionBody }; diff --git a/frontend/webapp/containers/main/actions/action-modal/action-options.ts b/frontend/webapp/containers/main/actions/action-modal/action-options.ts new file mode 100644 index 000000000..1ee2cb98d --- /dev/null +++ b/frontend/webapp/containers/main/actions/action-modal/action-options.ts @@ -0,0 +1,102 @@ +import { ActionsType } from '@/types'; +import { getActionIcon, SignalUppercase } from '@/utils'; + +export type ActionOption = { + id: string; + type?: ActionsType; + label: string; + description?: string; + docsEndpoint?: string; + docsDescription?: string; + icon?: string; + items?: ActionOption[]; + allowedSignals?: SignalUppercase[]; +}; + +export const ACTION_OPTIONS: ActionOption[] = [ + { + id: 'attributes', + label: 'Attributes', + icon: getActionIcon('attributes'), + items: [ + { + id: 'add_cluster_info', + label: 'Add Cluster Info', + description: 'Add static cluster-scoped attributes to your data.', + type: ActionsType.ADD_CLUSTER_INFO, + icon: getActionIcon(ActionsType.ADD_CLUSTER_INFO), + docsEndpoint: '/pipeline/actions/attributes/addclusterinfo', + docsDescription: 'The “Add Cluster Info” Odigos Action can be used to add resource attributes to telemetry signals originated from the k8s cluster where the Odigos is running.', + allowedSignals: ['TRACES', 'METRICS', 'LOGS'], + }, + { + id: 'delete_attribute', + label: 'Delete Attribute', + description: 'Delete attributes from logs, metrics, and traces.', + type: ActionsType.DELETE_ATTRIBUTES, + icon: getActionIcon(ActionsType.DELETE_ATTRIBUTES), + docsEndpoint: '/pipeline/actions/attributes/deleteattribute', + docsDescription: 'The “Delete Attribute” Odigos Action can be used to delete attributes from logs, metrics, and traces.', + allowedSignals: ['TRACES', 'METRICS', 'LOGS'], + }, + { + id: 'rename_attribute', + label: 'Rename Attribute', + description: 'Rename attributes in logs, metrics, and traces.', + type: ActionsType.RENAME_ATTRIBUTES, + icon: getActionIcon(ActionsType.RENAME_ATTRIBUTES), + docsEndpoint: '/pipeline/actions/attributes/rename-attribute', + docsDescription: + 'The “Rename Attribute” Odigos Action can be used to rename attributes from logs, metrics, and traces. Different instrumentations might use different attribute names for similar information. This action let’s you to consolidate the names across your cluster.', + allowedSignals: ['TRACES', 'METRICS', 'LOGS'], + }, + { + id: 'pii-masking', + label: 'PII Masking', + description: 'Mask PII data in your traces.', + type: ActionsType.PII_MASKING, + icon: getActionIcon(ActionsType.PII_MASKING), + docsEndpoint: '/pipeline/actions/attributes/piimasking', + docsDescription: 'The “PII Masking” Odigos Action can be used to mask PII data from span attribute values.', + allowedSignals: ['TRACES'], + }, + ], + }, + { + id: 'sampler', + label: 'Samplers', + icon: getActionIcon('sampler'), + items: [ + { + id: 'error-sampler', + label: 'Error Sampler', + description: 'Sample errors based on percentage.', + type: ActionsType.ERROR_SAMPLER, + icon: getActionIcon('sampler'), + docsEndpoint: '/pipeline/actions/sampling/errorsampler', + docsDescription: 'The “Error Sampler” Odigos Action is a Global Action that supports error sampling by filtering out non-error traces.', + allowedSignals: ['TRACES'], + }, + { + id: 'probabilistic-sampler', + label: 'Probabilistic Sampler', + description: 'Sample traces based on percentage.', + type: ActionsType.PROBABILISTIC_SAMPLER, + icon: getActionIcon('sampler'), + docsEndpoint: '/pipeline/actions/sampling/probabilisticsampler', + docsDescription: 'The “Probabilistic Sampler” Odigos Action supports probabilistic sampling based on a configured sampling percentage applied to the TraceID.', + allowedSignals: ['TRACES'], + }, + { + id: 'latency-action', + label: 'Latency Action', + description: 'Add latency to your traces.', + type: ActionsType.LATENCY_SAMPLER, + icon: getActionIcon('sampler'), + docsEndpoint: '/pipeline/actions/sampling/latencysampler', + docsDescription: 'The “Latency Sampler” Odigos Action is an Endpoint Action that samples traces based on their duration for a specific service and endpoint (HTTP route) filter.', + allowedSignals: ['TRACES'], + }, + ], + }, +]; diff --git a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx b/frontend/webapp/containers/main/actions/action-modal/index.tsx similarity index 86% rename from frontend/webapp/containers/main/actions/choose-action-modal/index.tsx rename to frontend/webapp/containers/main/actions/action-modal/index.tsx index df0171807..3aa6a5164 100644 --- a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx +++ b/frontend/webapp/containers/main/actions/action-modal/index.tsx @@ -1,16 +1,16 @@ -import { ChooseActionBody } from '../'; +import { ActionFormBody } from '../'; import React, { useMemo, useState } from 'react'; import { CenterThis, ModalBody } from '@/styles'; import { useActionCRUD, useActionFormData } from '@/hooks/actions'; import { ACTION_OPTIONS, type ActionOption } from './action-options'; import { AutocompleteInput, Modal, NavigationButtons, Divider, FadeLoader, SectionTitle } from '@/reuseable-components'; -interface AddActionModalProps { +interface Props { isOpen: boolean; onClose: () => void; } -export const AddActionModal: React.FC = ({ isOpen, onClose }) => { +export const ActionModal: React.FC = ({ isOpen, onClose }) => { const { formData, handleFormChange, resetFormData, validateForm } = useActionFormData(); const { createAction, loading } = useActionCRUD({ onSuccess: handleClose }); const [selectedItem, setSelectedItem] = useState(undefined); @@ -52,7 +52,7 @@ export const AddActionModal: React.FC = ({ isOpen, onClose } > - + {!!selectedItem?.type ? ( @@ -64,7 +64,7 @@ export const AddActionModal: React.FC = ({ isOpen, onClose ) : ( - + )} ) : null} diff --git a/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts b/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts deleted file mode 100644 index 50de8bdb3..000000000 --- a/frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ActionsType } from '@/types'; -import { getActionIcon, SignalUppercase } from '@/utils'; - -export type ActionOption = { - id: string; - type?: ActionsType; - label: string; - description?: string; - docsEndpoint?: string; - docsDescription?: string; - icon?: string; - items?: ActionOption[]; - allowedSignals?: SignalUppercase[]; -}; - -export const ACTION_OPTIONS: ActionOption[] = [ - { - id: 'add_cluster_info', - label: 'Add Cluster Info', - description: 'Add static cluster-scoped attributes to your data.', - type: ActionsType.ADD_CLUSTER_INFO, - icon: getActionIcon(ActionsType.ADD_CLUSTER_INFO), - docsEndpoint: '/pipeline/actions/attributes/addclusterinfo', - docsDescription: - 'The “Add Cluster Info” Odigos Action can be used to add resource attributes to telemetry signals originated from the k8s cluster where the Odigos is running.', - allowedSignals: ['TRACES', 'METRICS', 'LOGS'], - }, - { - id: 'delete_attribute', - label: 'Delete Attribute', - description: 'Delete attributes from logs, metrics, and traces.', - type: ActionsType.DELETE_ATTRIBUTES, - icon: getActionIcon(ActionsType.DELETE_ATTRIBUTES), - docsEndpoint: '/pipeline/actions/attributes/deleteattribute', - docsDescription: 'The “Delete Attribute” Odigos Action can be used to delete attributes from logs, metrics, and traces.', - allowedSignals: ['TRACES', 'METRICS', 'LOGS'], - }, - { - id: 'rename_attribute', - label: 'Rename Attribute', - description: 'Rename attributes in logs, metrics, and traces.', - type: ActionsType.RENAME_ATTRIBUTES, - icon: getActionIcon(ActionsType.RENAME_ATTRIBUTES), - docsEndpoint: '/pipeline/actions/attributes/rename-attribute', - docsDescription: - 'The “Rename Attribute” Odigos Action can be used to rename attributes from logs, metrics, and traces. Different instrumentations might use different attribute names for similar information. This action let’s you to consolidate the names across your cluster.', - allowedSignals: ['TRACES', 'METRICS', 'LOGS'], - }, - { - id: 'pii-masking', - label: 'PII Masking', - description: 'Mask PII data in your traces.', - type: ActionsType.PII_MASKING, - icon: getActionIcon(ActionsType.PII_MASKING), - docsEndpoint: '/pipeline/actions/attributes/piimasking', - docsDescription: 'The “PII Masking” Odigos Action can be used to mask PII data from span attribute values.', - allowedSignals: ['TRACES'], - }, - { - id: 'sampler', - label: 'Samplers', - icon: getActionIcon('sampler'), - items: [ - { - id: 'error-sampler', - label: 'Error Sampler', - description: 'Sample errors based on percentage.', - type: ActionsType.ERROR_SAMPLER, - icon: getActionIcon('sampler'), - docsEndpoint: '/pipeline/actions/sampling/errorsampler', - docsDescription: 'The “Error Sampler” Odigos Action is a Global Action that supports error sampling by filtering out non-error traces.', - allowedSignals: ['TRACES'], - }, - { - id: 'probabilistic-sampler', - label: 'Probabilistic Sampler', - description: 'Sample traces based on percentage.', - type: ActionsType.PROBABILISTIC_SAMPLER, - icon: getActionIcon('sampler'), - docsEndpoint: '/pipeline/actions/sampling/probabilisticsampler', - docsDescription: - 'The “Probabilistic Sampler” Odigos Action supports probabilistic sampling based on a configured sampling percentage applied to the TraceID.', - allowedSignals: ['TRACES'], - }, - { - id: 'latency-action', - label: 'Latency Action', - description: 'Add latency to your traces.', - type: ActionsType.LATENCY_SAMPLER, - icon: getActionIcon('sampler'), - docsEndpoint: '/pipeline/actions/sampling/latencysampler', - docsDescription: - 'The “Latency Sampler” Odigos Action is an Endpoint Action that samples traces based on their duration for a specific service and endpoint (HTTP route) filter.', - allowedSignals: ['TRACES'], - }, - ], - }, -]; diff --git a/frontend/webapp/containers/main/actions/index.ts b/frontend/webapp/containers/main/actions/index.ts index f3e35db2c..b588abd98 100644 --- a/frontend/webapp/containers/main/actions/index.ts +++ b/frontend/webapp/containers/main/actions/index.ts @@ -1,3 +1,3 @@ -export * from './choose-action-modal'; -export * from './choose-action-body'; -export * from './action-drawer-container'; +export * from './action-modal'; +export * from './action-form-body'; +export * from './action-drawer'; diff --git a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx deleted file mode 100644 index b8d158a97..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useState, useRef, useCallback } from 'react'; -import type { DestinationTypeItem } from '@/types'; -import { ChooseDestinationModalBody } from '../choose-destination-modal-body'; -import { ConnectDestinationModalBody } from '../connect-destination-modal-body'; -import { Modal, type NavigationButtonProps, NavigationButtons } from '@/reuseable-components'; - -interface AddDestinationModalProps { - isOpen: boolean; - onClose: () => void; -} - -export const AddDestinationModal: React.FC = ({ isOpen, onClose }) => { - const submitRef = useRef<(() => void) | null>(null); - const [selectedItem, setSelectedItem] = useState(); - const [isFormValid, setIsFormValid] = useState(false); - - const handleNextStep = useCallback((item: DestinationTypeItem) => { - setSelectedItem(item); - }, []); - - const handleNext = useCallback(() => { - if (submitRef.current) { - submitRef.current(); - setSelectedItem(undefined); - onClose(); - } - }, [onClose]); - - const handleBack = useCallback(() => { - setSelectedItem(undefined); - }, []); - - const handleClose = useCallback(() => { - setSelectedItem(undefined); - onClose(); - }, [onClose]); - - const renderHeaderButtons = () => { - const buttons: NavigationButtonProps[] = [ - { - label: 'DONE', - variant: 'primary' as const, - disabled: !isFormValid, - onClick: handleNext, - }, - ]; - - if (!!selectedItem) { - buttons.unshift({ - label: 'BACK', - iconSrc: '/icons/common/arrow-white.svg', - variant: 'secondary' as const, - onClick: handleBack, - }); - } - - return buttons; - }; - - const renderModalBody = () => { - return selectedItem ? ( - - ) : ( - - ); - }; - - return ( - }> - {renderModalBody()} - - ); -}; diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx deleted file mode 100644 index 87d91e948..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useState } from 'react'; -import styled from 'styled-components'; -import { DropdownOption } from '@/types'; -import { MONITORS_OPTIONS } from '@/utils'; -import { Checkbox, Dropdown, Input } from '@/reuseable-components'; - -interface FilterComponentProps { - selectedTag: DropdownOption | undefined; - onTagSelect: (option: DropdownOption) => void; - onSearch: (value: string) => void; - selectedMonitors: string[]; - onMonitorSelect: (monitor: string) => void; -} - -const InputAndDropdownContainer = styled.div` - display: flex; - gap: 12px; - width: 370px; -`; - -const FilterContainer = styled.div` - display: flex; - align-items: center; - padding: 24px 0; -`; - -const MonitorButtonsContainer = styled.div` - display: flex; - gap: 32px; - margin-left: 32px; -`; - -const DROPDOWN_OPTIONS = [ - { value: 'All types', id: 'all' }, - { value: 'Managed', id: 'managed' }, - { value: 'Self-hosted', id: 'self hosted' }, -]; - -const DestinationFilterComponent: React.FC = ({ selectedTag, selectedMonitors, onTagSelect, onSearch, onMonitorSelect }) => { - const [searchTerm, setSearchTerm] = useState(''); - - const handleSearchChange = (e: React.ChangeEvent) => { - const value = e.target.value; - setSearchTerm(value); - onSearch(value); - }; - - return ( - - -
- -
- -
- - - {MONITORS_OPTIONS.map((monitor) => ( - onMonitorSelect(monitor.id)} - disabled={selectedMonitors.length === 1 && selectedMonitors.includes(monitor.id)} - /> - ))} - -
- ); -}; - -export { DestinationFilterComponent }; diff --git a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx deleted file mode 100644 index a0e09537f..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useMemo, useState } from 'react'; -import { SideMenu } from '@/components'; -import { useDestinationTypes } from '@/hooks'; -import { DestinationsList } from '../destinations-list'; -import { Body, Container, SideMenuWrapper } from '../styled'; -import { Divider, SectionTitle } from '@/reuseable-components'; -import { DestinationFilterComponent } from '../choose-destination-menu'; -import { StepProps, DropdownOption, DestinationTypeItem } from '@/types'; - -interface ChooseDestinationModalBodyProps { - onSelect: (item: DestinationTypeItem) => void; -} - -const SIDE_MENU_DATA: StepProps[] = [ - { - title: 'DESTINATIONS', - state: 'active', - stepNumber: 1, - }, - { - title: 'CONNECTION', - state: 'disabled', - stepNumber: 2, - }, -]; - -const DEFAULT_MONITORS = ['logs', 'metrics', 'traces']; -const DEFAULT_DROPDOWN_VALUE = { id: 'all', value: 'All types' }; - -export function ChooseDestinationModalBody({ - onSelect, -}: ChooseDestinationModalBodyProps) { - const [searchValue, setSearchValue] = useState(''); - const [selectedMonitors, setSelectedMonitors] = - useState(DEFAULT_MONITORS); - const [dropdownValue, setDropdownValue] = useState( - DEFAULT_DROPDOWN_VALUE - ); - - const { destinations } = useDestinationTypes(); - - function handleTagSelect(option: DropdownOption) { - setDropdownValue(option); - } - - const filteredDestinations = useMemo(() => { - return destinations - .map((category) => { - const filteredItems = category.items.filter((item) => { - const matchesSearch = searchValue - ? item.displayName.toLowerCase().includes(searchValue.toLowerCase()) - : true; - - const matchesDropdown = - dropdownValue.id !== 'all' - ? category.name === dropdownValue.id - : true; - - const matchesMonitor = selectedMonitors.length - ? selectedMonitors.some( - (monitor) => item.supportedSignals[monitor]?.supported - ) - : true; - - return matchesSearch && matchesDropdown && matchesMonitor; - }); - - return { ...category, items: filteredItems }; - }) - .filter((category) => category.items.length > 0); // Filter out empty categories - }, [destinations, searchValue, dropdownValue, selectedMonitors]); - - function onMonitorSelect(monitor: string) { - if (selectedMonitors.includes(monitor)) { - setSelectedMonitors(selectedMonitors.filter((item) => item !== monitor)); - } else { - setSelectedMonitors([...selectedMonitors, monitor]); - } - } - - return ( - - - - - - - - - - - - ); -} diff --git a/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx index 68caac63e..175700e4e 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/configured-destinations-list/index.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useState } from 'react'; import Image from 'next/image'; import styled from 'styled-components'; -import { ConfiguredFields } from '@/components'; -import { ConfiguredDestination } from '@/types'; -import { Divider, Text } from '@/reuseable-components'; +import { ConfiguredFields, DeleteWarning } from '@/components'; +import { IAppState, useAppStore } from '@/store'; +import type { ConfiguredDestination } from '@/types'; +import { Button, Divider, Text } from '@/reuseable-components'; const Container = styled.div` display: flex; @@ -72,41 +73,22 @@ const TextWrapper = styled.div` justify-content: space-between; `; -const ExpandIconContainer = styled.div` +const IconsContainer = styled.div` display: flex; justify-content: center; align-items: center; margin-right: 16px; `; -const IconBorder = styled.div` - height: 16px; - width: 1px; - margin-right: 12px; - background: ${({ theme }) => theme.colors.border}; -`; - -const ExpandIconWrapper = styled.div<{ $expand?: boolean }>` - display: flex; - width: 36px; - height: 36px; - cursor: pointer; - justify-content: center; - align-items: center; - border-radius: 100%; +const IconButton = styled(Button)<{ $expand?: boolean }>` transition: background 0.3s ease 0s, transform 0.3s ease 0s; - transform: ${({ $expand }) => ($expand ? 'rotate(180deg)' : 'rotate(0deg)')}; - &:hover { - background: ${({ theme }) => theme.colors.translucent_bg}; - } + transform: ${({ $expand }) => ($expand ? 'rotate(-180deg)' : 'rotate(0deg)')}; `; -interface DestinationsListProps { - data: ConfiguredDestination[]; -} - -function ConfiguredDestinationsListItem({ item }: { item: ConfiguredDestination }) { - const [expand, setExpand] = React.useState(false); +const ConfiguredDestinationsListItem: React.FC<{ item: ConfiguredDestination; isLastItem: boolean }> = ({ item, isLastItem }) => { + const [expand, setExpand] = useState(false); + const [deleteWarning, setDeleteWarning] = useState(false); + const { removeConfiguredDestination } = useAppStore((state) => state); function renderSupportedSignals(item: ConfiguredDestination) { const supportedSignals = item.exportedSignals; @@ -127,44 +109,63 @@ function ConfiguredDestinationsListItem({ item }: { item: ConfiguredDestination } return ( - - - - - destination - - - {item.displayName} - {renderSupportedSignals(item)} - - - - - - setExpand(!expand)}> - destination - - - - - {expand && ( - - - - - )} - + <> + + + + + destination + + + {item.displayName} + {renderSupportedSignals(item)} + + + + + setDeleteWarning(true)}> + delete + + + setExpand(!expand)}> + show more + + + + + {expand && ( + + + + + )} + + + removeConfiguredDestination(item)} + onDeny={() => setDeleteWarning(false)} + /> + ); -} +}; -const ConfiguredDestinationsList: React.FC = ({ data }) => { +export const ConfiguredDestinationsList: React.FC<{ data: IAppState['configuredDestinations'] }> = ({ data }) => { return ( - {data.map((item) => ( - + {data.map(({ stored }) => ( + ))} ); }; - -export { ConfiguredDestinationsList }; diff --git a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx b/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx deleted file mode 100644 index c94d7ef21..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NotificationNote } from '@/reuseable-components'; -import styled from 'styled-components'; - -export const ConnectionNotification = ({ showConnectionError, destination }) => ( - <> - {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 deleted file mode 100644 index 4948674a1..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/form-container.tsx +++ /dev/null @@ -1,46 +0,0 @@ -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%; - 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 deleted file mode 100644 index ed970b343..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'; -import { useAppStore } from '@/store'; -import { INPUT_TYPES } from '@/utils'; -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 { Divider, SectionTitle } from '@/reuseable-components'; -import { ConnectionNotification } from './connection-notification'; -import { useComputePlatform, useConnectDestinationForm, useConnectEnv, useDestinationFormData, useEditDestinationFormHandlers } from '@/hooks'; -import { StepProps, DestinationInput, DestinationTypeItem, DestinationDetailsResponse, ConfiguredDestination } from '@/types'; - -const SIDE_MENU_DATA: StepProps[] = [ - { - title: 'DESTINATIONS', - state: 'finish', - stepNumber: 1, - }, - { - title: 'CONNECTION', - state: 'active', - stepNumber: 2, - }, -]; - -interface ConnectDestinationModalBodyProps { - destination: DestinationTypeItem | undefined; - onSubmitRef: React.MutableRefObject<(() => void) | null>; - onFormValidChange: (isValid: boolean) => void; -} - -export function ConnectDestinationModalBody({ destination, onSubmitRef, onFormValidChange }: ConnectDestinationModalBodyProps) { - const [destinationName, setDestinationName] = useState(''); - const [showConnectionError, setShowConnectionError] = useState(false); - - const { dynamicFields, exportedSignals, setDynamicFields, setExportedSignals } = useDestinationFormData(); - - const { connectEnv } = useConnectEnv(); - const { refetch } = useComputePlatform(); - const { buildFormDynamicFields } = useConnectDestinationForm(); - - const { handleDynamicFieldChange, handleSignalChange } = useEditDestinationFormHandlers(setExportedSignals, setDynamicFields); - - const addConfiguredDestination = useAppStore(({ addConfiguredDestination }) => addConfiguredDestination); - - const { data } = useQuery(GET_DESTINATION_TYPE_DETAILS, { - variables: { type: destination?.type }, - skip: !destination, - }); - - useLayoutEffect(() => { - if (!destination) return; - const { logs, metrics, traces } = destination.supportedSignals; - setExportedSignals({ - logs: logs.supported, - metrics: metrics.supported, - traces: traces.supported, - }); - }, [destination, setExportedSignals]); - - useEffect(() => { - if (data && destination) { - const df = buildFormDynamicFields(data.destinationTypeDetails.fields); - - const newDynamicFields = df.map((field) => { - if (destination.fields && field?.name in destination.fields) { - return { - ...field, - value: destination.fields[field.name], - }; - } - return field; - }); - - setDynamicFields(newDynamicFields); - } - }, [data, destination]); - - useEffect(() => { - // Assign handleSubmit to the onSubmitRef so it can be triggered externally - onSubmitRef.current = handleSubmit; - }, [dynamicFields, destinationName, exportedSignals]); - - useEffect(() => { - const isFormValid = dynamicFields.every((field) => (field.required ? field.value : true)); - onFormValidChange(isFormValid); - }, [dynamicFields]); - - const monitors = useMemo(() => { - if (!destination) return []; - const { logs, metrics, traces } = destination.supportedSignals; - - return [logs.supported && { id: 'logs', title: 'Logs' }, metrics.supported && { id: 'metrics', title: 'Metrics' }, traces.supported && { id: 'traces', title: 'Traces' }].filter(Boolean); - }, [destination]); - - function onDynamicFieldChange(name: string, value: any) { - setShowConnectionError(false); - handleDynamicFieldChange(name, value); - } - function processFieldValue(field) { - return field.componentType === INPUT_TYPES.DROPDOWN ? field.value.value : field.value; - } - - function processFormFields() { - // Prepare fields for the request body - return dynamicFields.map((field) => ({ - key: field.name, - value: processFieldValue(field), - })); - } - - async function handleSubmit() { - // Prepare fields for the request body - const fields = processFormFields(); - - // Function to store configured destination to display in the UI - function storeConfiguredDestination() { - const destinationTypeDetails = dynamicFields.map((field) => ({ - title: field.title, - value: processFieldValue(field), - })); - - // Add 'Destination name' as the first item - destinationTypeDetails.unshift({ - title: 'Destination name', - value: destinationName, - }); - - // Construct the configured destination object - const storedDestination: ConfiguredDestination = { - exportedSignals, - destinationTypeDetails, - type: destination?.type || '', - imageUrl: destination?.imageUrl || '', - category: '', // Could be handled in a more dynamic way if needed - displayName: destination?.displayName || '', - }; - - // Dispatch action to store the destination - addConfiguredDestination(storedDestination); - refetch(); - } - - // Prepare the request body - const body: DestinationInput = { - name: destinationName, - type: destination?.type || '', - exportedSignals, - fields, - }; - - try { - // Await connection and store the configured destination if successful - await connectEnv(body, storeConfiguredDestination); - // await connectEnv(body, refetch); - } catch (error) { - console.error('Failed to submit destination configuration:', error); - // Handle error (e.g., show notification or alert) - } - } - - const actionButton = useMemo(() => { - if (!!destination?.testConnectionSupported) { - return ( - { - setShowConnectionError(true); - onFormValidChange(false); - }} - destination={{ - name: destinationName, - type: destination?.type || '', - exportedSignals, - fields: processFormFields(), - }} - /> - ); - } - return null; - }, [destination, destinationName, exportedSignals, processFormFields, onFormValidChange]); - - if (!destination) return null; - - return ( - - - - - - - - - - - - - ); -} diff --git a/frontend/webapp/containers/main/destinations/add-destination/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/index.tsx index 7a34ce103..b2d90d994 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/index.tsx @@ -1,17 +1,15 @@ import React, { useState } from 'react'; +import Image from 'next/image'; import { ROUTES } from '@/utils'; +import theme from '@/styles/theme'; import { useAppStore } from '@/store'; import styled from 'styled-components'; +import { SetupHeader } from '@/components'; import { useRouter } from 'next/navigation'; -import { AddDestinationModal } from './add-destination-modal'; -import { AddDestinationButton, SetupHeader } from '@/components'; -import { NotificationNote, SectionTitle } from '@/reuseable-components'; +import { useDestinationCRUD, useSourceCRUD } from '@/hooks'; +import { DestinationModal } from '../destination-modal'; import { ConfiguredDestinationsList } from './configured-destinations-list'; - -const AddDestinationButtonWrapper = styled.div` - width: 100%; - margin-top: 24px; -`; +import { Button, NotificationNote, SectionTitle, Text } from '@/reuseable-components'; const ContentWrapper = styled.div` width: 640px; @@ -26,31 +24,44 @@ const NotificationNoteWrapper = styled.div` margin-top: 24px; `; -export function ChooseDestinationContainer() { - const [isModalOpen, setModalOpen] = useState(false); +const AddDestinationButtonWrapper = styled.div` + width: 100%; + margin-top: 24px; +`; + +const StyledAddDestinationButton = styled(Button)` + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; +`; +export function AddDestinationContainer() { const router = useRouter(); - const { configuredSources, configuredDestinations, resetState } = useAppStore((state) => state); - - const isSourcesListEmpty = () => { - const sourceLen = Object.keys(configuredSources).length === 0; - if (sourceLen) { - return true; - } - - let empty = true; - for (const source in configuredSources) { - if (configuredSources[source].length > 0) { - empty = false; - break; - } - } - return empty; - }; + const { createSources, loading: sourcesLoading } = useSourceCRUD(); + const { createDestination, loading: destinationsLoading } = useDestinationCRUD(); + const { configuredSources, configuredFutureApps, configuredDestinations, resetState } = useAppStore((state) => state); + const [isModalOpen, setModalOpen] = useState(false); const handleOpenModal = () => setModalOpen(true); const handleCloseModal = () => setModalOpen(false); + const clickBack = () => { + router.push(ROUTES.CHOOSE_SOURCES); + }; + + const clickDone = async () => { + await createSources(configuredSources, configuredFutureApps); + await Promise.all(configuredDestinations.map(async ({ form }) => await createDestination(form))); + + resetState(); + router.push(ROUTES.OVERVIEW); + }; + + const isSourcesListEmpty = () => !Object.values(configuredSources).some((sources) => !!sources.length); + const isCreating = sourcesLoading || destinationsLoading; + return ( <> @@ -59,27 +70,27 @@ export function ChooseDestinationContainer() { { label: 'BACK', iconSrc: '/icons/common/arrow-white.svg', - onClick: () => router.push(ROUTES.CHOOSE_SOURCES), variant: 'secondary', + onClick: clickBack, + disabled: isCreating, }, { label: 'DONE', - onClick: () => { - resetState(); - router.push(ROUTES.OVERVIEW); - }, variant: 'primary', + onClick: clickDone, + disabled: isCreating, }, ]} /> - {isSourcesListEmpty() && configuredDestinations.length === 0 && ( + + {isSourcesListEmpty() && ( router.push(ROUTES.CHOOSE_SOURCES), @@ -87,11 +98,19 @@ export function ChooseDestinationContainer() { /> )} + - handleOpenModal()} /> + handleOpenModal()}> + back + + ADD DESTINATION + + + + + - ); diff --git a/frontend/webapp/containers/main/destinations/add-destination/styled.ts b/frontend/webapp/containers/main/destinations/add-destination/styled.ts deleted file mode 100644 index 579e93e6d..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/styled.ts +++ /dev/null @@ -1,21 +0,0 @@ -import styled from 'styled-components'; - -export const Body = styled.div` - padding: 32px 24px 0; - border-left: 1px solid rgba(249, 249, 249, 0.08); - min-height: 600px; - width: 100%; - min-width: 770px; -`; - -export const SideMenuWrapper = styled.div` - padding: 32px; - width: 196px; - @media (max-width: 1050px) { - display: none; - } -`; - -export const Container = styled.div` - display: flex; -`; diff --git a/frontend/webapp/containers/main/destinations/add-destination/test-connection/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/test-connection/index.tsx deleted file mode 100644 index 88951e150..000000000 --- a/frontend/webapp/containers/main/destinations/add-destination/test-connection/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import Image from 'next/image'; -import styled from 'styled-components'; -import React, { useState } from 'react'; -import { DestinationInput } from '@/types'; -import { useTestConnection } from '@/hooks'; -import { Button, FadeLoader, Text } from '@/reuseable-components'; - -interface TestConnectionProps { - destination: DestinationInput | undefined; - onError?: () => void; -} - -const ActionButton = styled(Button)<{ $isTestConnectionSuccess?: boolean }>` - display: flex; - align-items: center; - gap: 8px; - background-color: ${({ $isTestConnectionSuccess }) => ($isTestConnectionSuccess ? 'rgba(129, 175, 101, 0.16)' : 'transparent')}; -`; - -const ActionButtonText = styled(Text)<{ $isTestConnectionSuccess?: boolean }>` - font-family: ${({ theme }) => theme.font_family.secondary}; - font-weight: 500; - text-decoration: underline; - text-transform: uppercase; - font-size: 14px; - line-height: 157.143%; - color: ${({ theme, $isTestConnectionSuccess }) => ($isTestConnectionSuccess ? theme.text.success : theme.colors.white)}; -`; - -const TestConnection: React.FC = ({ destination, onError }) => { - const [isTestConnectionSuccess, setIsTestConnectionSuccess] = useState(false); - const { testConnection, loading, error } = useTestConnection(); - - const onButtonClick = async () => { - if (!destination) { - return; - } - - const res = await testConnection(destination); - if (res) { - setIsTestConnectionSuccess(res.succeeded); - !res.succeeded && onError && onError(); - } - }; - return ( - - {isTestConnectionSuccess && checkmark} - {loading && } - - - {loading ? 'Checking' : isTestConnectionSuccess ? 'Connection ok' : 'Test Connection'} - - - ); -}; - -export { TestConnection }; diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx deleted file mode 100644 index 144094d13..000000000 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useState } from 'react'; -import styled from 'styled-components'; -import { useDrawerStore } from '@/store'; -import { ActualDestination } from '@/types'; -import OverviewDrawer from '../../overview/overview-drawer'; -import { CardDetails, EditDestinationForm } from '@/components'; -import { useDestinationCRUD, useDestinationFormData, useEditDestinationFormHandlers } from '@/hooks'; - -interface Props {} - -const DestinationDrawer: React.FC = () => { - const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); - const [isEditing, setIsEditing] = useState(false); - const [isFormDirty, setIsFormDirty] = useState(false); - - const { cardData, dynamicFields, exportedSignals, supportedSignals, destinationType, resetFormData, setDynamicFields, setExportedSignals } = useDestinationFormData(); - const { handleSignalChange, handleDynamicFieldChange } = useEditDestinationFormHandlers(setExportedSignals, setDynamicFields); - const { updateDestination, deleteDestination } = useDestinationCRUD(); - - if (!selectedItem?.item) return null; - const { id, item } = selectedItem; - - const handleEdit = (bool?: boolean) => { - if (typeof bool === 'boolean') { - setIsEditing(bool); - } else { - setIsEditing(true); - } - }; - - const handleCancel = () => { - resetFormData(); - setIsEditing(false); - }; - - const handleDelete = async () => { - await deleteDestination(id as string); - }; - - const handleSave = async (newTitle: string) => { - const title = newTitle !== (item as ActualDestination).destinationType.displayName ? newTitle : ''; - const payload = { - type: destinationType, - name: title, - exportedSignals, - fields: dynamicFields.map(({ name, value }) => ({ key: name, value })), - }; - - await updateDestination(id as string, payload); - }; - - return ( - - {isEditing ? ( - - { - setIsFormDirty(true); - handleSignalChange(...params); - }} - handleDynamicFieldChange={(...params) => { - setIsFormDirty(true); - handleDynamicFieldChange(...params); - }} - /> - - ) : ( - - )} - - ); -}; - -export { DestinationDrawer }; - -const FormContainer = styled.div` - width: 100%; - height: 100%; - max-height: calc(100vh - 220px); - overflow: overlay; - overflow-y: auto; -`; diff --git a/frontend/webapp/containers/main/destinations/destination-drawer/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer/index.tsx new file mode 100644 index 000000000..f2595f6e4 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-drawer/index.tsx @@ -0,0 +1,141 @@ +import React, { useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { safeJsonParse } from '@/utils'; +import { useDrawerStore } from '@/store'; +import { CardDetails } from '@/components'; +import type { ActualDestination } from '@/types'; +import OverviewDrawer from '../../overview/overview-drawer'; +import { DestinationFormBody } from '../destination-form-body'; +import { useDestinationCRUD, useDestinationFormData, useDestinationTypes } from '@/hooks'; + +interface Props {} + +const FormContainer = styled.div` + width: 100%; + height: 100%; + max-height: calc(100vh - 220px); + overflow: overlay; + overflow-y: auto; +`; + +export const DestinationDrawer: React.FC = () => { + const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); + const [isEditing, setIsEditing] = useState(false); + const [isFormDirty, setIsFormDirty] = useState(false); + + const { updateDestination, deleteDestination } = useDestinationCRUD(); + const { formData, handleFormChange, resetFormData, validateForm, loadFormWithDrawerItem, destinationTypeDetails, dynamicFields, setDynamicFields } = useDestinationFormData({ + destinationType: (selectedItem?.item as ActualDestination)?.destinationType?.type, + preLoadedFields: (selectedItem?.item as ActualDestination)?.fields, + // TODO: supportedSignals: thisDestination?.supportedSignals, + // currently, the real "supportedSignals" is being used by "destination" passed as prop to "DestinationFormBody" + }); + + const cardData = useMemo(() => { + if (!selectedItem) return []; + + const buildMonitorsList = (exportedSignals: ActualDestination['exportedSignals']): string => + Object.keys(exportedSignals) + .filter((key) => exportedSignals[key]) + .join(', ') || 'N/A'; + + const buildDestinationFieldData = (parsedFields: Record) => + Object.entries(parsedFields).map(([key, value]) => { + const found = destinationTypeDetails?.fields?.find((field) => field.name === key); + + const { type } = safeJsonParse(found?.componentProperties, { type: '' }); + const secret = type === 'password' ? new Array(value.length).fill('•').join('') : ''; + + return { + title: found?.displayName || key, + value: secret || value || 'N/A', + }; + }); + + const { exportedSignals, destinationType, fields } = selectedItem.item as ActualDestination; + const parsedFields = safeJsonParse>(fields, {}); + const fieldsData = buildDestinationFieldData(parsedFields); + + return [{ title: 'Destination', value: destinationType.displayName || 'N/A' }, { title: 'Monitors', value: buildMonitorsList(exportedSignals) }, ...fieldsData]; + }, [selectedItem, destinationTypeDetails]); + + const { destinations } = useDestinationTypes(); + const thisDestination = useMemo(() => { + if (!destinations.length || !selectedItem || !isEditing) { + resetFormData(); + return undefined; + } + + const { item } = selectedItem as { item: ActualDestination }; + const found = destinations.map(({ items }) => items.filter(({ type }) => type === item.destinationType.type)).filter((arr) => !!arr.length)[0][0]; + + if (!found) return undefined; + + loadFormWithDrawerItem(selectedItem); + + return found; + }, [destinations, selectedItem, isEditing]); + + if (!selectedItem?.item) return null; + const { id, item } = selectedItem; + + const handleEdit = (bool?: boolean) => { + if (typeof bool === 'boolean') { + setIsEditing(bool); + } else { + setIsEditing(true); + } + }; + + const handleCancel = () => { + resetFormData(); + setIsEditing(false); + }; + + const handleDelete = async () => { + await deleteDestination(id as string); + }; + + const handleSave = async (newTitle: string) => { + if (validateForm({ withAlert: true })) { + const title = newTitle !== (item as ActualDestination).destinationType.displayName ? newTitle : ''; + + await updateDestination(id as string, { ...formData, name: title }); + } + }; + + return ( + + {isEditing ? ( + + { + setIsFormDirty(true); + handleFormChange(...params); + }} + dynamicFields={dynamicFields} + setDynamicFields={(...params) => { + setIsFormDirty(true); + setDynamicFields(...params); + }} + /> + + ) : ( + + )} + + ); +}; diff --git a/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx b/frontend/webapp/containers/main/destinations/destination-form-body/dynamic-fields/index.tsx similarity index 86% rename from frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx rename to frontend/webapp/containers/main/destinations/destination-form-body/dynamic-fields/index.tsx index 033a13ac8..98a733483 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/dynamic-form-fields/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-form-body/dynamic-fields/index.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { INPUT_TYPES } from '@/utils'; import { Dropdown, Input, TextArea, InputList, KeyValueInputsList } from '@/reuseable-components'; -export function DynamicConnectDestinationFormFields({ fields, onChange }: { fields: any[]; onChange: (name: string, value: any) => void }) { +interface Props { + fields: any[]; + onChange: (name: string, value: any) => void; +} + +export const DestinationDynamicFields: React.FC = ({ fields, onChange }) => { return fields?.map((field: any) => { const { componentType, ...rest } = field; @@ -21,4 +26,4 @@ export function DynamicConnectDestinationFormFields({ fields, onChange }: { fiel return null; } }); -} +}; diff --git a/frontend/webapp/containers/main/destinations/destination-form-body/index.tsx b/frontend/webapp/containers/main/destinations/destination-form-body/index.tsx new file mode 100644 index 000000000..021a7558b --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-form-body/index.tsx @@ -0,0 +1,121 @@ +import React, { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { TestConnection } from './test-connection'; +import { DestinationDynamicFields } from './dynamic-fields'; +import type { DestinationInput, DestinationTypeItem, DynamicField } from '@/types'; +import { CheckboxList, Divider, Input, NotificationNote, SectionTitle } from '@/reuseable-components'; + +interface Props { + isUpdate?: boolean; + destination?: DestinationTypeItem; + isFormOk: boolean; + formData: DestinationInput; + handleFormChange: (key: keyof DestinationInput | string, val: any) => void; + dynamicFields: DynamicField[]; + setDynamicFields: Dispatch>; +} + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 24px; + padding: 0 4px; +`; + +export function DestinationFormBody({ isUpdate, destination, isFormOk, formData, handleFormChange, dynamicFields, setDynamicFields }: Props) { + const { supportedSignals, testConnectionSupported, displayName } = destination || {}; + + const [isFormDirty, setIsFormDirty] = useState(false); + const [showConnectionError, setShowConnectionError] = useState(false); + + // this is to allow test connection when there are default values loaded + useEffect(() => { + if (isFormOk) setIsFormDirty(true); + }, [isFormOk]); + + const supportedMonitors = useMemo(() => { + const { logs, metrics, traces } = supportedSignals || {}; + const arr: { id: string; title: string }[] = []; + + if (logs?.supported) arr.push({ id: 'logs', title: 'Logs' }); + if (metrics?.supported) arr.push({ id: 'metrics', title: 'Metrics' }); + if (traces?.supported) arr.push({ id: 'traces', title: 'Traces' }); + + return arr; + }, [supportedSignals]); + + return ( + + {!isUpdate && ( + <> + { + setIsFormDirty(false); + setShowConnectionError(false); + }} + onError={() => { + setIsFormDirty(false); + setShowConnectionError(true); + }} + /> + ) + } + /> + + {testConnectionSupported && showConnectionError ? ( + + ) : testConnectionSupported && !showConnectionError && !!displayName ? ( + + ) : null} + + + )} + + { + if (!isFormDirty) setIsFormDirty(true); + handleFormChange(`exportedSignals.${signal}`, value); + }} + /> + + {!isUpdate && ( + { + if (!isFormDirty) setIsFormDirty(true); + handleFormChange('name', e.target.value); + }} + /> + )} + + { + if (!isFormDirty) setIsFormDirty(true); + setDynamicFields((prev) => { + const payload = [...prev]; + const foundIndex = payload.findIndex((field) => field.name === name); + + if (foundIndex !== -1) { + payload[foundIndex] = { ...payload[foundIndex], value }; + } + + return payload; + }); + }} + /> + + ); +} diff --git a/frontend/webapp/containers/main/destinations/destination-form-body/test-connection/index.tsx b/frontend/webapp/containers/main/destinations/destination-form-body/test-connection/index.tsx new file mode 100644 index 000000000..fe3c756ab --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-form-body/test-connection/index.tsx @@ -0,0 +1,53 @@ +import React, { useEffect, useMemo } from 'react'; +import Image from 'next/image'; +import styled from 'styled-components'; +import { getStatusIcon } from '@/utils'; +import { useTestConnection } from '@/hooks'; +import type { DestinationInput } from '@/types'; +import { Button, FadeLoader, Text } from '@/reuseable-components'; + +interface TestConnectionProps { + destination: DestinationInput; + disabled: boolean; + clearStatus: () => void; + onError: () => void; +} + +const ActionButton = styled(Button)<{ $success?: boolean }>` + display: flex; + align-items: center; + gap: 8px; + background-color: ${({ $success }) => ($success ? 'rgba(129, 175, 101, 0.16)' : 'transparent')}; +`; + +const ActionButtonText = styled(Text)<{ $success?: boolean }>` + font-family: ${({ theme }) => theme.font_family.secondary}; + font-weight: 500; + text-decoration: underline; + text-transform: uppercase; + font-size: 14px; + line-height: 157.143%; + color: ${({ theme, $success }) => ($success ? theme.text.success : theme.colors.white)}; +`; + +export const TestConnection: React.FC = ({ destination, disabled, clearStatus, onError }) => { + const { testConnection, loading, data } = useTestConnection(); + const success = useMemo(() => data?.testConnectionForDestination.succeeded || false, [data]); + + useEffect(() => { + if (data) { + clearStatus(); + if (!success) onError && onError(); + } + }, [data, success]); + + return ( + testConnection(destination)} $success={success}> + {loading ? : success ? checkmark : null} + + + {loading ? 'Checking' : success ? 'Connection OK' : 'Test Connection'} + + + ); +}; diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/choose-destination-filters/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/choose-destination-filters/index.tsx new file mode 100644 index 000000000..cb652ffd2 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/choose-destination-filters/index.tsx @@ -0,0 +1,52 @@ +import React, { Dispatch, SetStateAction, useState } from 'react'; +import styled from 'styled-components'; +import { SignalUppercase } from '@/utils'; +import type { DropdownOption } from '@/types'; +import { Dropdown, Input, MonitoringCheckboxes } from '@/reuseable-components'; + +interface Props { + selectedTag: DropdownOption | undefined; + onTagSelect: (option: DropdownOption) => void; + onSearch: (value: string) => void; + selectedMonitors: SignalUppercase[]; + setSelectedMonitors: Dispatch>; +} + +const Container = styled.div` + display: flex; + align-items: center; + gap: 12px; +`; + +const WidthConstraint = styled.div` + width: 160px; + margin-right: 8px; +`; + +const DROPDOWN_OPTIONS = [ + { value: 'All types', id: 'all' }, + { value: 'Managed', id: 'managed' }, + { value: 'Self-hosted', id: 'self hosted' }, +]; + +export const ChooseDestinationFilters: React.FC = ({ selectedTag, onTagSelect, onSearch, selectedMonitors, setSelectedMonitors }) => { + const [searchTerm, setSearchTerm] = useState(''); + + const handleSearchChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchTerm(value); + onSearch(value); + }; + + return ( + + + + + + {}} /> + + + + ); +}; diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx similarity index 85% rename from frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx rename to frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx index aa16aea36..8ce00ad1e 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/destination-list-item/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/destination-list-item/index.tsx @@ -47,11 +47,7 @@ const DestinationIconWrapper = styled.div` align-items: center; gap: 8px; border-radius: 8px; - background: linear-gradient( - 180deg, - rgba(249, 249, 249, 0.06) 0%, - rgba(249, 249, 249, 0.02) 100% - ); + background: linear-gradient(180deg, rgba(249, 249, 249, 0.06) 0%, rgba(249, 249, 249, 0.02) 100%); `; const SignalsWrapper = styled.div` @@ -83,14 +79,9 @@ interface DestinationListItemProps { onSelect: (item: DestinationTypeItem) => void; } -const DestinationListItem: React.FC = ({ - item, - onSelect, -}) => { +export const DestinationListItem: React.FC = ({ item, onSelect }) => { const renderSupportedSignals = () => { - const signals = Object.keys(item.supportedSignals).filter( - (signal) => item.supportedSignals[signal].supported - ); + const signals = Object.keys(item.supportedSignals).filter((signal) => item.supportedSignals[signal].supported); return signals.map((signal, index) => ( @@ -104,7 +95,7 @@ const DestinationListItem: React.FC = ({ onSelect(item)}> - destination + destination {item.displayName} @@ -117,5 +108,3 @@ const DestinationListItem: React.FC = ({ ); }; - -export { DestinationListItem }; diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx similarity index 100% rename from frontend/webapp/containers/main/destinations/add-destination/destinations-list/index.tsx rename to frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/index.tsx diff --git a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx similarity index 53% rename from frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx rename to frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx index 8470f2e81..d52c75038 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/destinations-list/potential-destinations-list/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/destinations-list/potential-destinations-list/index.tsx @@ -15,36 +15,20 @@ interface PotentialDestinationsListProps { setSelectedItems: (item: DestinationTypeItem) => void; } -const PotentialDestinationsList: React.FC = ({ - setSelectedItems, -}) => { +export const PotentialDestinationsList: React.FC = ({ setSelectedItems }) => { const { loading, data } = usePotentialDestinations(); - if (!data.length) { - return null; - } + if (!data.length) return null; return ( - {loading ? ( - - ) : ( - data.map((item) => ( - - )) - )} + {loading ? : data.map((item) => )} ); }; - -export { PotentialDestinationsList }; diff --git a/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/index.tsx new file mode 100644 index 000000000..d40486e9e --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/index.tsx @@ -0,0 +1,60 @@ +import React, { useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { SignalUppercase } from '@/utils'; +import { useDestinationTypes } from '@/hooks'; +import { DestinationsList } from './destinations-list'; +import { Divider, SectionTitle } from '@/reuseable-components'; +import type { DropdownOption, DestinationTypeItem } from '@/types'; +import { ChooseDestinationFilters } from './choose-destination-filters'; + +interface Props { + onSelect: (item: DestinationTypeItem) => void; +} + +const DEFAULT_MONITORS: SignalUppercase[] = ['LOGS', 'METRICS', 'TRACES']; +const DEFAULT_DROPDOWN_VALUE = { id: 'all', value: 'All types' }; + +const Container = styled.div` + display: flex; + flex-direction: column; + gap: 24px; +`; + +export const ChooseDestinationBody: React.FC = ({ onSelect }) => { + const [searchValue, setSearchValue] = useState(''); + const [selectedMonitors, setSelectedMonitors] = useState(DEFAULT_MONITORS); + const [dropdownValue, setDropdownValue] = useState(DEFAULT_DROPDOWN_VALUE); + + const { destinations } = useDestinationTypes(); + + const filteredDestinations = useMemo(() => { + return destinations + .map((category) => { + const filteredItems = category.items.filter((item) => { + const matchesSearch = searchValue ? item.displayName.toLowerCase().includes(searchValue.toLowerCase()) : true; + const matchesDropdown = dropdownValue.id !== 'all' ? category.name === dropdownValue.id : true; + const matchesMonitor = selectedMonitors.length ? selectedMonitors.some((monitor) => item.supportedSignals[monitor.toLowerCase()]?.supported) : true; + + return matchesSearch && matchesDropdown && matchesMonitor; + }); + + return { ...category, items: filteredItems }; + }) + .filter((category) => category.items.length > 0); // Filter out empty categories + }, [destinations, searchValue, dropdownValue, selectedMonitors]); + + return ( + + + setDropdownValue(opt)} + onSearch={setSearchValue} + selectedMonitors={selectedMonitors} + setSelectedMonitors={setSelectedMonitors} + /> + + + + ); +}; diff --git a/frontend/webapp/containers/main/destinations/destination-modal/index.tsx b/frontend/webapp/containers/main/destinations/destination-modal/index.tsx new file mode 100644 index 000000000..433cd0945 --- /dev/null +++ b/frontend/webapp/containers/main/destinations/destination-modal/index.tsx @@ -0,0 +1,142 @@ +import React, { useState } from 'react'; +import { ModalBody } from '@/styles'; +import { useAppStore } from '@/store'; +import { INPUT_TYPES } from '@/utils'; +import styled from 'styled-components'; +import { SideMenu } from '@/components'; +import { DestinationFormBody } from '../destination-form-body'; +import { ChooseDestinationBody } from './choose-destination-body'; +import { useDestinationCRUD, useDestinationFormData } from '@/hooks'; +import type { ConfiguredDestination, DestinationTypeItem } from '@/types'; +import { Modal, type NavigationButtonProps, NavigationButtons } from '@/reuseable-components'; + +interface AddDestinationModalProps { + isOnboarding?: boolean; + isOpen: boolean; + onClose: () => void; +} + +const Container = styled.div` + display: flex; +`; + +const SideMenuWrapper = styled.div` + border-right: 1px solid ${({ theme }) => theme.colors.border}; + padding: 32px; + width: 200px; + @media (max-width: 1050px) { + display: none; + } +`; + +export const DestinationModal: React.FC = ({ isOnboarding, isOpen, onClose }) => { + const [selectedItem, setSelectedItem] = useState(); + + const { createDestination } = useDestinationCRUD(); + const addConfiguredDestination = useAppStore(({ addConfiguredDestination }) => addConfiguredDestination); + const { formData, handleFormChange, resetFormData, validateForm, dynamicFields, setDynamicFields } = useDestinationFormData({ + supportedSignals: selectedItem?.supportedSignals, + preLoadedFields: selectedItem?.fields, + }); + + const isFormOk = !!selectedItem && validateForm(); + + const handleClose = () => { + resetFormData(); + setSelectedItem(undefined); + onClose(); + }; + + const handleBack = () => { + resetFormData(); + setSelectedItem(undefined); + }; + + const handleSelect = (item: DestinationTypeItem) => { + resetFormData(); + handleFormChange('type', item.type); + setSelectedItem(item); + }; + + const handleSubmit = async () => { + if (isOnboarding) { + const destinationTypeDetails = dynamicFields.map((field) => ({ + title: field.title, + value: field.componentType === INPUT_TYPES.DROPDOWN ? field.value.value : field.value, + })); + + destinationTypeDetails.unshift({ + title: 'Destination name', + value: formData.name, + }); + + const storedDestination: ConfiguredDestination = { + type: selectedItem?.type || '', + displayName: selectedItem?.displayName || '', + imageUrl: selectedItem?.imageUrl || '', + exportedSignals: formData.exportedSignals, + destinationTypeDetails, + category: '', // Could be handled in a more dynamic way if needed + }; + + addConfiguredDestination({ stored: storedDestination, form: formData }); + } else { + createDestination(formData); + } + + handleClose(); + }; + + const renderHeaderButtons = () => { + const buttons: NavigationButtonProps[] = [ + { + label: 'DONE', + variant: 'primary' as const, + onClick: handleSubmit, + disabled: !isFormOk, + }, + ]; + + if (!!selectedItem) { + buttons.unshift({ + label: 'BACK', + iconSrc: '/icons/common/arrow-white.svg', + variant: 'secondary' as const, + onClick: handleBack, + }); + } + + return buttons; + }; + + return ( + }> + + + + + + + {!!selectedItem ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/frontend/webapp/containers/main/destinations/index.tsx b/frontend/webapp/containers/main/destinations/index.tsx index 6872095ac..d1c0e791f 100644 --- a/frontend/webapp/containers/main/destinations/index.tsx +++ b/frontend/webapp/containers/main/destinations/index.tsx @@ -1,2 +1,4 @@ export * from './add-destination'; -export * from './destination-drawer-container'; +export * from './destination-drawer'; +export * from './destination-form-body'; +export * from './destination-modal'; diff --git a/frontend/webapp/containers/main/instrumentation-rules/index.ts b/frontend/webapp/containers/main/instrumentation-rules/index.ts index c5c586edb..e49028254 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/index.ts +++ b/frontend/webapp/containers/main/instrumentation-rules/index.ts @@ -1 +1,3 @@ -export * from './add-rule-modal'; +export * from './rule-drawer'; +export * from './rule-form-body'; +export * from './rule-modal'; diff --git a/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/build-card-from-rule-spec.ts b/frontend/webapp/containers/main/instrumentation-rules/rule-drawer/build-card-from-rule-spec.ts similarity index 100% rename from frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/build-card-from-rule-spec.ts rename to frontend/webapp/containers/main/instrumentation-rules/rule-drawer/build-card-from-rule-spec.ts diff --git a/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/rule-drawer/index.tsx similarity index 93% rename from frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx rename to frontend/webapp/containers/main/instrumentation-rules/rule-drawer/index.tsx index 417974e2a..4a64b1d67 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/rule-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/rule-drawer/index.tsx @@ -1,18 +1,26 @@ import React, { useMemo, useState } from 'react'; +import { RuleFormBody } from '../'; import styled from 'styled-components'; import { getRuleIcon } from '@/utils'; import { useDrawerStore } from '@/store'; import { CardDetails } from '@/components'; -import { ChooseRuleBody } from '../choose-rule-body'; import type { InstrumentationRuleSpec } from '@/types'; +import { RULE_OPTIONS } from '../rule-modal/rule-options'; import OverviewDrawer from '../../overview/overview-drawer'; -import { RULE_OPTIONS } from '../add-rule-modal/rule-options'; import buildCardFromRuleSpec from './build-card-from-rule-spec'; import { useInstrumentationRuleCRUD, useInstrumentationRuleFormData } from '@/hooks'; interface Props {} -const RuleDrawer: React.FC = () => { +const FormContainer = styled.div` + width: 100%; + height: 100%; + max-height: calc(100vh - 220px); + overflow: overlay; + overflow-y: auto; +`; + +export const RuleDrawer: React.FC = () => { const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); const [isEditing, setIsEditing] = useState(false); const [isFormDirty, setIsFormDirty] = useState(false); @@ -86,7 +94,7 @@ const RuleDrawer: React.FC = () => { > {isEditing && thisRule ? ( - = () => { ); }; - -export { RuleDrawer }; - -const FormContainer = styled.div` - width: 100%; - height: 100%; - max-height: calc(100vh - 220px); - overflow: overlay; - overflow-y: auto; -`; diff --git a/frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/custom-fields/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/rule-form-body/custom-fields/index.tsx similarity index 100% rename from frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/custom-fields/index.tsx rename to frontend/webapp/containers/main/instrumentation-rules/rule-form-body/custom-fields/index.tsx diff --git a/frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/custom-fields/payload-collection.tsx b/frontend/webapp/containers/main/instrumentation-rules/rule-form-body/custom-fields/payload-collection.tsx similarity index 100% rename from frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/custom-fields/payload-collection.tsx rename to frontend/webapp/containers/main/instrumentation-rules/rule-form-body/custom-fields/payload-collection.tsx diff --git a/frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/rule-form-body/index.tsx similarity index 88% rename from frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/index.tsx rename to frontend/webapp/containers/main/instrumentation-rules/rule-form-body/index.tsx index a038de5af..fea52ec03 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/choose-rule-body/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/rule-form-body/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from 'styled-components'; import RuleCustomFields from './custom-fields'; import type { InstrumentationRuleInput } from '@/types'; -import type { RuleOption } from '../add-rule-modal/rule-options'; +import type { RuleOption } from '../rule-modal/rule-options'; import { DocsButton, Input, Text, TextArea, SectionTitle, ToggleButtons } from '@/reuseable-components'; interface Props { @@ -23,7 +23,7 @@ const FieldTitle = styled(Text)` margin-bottom: 12px; `; -const ChooseRuleBody: React.FC = ({ isUpdate, rule, formData, handleFormChange }) => { +export const RuleFormBody: React.FC = ({ isUpdate, rule, formData, handleFormChange }) => { return ( {isUpdate && ( @@ -43,5 +43,3 @@ const ChooseRuleBody: React.FC = ({ isUpdate, rule, formData, handleFormC ); }; - -export { ChooseRuleBody }; diff --git a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/rule-modal/index.tsx similarity index 91% rename from frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx rename to frontend/webapp/containers/main/instrumentation-rules/rule-modal/index.tsx index 80cd7e9ed..89a9530b3 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/rule-modal/index.tsx @@ -1,7 +1,7 @@ +import React, { useMemo, useState } from 'react'; import { CenterThis, ModalBody } from '@/styles'; -import { ChooseRuleBody } from '../choose-rule-body'; +import { RuleFormBody } from '../'; import { RULE_OPTIONS, RuleOption } from './rule-options'; -import React, { useMemo, useState } from 'react'; import { useInstrumentationRuleCRUD, useInstrumentationRuleFormData } from '@/hooks'; import { AutocompleteInput, Divider, FadeLoader, Modal, NavigationButtons, NotificationNote, SectionTitle } from '@/reuseable-components'; @@ -10,7 +10,7 @@ interface Props { onClose: () => void; } -export const AddRuleModal: React.FC = ({ isOpen, onClose }) => { +export const RuleModal: React.FC = ({ isOpen, onClose }) => { const { formData, handleFormChange, resetFormData, validateForm } = useInstrumentationRuleFormData(); const { createInstrumentationRule, loading } = useInstrumentationRuleCRUD({ onSuccess: handleClose }); const [selectedItem, setSelectedItem] = useState(RULE_OPTIONS[0]); @@ -64,7 +64,7 @@ export const AddRuleModal: React.FC = ({ isOpen, onClose }) => { ) : ( - + )} ) : null} diff --git a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/rule-options.ts b/frontend/webapp/containers/main/instrumentation-rules/rule-modal/rule-options.ts similarity index 100% rename from frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/rule-options.ts rename to frontend/webapp/containers/main/instrumentation-rules/rule-modal/rule-options.ts diff --git a/frontend/webapp/containers/main/overview/all-drawers/index.tsx b/frontend/webapp/containers/main/overview/all-drawers/index.tsx index 0600ceb69..65b2bba52 100644 --- a/frontend/webapp/containers/main/overview/all-drawers/index.tsx +++ b/frontend/webapp/containers/main/overview/all-drawers/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { useDrawerStore } from '@/store'; -import { OVERVIEW_ENTITY_TYPES } from '@/types'; import { SourceDrawer } from '../../sources'; import { ActionDrawer } from '../../actions'; +import { OVERVIEW_ENTITY_TYPES } from '@/types'; import { DestinationDrawer } from '../../destinations'; -import { RuleDrawer } from '../../instrumentation-rules/rule-drawer-container'; +import { RuleDrawer } from '../../instrumentation-rules'; const AllDrawers = () => { const selected = useDrawerStore(({ selectedItem }) => selectedItem); diff --git a/frontend/webapp/containers/main/overview/all-modals/index.tsx b/frontend/webapp/containers/main/overview/all-modals/index.tsx index db2ac9bb2..96039338a 100644 --- a/frontend/webapp/containers/main/overview/all-modals/index.tsx +++ b/frontend/webapp/containers/main/overview/all-modals/index.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useModalStore } from '@/store'; +import { ActionModal } from '../../actions'; import { OVERVIEW_ENTITY_TYPES } from '@/types'; -import { AddRuleModal } from '../../instrumentation-rules'; -import { AddActionModal } from '../../actions'; -import { AddDestinationModal } from '../../destinations/add-destination/add-destination-modal'; +import { DestinationModal } from '../../destinations'; +import { RuleModal } from '../../instrumentation-rules'; import { AddSourceModal } from '../../sources/choose-sources/choose-source-modal'; const AllModals = () => { @@ -16,16 +16,16 @@ const AllModals = () => { switch (selected) { case OVERVIEW_ENTITY_TYPES.RULE: - return ; + return ; case OVERVIEW_ENTITY_TYPES.SOURCE: return ; case OVERVIEW_ENTITY_TYPES.ACTION: - return ; + return ; case OVERVIEW_ENTITY_TYPES.DESTINATION: - return ; + return ; default: return <>; diff --git a/frontend/webapp/containers/main/overview/multi-source-control/index.tsx b/frontend/webapp/containers/main/overview/multi-source-control/index.tsx index 321fa9888..b54856a0d 100644 --- a/frontend/webapp/containers/main/overview/multi-source-control/index.tsx +++ b/frontend/webapp/containers/main/overview/multi-source-control/index.tsx @@ -4,9 +4,9 @@ import { slide } from '@/styles'; import theme from '@/styles/theme'; import { useAppStore } from '@/store'; import styled from 'styled-components'; -import { useSourceCRUD } from '@/hooks'; import { DeleteWarning } from '@/components'; -import { Badge, Button, Divider, Text, Transition } from '@/reuseable-components'; +import { useSourceCRUD, useTransition } from '@/hooks'; +import { Badge, Button, Divider, Text } from '@/reuseable-components'; const Container = styled.div` position: fixed; @@ -24,6 +24,12 @@ const Container = styled.div` `; const MultiSourceControl = () => { + const Transition = useTransition({ + container: Container, + animateIn: slide.in['center'], + animateOut: slide.out['center'], + }); + const { sources, deleteSources } = useSourceCRUD(); const { configuredSources, setConfiguredSources } = useAppStore((state) => state); const [isWarnModalOpen, setIsWarnModalOpen] = useState(false); @@ -50,7 +56,7 @@ const MultiSourceControl = () => { return ( <> - + Selected sources diff --git a/frontend/webapp/containers/main/overview/overview-actions-menu/search/search-results/index.tsx b/frontend/webapp/containers/main/overview/overview-actions-menu/search/search-results/index.tsx index a359f2e54..b8fbe254a 100644 --- a/frontend/webapp/containers/main/overview/overview-actions-menu/search/search-results/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-actions-menu/search/search-results/index.tsx @@ -18,14 +18,14 @@ const HorizontalScroll = styled.div` align-items: center; padding: 12px; border-bottom: ${({ theme }) => `1px solid ${theme.colors.border}`}; - overflow-x: auto; + overflow-x: scroll; `; const VerticalScroll = styled.div` display: flex; flex-direction: column; padding: 12px; - overflow-y: auto; + overflow-y: scroll; `; export const SearchResults = ({ searchText, onClose }: Props) => { diff --git a/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx index fbfb57dd9..d1a4cc9fe 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/drawer-header/index.tsx @@ -87,14 +87,9 @@ const DrawerHeader = forwardRef(({ title, ti Drawer Item {!isEdit && ( - <> + {title} - {!!titleTooltip && ( - - Info - - )} - + )} diff --git a/frontend/webapp/containers/main/sources/choose-sources/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/index.tsx index 7cb934222..355841b93 100644 --- a/frontend/webapp/containers/main/sources/choose-sources/index.tsx +++ b/frontend/webapp/containers/main/sources/choose-sources/index.tsx @@ -17,14 +17,12 @@ export function ChooseSourcesContainer() { const menuState = useSourceFormData(); const onNext = () => { - const { selectedNamespace, availableSources, selectedSources, selectedFutureApps } = menuState; + const { availableSources, selectedSources, selectedFutureApps } = menuState; const { setAvailableSources, setConfiguredSources, setConfiguredFutureApps } = appState; - if (selectedNamespace) { - setAvailableSources(availableSources); - setConfiguredSources(selectedSources); - setConfiguredFutureApps(selectedFutureApps); - } + setAvailableSources(availableSources); + setConfiguredSources(selectedSources); + setConfiguredFutureApps(selectedFutureApps); router.push(ROUTES.CHOOSE_DESTINATION); }; diff --git a/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx b/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx index c937fa563..cd1740972 100644 --- a/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx @@ -91,11 +91,7 @@ const SourceDrawer: React.FC = () => { return ( , HTMLElement>, {}>> & string; + animateIn: Keyframes; + animateOut?: Keyframes; + duration?: number; // in milliseconds +} + +type TransitionProps = PropsWithChildren<{ + enter: boolean; + [key: string]: any; +}>; + +export const useTransition = ({ container, animateIn, animateOut, duration = 300 }: HookProps) => { + const Animated = styled(container)<{ $isEntering: boolean; $isLeaving: boolean }>` + animation-name: ${({ $isEntering, $isLeaving }) => ($isEntering ? animateIn : $isLeaving ? animateOut : 'none')}; + animation-duration: ${duration}ms; + animation-fill-mode: forwards; + `; + + const Transition = useCallback(({ children, enter, ...props }: TransitionProps) => { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + const t = setTimeout(() => setMounted(enter), duration + 50); // +50ms to ensure the animation is finished + return () => clearTimeout(t); + }, [enter, duration]); + + return ( + + {children} + + ); + + // do not add dependencies here, it will cause re-renders which we want to avoid + }, []); + + return Transition; +}; diff --git a/frontend/webapp/hooks/compute-platform/useComputePlatform.ts b/frontend/webapp/hooks/compute-platform/useComputePlatform.ts index 4d9b48664..b8445d4f9 100644 --- a/frontend/webapp/hooks/compute-platform/useComputePlatform.ts +++ b/frontend/webapp/hooks/compute-platform/useComputePlatform.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { safeJsonParse } from '@/utils'; import { useQuery } from '@apollo/client'; import { useBooleanStore } from '@/store'; @@ -24,7 +24,7 @@ export const useComputePlatform = (): UseComputePlatformHook => { let retries = 0; const maxRetries = 5; - const retryInterval = 1 * 1000; // time in milliseconds + const retryInterval = 2 * 1000; // time in milliseconds while (retries < maxRetries) { await new Promise((resolve) => setTimeout(resolve, retryInterval)); @@ -35,6 +35,11 @@ export const useComputePlatform = (): UseComputePlatformHook => { togglePolling(false); }, [refetch, togglePolling]); + // this is to start polling on component mount in an attempt to fix any initial errors with sources/destinations + useEffect(() => { + startPolling(); + }, []); + const filteredData = useMemo(() => { if (!data) return undefined; diff --git a/frontend/webapp/hooks/destinations/index.ts b/frontend/webapp/hooks/destinations/index.ts index 071f0c685..3b987c456 100644 --- a/frontend/webapp/hooks/destinations/index.ts +++ b/frontend/webapp/hooks/destinations/index.ts @@ -3,5 +3,4 @@ export * from './useConnectDestinationForm'; export * from './usePotentialDestinations'; export * from './useDestinationCRUD'; export * from './useDestinationFormData'; -export * from './useEditDestinationFormHandlers'; export * from './useDestinationTypes'; diff --git a/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts b/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts index 7e8c989b8..67295a5f8 100644 --- a/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts +++ b/frontend/webapp/hooks/destinations/useConnectDestinationForm.ts @@ -2,34 +2,26 @@ import { safeJsonParse, INPUT_TYPES } from '@/utils'; import { DestinationDetailsField, DynamicField } from '@/types'; export function useConnectDestinationForm() { - function buildFormDynamicFields( - fields: DestinationDetailsField[] - ): DynamicField[] { + function buildFormDynamicFields(fields: DestinationDetailsField[]): DynamicField[] { return fields .map((field) => { - const { - name, - componentType, - displayName, - componentProperties, - initialValue, - } = field; + const { name, componentType, displayName, componentProperties, initialValue } = field; let componentPropertiesJson; let initialValuesJson; switch (componentType) { case INPUT_TYPES.DROPDOWN: - componentPropertiesJson = safeJsonParse<{ [key: string]: string }>( - componentProperties, - {} - ); + componentPropertiesJson = safeJsonParse<{ [key: string]: string }>(componentProperties, {}); - const options = Object.entries(componentPropertiesJson.values).map( - ([key, value]) => ({ - id: key, - value, - }) - ); + const options = Array.isArray(componentPropertiesJson.values) + ? componentPropertiesJson.values.map((value) => ({ + id: value, + value, + })) + : Object.entries(componentPropertiesJson.values).map(([key, value]) => ({ + id: key, + value, + })); return { name, @@ -43,10 +35,8 @@ export function useConnectDestinationForm() { case INPUT_TYPES.INPUT: case INPUT_TYPES.TEXTAREA: - componentPropertiesJson = safeJsonParse( - componentProperties, - [] - ); + componentPropertiesJson = safeJsonParse(componentProperties, []); + return { name, componentType, @@ -55,10 +45,7 @@ export function useConnectDestinationForm() { }; case INPUT_TYPES.MULTI_INPUT: - componentPropertiesJson = safeJsonParse( - componentProperties, - [] - ); + componentPropertiesJson = safeJsonParse(componentProperties, []); initialValuesJson = safeJsonParse(initialValue, []); return { @@ -69,6 +56,7 @@ export function useConnectDestinationForm() { value: initialValuesJson, ...componentPropertiesJson, }; + case INPUT_TYPES.KEY_VALUE_PAIR: return { name, @@ -76,6 +64,7 @@ export function useConnectDestinationForm() { title: displayName, ...componentPropertiesJson, }; + default: return undefined; } diff --git a/frontend/webapp/hooks/destinations/useDestinationFormData.ts b/frontend/webapp/hooks/destinations/useDestinationFormData.ts index 069ee25de..4cf324778 100644 --- a/frontend/webapp/hooks/destinations/useDestinationFormData.ts +++ b/frontend/webapp/hooks/destinations/useDestinationFormData.ts @@ -1,137 +1,159 @@ -import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import { safeJsonParse } from '@/utils'; -import { useDrawerStore } from '@/store'; +import { useState, useEffect } from 'react'; +import { DrawerBaseItem } from '@/store'; import { useQuery } from '@apollo/client'; -import { useConnectDestinationForm } from '@/hooks'; import { GET_DESTINATION_TYPE_DETAILS } from '@/graphql'; -import { DynamicField, ActualDestination, isActualDestination, DestinationDetailsResponse, SupportedDestinationSignals, DestinationDetailsField } from '@/types'; - -const DEFAULT_SUPPORTED_SIGNALS: SupportedDestinationSignals = { - logs: { supported: false }, - metrics: { supported: false }, - traces: { supported: false }, -}; - -export function useDestinationFormData() { - const [dynamicFields, setDynamicFields] = useState([]); - const [supportedSignals, setSupportedSignals] = useState(DEFAULT_SUPPORTED_SIGNALS); - const [exportedSignals, setExportedSignals] = useState({ +import { useConnectDestinationForm, useNotify } from '@/hooks'; +import { ACTION, FORM_ALERTS, NOTIFICATION, safeJsonParse } from '@/utils'; +import { + type DynamicField, + type DestinationDetailsResponse, + type DestinationInput, + type DestinationTypeItem, + type ActualDestination, + type SupportedDestinationSignals, + OVERVIEW_ENTITY_TYPES, +} from '@/types'; + +const INITIAL: DestinationInput = { + type: '', + name: '', + exportedSignals: { logs: false, metrics: false, traces: false, - }); + }, + fields: [], +}; - const destination = useDrawerStore(({ selectedItem }) => selectedItem); - const shouldSkip = !isActualDestination(destination?.item); - const destinationType = isActualDestination(destination?.item) ? destination.item.destinationType.type : null; +export function useDestinationFormData(params?: { destinationType?: string; supportedSignals?: SupportedDestinationSignals; preLoadedFields?: string | DestinationTypeItem['fields'] }) { + const { destinationType, supportedSignals, preLoadedFields } = params || {}; + const notify = useNotify(); const { buildFormDynamicFields } = useConnectDestinationForm(); - const { data: destinationFields } = useQuery(GET_DESTINATION_TYPE_DETAILS, { - variables: { type: destinationType }, - skip: shouldSkip, - }); - - // Memoize the buildFormDynamicFields to ensure it's stable across renders - const memoizedBuildFormDynamicFields = useCallback(buildFormDynamicFields, []); + const [formData, setFormData] = useState({ ...INITIAL }); + const [dynamicFields, setDynamicFields] = useState([]); - const initialDynamicFieldsRef = useRef([]); - const initialExportedSignalsRef = useRef({ - logs: false, - metrics: false, - traces: false, + const t = destinationType || formData.type; + const { data: { destinationTypeDetails } = {} } = useQuery(GET_DESTINATION_TYPE_DETAILS, { + variables: { type: t }, + skip: !t, + onError: (error) => notify({ type: NOTIFICATION.ERROR, title: ACTION.FETCH, message: error.message, crdType: OVERVIEW_ENTITY_TYPES.DESTINATION }), }); - const initialSupportedSignalsRef = useRef(DEFAULT_SUPPORTED_SIGNALS); useEffect(() => { - if (destinationFields && isActualDestination(destination?.item)) { - const { fields, exportedSignals, destinationType } = destination.item; - const destinationTypeDetails = destinationFields.destinationTypeDetails; + if (destinationTypeDetails) { + setDynamicFields( + buildFormDynamicFields(destinationTypeDetails.fields).map((field) => { + // if we have preloaded fields, we need to set the value of the field + // (this can be from an odigos-detected-destination during create, or from an existing destination during edit/update) + if (!!preLoadedFields) { + const parsedFields = typeof preLoadedFields === 'string' ? safeJsonParse>(preLoadedFields, {}) : preLoadedFields; + + if (field.name in parsedFields) { + return { + ...field, + value: parsedFields[field.name], + }; + } + } - const parsedFields = safeJsonParse>(fields, {}); - const formFields = memoizedBuildFormDynamicFields(destinationTypeDetails?.fields || []); + return field; + }), + ); + } else { + setDynamicFields([]); + } + }, [destinationTypeDetails, preLoadedFields]); - const df = formFields.map((field) => { - let fieldValue: any = parsedFields[field.name] || ''; + useEffect(() => { + handleFormChange( + 'fields', + dynamicFields.map((field) => ({ + key: field.name, + value: field.value, + })), + ); + }, [dynamicFields]); - // Check if fieldValue is a JSON string that needs stringifying - try { - const parsedValue = JSON.parse(fieldValue); + useEffect(() => { + const { logs, metrics, traces } = supportedSignals || {}; + + handleFormChange('exportedSignals', { + logs: logs?.supported || false, + metrics: metrics?.supported || false, + traces: traces?.supported || false, + }); + }, [supportedSignals]); + + function handleFormChange(key: keyof typeof INITIAL | string, val: any) { + // this is for a case where "exportedSignals" have been changed, it's an object so they children are targeted as: "exportedSignals.logs" + const [parentKey, childKey] = key.split('.'); + + if (!!childKey) { + setFormData((prev) => ({ + ...prev, + [parentKey]: { + ...prev[parentKey], + [childKey]: val, + }, + })); + } else { + setFormData((prev) => ({ + ...prev, + [parentKey]: val, + })); + } + } - 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, - }; - }); + const resetFormData = () => { + setFormData({ ...INITIAL }); + }; - setDynamicFields(df); - setExportedSignals(exportedSignals); - setSupportedSignals(destinationType.supportedSignals); + const validateForm = (params?: { withAlert?: boolean }) => { + let ok = true; - initialDynamicFieldsRef.current = df; - initialExportedSignalsRef.current = exportedSignals; - initialSupportedSignalsRef.current = destinationType.supportedSignals; - } - }, [destinationFields, destination, memoizedBuildFormDynamicFields]); + ok = dynamicFields.every((field) => (field.required ? !!field.value : true)); - const cardData = useMemo(() => { - if (shouldSkip || !isActualDestination(destination?.item) || !destinationFields) { - return [{ title: 'Error', value: 'No destination selected or data missing' }]; + if (!ok && params?.withAlert) { + notify({ + type: NOTIFICATION.WARNING, + title: ACTION.UPDATE, + message: FORM_ALERTS.REQUIRED_FIELDS, + }); } - const { exportedSignals, destinationType, fields } = destination.item; - const parsedFields = safeJsonParse>(fields, {}); - const destinationDetails = destinationFields.destinationTypeDetails?.fields; - const fieldsData = buildDestinationFieldData(parsedFields, destinationDetails); + return ok; + }; - return [{ title: 'Destination', value: destinationType.displayName || 'N/A' }, { title: 'Monitors', value: buildMonitorsList(exportedSignals) }, ...fieldsData]; - }, [shouldSkip, destination, destinationFields]); + const loadFormWithDrawerItem = (drawerItem: DrawerBaseItem) => { + const { + destinationType: { type }, + name, + exportedSignals, + fields, + } = drawerItem.item as ActualDestination; + + const updatedData: DestinationInput = { + ...INITIAL, + type, + name, + exportedSignals, + fields: Object.entries(safeJsonParse(fields, {})).map(([key, value]: [string, string]) => ({ key, value })), + }; - // Reset function using initial values from refs - const resetFormData = useCallback(() => { - setDynamicFields(initialDynamicFieldsRef.current); - setExportedSignals(initialExportedSignalsRef.current); - setSupportedSignals(initialSupportedSignalsRef.current); - }, []); + setFormData(updatedData); + }; return { - cardData, + formData, + handleFormChange, + resetFormData, + validateForm, + loadFormWithDrawerItem, + + destinationTypeDetails, dynamicFields, - destinationType: destinationType || '', - exportedSignals, - supportedSignals, - setExportedSignals, setDynamicFields, - resetFormData, }; } - -function buildDestinationFieldData(parsedFields: Record, fieldDetails?: DestinationDetailsField[]) { - return Object.entries(parsedFields).map(([key, value]) => { - const found = fieldDetails?.find((field) => field.name === key); - - const { type } = safeJsonParse(found?.componentProperties, { type: '' }); - const secret = type === 'password' ? new Array(value.length).fill('•').join('') : ''; - - return { - title: found?.displayName || key, - value: secret || 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/useDestinationTypes.ts b/frontend/webapp/hooks/destinations/useDestinationTypes.ts index ed6ae8265..0b4ac1867 100644 --- a/frontend/webapp/hooks/destinations/useDestinationTypes.ts +++ b/frontend/webapp/hooks/destinations/useDestinationTypes.ts @@ -5,8 +5,7 @@ import { DestinationsCategory, GetDestinationTypesResponse } from '@/types'; const CATEGORIES_DESCRIPTION = { managed: 'Effortless Monitoring with Scalable Performance Management', - 'self hosted': - 'Full Control and Customization for Advanced Application Monitoring', + 'self hosted': 'Full Control and Customization for Advanced Application Monitoring', }; export interface IDestinationListItem extends DestinationsCategory { @@ -19,16 +18,13 @@ export function useDestinationTypes() { useEffect(() => { if (data) { - const destinationsCategories = data.destinationTypes.categories.map( - (category) => { - return { - name: category.name, - description: CATEGORIES_DESCRIPTION[category.name], - items: category.items, - }; - } + setDestinations( + data.destinationTypes.categories.map((category) => ({ + name: category.name, + description: CATEGORIES_DESCRIPTION[category.name], + items: category.items, + })), ); - setDestinations(destinationsCategories); } }, [data]); diff --git a/frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts b/frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts deleted file mode 100644 index 330b1390f..000000000 --- a/frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts +++ /dev/null @@ -1,22 +0,0 @@ -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 }; -} diff --git a/frontend/webapp/hooks/destinations/useTestConnection.ts b/frontend/webapp/hooks/destinations/useTestConnection.ts index a5838db92..ddf4e7bac 100644 --- a/frontend/webapp/hooks/destinations/useTestConnection.ts +++ b/frontend/webapp/hooks/destinations/useTestConnection.ts @@ -10,33 +10,20 @@ interface TestConnectionResponse { reason: string; } -interface UseTestConnectionResult { - testConnection: ( - destination: DestinationInput - ) => Promise; - loading: boolean; - error?: Error; -} - -export const useTestConnection = (): UseTestConnectionResult => { - const [testConnectionMutation, { loading, error }] = useMutation< - { testConnectionForDestination: TestConnectionResponse }, - { destination: DestinationInput } - >(TEST_CONNECTION_MUTATION); +export const useTestConnection = () => { + const [testConnectionMutation, { loading, error, data }] = useMutation<{ testConnectionForDestination: TestConnectionResponse }, { destination: DestinationInput }>(TEST_CONNECTION_MUTATION, { + onError: (error, clientOptions) => { + console.error('Error testing connection:', error); + }, + onCompleted: (data, clientOptions) => { + console.log('Successfully tested connection:', data); + }, + }); - const testConnection = async ( - destination: DestinationInput - ): Promise => { - try { - const { data } = await testConnectionMutation({ - variables: { destination }, - }); - return data?.testConnectionForDestination; - } catch (err) { - console.error('Error testing connection:', err); - return undefined; - } + return { + testConnection: (destination: DestinationInput) => testConnectionMutation({ variables: { destination } }), + loading, + error, + data, }; - - return { testConnection, loading, error }; }; diff --git a/frontend/webapp/hooks/index.tsx b/frontend/webapp/hooks/index.tsx index c06f1e88c..6198e2ae9 100644 --- a/frontend/webapp/hooks/index.tsx +++ b/frontend/webapp/hooks/index.tsx @@ -1,4 +1,3 @@ -export * from './setup'; export * from './common'; export * from './config'; export * from './sources'; diff --git a/frontend/webapp/hooks/setup/index.ts b/frontend/webapp/hooks/setup/index.ts deleted file mode 100644 index 34949e277..000000000 --- a/frontend/webapp/hooks/setup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useConnectEnv'; diff --git a/frontend/webapp/hooks/setup/useConnectEnv.ts b/frontend/webapp/hooks/setup/useConnectEnv.ts deleted file mode 100644 index 6a00d4ae9..000000000 --- a/frontend/webapp/hooks/setup/useConnectEnv.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useAppStore } from '@/store'; -import { DestinationInput } from '@/types'; -import { useSourceCRUD } from '../sources'; -import { useState, useCallback } from 'react'; -import { useDestinationCRUD } from '../destinations'; - -type ConnectEnvResult = { - success: boolean; - destinationId?: string; -}; - -export const useConnectEnv = () => { - const { createSources } = useSourceCRUD(); - const { createDestination } = useDestinationCRUD(); - const { configuredSources, configuredFutureApps, resetSources } = useAppStore((state) => state); - - const [result, setResult] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const connectEnv = useCallback( - async (destination: DestinationInput, callback?: () => void) => { - setLoading(true); - setError(null); - setResult(null); - - try { - await createSources(configuredSources, configuredFutureApps); - resetSources(); - - const { data } = await createDestination(destination); - const destinationId = data?.createNewDestination.id; - - callback && callback(); - setResult({ success: true, destinationId }); - } catch (err) { - setError((err as Error).message); - setResult({ success: false }); - } finally { - setLoading(false); - } - }, - [configuredSources, configuredFutureApps, createSources, resetSources, createDestination], - ); - - return { - connectEnv, - result, - loading, - error, - }; -}; diff --git a/frontend/webapp/hooks/sources/useSourceFormData.ts b/frontend/webapp/hooks/sources/useSourceFormData.ts index 9987d25e4..c2d4e5265 100644 --- a/frontend/webapp/hooks/sources/useSourceFormData.ts +++ b/frontend/webapp/hooks/sources/useSourceFormData.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'; +import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; import { useAppStore } from '@/store'; import type { K8sActualSource } from '@/types'; import { useNamespace } from '../compute-platform'; @@ -31,7 +31,7 @@ export interface UseSourceFormDataResponse { selectAllForNamespace: string; showSelectedOnly: boolean; setSearchText: Dispatch>; - onSelectAll: (bool: boolean, namespace?: string) => void; + onSelectAll: (bool: boolean, namespace?: string, isFromInterval?: boolean) => void; setShowSelectedOnly: Dispatch>; filterSources: (namespace?: string, options?: { cancelSearch?: boolean; cancelSelected?: boolean }) => K8sActualSource[]; @@ -108,9 +108,11 @@ export const useSourceFormData = (params?: UseSourceFormDataParams): UseSourceFo }); }; + const namespaceWasSelected = useRef(false); const onSelectAll: UseSourceFormDataResponse['onSelectAll'] = useCallback( - (bool, namespace) => { + (bool, namespace, isFromInterval) => { if (!!namespace) { + if (!isFromInterval) namespaceWasSelected.current = selectedNamespace === namespace; const nsAvailableSources = availableSources[namespace]; const nsSelectedSources = selectedSources[namespace]; @@ -120,7 +122,8 @@ export const useSourceFormData = (params?: UseSourceFormDataParams): UseSourceFo } else { setSelectedSources((prev) => ({ ...prev, [namespace]: bool ? nsAvailableSources : [] })); setSelectAllForNamespace(''); - if (!!nsAvailableSources.length) setSelectedNamespace(''); + if (!!nsAvailableSources.length && !namespaceWasSelected.current) setSelectedNamespace(''); + namespaceWasSelected.current = false; } } else { setSelectAll(bool); @@ -139,7 +142,7 @@ export const useSourceFormData = (params?: UseSourceFormDataParams): UseSourceFo // if selectedSources returns an emtpy array, it will stop to prevent inifnite loop where no availableSources ever exist for that namespace useEffect(() => { if (!!selectAllForNamespace) { - const interval = setInterval(() => onSelectAll(true, selectAllForNamespace), 100); + const interval = setInterval(() => onSelectAll(true, selectAllForNamespace, true), 100); return () => clearInterval(interval); } }, [selectAllForNamespace, onSelectAll]); diff --git a/frontend/webapp/reuseable-components/checkbox/index.tsx b/frontend/webapp/reuseable-components/checkbox/index.tsx index 874a52599..ccd3a16f5 100644 --- a/frontend/webapp/reuseable-components/checkbox/index.tsx +++ b/frontend/webapp/reuseable-components/checkbox/index.tsx @@ -60,14 +60,12 @@ const Checkbox: React.FC = ({ title, titleColor, tooltip, initial {isChecked && } + {title && ( - - {title} - - )} - {tooltip && ( - - + + + {title} + )} diff --git a/frontend/webapp/reuseable-components/divider/index.tsx b/frontend/webapp/reuseable-components/divider/index.tsx index b941378b6..c2b81abf0 100644 --- a/frontend/webapp/reuseable-components/divider/index.tsx +++ b/frontend/webapp/reuseable-components/divider/index.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; interface Props { orientation?: 'horizontal' | 'vertical'; thickness?: number; - length?: number | string; + length?: string; color?: string; margin?: string; } diff --git a/frontend/webapp/reuseable-components/drawer/index.tsx b/frontend/webapp/reuseable-components/drawer/index.tsx index a954d2b92..fef39cd29 100644 --- a/frontend/webapp/reuseable-components/drawer/index.tsx +++ b/frontend/webapp/reuseable-components/drawer/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { useKeyDown } from '@/hooks'; import styled from 'styled-components'; import { slide, Overlay } from '@/styles'; +import { useKeyDown, useTransition } from '@/hooks'; -interface DrawerProps { +interface Props { isOpen: boolean; onClose: () => void; closeOnEscape?: boolean; @@ -13,11 +13,9 @@ interface DrawerProps { children: React.ReactNode; } -// Styled-component for drawer container -const DrawerContainer = styled.div<{ - $isOpen: DrawerProps['isOpen']; - $position: DrawerProps['position']; - $width: DrawerProps['width']; +const Container = styled.div<{ + $position: Props['position']; + $width: Props['width']; }>` position: fixed; top: 0; @@ -28,26 +26,26 @@ const DrawerContainer = styled.div<{ background: ${({ theme }) => theme.colors.translucent_bg}; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); overflow-y: auto; - animation: ${({ $isOpen, $position = 'right' }) => ($isOpen ? slide.in[$position] : slide.out[$position])} 0.3s ease; `; -export const Drawer: React.FC = ({ isOpen, onClose, position = 'right', width = '300px', children, closeOnEscape = true }) => { - useKeyDown( - { - key: 'Escape', - active: isOpen && closeOnEscape, - }, - () => onClose(), - ); +export const Drawer: React.FC = ({ isOpen, onClose, position = 'right', width = '300px', children, closeOnEscape = true }) => { + useKeyDown({ key: 'Escape', active: isOpen && closeOnEscape }, () => onClose()); + + const Transition = useTransition({ + container: Container, + animateIn: slide.in[position], + animateOut: slide.out[position], + }); if (!isOpen) return null; return ReactDOM.createPortal( <> , document.body, ); diff --git a/frontend/webapp/reuseable-components/field-label/index.tsx b/frontend/webapp/reuseable-components/field-label/index.tsx index 504518c30..d59fff73b 100644 --- a/frontend/webapp/reuseable-components/field-label/index.tsx +++ b/frontend/webapp/reuseable-components/field-label/index.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import Image from 'next/image'; import { Text } from '../text'; import { Tooltip } from '../tooltip'; import styled from 'styled-components'; @@ -30,15 +29,12 @@ const FieldLabel = ({ title, required, tooltip, style }: { title?: string; requi if (!title) return null; return ( - - {title} - {!required && (optional)} - {tooltip && ( - - - - )} - + + + {title} + {!required && (optional)} + + ); }; diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index 4c9ae0197..85b87b864 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -31,4 +31,3 @@ export * from './drawer'; export * from './input-table'; export * from './status'; export * from './field-label'; -export * from './transition'; diff --git a/frontend/webapp/reuseable-components/input-list/index.tsx b/frontend/webapp/reuseable-components/input-list/index.tsx index d4591afeb..26ce29e18 100644 --- a/frontend/webapp/reuseable-components/input-list/index.tsx +++ b/frontend/webapp/reuseable-components/input-list/index.tsx @@ -6,13 +6,15 @@ import styled from 'styled-components'; import { FieldLabel } from '../field-label'; import React, { useEffect, useMemo, useRef, useState } from 'react'; +type Row = string; + interface InputListProps { - initialValues?: string[]; + initialValues?: Row[]; + value?: Row[]; + onChange: (values: Row[]) => void; title?: string; tooltip?: string; required?: boolean; - value?: string[]; - onChange: (values: string[]) => void; } const Container = styled.div` @@ -54,13 +56,13 @@ const ButtonText = styled(Text)` text-decoration-line: underline; `; -const INITIAL = ['']; +const INITIAL_ROW: Row = ''; -const InputList: React.FC = ({ initialValues = INITIAL, value = INITIAL, onChange, title, tooltip, required }) => { - const [rows, setRows] = useState(value || initialValues); +const InputList: React.FC = ({ initialValues = [], value, onChange, title, tooltip, required }) => { + const [rows, setRows] = useState(value || initialValues); useEffect(() => { - if (!rows.length) setRows(INITIAL); + if (!rows.length) setRows([INITIAL_ROW]); }, []); // Filter out rows where either key or value is empty @@ -79,7 +81,11 @@ const InputList: React.FC = ({ initialValues = INITIAL, value = }, [validRows, onChange]); const handleAddInput = () => { - setRows((prev) => [...prev, '']); + setRows((prev) => { + const payload = [...prev]; + payload.push(INITIAL_ROW); + return payload; + }); }; const handleDeleteInput = (idx: number) => { diff --git a/frontend/webapp/reuseable-components/input-table/index.tsx b/frontend/webapp/reuseable-components/input-table/index.tsx index 608ccc560..c158c116c 100644 --- a/frontend/webapp/reuseable-components/input-table/index.tsx +++ b/frontend/webapp/reuseable-components/input-table/index.tsx @@ -6,6 +6,10 @@ import styled from 'styled-components'; import { FieldLabel } from '../field-label'; import React, { useState, useEffect, useRef, useMemo } from 'react'; +type Row = { + [key: string]: any; +}; + interface Props { columns: { title: string; @@ -15,9 +19,9 @@ interface Props { tooltip?: string; required?: boolean; }[]; - initialValues?: Record[]; - value?: Record[]; - onChange?: (values: Record[]) => void; + initialValues?: Row[]; + value?: Row[]; + onChange?: (values: Row[]) => void; } const Container = styled.div` @@ -53,16 +57,18 @@ const ButtonText = styled(Text)` text-decoration-line: underline; `; -export const InputTable: React.FC = ({ columns, initialValues = [], value = [], onChange }) => { - const [initialObject, setInitialObject] = useState({}); - const [rows, setRows] = useState(value || initialValues); +export const InputTable: React.FC = ({ columns, initialValues = [], value, onChange }) => { + // INITIAL_ROW as state, because it's dynamic to the "columns" prop + const [initialRow, setInitialRow] = useState({}); + const [rows, setRows] = useState(value || initialValues); useEffect(() => { - const init = {}; - columns.forEach(({ keyName }) => (init[keyName] = '')); - setInitialObject(init); - - if (!rows.length) setRows([{ ...init }]); + if (!rows.length) { + const init = {}; + columns.forEach(({ keyName }) => (init[keyName] = '')); + setInitialRow(init); + setRows([{ ...init }]); + } }, []); // Filter out rows where either key or value is empty @@ -83,7 +89,7 @@ export const InputTable: React.FC = ({ columns, initialValues = [], value const handleAddRow = () => { setRows((prev) => { const payload = [...prev]; - payload.push({ ...initialObject }); + payload.push({ ...initialRow }); return payload; }); }; 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 18e7f3a22..2824906d2 100644 --- a/frontend/webapp/reuseable-components/key-value-input-list/index.tsx +++ b/frontend/webapp/reuseable-components/key-value-input-list/index.tsx @@ -6,13 +6,18 @@ import styled from 'styled-components'; import { FieldLabel } from '../field-label'; import React, { useState, useEffect, useRef, useMemo } from 'react'; +type Row = { + key: string; + value: string; +}; + interface KeyValueInputsListProps { - initialKeyValuePairs?: { key: string; value: string }[]; - value?: { key: string; value: string }[]; + initialKeyValuePairs?: Row[]; + value?: Row[]; + onChange?: (validKeyValuePairs: Row[]) => void; title?: string; tooltip?: string; required?: boolean; - onChange?: (validKeyValuePairs: { key: string; value: string }[]) => void; } const Container = styled.div` @@ -55,13 +60,16 @@ const ButtonText = styled(Text)` text-decoration-line: underline; `; -const INITIAL = [{ key: '', value: '' }]; +const INITIAL_ROW: Row = { + key: '', + value: '', +}; -export const KeyValueInputsList: React.FC = ({ initialKeyValuePairs = INITIAL, value = INITIAL, onChange, title, tooltip, required }) => { - const [rows, setRows] = useState<{ key: string; value: string }[]>(value || initialKeyValuePairs); +export const KeyValueInputsList: React.FC = ({ initialKeyValuePairs = [], value, onChange, title, tooltip, required }) => { + const [rows, setRows] = useState(value || initialKeyValuePairs); useEffect(() => { - if (!rows.length) setRows(INITIAL); + if (!rows.length) setRows([{ ...INITIAL_ROW }]); }, []); // Filter out rows where either key or value is empty @@ -82,7 +90,7 @@ export const KeyValueInputsList: React.FC = ({ initialK const handleAddRow = () => { setRows((prev) => { const payload = [...prev]; - payload.push({ key: '', value: '' }); + payload.push({ ...INITIAL_ROW }); return payload; }); }; diff --git a/frontend/webapp/reuseable-components/modal/index.tsx b/frontend/webapp/reuseable-components/modal/index.tsx index c278a2a3c..f961f0adb 100644 --- a/frontend/webapp/reuseable-components/modal/index.tsx +++ b/frontend/webapp/reuseable-components/modal/index.tsx @@ -2,11 +2,11 @@ import React from 'react'; import Image from 'next/image'; import { Text } from '../text'; import ReactDOM from 'react-dom'; -import { useKeyDown } from '@/hooks'; import styled from 'styled-components'; +import { useKeyDown, useTransition } from '@/hooks'; import { slide, Overlay, CenterThis } from '@/styles'; -interface ModalProps { +interface Props { isOpen: boolean; noOverlay?: boolean; header?: { @@ -17,7 +17,7 @@ interface ModalProps { children: React.ReactNode; } -const ModalWrapper = styled.div<{ $isOpen: ModalProps['isOpen'] }>` +const Container = styled.div` position: fixed; top: 50%; left: 50%; @@ -30,7 +30,6 @@ const ModalWrapper = styled.div<{ $isOpen: ModalProps['isOpen'] }>` border-radius: 40px; box-shadow: 0px 1px 1px 0px rgba(17, 17, 17, 0.8), 0px 2px 2px 0px rgba(17, 17, 17, 0.8), 0px 5px 5px 0px rgba(17, 17, 17, 0.8), 0px 10px 10px 0px rgba(17, 17, 17, 0.8), 0px 0px 8px 0px rgba(17, 17, 17, 0.8); - animation: ${({ $isOpen }) => ($isOpen ? slide.in['center'] : slide.out['center'])} 0.3s ease; `; const ModalHeader = styled.div` @@ -83,22 +82,22 @@ const CancelText = styled(Text)` cursor: pointer; `; -const Modal: React.FC = ({ isOpen, noOverlay, header, actionComponent, onClose, children }) => { - useKeyDown( - { - key: 'Escape', - active: isOpen, - }, - () => onClose(), - ); +const Modal: React.FC = ({ isOpen, noOverlay, header, actionComponent, onClose, children }) => { + useKeyDown({ key: 'Escape', active: isOpen }, () => onClose()); + + const Transition = useTransition({ + container: Container, + animateIn: slide.in['center'], + animateOut: slide.out['center'], + }); if (!isOpen) return null; return ReactDOM.createPortal( <> - + , document.body, ); diff --git a/frontend/webapp/reuseable-components/monitoring-checkboxes/index.tsx b/frontend/webapp/reuseable-components/monitoring-checkboxes/index.tsx index 3062df3bd..a8172c8d7 100644 --- a/frontend/webapp/reuseable-components/monitoring-checkboxes/index.tsx +++ b/frontend/webapp/reuseable-components/monitoring-checkboxes/index.tsx @@ -6,6 +6,7 @@ import { MONITORING_OPTIONS, SignalLowercase, SignalUppercase } from '@/utils'; interface Props { isVertical?: boolean; + title?: string; allowedSignals?: SignalUppercase[]; selectedSignals: SignalUppercase[]; setSelectedSignals: (value: SignalUppercase[]) => void; @@ -14,7 +15,7 @@ interface Props { const ListContainer = styled.div<{ $isVertical?: Props['isVertical'] }>` display: flex; flex-direction: ${({ $isVertical }) => ($isVertical ? 'column' : 'row')}; - gap: ${({ $isVertical }) => ($isVertical ? '16px' : '32px')}; + gap: ${({ $isVertical }) => ($isVertical ? '12px' : '24px')}; `; const monitors = MONITORING_OPTIONS; @@ -27,7 +28,7 @@ const isSelected = (type: SignalLowercase, selectedSignals: Props['selectedSigna return !!selectedSignals?.find((str) => str === type.toUpperCase()); }; -const MonitoringCheckboxes: React.FC = ({ isVertical, allowedSignals, selectedSignals, setSelectedSignals }) => { +const MonitoringCheckboxes: React.FC = ({ isVertical, title = 'Monitoring', allowedSignals, selectedSignals, setSelectedSignals }) => { const [isLastSelection, setIsLastSelection] = useState(selectedSignals.length === 1); const recordedRows = useRef(JSON.stringify(selectedSignals)); @@ -47,6 +48,10 @@ const MonitoringCheckboxes: React.FC = ({ isVertical, allowedSignals, sel setSelectedSignals(payload); setIsLastSelection(payload.length === 1); } + + return () => { + recordedRows.current = ''; + }; // eslint-disable-next-line }, [allowedSignals]); @@ -60,7 +65,7 @@ const MonitoringCheckboxes: React.FC = ({ isVertical, allowedSignals, sel return (
- + {title && } {monitors.map((monitor) => { diff --git a/frontend/webapp/reuseable-components/tab-list/index.tsx b/frontend/webapp/reuseable-components/tab-list/index.tsx index 02b056327..7e9e42596 100644 --- a/frontend/webapp/reuseable-components/tab-list/index.tsx +++ b/frontend/webapp/reuseable-components/tab-list/index.tsx @@ -50,7 +50,7 @@ const TabListContainer = styled.div` // Tab component const Tab: React.FC = ({ title, tooltip, icon, selected, disabled, onClick }) => { return ( - + {title} {title} diff --git a/frontend/webapp/reuseable-components/textarea/index.tsx b/frontend/webapp/reuseable-components/textarea/index.tsx index 435cb13dd..d63208bc9 100644 --- a/frontend/webapp/reuseable-components/textarea/index.tsx +++ b/frontend/webapp/reuseable-components/textarea/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Text } from '../text'; import { FieldLabel } from '../field-label'; import styled, { css } from 'styled-components'; @@ -61,7 +61,7 @@ const StyledTextArea = styled.textarea` background: none; color: ${({ theme }) => theme.colors.text}; font-size: 14px; - padding: 12px 20px; + padding: 12px 20px 0; font-family: ${({ theme }) => theme.font_family.primary}; font-weight: 300; line-height: 22px; @@ -93,13 +93,32 @@ const ErrorMessage = styled(Text)` margin-top: 4px; `; -const TextArea: React.FC = ({ errorMessage, title, tooltip, required, ...props }) => { +const TextArea: React.FC = ({ errorMessage, title, tooltip, required, onChange, ...props }) => { + const ref = useRef(null); + + const resize = () => { + // this is to auto-resize the textarea according to the number of rows typed + if (ref.current) { + ref.current.style.height = 'auto'; + ref.current.style.height = `${ref.current.scrollHeight}px`; + } + }; + return ( - + { + resize(); + onChange?.(e); + }} + {...props} + /> {errorMessage && ( diff --git a/frontend/webapp/reuseable-components/toggle-buttons/index.tsx b/frontend/webapp/reuseable-components/toggle-buttons/index.tsx index b051a6ef3..d40ed610a 100644 --- a/frontend/webapp/reuseable-components/toggle-buttons/index.tsx +++ b/frontend/webapp/reuseable-components/toggle-buttons/index.tsx @@ -77,7 +77,7 @@ const ToggleButtons: React.FC = ({ activeText = 'Active', inactiveT }; return ( - + handleToggle(true)} disabled={disabled}> @@ -88,8 +88,6 @@ const ToggleButtons: React.FC = ({ activeText = 'Active', inactiveT {inactiveText} - - {tooltip && } ); }; diff --git a/frontend/webapp/reuseable-components/toggle/index.tsx b/frontend/webapp/reuseable-components/toggle/index.tsx index b0b752773..010d525ac 100644 --- a/frontend/webapp/reuseable-components/toggle/index.tsx +++ b/frontend/webapp/reuseable-components/toggle/index.tsx @@ -61,14 +61,12 @@ const Toggle: React.FC = ({ title, tooltip, initialValue = false, o }; return ( - - - + + + {title} - - - {tooltip && } - + + ); }; diff --git a/frontend/webapp/reuseable-components/tooltip/index.tsx b/frontend/webapp/reuseable-components/tooltip/index.tsx index be4a0771d..90d7d82e1 100644 --- a/frontend/webapp/reuseable-components/tooltip/index.tsx +++ b/frontend/webapp/reuseable-components/tooltip/index.tsx @@ -1,73 +1,71 @@ -import React, { useState, useRef, ReactNode, useEffect } from 'react'; -import { Text } from '../text'; +import React, { useState, PropsWithChildren } from 'react'; +import Image from 'next/image'; import ReactDOM from 'react-dom'; +import { Text } from '../text'; import styled from 'styled-components'; -interface TooltipProps { - text: ReactNode; - children: ReactNode; +interface Position { + top: number; + left: number; } -const TooltipWrapper = styled.div` - display: flex; +interface TooltipProps extends PropsWithChildren { + text?: string; + withIcon?: boolean; +} + +interface PopupProps extends PropsWithChildren, Position {} + +const TooltipContainer = styled.div` position: relative; + display: flex; align-items: center; + gap: 4px; `; -const TooltipContent = styled.div<{ $top: number; $left: number }>` - position: absolute; - top: ${({ $top }) => $top}px; - left: ${({ $left }) => $left}px; - border-radius: 32px; - background-color: ${({ theme }) => theme.colors.dark_grey}; - border: 1px solid ${({ theme }) => theme.colors.border}; - color: ${({ theme }) => theme.text.primary}; - padding: 16px; - z-index: 9999; - pointer-events: none; - max-width: 300px; -`; - -const Tooltip: React.FC = ({ text, children }) => { +export const Tooltip: React.FC = ({ text, withIcon, children }) => { const [isHovered, setIsHovered] = useState(false); - const [position, setPosition] = useState({ top: 0, left: 0 }); - const wrapperRef = useRef(null); - - useEffect(() => { - const handleMouseMove = (e: MouseEvent) => { - if (wrapperRef.current) { - const { top, left } = wrapperRef.current.getBoundingClientRect(); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); - setPosition({ - top: top + window.scrollY, - left: left + window.scrollX, - }); - } - }; + const handleMouseEvent = (e: React.MouseEvent) => { + const { type, clientX, clientY } = e; - if (isHovered) { - document.addEventListener('mousemove', handleMouseMove); - } else { - document.removeEventListener('mousemove', handleMouseMove); - } - - return () => document.removeEventListener('mousemove', handleMouseMove); - }, [isHovered]); + setIsHovered(type !== 'mouseleave'); + setPopupPosition({ top: clientY, left: clientX + 24 }); + }; if (!text) return <>{children}; - const tooltipContent = ( - - {text} - - ); - return ( - setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> + {children} - {isHovered && ReactDOM.createPortal(tooltipContent, document.body)} - + {withIcon && info} + {isHovered && {text}} + ); }; -export { Tooltip }; +const PopupContainer = styled.div<{ $top: number; $left: number }>` + position: absolute; + top: ${({ $top }) => $top}px; + left: ${({ $left }) => $left}px; + z-index: 9999; + + max-width: 270px; + padding: 8px 12px; + border-radius: 16px; + border: 1px solid ${({ theme }) => theme.colors.white_opacity['008']}; + background-color: ${({ theme }) => theme.colors.info}; + color: ${({ theme }) => theme.text.primary}; + + pointer-events: none; +`; + +const Popup: React.FC = ({ top, left, children }) => { + return ReactDOM.createPortal( + + {children} + , + document.body, + ); +}; diff --git a/frontend/webapp/reuseable-components/transition/index.tsx b/frontend/webapp/reuseable-components/transition/index.tsx deleted file mode 100644 index 75e0ad43d..000000000 --- a/frontend/webapp/reuseable-components/transition/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { PropsWithChildren, useEffect, useState } from 'react'; -import styled from 'styled-components'; -import type { IStyledComponentBase, Keyframes, Substitute } from 'styled-components/dist/types'; - -interface Props { - container: IStyledComponentBase<'web', Substitute, HTMLElement>, {}>> & string; - animateIn: Keyframes; - animateOut: Keyframes; - enter: boolean; -} - -const Animated = (Container: Props['container']) => styled(Container)<{ - $isEntering: boolean; - $isLeaving: boolean; - $animateIn: Props['animateIn']; - $animateOut: Props['animateOut']; -}>` - animation: ${({ $isEntering, $isLeaving, $animateIn, $animateOut }) => ($isEntering ? $animateIn : $isLeaving ? $animateOut : 'none')} 0.3s forwards; -`; - -export const Transition: React.FC> = ({ container: Container, children, animateIn, animateOut, enter }) => { - const AnimatedContainer = Animated(Container); - const [isEntered, setIsEntered] = useState(false); - - useEffect(() => { - if (enter) setIsEntered(true); - }, [enter]); - - if (!enter && !isEntered) return null; - - return ( - - {children} - - ); -}; diff --git a/frontend/webapp/store/useAppStore.ts b/frontend/webapp/store/useAppStore.ts index 680a4eb2c..e3013f0bf 100644 --- a/frontend/webapp/store/useAppStore.ts +++ b/frontend/webapp/store/useAppStore.ts @@ -1,20 +1,22 @@ import { create } from 'zustand'; -import type { ConfiguredDestination, K8sActualSource } from '@/types'; +import type { ConfiguredDestination, DestinationInput, K8sActualSource } from '@/types'; export interface IAppState { availableSources: { [key: string]: K8sActualSource[] }; configuredSources: { [key: string]: K8sActualSource[] }; configuredFutureApps: { [key: string]: boolean }; - configuredDestinations: ConfiguredDestination[]; + configuredDestinations: { stored: ConfiguredDestination; form: DestinationInput }[]; } interface IAppStateSetters { setAvailableSources: (payload: IAppState['availableSources']) => void; setConfiguredSources: (payload: IAppState['configuredSources']) => void; setConfiguredFutureApps: (payload: IAppState['configuredFutureApps']) => void; + setConfiguredDestinations: (payload: IAppState['configuredDestinations']) => void; - addConfiguredDestination: (payload: ConfiguredDestination) => void; - resetSources: () => void; + addConfiguredDestination: (payload: { stored: ConfiguredDestination; form: DestinationInput }) => void; + removeConfiguredDestination: (payload: { type: string }) => void; + resetState: () => void; } @@ -27,10 +29,11 @@ const useAppStore = create((set) => ({ setAvailableSources: (payload) => set({ availableSources: payload }), setConfiguredSources: (payload) => set({ configuredSources: payload }), setConfiguredFutureApps: (payload) => set({ configuredFutureApps: payload }), + setConfiguredDestinations: (payload) => set({ configuredDestinations: payload }), addConfiguredDestination: (payload) => set((state) => ({ configuredDestinations: [...state.configuredDestinations, payload] })), + removeConfiguredDestination: (payload) => set((state) => ({ configuredDestinations: state.configuredDestinations.filter(({ stored }) => stored.type !== payload.type) })), - resetSources: () => set(() => ({ availableSources: {}, configuredSources: {}, configuredFutureApps: {} })), resetState: () => set(() => ({ availableSources: {}, configuredSources: {}, configuredFutureApps: {}, configuredDestinations: [] })), })); diff --git a/frontend/webapp/styles/styled.tsx b/frontend/webapp/styles/styled.tsx index bfc3ecf3e..891638cda 100644 --- a/frontend/webapp/styles/styled.tsx +++ b/frontend/webapp/styles/styled.tsx @@ -24,7 +24,6 @@ export const Overlay = styled.div` export const ModalBody = styled.div` width: 640px; height: calc(100vh - 300px); - margin: 0 7vw; - padding-top: 64px; + margin: 64px 7vw 0 7vw; overflow-y: scroll; `; diff --git a/frontend/webapp/types/destinations.ts b/frontend/webapp/types/destinations.ts index 55850608b..4a83057fe 100644 --- a/frontend/webapp/types/destinations.ts +++ b/frontend/webapp/types/destinations.ts @@ -131,7 +131,6 @@ export interface DestinationConfig { export interface ActualDestination { id: string; name: string; - type: string; exportedSignals: { traces: boolean; metrics: boolean; diff --git a/frontend/webapp/utils/constants/string.tsx b/frontend/webapp/utils/constants/string.tsx index c65eb8124..457bbfa68 100644 --- a/frontend/webapp/utils/constants/string.tsx +++ b/frontend/webapp/utils/constants/string.tsx @@ -29,6 +29,7 @@ export const ACTION = { CREATE: 'Create', UPDATE: 'Update', DELETE: 'Delete', + FETCH: 'Fetch', }; export const FORM_ALERTS = { diff --git a/frontend/webapp/utils/functions/icons.ts b/frontend/webapp/utils/functions/icons.ts index 17e8f727c..9f747bd77 100644 --- a/frontend/webapp/utils/functions/icons.ts +++ b/frontend/webapp/utils/functions/icons.ts @@ -33,11 +33,14 @@ export const getRuleIcon = (type?: InstrumentationRuleType) => { return `/icons/rules/${typeLowerCased}.svg`; }; -export const getActionIcon = (type?: ActionsType | 'sampler') => { +export const getActionIcon = (type?: ActionsType | 'sampler' | 'attributes') => { if (!type) return BRAND_ICON; const typeLowerCased = type.toLowerCase(); const isSampler = typeLowerCased.includes('sampler'); + const isAttributes = typeLowerCased === 'attributes'; - return `/icons/actions/${isSampler ? 'sampler' : typeLowerCased}.svg`; + const iconName = isSampler ? 'sampler' : isAttributes ? 'piimasking' : typeLowerCased; + + return `/icons/actions/${iconName}.svg`; }; diff --git a/helm/odigos/templates/ui/clusterrole.yaml b/helm/odigos/templates/ui/clusterrole.yaml index 08a7bb5ba..80a07c803 100644 --- a/helm/odigos/templates/ui/clusterrole.yaml +++ b/helm/odigos/templates/ui/clusterrole.yaml @@ -12,6 +12,12 @@ rules: - list - watch - patch + - apiGroups: + - "" + resources: + - services + verbs: + - list - apiGroups: - "" resources: diff --git a/instrumentor/go.mod b/instrumentor/go.mod index 79d0deb64..5564b2039 100644 --- a/instrumentor/go.mod +++ b/instrumentor/go.mod @@ -10,7 +10,7 @@ require ( github.com/odigos-io/odigos/common v0.0.0 github.com/odigos-io/odigos/k8sutils v0.0.0 github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.34.2 + github.com/onsi/gomega v1.36.0 github.com/stretchr/testify v1.10.0 k8s.io/api v0.31.0 k8s.io/apimachinery v0.31.0 @@ -77,14 +77,14 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/term v0.23.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/instrumentor/go.sum b/instrumentor/go.sum index e0b27b0ed..671315dee 100644 --- a/instrumentor/go.sum +++ b/instrumentor/go.sum @@ -127,8 +127,8 @@ github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y= +github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -181,8 +181,8 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -193,8 +193,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -212,14 +212,14 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -249,8 +249,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=