diff --git a/frontend/webapp/components/modals/cancel-warning/index.tsx b/frontend/webapp/components/modals/cancel-warning/index.tsx new file mode 100644 index 000000000..4426179b7 --- /dev/null +++ b/frontend/webapp/components/modals/cancel-warning/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { WarningModal } from '@/reuseable-components'; + +interface Props { + isOpen: boolean; + name?: string; + onApprove: () => void; + onDeny: () => void; +} + +const CancelWarning: React.FC = ({ isOpen, name, onApprove, onDeny }) => { + return ( + + ); +}; + +export { CancelWarning }; diff --git a/frontend/webapp/components/modals/delete-entity-modal/index.tsx b/frontend/webapp/components/modals/delete-entity-modal/index.tsx deleted file mode 100644 index ac771fc72..000000000 --- a/frontend/webapp/components/modals/delete-entity-modal/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React, { useCallback } from 'react'; -import { Button, Modal, Text } from '@/reuseable-components'; -import styled from 'styled-components'; - -interface AddActionModalProps { - title: string; - description: string; - isModalOpen: boolean; - handleDelete: () => void; - handleCloseModal: () => void; -} - -export const DeleteEntityModal: React.FC = ({ - isModalOpen, - handleCloseModal, - title = '', - handleDelete, - description = '', -}) => { - const handleClose = useCallback(() => { - handleCloseModal(); - }, [handleCloseModal]); - - return ( - - - Delete {title} - - {description} - - - - Delete - - - Cancel - - - - - ); -}; - -const ModalTitle = styled(Text)` - font-size: 20px; - line-height: 28px; -`; - -const ModalDescription = styled(Text)` - color: ${({ theme }) => theme.text.grey}; - width: 416px; - font-style: normal; - font-weight: 300; - line-height: 24px; -`; - -const ModalContent = styled.div` - padding: 12px 0px 32px 0; -`; - -const ModalFooter = styled.div` - display: flex; - justify-content: space-between; - gap: 12px; -`; - -const FooterButton = styled(Button)` - width: 224px; -`; - -const DeleteEntityModalContainer = styled.div` - padding: 24px 32px; -`; diff --git a/frontend/webapp/components/modals/delete-warning/index.tsx b/frontend/webapp/components/modals/delete-warning/index.tsx new file mode 100644 index 000000000..79b3cfad3 --- /dev/null +++ b/frontend/webapp/components/modals/delete-warning/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { WarningModal } from '@/reuseable-components'; + +interface Props { + isOpen: boolean; + name?: string; + onApprove: () => void; + onDeny: () => void; +} + +const DeleteWarning: React.FC = ({ isOpen, name, onApprove, onDeny }) => { + return ( + + ); +}; + +export { DeleteWarning }; diff --git a/frontend/webapp/components/modals/index.ts b/frontend/webapp/components/modals/index.ts new file mode 100644 index 000000000..96e4964db --- /dev/null +++ b/frontend/webapp/components/modals/index.ts @@ -0,0 +1,2 @@ +export * from './cancel-warning'; +export * from './delete-warning'; diff --git a/frontend/webapp/components/modals/index.tsx b/frontend/webapp/components/modals/index.tsx deleted file mode 100644 index 5b27ff586..000000000 --- a/frontend/webapp/components/modals/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './delete-entity-modal'; diff --git a/frontend/webapp/components/setup/connection/create.connection.form/create.connection.form.tsx b/frontend/webapp/components/setup/connection/create.connection.form/create.connection.form.tsx index 98112cbed..b39030702 100644 --- a/frontend/webapp/components/setup/connection/create.connection.form/create.connection.form.tsx +++ b/frontend/webapp/components/setup/connection/create.connection.form/create.connection.form.tsx @@ -95,7 +95,7 @@ export function CreateConnectionForm({ filterSupportedMonitors(); }, [destination]); - useKeyDown('Enter', handleKeyPress); + useKeyDown({ key: 'Enter', active: true }, handleKeyPress); function handleKeyPress(e: any) { if (!isCreateButtonDisabled) { diff --git a/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx b/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx index 4213b8e0a..dae5a8626 100644 --- a/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/actions/action-drawer-container/index.tsx @@ -1,25 +1,24 @@ -import React, { forwardRef, useImperativeHandle, useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import styled from 'styled-components'; +import { getActionIcon } from '@/utils'; import { useDrawerStore } from '@/store'; import { CardDetails } from '@/components'; -import { useActionFormData, useNotify } from '@/hooks'; +import type { ActionDataParsed } from '@/types'; import { ChooseActionBody } from '../choose-action-body'; -import type { ActionDataParsed, ActionInput } from '@/types'; +import OverviewDrawer from '../../overview/overview-drawer'; import buildCardFromActionSpec from './build-card-from-action-spec'; +import { useActionCRUD, useActionFormData, useNotify } from '@/hooks'; import { ACTION_OPTIONS } from '../choose-action-modal/action-options'; -export type ActionDrawerHandle = { - getCurrentData: () => ActionInput | null; -}; - -interface Props { - isEditing: boolean; -} +interface Props {} -const ActionDrawer = forwardRef(({ isEditing }, ref) => { +const ActionDrawer: React.FC = () => { const notify = useNotify(); const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); + const [isEditing, setIsEditing] = useState(false); + const { formData, handleFormChange, resetFormData, validateForm, loadFormWithDrawerItem } = useActionFormData(); + const { updateAction, deleteAction } = useActionCRUD(); const cardData = useMemo(() => { if (!selectedItem) return []; @@ -48,33 +47,65 @@ const ActionDrawer = forwardRef(({ isEditing }, ref) return found; }, [selectedItem, isEditing]); - useImperativeHandle(ref, () => ({ - getCurrentData: () => { - if (validateForm()) { - return formData; - } else { - notify({ - message: 'Required fields are missing!', - title: 'Update Action Error', - type: 'error', - target: 'notification', - crdType: 'notification', - }); - return null; - } - }, - })); - - return isEditing && thisAction ? ( - - - - ) : ( - - ); -}); + if (!selectedItem?.item) return null; + const { id, item } = selectedItem; -ActionDrawer.displayName = 'ActionDrawer'; + const handleEdit = (bool?: boolean) => { + if (typeof bool === 'boolean') { + setIsEditing(bool); + } else { + setIsEditing(true); + } + }; + + const handleCancel = () => { + resetFormData(); + setIsEditing(false); + }; + + const handleDelete = async () => { + await deleteAction(id as string, (item as ActionDataParsed).type); + }; + + const handleSave = async (newTitle: string) => { + if (!validateForm()) { + notify({ + message: 'Required fields are missing!', + title: 'Update Action Error', + type: 'error', + target: 'notification', + crdType: 'notification', + }); + } else { + const payload = { + ...formData, + name: newTitle, + }; + + await updateAction(id as string, payload); + } + }; + + return ( + + {isEditing && thisAction ? ( + + + + ) : ( + + )} + + ); +}; export { ActionDrawer }; diff --git a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx b/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx index c59b009e5..ed8fdad7a 100644 --- a/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx +++ b/frontend/webapp/containers/main/actions/choose-action-modal/index.tsx @@ -5,11 +5,11 @@ import { ACTION_OPTIONS, type ActionOption } from './action-options'; import { AutocompleteInput, Modal, NavigationButtons, Divider, FadeLoader, SectionTitle, ModalContent, Center } from '@/reuseable-components'; interface AddActionModalProps { - isModalOpen: boolean; - handleCloseModal: () => void; + isOpen: boolean; + onClose: () => void; } -export const AddActionModal: React.FC = ({ isModalOpen, handleCloseModal }) => { +export const AddActionModal: React.FC = ({ isOpen, onClose }) => { const { formData, handleFormChange, resetFormData, validateForm } = useActionFormData(); const { createAction, loading } = useActionCRUD({ onSuccess: handleClose }); const [selectedItem, setSelectedItem] = useState(undefined); @@ -23,7 +23,7 @@ export const AddActionModal: React.FC = ({ isModalOpen, han function handleClose() { resetFormData(); setSelectedItem(undefined); - handleCloseModal(); + onClose(); } const handleSelect = (item?: ActionOption) => { @@ -34,7 +34,7 @@ export const AddActionModal: React.FC = ({ isModalOpen, han return ( void; + isOpen: boolean; + onClose: () => void; } -interface ModalActionComponentProps { - onNext: () => void; - onBack: () => void; - isFormValid?: boolean; - item?: DestinationTypeItem; -} - -const ModalActionComponent: React.FC = React.memo( - ({ onNext, onBack, isFormValid, item }) => { - if (!item) return null; - - const buttons = [ - { - label: 'BACK', - iconSrc: '/icons/common/arrow-white.svg', - onClick: onBack, - variant: 'secondary' as const, - }, - { - label: 'DONE', - onClick: onNext, - variant: 'primary' as const, - disabled: !isFormValid, - }, - ]; - - return ; - } -); - -export const AddDestinationModal: React.FC = ({ - isModalOpen, - handleCloseModal, -}) => { +export const AddDestinationModal: React.FC = ({ isOpen, onClose }) => { const submitRef = useRef<(() => void) | null>(null); - const [selectedItem, setSelectedItem] = useState< - DestinationTypeItem | undefined - >(); + const [selectedItem, setSelectedItem] = useState(); const [isFormValid, setIsFormValid] = useState(false); const handleNextStep = useCallback((item: DestinationTypeItem) => { @@ -57,9 +22,9 @@ export const AddDestinationModal: React.FC = ({ if (submitRef.current) { submitRef.current(); setSelectedItem(undefined); - handleCloseModal(); + onClose(); } - }, [handleCloseModal]); + }, [onClose]); const handleBack = useCallback(() => { setSelectedItem(undefined); @@ -67,16 +32,12 @@ export const AddDestinationModal: React.FC = ({ const handleClose = useCallback(() => { setSelectedItem(undefined); - handleCloseModal(); - }, [handleCloseModal]); + onClose(); + }, [onClose]); const renderModalBody = () => { return selectedItem ? ( - + ) : ( ); @@ -84,17 +45,27 @@ export const AddDestinationModal: React.FC = ({ return ( } - header={{ title: 'Add Destination' }} - onClose={handleClose} > {renderModalBody()} diff --git a/frontend/webapp/containers/main/destinations/add-destination/index.tsx b/frontend/webapp/containers/main/destinations/add-destination/index.tsx index 0312cd4bb..0f63dc7a9 100644 --- a/frontend/webapp/containers/main/destinations/add-destination/index.tsx +++ b/frontend/webapp/containers/main/destinations/add-destination/index.tsx @@ -73,8 +73,8 @@ export function ChooseDestinationContainer() { {isSourcesListEmpty() && destinations.length === 0 && ( @@ -92,10 +92,7 @@ export function ChooseDestinationContainer() { handleOpenModal()} /> - + ); diff --git a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx index 0190e3d1e..c7ef22ef4 100644 --- a/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/destinations/destination-drawer-container/index.tsx @@ -1,69 +1,79 @@ -import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; -import { ExportedSignals } from '@/types'; +import { useDrawerStore } from '@/store'; +import { ActualDestination } from '@/types'; +import OverviewDrawer from '../../overview/overview-drawer'; import { CardDetails, EditDestinationForm } from '@/components'; -import { useDestinationFormData, useEditDestinationFormHandlers } from '@/hooks'; +import { useDestinationCRUD, useDestinationFormData, useEditDestinationFormHandlers } from '@/hooks'; -export type DestinationDrawerHandle = { - getCurrentData: () => { - type: string; - exportedSignals: ExportedSignals; - fields: { key: string; value: any }[]; - }; -}; +interface Props {} -interface DestinationDrawerProps { - isEditing: boolean; -} +const DestinationDrawer: React.FC = () => { + const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); + const [isEditing, setIsEditing] = useState(false); -const DestinationDrawer = forwardRef(({ isEditing }, ref) => { - 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(); - useEffect(() => { - if (!isEditing && isFormDirty) { - setIsFormDirty(false); - resetFormData(); + if (!selectedItem?.item) return null; + const { id, item } = selectedItem; + + const handleEdit = (bool?: boolean) => { + if (typeof bool === 'boolean') { + setIsEditing(bool); + } else { + setIsEditing(true); } - }, [isEditing]); + }; - const onDynamicFieldChange = (name: string, value: any) => { - handleDynamicFieldChange(name, value); - setIsFormDirty(true); + const handleCancel = () => { + resetFormData(); + setIsEditing(false); }; - const onSignalChange = (signal: keyof ExportedSignals, value: boolean) => { - handleSignalChange(signal, value); - setIsFormDirty(true); + const handleDelete = async () => { + await deleteDestination(id as string); }; - useImperativeHandle(ref, () => ({ - getCurrentData: () => ({ + const handleSave = async (newTitle: string) => { + const payload = { type: destinationType, + name: newTitle, exportedSignals, fields: dynamicFields.map(({ name, value }) => ({ key: name, value })), - }), - })); + }; - return isEditing ? ( - - - - ) : ( - - ); -}); + await updateDestination(id as string, payload); + }; -DestinationDrawer.displayName = 'DestinationDrawer'; + return ( + + {isEditing ? ( + + + + ) : ( + + )} + + ); +}; export { DestinationDrawer }; diff --git a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx b/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx index 777ec57d8..5c96ad3a8 100644 --- a/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx +++ b/frontend/webapp/containers/main/instrumentation-rules/add-rule-modal/index.tsx @@ -15,11 +15,11 @@ import { } from '@/reuseable-components'; interface Props { - isModalOpen: boolean; - handleCloseModal: () => void; + isOpen: boolean; + onClose: () => void; } -export const AddRuleModal: React.FC = ({ isModalOpen, handleCloseModal }) => { +export const AddRuleModal: React.FC = ({ isOpen, onClose }) => { const { formData, handleFormChange, resetFormData, validateForm } = useInstrumentationRuleFormData(); const { createInstrumentationRule, loading } = useInstrumentationRuleCRUD({ onSuccess: handleClose }); const [selectedItem, setSelectedItem] = useState(undefined); @@ -37,7 +37,7 @@ export const AddRuleModal: React.FC = ({ isModalOpen, handleCloseModal }) function handleClose() { resetFormData(); setSelectedItem(undefined); - handleCloseModal(); + onClose(); } const handleSelect = (item?: RuleOption) => { @@ -47,7 +47,7 @@ export const AddRuleModal: React.FC = ({ isModalOpen, handleCloseModal }) return ( InstrumentationRuleInput | null; -}; - -interface Props { - isEditing: boolean; -} +interface Props {} -const RuleDrawer = forwardRef(({ isEditing }, ref) => { +const RuleDrawer: React.FC = () => { const notify = useNotify(); const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); + const [isEditing, setIsEditing] = useState(false); + const { formData, handleFormChange, resetFormData, validateForm, loadFormWithDrawerItem } = useInstrumentationRuleFormData(); + const { updateInstrumentationRule, deleteInstrumentationRule } = useInstrumentationRuleCRUD(); const cardData = useMemo(() => { if (!selectedItem) return []; @@ -46,33 +45,65 @@ const RuleDrawer = forwardRef(({ isEditing }, ref) => { return found; }, [selectedItem, isEditing]); - useImperativeHandle(ref, () => ({ - getCurrentData: () => { - if (validateForm()) { - return formData; - } else { - notify({ - message: 'Required fields are missing!', - title: 'Update Rule Error', - type: 'error', - target: 'notification', - crdType: 'notification', - }); - return null; - } - }, - })); - - return isEditing && thisRule ? ( - - - - ) : ( - - ); -}); + if (!selectedItem?.item) return null; + const { id, item } = selectedItem; -RuleDrawer.displayName = 'RuleDrawer'; + const handleEdit = (bool?: boolean) => { + if (typeof bool === 'boolean') { + setIsEditing(bool); + } else { + setIsEditing(true); + } + }; + + const handleCancel = () => { + resetFormData(); + setIsEditing(false); + }; + + const handleDelete = async () => { + await deleteInstrumentationRule(id as string); + }; + + const handleSave = async (newTitle: string) => { + if (!validateForm()) { + notify({ + message: 'Required fields are missing!', + title: 'Update Rule Error', + type: 'error', + target: 'notification', + crdType: 'notification', + }); + } else { + const payload = { + ...formData, + ruleName: newTitle, + }; + + await updateInstrumentationRule(id as string, payload); + } + }; + + return ( + + {isEditing && thisRule ? ( + + + + ) : ( + + )} + + ); +}; export { RuleDrawer }; diff --git a/frontend/webapp/containers/main/overview/add-entity/index.tsx b/frontend/webapp/containers/main/overview/add-entity/index.tsx index d60433a3f..c353c5dd4 100644 --- a/frontend/webapp/containers/main/overview/add-entity/index.tsx +++ b/frontend/webapp/containers/main/overview/add-entity/index.tsx @@ -3,13 +3,9 @@ import theme from '@/styles/theme'; import { useModalStore } from '@/store'; import React, { useState, useRef } from 'react'; import styled, { css } from 'styled-components'; -import { AddActionModal } from '../../actions'; -import { AddRuleModal } from '../../instrumentation-rules'; import { useActualSources, useOnClickOutside } from '@/hooks'; import { DropdownOption, OVERVIEW_ENTITY_TYPES } from '@/types'; import { Button, FadeLoader, Text } from '@/reuseable-components'; -import { AddSourceModal } from '../../sources/choose-sources/choose-source-modal'; -import { AddDestinationModal } from '../../destinations/add-destination/add-destination-modal'; // Styled components for the dropdown UI const Container = styled.div` @@ -75,11 +71,11 @@ interface AddEntityButtonDropdownProps { } const AddEntityButtonDropdown: React.FC = ({ options = DEFAULT_OPTIONS, placeholder = 'ADD...' }) => { - const { currentModal, setCurrentModal } = useModalStore(); + const { setCurrentModal } = useModalStore(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const dropdownRef = useRef(null); - const { isPolling, createSourcesForNamespace, persistNamespaceItems } = useActualSources(); + const { isPolling } = useActualSources(); useOnClickOutside(dropdownRef, () => setIsDropdownOpen(false)); @@ -92,10 +88,6 @@ const AddEntityButtonDropdown: React.FC = ({ optio setIsDropdownOpen(false); }; - const handleCloseModal = () => { - setCurrentModal(''); - }; - return ( @@ -113,16 +105,6 @@ const AddEntityButtonDropdown: React.FC = ({ optio ))} )} - - - - - ); }; diff --git a/frontend/webapp/containers/main/overview/all-drawers/index.tsx b/frontend/webapp/containers/main/overview/all-drawers/index.tsx new file mode 100644 index 000000000..0600ceb69 --- /dev/null +++ b/frontend/webapp/containers/main/overview/all-drawers/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { useDrawerStore } from '@/store'; +import { OVERVIEW_ENTITY_TYPES } from '@/types'; +import { SourceDrawer } from '../../sources'; +import { ActionDrawer } from '../../actions'; +import { DestinationDrawer } from '../../destinations'; +import { RuleDrawer } from '../../instrumentation-rules/rule-drawer-container'; + +const AllDrawers = () => { + const selected = useDrawerStore(({ selectedItem }) => selectedItem); + + if (!selected?.item) return null; + + switch (selected.type) { + case OVERVIEW_ENTITY_TYPES.RULE: + return ; + + case OVERVIEW_ENTITY_TYPES.SOURCE: + return ; + + case OVERVIEW_ENTITY_TYPES.ACTION: + return ; + + case OVERVIEW_ENTITY_TYPES.DESTINATION: + return ; + + default: + return <>; + } +}; + +export default AllDrawers; diff --git a/frontend/webapp/containers/main/overview/all-modals/index.tsx b/frontend/webapp/containers/main/overview/all-modals/index.tsx new file mode 100644 index 000000000..db2ac9bb2 --- /dev/null +++ b/frontend/webapp/containers/main/overview/all-modals/index.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useModalStore } from '@/store'; +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 { AddSourceModal } from '../../sources/choose-sources/choose-source-modal'; + +const AllModals = () => { + const selected = useModalStore(({ currentModal }) => currentModal); + const setSelected = useModalStore(({ setCurrentModal }) => setCurrentModal); + + if (!selected) return null; + + const handleClose = () => setSelected(''); + + switch (selected) { + case OVERVIEW_ENTITY_TYPES.RULE: + return ; + + case OVERVIEW_ENTITY_TYPES.SOURCE: + return ; + + case OVERVIEW_ENTITY_TYPES.ACTION: + return ; + + case OVERVIEW_ENTITY_TYPES.DESTINATION: + return ; + + default: + return <>; + } +}; + +export default AllModals; diff --git a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx index fdaccfc23..bd233065c 100644 --- a/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-data-flow/index.tsx @@ -7,7 +7,11 @@ import { buildNodesAndEdges, NodeBaseDataFlow } from '@/reuseable-components'; import { useGetActions, useActualSources, useContainerWidth, useActualDestination, useNodeDataFlowHandlers } from '@/hooks'; import { useGetInstrumentationRules } from '@/hooks/instrumentation-rules/useGetInstrumentationRules'; -const OverviewDrawer = dynamic(() => import('../overview-drawer'), { +const AllDrawers = dynamic(() => import('../all-drawers'), { + ssr: false, +}); + +const AllModals = dynamic(() => import('../all-modals'), { ssr: false, }); @@ -46,9 +50,11 @@ export function OverviewDataFlowContainer() { return ( - + + + ); } 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 dd8492c6e..dec2d8c40 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 @@ -1,5 +1,5 @@ // DrawerHeader.tsx -import React, { useEffect, useState, forwardRef } from 'react'; +import React, { useEffect, useState, forwardRef, useImperativeHandle } from 'react'; import Image from 'next/image'; import styled from 'styled-components'; import { Button, Input, Text } from '@/reuseable-components'; @@ -37,11 +37,7 @@ const DrawerItemImageWrapper = 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 EditButton = styled(Button)` @@ -59,64 +55,61 @@ const ButtonText = styled(Text)` width: fit-content; `; +export interface DrawerHeaderRef { + getTitle: () => string; + clearTitle: () => void; +} + interface DrawerHeaderProps { title: string; imageUri: string; + isEdit: boolean; + onEdit: () => void; onClose: () => void; - isEditing: boolean; - setIsEditing: (isEditing: boolean) => void; } -const DrawerHeader = forwardRef( - ({ title, imageUri, isEditing, setIsEditing, onClose }, ref) => { - const [inputValue, setInputValue] = useState(title); - - useEffect(() => { - setInputValue(title); - }, [title]); - - return ( - - - - Drawer Item - - {!isEditing && {title}} - - {isEditing && ( - - setInputValue(e.target.value)} - autoFocus - ref={ref} - /> - +const DrawerHeader = forwardRef(({ title, imageUri, isEdit, onEdit, onClose }, ref) => { + const [inputValue, setInputValue] = useState(title); + + useEffect(() => { + setInputValue(title); + }, [title]); + + useImperativeHandle(ref, () => ({ + getTitle: () => inputValue, + clearTitle: () => setInputValue(title), + })); + + return ( + + + + Drawer Item + + {!isEdit && {title}} + + + {isEdit && ( + + setInputValue(e.target.value)} /> + + )} + + + {!isEdit && ( + + Edit + Edit + )} - - {!isEditing && ( - setIsEditing(true)}> - Edit - Edit - - )} - - Close - - - - ); - } -); + + Close + + + + ); +}); + +DrawerHeader.displayName = 'DrawerHeader'; export default DrawerHeader; diff --git a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx index 6fc795165..6e7bc319a 100644 --- a/frontend/webapp/containers/main/overview/overview-drawer/index.tsx +++ b/frontend/webapp/containers/main/overview/overview-drawer/index.tsx @@ -1,309 +1,109 @@ -import { useEffect, useRef, useState } from 'react'; +import { PropsWithChildren, useRef, useState } from 'react'; import styled from 'styled-components'; import { useDrawerStore } from '@/store'; -import { getActionIcon, getRuleIcon, LANGUAGES_LOGOS } from '@/utils'; -import DrawerHeader from './drawer-header'; import DrawerFooter from './drawer-footer'; -import { SourceDrawer } from '../../sources'; import { Drawer } from '@/reuseable-components'; -import { DeleteEntityModal } from '@/components'; -import { ActionDrawer, type ActionDrawerHandle } from '../../actions'; -import { DestinationDrawer, type DestinationDrawerHandle } from '../../destinations'; -import { RuleDrawer, RuleDrawerHandle } from '../../instrumentation-rules/rule-drawer-container'; -import { useActionCRUD, useActualSources, useDestinationCRUD, useInstrumentationRuleCRUD } from '@/hooks'; -import { getMainContainerLanguageLogo, WORKLOAD_PROGRAMMING_LANGUAGES } from '@/utils/constants/programming-languages'; -import { - WorkloadId, - K8sActualSource, - ActualDestination, - OVERVIEW_ENTITY_TYPES, - PatchSourceRequestInput, - ActionDataParsed, - InstrumentationRuleSpec, -} from '@/types'; - -const componentMap = { - [OVERVIEW_ENTITY_TYPES.RULE]: RuleDrawer, - [OVERVIEW_ENTITY_TYPES.SOURCE]: SourceDrawer, - [OVERVIEW_ENTITY_TYPES.ACTION]: ActionDrawer, - [OVERVIEW_ENTITY_TYPES.DESTINATION]: DestinationDrawer, -}; +import DrawerHeader, { DrawerHeaderRef } from './drawer-header'; +import { CancelWarning, DeleteWarning } from '@/components/modals'; const DRAWER_WIDTH = '640px'; -const OverviewDrawer = () => { - const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); - const setSelectedItem = useDrawerStore(({ setSelectedItem }) => setSelectedItem); - - const [isEditing, setIsEditing] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [title, setTitle] = useState(''); - - const { updateAction, deleteAction } = useActionCRUD(); - const { updateDestination, deleteDestination } = useDestinationCRUD(); - const { updateActualSource, deleteSourcesForNamespace } = useActualSources(); - const { updateInstrumentationRule, deleteInstrumentationRule } = useInstrumentationRuleCRUD(); - - const titleRef = useRef(null); - const ruleDrawerRef = useRef(null); - const actionDrawerRef = useRef(null); - const destinationDrawerRef = useRef(null); - - const refMap = { - [OVERVIEW_ENTITY_TYPES.RULE]: ruleDrawerRef, - [OVERVIEW_ENTITY_TYPES.SOURCE]: undefined, - [OVERVIEW_ENTITY_TYPES.ACTION]: actionDrawerRef, - [OVERVIEW_ENTITY_TYPES.DESTINATION]: destinationDrawerRef, - }; - - useEffect(initialTitle, [selectedItem]); - - //TODO: split file to separate components by type: source, destination, action +interface Props { + title: string; + imageUri: string; + isEdit: boolean; + clickEdit: (bool?: boolean) => void; + clickSave: (newTitle: string) => void; + clickDelete: () => void; + clickCancel: () => void; +} - function initialTitle() { - let str = ''; +const DrawerContent = styled.div` + display: flex; + flex-direction: column; + height: 100%; +`; - if (!!selectedItem?.item) { - const { type, item } = selectedItem; +const ContentArea = styled.div` + flex-grow: 1; + padding: 24px 32px; + overflow-y: auto; +`; - if (type === OVERVIEW_ENTITY_TYPES.RULE) { - str = (item as InstrumentationRuleSpec).ruleName; - } else if (type === OVERVIEW_ENTITY_TYPES.SOURCE) { - str = (item as K8sActualSource).reportedName; - } else if (type === OVERVIEW_ENTITY_TYPES.ACTION) { - str = (item as ActionDataParsed).spec.actionName; - } else if (type === OVERVIEW_ENTITY_TYPES.DESTINATION) { - str = (item as ActualDestination).name; - } - } +const OverviewDrawer: React.FC = ({ + children, + title, + imageUri, + isEdit, + clickEdit, + clickSave, + clickDelete, + clickCancel, +}) => { + const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); + const setSelectedItem = useDrawerStore(({ setSelectedItem }) => setSelectedItem); - setTitle(str); - } + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isCancelModalOpen, setIsCancelModalOpen] = useState(false); - const handleCancel = () => { - setIsEditing(false); - initialTitle(); - }; + const titleRef = useRef(null); - const handleClose = () => { - setIsEditing(false); + const closeDrawer = () => { setSelectedItem(null); + clickEdit(false); setIsDeleteModalOpen(false); + setIsCancelModalOpen(false); }; - const handleCloseDeleteModal = () => { + const closeWarningModals = () => { setIsDeleteModalOpen(false); + setIsCancelModalOpen(false); }; - const handleSave = async () => { - if (!selectedItem?.item) return null; - const { type, id, item } = selectedItem; - - if (type === OVERVIEW_ENTITY_TYPES.RULE) { - const thisRef = refMap[type]; - - if (thisRef.current && titleRef.current) { - const newTitle = titleRef.current.value; - const formData = thisRef.current.getCurrentData(); - - if (formData) { - const payload = { - ...formData, - ruleName: newTitle, - }; - - await updateInstrumentationRule(id as string, payload); - setIsEditing(false); - } - } - } - - if (type === OVERVIEW_ENTITY_TYPES.SOURCE) { - if (titleRef.current) { - const newTitle = titleRef.current.value; - setTitle(newTitle); - - const { namespace, name, kind } = item as K8sActualSource; - - const sourceId: WorkloadId = { - namespace, - kind, - name, - }; - - const patchRequest: PatchSourceRequestInput = { - reportedName: newTitle, - }; - - try { - await updateActualSource(sourceId, patchRequest); - } catch (error) { - console.error('Error updating source:', error); - } - } - setIsEditing(false); - } - - if (type === OVERVIEW_ENTITY_TYPES.ACTION) { - const thisRef = refMap[type]; - - if (thisRef.current && titleRef.current) { - const newTitle = titleRef.current.value; - const formData = thisRef.current.getCurrentData(); - - if (formData) { - const payload = { - ...formData, - name: newTitle, - }; - - await updateAction(id as string, payload); - setIsEditing(false); - } - } - } + const handleCancel = () => setIsCancelModalOpen(true); + const handleDelete = () => setIsDeleteModalOpen(true); - if (type === OVERVIEW_ENTITY_TYPES.DESTINATION) { - const thisRef = refMap[type]; - - if (thisRef.current && titleRef.current) { - const newTitle = titleRef.current.value; - const formData = thisRef.current.getCurrentData(); - const payload = { - ...formData, - name: newTitle, - }; - - try { - await updateDestination(id as string, payload); - } catch (error) { - console.error('Error updating destination:', error); - } - setIsEditing(false); - } - } - }; - - const handleDelete = async () => { - if (!selectedItem?.item) return null; - const { type, item } = selectedItem; - - if (type === OVERVIEW_ENTITY_TYPES.RULE) { - const { ruleId } = item as InstrumentationRuleSpec; - - await deleteInstrumentationRule(ruleId); - } - - if (type === OVERVIEW_ENTITY_TYPES.SOURCE) { - const { namespace, name, kind } = item as K8sActualSource; - - try { - await deleteSourcesForNamespace(namespace, [ - { - kind, - name, - selected: false, - }, - ]); - } catch (error) { - console.error('Error deleting source:', error); - } - } - - if (type === OVERVIEW_ENTITY_TYPES.ACTION) { - const { id, type } = item as ActionDataParsed; - - await deleteAction(id, type); - } - - if (type === OVERVIEW_ENTITY_TYPES.DESTINATION) { - const { id } = item as ActualDestination; - - await deleteDestination(id); - } - - handleClose(); - }; - - if (!selectedItem?.item) return null; - - const { type, item } = selectedItem; - const SpecificComponent = componentMap[type]; - const specificRef = refMap[type]; - - return SpecificComponent ? ( + return ( <> - + clickEdit(true)} + onClose={isEdit ? handleCancel : closeDrawer} /> - - {/* @ts-ignore (because of ref) */} - - + {children} - {isEditing && setIsDeleteModalOpen(true)} />} + {isEdit && clickSave(titleRef.current?.getTitle() || '')} onCancel={handleCancel} onDelete={handleDelete} />} - { + clickDelete(); + closeWarningModals(); + }} + onDeny={closeWarningModals} + /> + + { + titleRef.current?.clearTitle(); + clickCancel(); + closeWarningModals(); + }} + onDeny={closeWarningModals} /> - ) : null; + ); }; -function getItemImageByType( - type: OVERVIEW_ENTITY_TYPES, - item: InstrumentationRuleSpec | K8sActualSource | ActionDataParsed | ActualDestination -): string { - let src = ''; - - switch (type) { - case OVERVIEW_ENTITY_TYPES.RULE: - src = getRuleIcon((item as InstrumentationRuleSpec).type); - break; - - case OVERVIEW_ENTITY_TYPES.SOURCE: - src = getMainContainerLanguageLogo(item as K8sActualSource); - break; - - case OVERVIEW_ENTITY_TYPES.ACTION: - src = getActionIcon((item as ActionDataParsed).type); - break; - - case OVERVIEW_ENTITY_TYPES.DESTINATION: - src = (item as ActualDestination).destinationType.imageUrl; - break; - - default: - break; - } - - return src || LANGUAGES_LOGOS[WORKLOAD_PROGRAMMING_LANGUAGES.UNKNOWN]; -} - export default OverviewDrawer; - -const DrawerContent = styled.div` - display: flex; - flex-direction: column; - height: 100%; -`; - -const ContentArea = styled.div` - flex-grow: 1; - padding: 24px 32px; - overflow-y: auto; -`; diff --git a/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx b/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx index a706cde73..1a9dcdecf 100644 --- a/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx +++ b/frontend/webapp/containers/main/sources/choose-sources/choose-source-modal/index.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; import React, { useState, useCallback } from 'react'; -import { useConnectSourcesMenuState } from '@/hooks'; +import { useActualSources, useConnectSourcesMenuState } from '@/hooks'; import { ChooseSourcesBody } from '../choose-sources-body'; import { Modal, NavigationButtons } from '@/reuseable-components'; import { K8sActualSource, PersistNamespaceItemInput } from '@/types'; @@ -15,15 +15,12 @@ const ChooseSourcesBodyWrapper = styled.div` interface AddSourceModalProps { isOpen: boolean; onClose: () => void; - createSourcesForNamespace: (namespaceName: string, sources: { kind: string; name: string; selected: boolean }[]) => Promise; - persistNamespaceItems: (namespaceItems: PersistNamespaceItemInput[]) => Promise; } -export const AddSourceModal: React.FC = ({ isOpen, onClose, createSourcesForNamespace, persistNamespaceItems }) => { +export const AddSourceModal: React.FC = ({ isOpen, onClose }) => { const [sourcesList, setSourcesList] = useState([]); - const { stateMenu, stateHandlers } = useConnectSourcesMenuState({ - sourcesList, - }); + const { stateMenu, stateHandlers } = useConnectSourcesMenuState({ sourcesList }); + const { createSourcesForNamespace, persistNamespaceItems } = useActualSources(); const handleNextClick = useCallback(async () => { try { @@ -49,22 +46,25 @@ export const AddSourceModal: React.FC = ({ isOpen, onClose, } catch (error) { console.error('Error during handleNextClick:', error); } - }, [createSourcesForNamespace, persistNamespaceItems, stateMenu, onClose]); - - const ModalActionComponent = ( - - ); + }, [stateMenu, onClose, createSourcesForNamespace, persistNamespaceItems]); return ( - + + } + > diff --git a/frontend/webapp/containers/main/sources/edit.source/index.tsx b/frontend/webapp/containers/main/sources/edit.source/index.tsx index d76dbea46..034ad237f 100644 --- a/frontend/webapp/containers/main/sources/edit.source/index.tsx +++ b/frontend/webapp/containers/main/sources/edit.source/index.tsx @@ -69,7 +69,7 @@ export function EditSourceForm() { setInputValue(currentSource?.reported_name || ''); }, [currentSource]); - useKeyDown('Enter', handleKeyPress); + useKeyDown({ key: 'Enter', active: true }, handleKeyPress); function handleKeyPress(e: any) { onSaveClick(); 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 3676e2d43..f19c48258 100644 --- a/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx +++ b/frontend/webapp/containers/main/sources/source-drawer-container/index.tsx @@ -1,17 +1,23 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { useDrawerStore } from '@/store'; -import { K8sActualSource } from '@/types'; +import { useActualSources } from '@/hooks'; import { CardDetails } from '@/components'; +import OverviewDrawer from '../../overview/overview-drawer'; +import { K8sActualSource, PatchSourceRequestInput, WorkloadId } from '@/types'; +import { getMainContainerLanguageLogo } from '@/utils/constants/programming-languages'; const SourceDrawer: React.FC = () => { const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); + const setSelectedItem = useDrawerStore(({ setSelectedItem }) => setSelectedItem); + const [isEditing, setIsEditing] = useState(false); + + const { updateActualSource, deleteSourcesForNamespace } = useActualSources(); const cardData = useMemo(() => { if (!selectedItem) return []; // Destructure necessary fields from the selected item - const { name, kind, namespace, instrumentedApplicationDetails } = - selectedItem.item as K8sActualSource; + const { name, kind, namespace, instrumentedApplicationDetails } = selectedItem.item as K8sActualSource; // Extract the first container and condition if available const container = instrumentedApplicationDetails?.containers?.[0]; @@ -25,7 +31,72 @@ const SourceDrawer: React.FC = () => { ]; }, [selectedItem]); - return ; + if (!selectedItem?.item) return null; + const { item } = selectedItem; + + const handleEdit = (bool?: boolean) => { + if (typeof bool === 'boolean') { + setIsEditing(bool); + } else { + setIsEditing(true); + } + }; + + const handleCancel = () => { + setIsEditing(false); + }; + + const handleDelete = async () => { + const { namespace, name, kind } = item as K8sActualSource; + + try { + await deleteSourcesForNamespace(namespace, [ + { + kind, + name, + selected: false, + }, + ]); + setSelectedItem(null); + } catch (error) { + console.error('Error deleting source:', error); + } + }; + + const handleSave = async (newTitle: string) => { + const { namespace, name, kind } = item as K8sActualSource; + + const sourceId: WorkloadId = { + namespace, + kind, + name, + }; + + const patchRequest: PatchSourceRequestInput = { + reportedName: newTitle, + }; + + try { + await updateActualSource(sourceId, patchRequest); + setSelectedItem(null); + } catch (error) { + console.error('Error updating source:', error); + } + }; + + return ( + + + + ); }; export { SourceDrawer }; diff --git a/frontend/webapp/hooks/useKeyDown.ts b/frontend/webapp/hooks/useKeyDown.ts index ae3a2a66e..5598b8673 100644 --- a/frontend/webapp/hooks/useKeyDown.ts +++ b/frontend/webapp/hooks/useKeyDown.ts @@ -1,9 +1,28 @@ import { useEffect } from 'react'; -export function useKeyDown(key: Key | null, callback: (e: KeyboardEvent) => void) { +interface KeyDownOptions { + active: boolean; + key: Key; + withAltKey?: boolean; + withCtrlKey?: boolean; + withShiftKey?: boolean; + withMetaKey?: boolean; +} + +export function useKeyDown( + { active, key, withAltKey, withCtrlKey, withShiftKey, withMetaKey }: KeyDownOptions, + callback: (e: KeyboardEvent) => void +) { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (key && key === e.key) { + if ( + active && + key === e.key && + (!withAltKey || (withAltKey && e.altKey)) && + (!withCtrlKey || (withCtrlKey && e.ctrlKey)) && + (!withShiftKey || (withShiftKey && e.shiftKey)) && + (!withMetaKey || (withMetaKey && e.metaKey)) + ) { e.preventDefault(); e.stopPropagation(); @@ -16,7 +35,7 @@ export function useKeyDown(key: Key | null, callback: (e: KeyboardEvent) => void return () => { window.removeEventListener('keydown', handleKeyDown); }; - }, [key, callback]); + }, [key, active, withAltKey, withCtrlKey, withShiftKey, withMetaKey, callback]); return null; } diff --git a/frontend/webapp/reuseable-components/button/index.tsx b/frontend/webapp/reuseable-components/button/index.tsx index 79d049acb..b86c66b74 100644 --- a/frontend/webapp/reuseable-components/button/index.tsx +++ b/frontend/webapp/reuseable-components/button/index.tsx @@ -2,7 +2,7 @@ import React, { ButtonHTMLAttributes } from 'react'; import styled, { css } from 'styled-components'; export interface ButtonProps extends ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'tertiary' | 'danger'; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning'; isDisabled?: boolean; } @@ -63,6 +63,20 @@ const variantStyles = { background: ${({ theme }) => theme.colors.danger}; } `, + warning: css` + border-color: transparent; + background: ${({ theme }) => theme.colors.warning}; + &:hover { + background: ${({ theme }) => theme.colors.warning}; + opacity: 0.9; + } + &:active { + background: ${({ theme }) => theme.colors.warning}; + } + &:focus { + background: ${({ theme }) => theme.colors.warning}; + } + `, }; const StyledButton = styled.button` @@ -91,7 +105,7 @@ const StyledButton = styled.button` `; const ButtonContainer = styled.div<{ - variant?: 'primary' | 'secondary' | 'tertiary' | 'danger'; + variant: ButtonProps['variant']; }>` height: fit-content; border: 2px solid transparent; diff --git a/frontend/webapp/reuseable-components/drawer/index.tsx b/frontend/webapp/reuseable-components/drawer/index.tsx index 3f3980dd9..b0c3a84ad 100644 --- a/frontend/webapp/reuseable-components/drawer/index.tsx +++ b/frontend/webapp/reuseable-components/drawer/index.tsx @@ -32,10 +32,13 @@ const DrawerContainer = styled.div<{ `; export const Drawer: React.FC = ({ isOpen, onClose, position = 'right', width = '300px', children, closeOnEscape = true }) => { - useKeyDown(isOpen ? 'Escape' : null, () => { - if (!closeOnEscape) return; - onClose(); - }); + useKeyDown( + { + key: 'Escape', + active: isOpen && closeOnEscape, + }, + () => onClose() + ); if (!isOpen) return null; diff --git a/frontend/webapp/reuseable-components/index.ts b/frontend/webapp/reuseable-components/index.ts index 0172e7c88..d20bac721 100644 --- a/frontend/webapp/reuseable-components/index.ts +++ b/frontend/webapp/reuseable-components/index.ts @@ -10,6 +10,7 @@ export * from './counter'; export * from './toggle'; export * from './checkbox'; export * from './modal'; +export * from './modal/warning-modal'; export * from './modal/styled'; export * from './navigation-buttons'; export * from './tag'; diff --git a/frontend/webapp/reuseable-components/modal/index.tsx b/frontend/webapp/reuseable-components/modal/index.tsx index 02c8411e0..c1c2d10a0 100644 --- a/frontend/webapp/reuseable-components/modal/index.tsx +++ b/frontend/webapp/reuseable-components/modal/index.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import React, { useRef } 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 { fade, Overlay } from '@/styles'; +import { useKeyDown, useOnClickOutside } from '@/hooks'; interface ModalProps { isOpen: boolean; + noOverlay?: boolean; header?: { title: string; }; @@ -82,17 +83,24 @@ const CancelText = styled(Text)` cursor: pointer; `; -const Modal: React.FC = ({ isOpen, header, onClose, children, actionComponent }) => { - useKeyDown(isOpen ? 'Escape' : null, () => { - onClose(); - }); +const Modal: React.FC = ({ isOpen, noOverlay, header, onClose, children, actionComponent }) => { + const ref = useRef(null); + useOnClickOutside(ref, () => onClose()); + useKeyDown( + { + key: 'Escape', + active: isOpen, + }, + () => onClose() + ); if (!isOpen) return null; return ReactDOM.createPortal( <> -