From a7a7dfa81a64fc2cc83f69d0140a9f840ccb8c47 Mon Sep 17 00:00:00 2001 From: Ben Elferink Date: Tue, 26 Nov 2024 12:45:37 +0200 Subject: [PATCH] UI bugs party (high priority tasks) (#1851) This pull request includes significant changes to the `frontend/webapp` directory, focusing on renaming components, updating styles, and removing unused components. The most important changes include renaming several files and components, updating CSS styles, and removing deprecated components. ### Component Renaming: * [`frontend/webapp/containers/main/actions/action-drawer/index.tsx`](diffhunk://#diff-5f56695cd2d0ca6bcd28f372653c71d8c4dab572b08715c1f36b7acc5cf50f60R2-L11): Renamed from `action-drawer-container/index.tsx` and updated imports and references to reflect the new name. [[1]](diffhunk://#diff-5f56695cd2d0ca6bcd28f372653c71d8c4dab572b08715c1f36b7acc5cf50f60R2-L11) [[2]](diffhunk://#diff-5f56695cd2d0ca6bcd28f372653c71d8c4dab572b08715c1f36b7acc5cf50f60L39-R42) [[3]](diffhunk://#diff-5f56695cd2d0ca6bcd28f372653c71d8c4dab572b08715c1f36b7acc5cf50f60L89-R92) * [`frontend/webapp/containers/main/actions/action-form-body/index.tsx`](diffhunk://#diff-13a4f6f896b629d2504615533aa1d48fe792fbadb7132f640b09c03312243ee9L5-R8): Renamed from `choose-action-body/index.tsx` and updated imports and references to reflect the new name. [[1]](diffhunk://#diff-13a4f6f896b629d2504615533aa1d48fe792fbadb7132f640b09c03312243ee9L5-R8) [[2]](diffhunk://#diff-13a4f6f896b629d2504615533aa1d48fe792fbadb7132f640b09c03312243ee9L26-R26) [[3]](diffhunk://#diff-13a4f6f896b629d2504615533aa1d48fe792fbadb7132f640b09c03312243ee9L48-L49) ### CSS Updates: * [`frontend/webapp/app/globals.css`](diffhunk://#diff-0ad5feab59ca691369930750cde64b75419ddd0dbe0e567e069550de11c4b051R5-R9): Added custom scrollbar styles to improve UI consistency. ### Component Updates: * [`frontend/webapp/app/(setup)/choose-destination/page.tsx`](diffhunk://#diff-a5bf9835aacf24e306780f3d34f808144b9bd35c4621caa9e84ef65f74c5110bL5-R13): Replaced `ChooseDestinationContainer` with `AddDestinationContainer` to reflect updated component usage. * [`frontend/webapp/components/common/configured-fields/index.tsx`](diffhunk://#diff-03463bf02731c500640be42702debbd088df7a64298eac7d7474f5864f0c6022L85-L87): Added `withIcon` prop to `Tooltip` component for enhanced UI. ### Component Removal: * Removed deprecated components and their exports in the `frontend/webapp/components/destinations` directory, including `add-destination-button`, `edit-destination-form`, and `monitors-tap-list`. [[1]](diffhunk://#diff-f7c0d745d7d9b8402a9ef58a20e357fecbbba57a4a959b506f0b4bf35cbea521L1-L33) [[2]](diffhunk://#diff-4550d93f531d2fd35db5e6a0c5b43d366ccf81f90ce98f728b2babc2d0ab1c53L1-L35) [[3]](diffhunk://#diff-8abd4690321df46172b1c1666c6580c02f3352e9ba818d1a58abfd856b70f86eL1-L53) [[4]](diffhunk://#diff-a103f3e48f6bb832f2ca24b94c97f279b7d1e8a32ee3b59d6b5ac026d259e507L1-L3) [[5]](diffhunk://#diff-9a255ecda06fb13562b464a919eb789a51ea8728251b2aa31c28babfd8f3405dL4) These changes collectively improve the codebase by updating component names for clarity, enhancing UI elements, and removing unused components to streamline the project. --- cli/cmd/resources/ui.go | 5 + frontend/main.go | 7 +- .../app/(setup)/choose-destination/page.tsx | 4 +- frontend/webapp/app/globals.css | 5 + frontend/webapp/app/layout.tsx | 3 +- .../common/configured-fields/index.tsx | 3 +- .../add-destination-button/index.tsx | 33 --- .../edit-destination-form/index.tsx | 35 --- .../webapp/components/destinations/index.ts | 3 - .../destinations/monitors-tap-list/index.tsx | 53 ---- frontend/webapp/components/index.ts | 1 - .../build-card-from-action-spec.ts | 0 .../index.tsx | 11 +- .../custom-fields/add-cluster-info.tsx | 11 +- .../custom-fields/delete-attributes.tsx | 0 .../custom-fields/error-sampler.tsx | 6 +- .../custom-fields/index.tsx | 0 .../custom-fields/latency-sampler.tsx | 0 .../custom-fields/pii-masking.tsx | 0 .../custom-fields/probabilistic-sampler.tsx | 0 .../custom-fields/rename-attributes.tsx | 0 .../index.tsx | 8 +- .../actions/action-modal/action-options.ts | 102 ++++++++ .../index.tsx | 10 +- .../choose-action-modal/action-options.ts | 98 -------- .../webapp/containers/main/actions/index.ts | 6 +- .../add-destination-modal/index.tsx | 73 ------ .../choose-destination-menu/index.tsx | 72 ------ .../choose-destination-modal-body/index.tsx | 106 -------- .../configured-destinations-list/index.tsx | 127 +++++----- .../connection-notification.tsx | 21 -- .../form-container.tsx | 46 ---- .../connect-destination-modal-body/index.tsx | 208 --------------- .../destinations/add-destination/index.tsx | 93 ++++--- .../destinations/add-destination/styled.ts | 21 -- .../add-destination/test-connection/index.tsx | 57 ----- .../destination-drawer-container/index.tsx | 94 ------- .../destinations/destination-drawer/index.tsx | 141 +++++++++++ .../dynamic-fields}/index.tsx | 9 +- .../destination-form-body/index.tsx | 121 +++++++++ .../test-connection/index.tsx | 53 ++++ .../choose-destination-filters/index.tsx | 52 ++++ .../destination-list-item/index.tsx | 19 +- .../destinations-list/index.tsx | 0 .../potential-destinations-list/index.tsx | 30 +-- .../choose-destination-body/index.tsx | 60 +++++ .../destinations/destination-modal/index.tsx | 142 +++++++++++ .../containers/main/destinations/index.tsx | 4 +- .../main/instrumentation-rules/index.ts | 4 +- .../build-card-from-rule-spec.ts | 0 .../index.tsx | 26 +- .../custom-fields/index.tsx | 0 .../custom-fields/payload-collection.tsx | 0 .../index.tsx | 6 +- .../{add-rule-modal => rule-modal}/index.tsx | 8 +- .../rule-options.ts | 0 .../main/overview/all-drawers/index.tsx | 4 +- .../main/overview/all-modals/index.tsx | 12 +- .../overview/multi-source-control/index.tsx | 12 +- .../search/search-results/index.tsx | 4 +- .../overview-drawer/drawer-header/index.tsx | 9 +- .../main/sources/choose-sources/index.tsx | 10 +- .../sources/source-drawer-container/index.tsx | 6 +- frontend/webapp/hooks/common/index.ts | 1 + .../webapp/hooks/common/useTransition.tsx | 42 ++++ .../compute-platform/useComputePlatform.ts | 9 +- frontend/webapp/hooks/destinations/index.ts | 1 - .../destinations/useConnectDestinationForm.ts | 45 ++-- .../destinations/useDestinationFormData.ts | 236 ++++++++++-------- .../hooks/destinations/useDestinationTypes.ts | 18 +- .../useEditDestinationFormHandlers.ts | 22 -- .../hooks/destinations/useTestConnection.ts | 41 ++- frontend/webapp/hooks/index.tsx | 1 - frontend/webapp/hooks/setup/index.ts | 1 - frontend/webapp/hooks/setup/useConnectEnv.ts | 52 ---- .../webapp/hooks/sources/useSourceFormData.ts | 13 +- .../reuseable-components/checkbox/index.tsx | 12 +- .../reuseable-components/divider/index.tsx | 2 +- .../reuseable-components/drawer/index.tsx | 34 ++- .../field-label/index.tsx | 16 +- frontend/webapp/reuseable-components/index.ts | 1 - .../reuseable-components/input-list/index.tsx | 22 +- .../input-table/index.tsx | 30 ++- .../key-value-input-list/index.tsx | 24 +- .../reuseable-components/modal/index.tsx | 29 ++- .../monitoring-checkboxes/index.tsx | 11 +- .../reuseable-components/tab-list/index.tsx | 2 +- .../reuseable-components/textarea/index.tsx | 27 +- .../toggle-buttons/index.tsx | 4 +- .../reuseable-components/toggle/index.tsx | 12 +- .../reuseable-components/tooltip/index.tsx | 104 ++++---- .../reuseable-components/transition/index.tsx | 36 --- frontend/webapp/store/useAppStore.ts | 13 +- frontend/webapp/styles/styled.tsx | 3 +- frontend/webapp/types/destinations.ts | 1 - frontend/webapp/utils/constants/string.tsx | 1 + frontend/webapp/utils/functions/icons.ts | 7 +- helm/odigos/templates/ui/clusterrole.yaml | 6 + 98 files changed, 1326 insertions(+), 1607 deletions(-) delete mode 100644 frontend/webapp/components/destinations/add-destination-button/index.tsx delete mode 100644 frontend/webapp/components/destinations/edit-destination-form/index.tsx delete mode 100644 frontend/webapp/components/destinations/index.ts delete mode 100644 frontend/webapp/components/destinations/monitors-tap-list/index.tsx rename frontend/webapp/containers/main/actions/{action-drawer-container => action-drawer}/build-card-from-action-spec.ts (100%) rename frontend/webapp/containers/main/actions/{action-drawer-container => action-drawer}/index.tsx (87%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/add-cluster-info.tsx (80%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/delete-attributes.tsx (100%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/error-sampler.tsx (90%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/index.tsx (100%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/latency-sampler.tsx (100%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/pii-masking.tsx (100%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/probabilistic-sampler.tsx (100%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/custom-fields/rename-attributes.tsx (100%) rename frontend/webapp/containers/main/actions/{choose-action-body => action-form-body}/index.tsx (86%) create mode 100644 frontend/webapp/containers/main/actions/action-modal/action-options.ts rename frontend/webapp/containers/main/actions/{choose-action-modal => action-modal}/index.tsx (86%) delete mode 100644 frontend/webapp/containers/main/actions/choose-action-modal/action-options.ts delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/add-destination-modal/index.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/choose-destination-menu/index.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/choose-destination-modal-body/index.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/connection-notification.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/form-container.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/connect-destination-modal-body/index.tsx delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/styled.ts delete mode 100644 frontend/webapp/containers/main/destinations/add-destination/test-connection/index.tsx delete mode 100644 frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx create mode 100644 frontend/webapp/containers/main/destinations/destination-drawer/index.tsx rename frontend/webapp/containers/main/destinations/{add-destination/dynamic-form-fields => destination-form-body/dynamic-fields}/index.tsx (86%) create mode 100644 frontend/webapp/containers/main/destinations/destination-form-body/index.tsx create mode 100644 frontend/webapp/containers/main/destinations/destination-form-body/test-connection/index.tsx create mode 100644 frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/choose-destination-filters/index.tsx rename frontend/webapp/containers/main/destinations/{add-destination => destination-modal/choose-destination-body}/destinations-list/destination-list-item/index.tsx (85%) rename frontend/webapp/containers/main/destinations/{add-destination => destination-modal/choose-destination-body}/destinations-list/index.tsx (100%) rename frontend/webapp/containers/main/destinations/{add-destination => destination-modal/choose-destination-body}/destinations-list/potential-destinations-list/index.tsx (53%) create mode 100644 frontend/webapp/containers/main/destinations/destination-modal/choose-destination-body/index.tsx create mode 100644 frontend/webapp/containers/main/destinations/destination-modal/index.tsx rename frontend/webapp/containers/main/instrumentation-rules/{rule-drawer-container => rule-drawer}/build-card-from-rule-spec.ts (100%) rename frontend/webapp/containers/main/instrumentation-rules/{rule-drawer-container => rule-drawer}/index.tsx (93%) rename frontend/webapp/containers/main/instrumentation-rules/{choose-rule-body => rule-form-body}/custom-fields/index.tsx (100%) rename frontend/webapp/containers/main/instrumentation-rules/{choose-rule-body => rule-form-body}/custom-fields/payload-collection.tsx (100%) rename frontend/webapp/containers/main/instrumentation-rules/{choose-rule-body => rule-form-body}/index.tsx (88%) rename frontend/webapp/containers/main/instrumentation-rules/{add-rule-modal => rule-modal}/index.tsx (91%) rename frontend/webapp/containers/main/instrumentation-rules/{add-rule-modal => rule-modal}/rule-options.ts (100%) create mode 100644 frontend/webapp/hooks/common/useTransition.tsx delete mode 100644 frontend/webapp/hooks/destinations/useEditDestinationFormHandlers.ts delete mode 100644 frontend/webapp/hooks/setup/index.ts delete mode 100644 frontend/webapp/hooks/setup/useConnectEnv.ts delete mode 100644 frontend/webapp/reuseable-components/transition/index.tsx diff --git a/cli/cmd/resources/ui.go b/cli/cmd/resources/ui.go index 7af7877bfa..23e34ee090 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/frontend/main.go b/frontend/main.go index ded2f20006..eddbb7f106 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 07187db2a0..eae2863c9a 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 6bbdf7e7cb..a7d610642c 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 2d44f2bd9b..62d808b5d5 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 97df981b26..0070c5a446 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 281d403c7c..0000000000 --- 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 674de1f672..0000000000 --- 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 e852a65cca..0000000000 --- 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 fbc4685b4b..0000000000 --- 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 aa76f1ec50..73363b759a 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 d4fb5d1230..e64aefc18f 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 692fa30aa4..e089dcf90c 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 8421a32c00..cc55afe26f 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 0000000000..1ee2cb98d9 --- /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 df0171807b..3aa6a51643 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 50de8bdb3e..0000000000 --- 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 f3e35db2cc..b588abd985 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 b8d158a97c..0000000000 --- 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 87d91e9480..0000000000 --- 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 a0e09537f9..0000000000 --- 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 68caac63ea..175700e4ec 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 c94d7ef21a..0000000000 --- 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 4948674a19..0000000000 --- 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 ed970b343c..0000000000 --- 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 7a34ce1033..b2d90d9947 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 579e93e6d9..0000000000 --- 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 88951e150c..0000000000 --- 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 144094d130..0000000000 --- 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 0000000000..f2595f6e4c --- /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 033a13ac88..98a7334833 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 0000000000..021a7558bd --- /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 0000000000..fe3c756abb --- /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 0000000000..cb652ffd2c --- /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 aa16aea363..8ce00ad1e7 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 8470f2e81a..d52c750389 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 0000000000..d40486e9ee --- /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 0000000000..433cd09451 --- /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 6872095acd..d1c0e791f4 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 c5c586edbd..e49028254a 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 417974e2a1..4a64b1d672 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 a038de5afa..fea52ec031 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 80cd7e9ede..89a9530b39 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 0600ceb69f..65b2bba528 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 db2ac9bb26..96039338ac 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 321fa9888c..b54856a0d8 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 a359f2e547..b8fbe254a8 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 fbfb57dd9b..d1a4cc9fe5 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 7cb9342221..355841b937 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 c937fa5635..cd17409729 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 4d9b48664d..b8445d4f9a 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 071f0c6857..3b987c4561 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 7e8c989b8f..67295a5f89 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 069ee25de8..4cf324778e 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 ed6ae82657..0b4ac1867f 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 330b1390ff..0000000000 --- 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 a5838db921..ddf4e7bac1 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 c06f1e88c4..6198e2ae91 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 34949e277a..0000000000 --- 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 6a00d4ae9d..0000000000 --- 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 9987d25e45..c2d4e52653 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 874a52599b..ccd3a16f59 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 b941378b68..c2b81abf0d 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 a954d2b920..fef39cd299 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 504518c30b..d59fff73b4 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 4c9ae01971..85b87b8648 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 d4591afeb1..26ce29e183 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 608ccc5606..c158c116ce 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 18e7f3a22e..2824906d27 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 c278a2a3c4..f961f0adbd 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 3062df3bd8..a8172c8d7d 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 02b0563275..7e9e425968 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 435cb13dd9..d63208bc9c 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 b051a6ef3f..d40ed610a7 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 b0b7527736..010d525ac9 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 be4a0771d3..90d7d82e14 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 75e0ad43d2..0000000000 --- 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 680a4eb2c7..e3013f0bf2 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 bfc3ecf3e9..891638cdac 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 55850608bb..4a83057fe1 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 c65eb81245..457bbfa681 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 17e8f727c9..9f747bd772 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 08a7bb5ba1..80a07c8031 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: