diff --git a/frontend/webapp/app/(overview)/overview/page.tsx b/frontend/webapp/app/(overview)/overview/page.tsx index 678d41714..c24075f34 100644 --- a/frontend/webapp/app/(overview)/overview/page.tsx +++ b/frontend/webapp/app/(overview)/overview/page.tsx @@ -2,13 +2,17 @@ import React from 'react'; import dynamic from 'next/dynamic'; -const OverviewDataFlowContainer = dynamic(() => import('@/containers/main/overview/overview-data-flow'), { - ssr: false, -}); +const ToastList = dynamic(() => import('@/components/notification/toast-list'), { ssr: false }); +const AllDrawers = dynamic(() => import('@/components/overview/all-drawers'), { ssr: false }); +const AllModals = dynamic(() => import('@/components/overview/all-modals'), { ssr: false }); +const OverviewDataFlowContainer = dynamic(() => import('@/containers/main/overview/overview-data-flow'), { ssr: false }); export default function MainPage() { return ( <> + + + ); diff --git a/frontend/webapp/components/common/dropdowns/error-dropdown/index.tsx b/frontend/webapp/components/common/dropdowns/error-dropdown/index.tsx index 30410af3a..571fc3bcd 100644 --- a/frontend/webapp/components/common/dropdowns/error-dropdown/index.tsx +++ b/frontend/webapp/components/common/dropdowns/error-dropdown/index.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { useSourceCRUD } from '@/hooks'; import { DropdownOption } from '@/types'; +import { BACKEND_BOOLEAN } from '@/utils'; import { Dropdown } from '@/reuseable-components'; interface Props { @@ -21,7 +22,7 @@ export const ErrorDropdown: React.FC = ({ title = 'Error Message', value, sources.forEach(({ instrumentedApplicationDetails: { conditions } }) => { conditions.forEach(({ type, status, message }) => { - if (status === 'False' && !payload.find((opt) => opt.value === message)) { + if (status === BACKEND_BOOLEAN.FALSE && !payload.find((opt) => opt.id === message)) { payload.push({ id: message, value: message }); } }); diff --git a/frontend/webapp/components/common/dropdowns/index.ts b/frontend/webapp/components/common/dropdowns/index.ts index 096f148b8..13265f2a1 100644 --- a/frontend/webapp/components/common/dropdowns/index.ts +++ b/frontend/webapp/components/common/dropdowns/index.ts @@ -1,4 +1,5 @@ export * from './error-dropdown'; +export * from './language-dropdown'; export * from './monitor-dropdown'; export * from './namespace-dropdown'; export * from './type-dropdown'; diff --git a/frontend/webapp/components/common/dropdowns/language-dropdown/index.tsx b/frontend/webapp/components/common/dropdowns/language-dropdown/index.tsx new file mode 100644 index 000000000..31c53ac9a --- /dev/null +++ b/frontend/webapp/components/common/dropdowns/language-dropdown/index.tsx @@ -0,0 +1,34 @@ +import React, { useMemo } from 'react'; +import { useSourceCRUD } from '@/hooks'; +import { DropdownOption } from '@/types'; +import { Dropdown } from '@/reuseable-components'; + +interface Props { + title?: string; + value?: DropdownOption[]; + onSelect: (val: DropdownOption) => void; + onDeselect: (val: DropdownOption) => void; + isMulti?: boolean; + required?: boolean; + showSearch?: boolean; +} + +export const LanguageDropdown: React.FC = ({ title = 'Programming Languages', value, onSelect, onDeselect, ...props }) => { + const { sources } = useSourceCRUD(); + + const options = useMemo(() => { + const payload: DropdownOption[] = []; + + sources.forEach(({ instrumentedApplicationDetails: { containers } }) => { + containers.forEach(({ language }) => { + if (!payload.find((opt) => opt.id === language)) { + payload.push({ id: language, value: language }); + } + }); + }); + + return payload.sort((a, b) => a.id.localeCompare(b.id)); + }, [sources]); + + return ; +}; diff --git a/frontend/webapp/components/main/header/index.tsx b/frontend/webapp/components/main/header/index.tsx index 345ca679b..f74f9aeb4 100644 --- a/frontend/webapp/components/main/header/index.tsx +++ b/frontend/webapp/components/main/header/index.tsx @@ -16,7 +16,7 @@ const Flex = styled.div` const HeaderContainer = styled(Flex)` width: 100%; padding: 12px 0; - background-color: ${({ theme }) => theme.colors.dark_grey}; + background-color: ${({ theme }) => theme.colors.darker_grey}; border-bottom: 1px solid rgba(249, 249, 249, 0.16); `; diff --git a/frontend/webapp/components/modals/cancel-warning/index.tsx b/frontend/webapp/components/modals/cancel-warning/index.tsx index a59ea05e1..82221bc9f 100644 --- a/frontend/webapp/components/modals/cancel-warning/index.tsx +++ b/frontend/webapp/components/modals/cancel-warning/index.tsx @@ -15,9 +15,9 @@ const CancelWarning: React.FC = ({ isOpen, noOverlay, name, onApprove, on isOpen={isOpen} noOverlay={noOverlay} title={`Cancel${name ? ` ${name}` : ''}`} - description='Are you sure you want to discard your changes?' + description='Are you sure you want to cancel?' approveButton={{ - text: 'Cancel', + text: 'Confirm', variant: 'warning', onClick: onApprove, }} diff --git a/frontend/webapp/components/modals/delete-warning/index.tsx b/frontend/webapp/components/modals/delete-warning/index.tsx index a95c1bb0b..efb1db297 100644 --- a/frontend/webapp/components/modals/delete-warning/index.tsx +++ b/frontend/webapp/components/modals/delete-warning/index.tsx @@ -24,12 +24,12 @@ const DeleteWarning: React.FC = ({ isOpen, noOverlay, name, note, onAppro description='Are you sure you want to delete?' note={note} approveButton={{ - text: 'Delete', + text: 'Confirm', variant: 'danger', onClick: onApprove, }} denyButton={{ - text: 'Cancel', + text: 'Go Back', onClick: onDeny, }} /> diff --git a/frontend/webapp/components/notification/index.ts b/frontend/webapp/components/notification/index.ts new file mode 100644 index 000000000..f3993ea5b --- /dev/null +++ b/frontend/webapp/components/notification/index.ts @@ -0,0 +1,4 @@ +export * from './notification-manager'; +import ToastList from './toast-list'; + +export { ToastList }; diff --git a/frontend/webapp/components/notification/index.tsx b/frontend/webapp/components/notification/index.tsx deleted file mode 100644 index fe2628507..000000000 --- a/frontend/webapp/components/notification/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './notification-manager'; -export * from './toast-list'; diff --git a/frontend/webapp/components/notification/toast-list.tsx b/frontend/webapp/components/notification/toast-list.tsx index ee3bedf99..e7b5f0540 100644 --- a/frontend/webapp/components/notification/toast-list.tsx +++ b/frontend/webapp/components/notification/toast-list.tsx @@ -17,7 +17,7 @@ const Container = styled.div` min-width: 600px; `; -export const ToastList: React.FC = () => { +const ToastList: React.FC = () => { const { notifications } = useNotificationStore(); return ( @@ -59,3 +59,5 @@ const Toast: React.FC = (props) => { /> ); }; + +export default ToastList; diff --git a/frontend/webapp/containers/main/overview/all-drawers/index.tsx b/frontend/webapp/components/overview/all-drawers/index.tsx similarity index 75% rename from frontend/webapp/containers/main/overview/all-drawers/index.tsx rename to frontend/webapp/components/overview/all-drawers/index.tsx index 65b2bba52..65cbb3b59 100644 --- a/frontend/webapp/containers/main/overview/all-drawers/index.tsx +++ b/frontend/webapp/components/overview/all-drawers/index.tsx @@ -1,10 +1,7 @@ import React from 'react'; import { useDrawerStore } from '@/store'; -import { SourceDrawer } from '../../sources'; -import { ActionDrawer } from '../../actions'; import { OVERVIEW_ENTITY_TYPES } from '@/types'; -import { DestinationDrawer } from '../../destinations'; -import { RuleDrawer } from '../../instrumentation-rules'; +import { ActionDrawer, DestinationDrawer, RuleDrawer, SourceDrawer } from '@/containers'; const AllDrawers = () => { const selected = useDrawerStore(({ selectedItem }) => selectedItem); diff --git a/frontend/webapp/containers/main/overview/all-modals/index.tsx b/frontend/webapp/components/overview/all-modals/index.tsx similarity index 78% rename from frontend/webapp/containers/main/overview/all-modals/index.tsx rename to frontend/webapp/components/overview/all-modals/index.tsx index 96039338a..50793a9c9 100644 --- a/frontend/webapp/containers/main/overview/all-modals/index.tsx +++ b/frontend/webapp/components/overview/all-modals/index.tsx @@ -1,10 +1,7 @@ import React from 'react'; import { useModalStore } from '@/store'; -import { ActionModal } from '../../actions'; import { OVERVIEW_ENTITY_TYPES } from '@/types'; -import { DestinationModal } from '../../destinations'; -import { RuleModal } from '../../instrumentation-rules'; -import { AddSourceModal } from '../../sources/choose-sources/choose-source-modal'; +import { ActionModal, AddSourceModal, DestinationModal, RuleModal } from '@/containers'; const AllModals = () => { const selected = useModalStore(({ currentModal }) => currentModal); diff --git a/frontend/webapp/components/overview/index.ts b/frontend/webapp/components/overview/index.ts new file mode 100644 index 000000000..ac94bf260 --- /dev/null +++ b/frontend/webapp/components/overview/index.ts @@ -0,0 +1,6 @@ +export * from './add-entity'; +import AllDrawers from './all-drawers'; +import AllModals from './all-modals'; +export * from './monitors-legend'; + +export { AllDrawers, AllModals }; diff --git a/frontend/webapp/components/overview/index.tsx b/frontend/webapp/components/overview/index.tsx deleted file mode 100644 index a84120e29..000000000 --- a/frontend/webapp/components/overview/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './add-entity'; -export * from './monitors-legend'; diff --git a/frontend/webapp/components/setup/header/index.tsx b/frontend/webapp/components/setup/header/index.tsx index 0c22d70ba..eaf1b3bc4 100644 --- a/frontend/webapp/components/setup/header/index.tsx +++ b/frontend/webapp/components/setup/header/index.tsx @@ -18,7 +18,7 @@ const HeaderContainer = styled.div` justify-content: space-between; padding: 0 24px 0 32px; align-items: center; - background-color: ${({ theme }) => theme.colors.dark_grey}; + background-color: ${({ theme }) => theme.colors.darker_grey}; border-bottom: 1px solid rgba(249, 249, 249, 0.16); height: 80px; `; diff --git a/frontend/webapp/containers/main/actions/action-drawer/build-card-from-action-spec.ts b/frontend/webapp/containers/main/actions/action-drawer/build-card.ts similarity index 93% rename from frontend/webapp/containers/main/actions/action-drawer/build-card-from-action-spec.ts rename to frontend/webapp/containers/main/actions/action-drawer/build-card.ts index 7b872ebf2..50953951c 100644 --- a/frontend/webapp/containers/main/actions/action-drawer/build-card-from-action-spec.ts +++ b/frontend/webapp/containers/main/actions/action-drawer/build-card.ts @@ -1,6 +1,6 @@ import type { ActionDataParsed } from '@/types'; -const buildCardFromActionSpec = (action: ActionDataParsed) => { +const buildCard = (action: ActionDataParsed) => { const { type, spec: { @@ -17,14 +17,14 @@ const buildCardFromActionSpec = (action: ActionDataParsed) => { sampling_percentage, endpoints_filters, }, - } = action as ActionDataParsed; + } = action; const arr = [ - { title: 'Type', value: type || 'N/A' }, + { title: 'Type', value: type }, { title: 'Status', value: String(!disabled) }, + { title: 'Monitors', value: signals.map((str) => str.toLowerCase()).join(', ') }, { title: 'Name', value: actionName || 'N/A' }, { title: 'Notes', value: notes || 'N/A' }, - { title: 'Monitors', value: signals.map((str) => str.toLowerCase()).join(', ') }, ]; if (clusterAttributes) { @@ -91,4 +91,4 @@ const buildCardFromActionSpec = (action: ActionDataParsed) => { return arr; }; -export default buildCardFromActionSpec; +export default buildCard; diff --git a/frontend/webapp/containers/main/actions/action-drawer/build-drawer-item.ts b/frontend/webapp/containers/main/actions/action-drawer/build-drawer-item.ts new file mode 100644 index 000000000..d3c03810b --- /dev/null +++ b/frontend/webapp/containers/main/actions/action-drawer/build-drawer-item.ts @@ -0,0 +1,21 @@ +import { safeJsonParse } from '@/utils'; +import type { ActionDataParsed, ActionInput } from '@/types'; + +const buildDrawerItem = (id: string, formData: ActionInput, drawerItem: ActionDataParsed): ActionDataParsed => { + const { type, name, notes, signals, disable, details } = formData; + const {} = drawerItem; + + return { + id, + type, + spec: { + actionName: name, + notes: notes, + signals: signals, + disabled: disable, + ...safeJsonParse(details, {}), + }, + }; +}; + +export default buildDrawerItem; diff --git a/frontend/webapp/containers/main/actions/action-drawer/index.tsx b/frontend/webapp/containers/main/actions/action-drawer/index.tsx index e64aefc18..9204193a2 100644 --- a/frontend/webapp/containers/main/actions/action-drawer/index.tsx +++ b/frontend/webapp/containers/main/actions/action-drawer/index.tsx @@ -1,30 +1,53 @@ import React, { useMemo, useState } from 'react'; +import buildCard from './build-card'; 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 { ACTION, getActionIcon } from '@/utils'; +import buildDrawerItem from './build-drawer-item'; 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 { OVERVIEW_ENTITY_TYPES, type ActionDataParsed } from '@/types'; interface Props {} -const ActionDrawer: React.FC = () => { - const selectedItem = useDrawerStore(({ selectedItem }) => selectedItem); +const FormContainer = styled.div` + width: 100%; + height: 100%; + max-height: calc(100vh - 220px); + overflow: overlay; + overflow-y: auto; +`; + +export const ActionDrawer: React.FC = () => { + const { selectedItem, setSelectedItem } = useDrawerStore(); + const { formData, formErrors, handleFormChange, resetFormData, validateForm, loadFormWithDrawerItem } = useActionFormData(); + + const { updateAction, deleteAction } = useActionCRUD({ + onSuccess: (type) => { + setIsEditing(false); + setIsFormDirty(false); + + if (type === ACTION.DELETE) { + setSelectedItem(null); + } else { + const { item } = selectedItem as { item: ActionDataParsed }; + const { id } = item; + setSelectedItem({ id, type: OVERVIEW_ENTITY_TYPES.ACTION, item: buildDrawerItem(id, formData, item) }); + } + }, + }); + const [isEditing, setIsEditing] = useState(false); const [isFormDirty, setIsFormDirty] = useState(false); - const { formData, handleFormChange, resetFormData, validateForm, loadFormWithDrawerItem } = useActionFormData(); - const { updateAction, deleteAction } = useActionCRUD(); - const cardData = useMemo(() => { if (!selectedItem) return []; const { item } = selectedItem as { item: ActionDataParsed }; - const arr = buildCardFromActionSpec(item); + const arr = buildCard(item); return arr; }, [selectedItem]); @@ -49,37 +72,33 @@ const ActionDrawer: React.FC = () => { }, [selectedItem, isEditing]); if (!selectedItem?.item) return null; - const { id, item } = selectedItem; + const { id, item } = selectedItem as { id: string; item: ActionDataParsed }; const handleEdit = (bool?: boolean) => { - if (typeof bool === 'boolean') { - setIsEditing(bool); - } else { - setIsEditing(true); - } + setIsEditing(typeof bool === 'boolean' ? bool : true); }; const handleCancel = () => { - resetFormData(); setIsEditing(false); + setIsFormDirty(false); }; const handleDelete = async () => { - await deleteAction(id as string, (item as ActionDataParsed).type); + await deleteAction(id, item.type); }; const handleSave = async (newTitle: string) => { - if (validateForm({ withAlert: true })) { - const title = newTitle !== (item as ActionDataParsed).type ? newTitle : ''; - - await updateAction(id as string, { ...formData, name: title }); + if (validateForm({ withAlert: true, alertTitle: ACTION.UPDATE })) { + const title = newTitle !== item.type ? newTitle : ''; + handleFormChange('name', title); + await updateAction(id, { ...formData, name: title }); } }; return ( = () => { isUpdate action={thisAction} formData={formData} + formErrors={formErrors} handleFormChange={(...params) => { setIsFormDirty(true); handleFormChange(...params); @@ -100,18 +120,8 @@ const ActionDrawer: React.FC = () => { /> ) : ( - + )} ); }; - -export { ActionDrawer }; - -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/actions/action-form-body/custom-fields/add-cluster-info.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/add-cluster-info.tsx index 43206bc02..9249aebba 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/add-cluster-info.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/add-cluster-info.tsx @@ -6,11 +6,12 @@ import { KeyValueInputsList } from '@/reuseable-components'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = AddClusterInfoSpec; -const AddClusterInfo: React.FC = ({ value, setValue }) => { +const AddClusterInfo: React.FC = ({ value, setValue, errorMessage }) => { const mappedValue = useMemo( () => safeJsonParse(value, { clusterAttributes: [] }).clusterAttributes.map((obj) => ({ @@ -33,7 +34,7 @@ const AddClusterInfo: React.FC = ({ value, setValue }) => { setValue(str); }; - return ; + return ; }; export default AddClusterInfo; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/delete-attributes.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/delete-attributes.tsx index cc99960ec..9b98161c2 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/delete-attributes.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/delete-attributes.tsx @@ -6,11 +6,12 @@ import type { DeleteAttributesSpec } from '@/types'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = DeleteAttributesSpec; -const DeleteAttributes: React.FC = ({ value, setValue }) => { +const DeleteAttributes: React.FC = ({ value, setValue, errorMessage }) => { const mappedValue = useMemo(() => safeJsonParse(value, { attributeNamesToDelete: [] }).attributeNamesToDelete, [value]); const handleChange = (arr: string[]) => { @@ -23,7 +24,7 @@ const DeleteAttributes: React.FC = ({ value, setValue }) => { setValue(str); }; - return ; + return ; }; export default DeleteAttributes; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx index e089dcf90..bc78c4a7b 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/error-sampler.tsx @@ -6,6 +6,7 @@ import type { ErrorSamplerSpec } from '@/types'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = ErrorSamplerSpec; @@ -13,7 +14,7 @@ type Parsed = ErrorSamplerSpec; const MIN = 0, MAX = 100; -const ErrorSampler: React.FC = ({ value, setValue }) => { +const ErrorSampler: React.FC = ({ value, setValue, errorMessage }) => { const mappedValue = useMemo(() => safeJsonParse(value, { fallback_sampling_ratio: 0 }).fallback_sampling_ratio, [value]); const handleChange = (val: string) => { @@ -28,17 +29,7 @@ const ErrorSampler: React.FC = ({ value, setValue }) => { setValue(str); }; - return ( - handleChange(v)} - /> - ); + return handleChange(v)} errorMessage={errorMessage} />; }; export default ErrorSampler; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/index.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/index.tsx index d0e0f4c93..e9486614a 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/index.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/index.tsx @@ -8,16 +8,18 @@ import DeleteAttributes from './delete-attributes'; import RenameAttributes from './rename-attributes'; import ProbabilisticSampler from './probabilistic-sampler'; -interface ActionCustomFieldsProps { +interface Props { actionType?: ActionsType; value: string; setValue: (value: string) => void; + errorMessage?: string; } -type ComponentProps = { - value: string; - setValue: (value: string) => void; -}; +interface ComponentProps { + value: Props['value']; + setValue: Props['setValue']; + errorMessage?: Props['errorMessage']; +} type ComponentType = React.FC | null; @@ -31,12 +33,12 @@ const componentsMap: Record = { [ActionsType.LATENCY_SAMPLER]: LatencySampler, }; -const ActionCustomFields: React.FC = ({ actionType, value, setValue }) => { +const ActionCustomFields: React.FC = ({ actionType, value, setValue, errorMessage }) => { if (!actionType) return null; const Component = componentsMap[actionType]; - return Component ? : null; + return Component ? : null; }; export default ActionCustomFields; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/latency-sampler.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/latency-sampler.tsx index 809c2919e..d833071f9 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/latency-sampler.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/latency-sampler.tsx @@ -6,11 +6,12 @@ import type { LatencySamplerSpec } from '@/types'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = LatencySamplerSpec; -const LatencySampler: React.FC = ({ value, setValue }) => { +const LatencySampler: React.FC = ({ value, setValue, errorMessage }) => { const mappedValue = useMemo(() => safeJsonParse(value, { endpoints_filters: [] }).endpoints_filters, [value]); const handleChange = (arr: Parsed['endpoints_filters']) => { @@ -31,8 +32,7 @@ const LatencySampler: React.FC = ({ value, setValue }) => { keyName: 'service_name', placeholder: 'Choose service', required: true, - tooltip: - 'Service name: The rule applies to a specific service name. Only traces originating from this service’s root span will be considered.', + tooltip: 'Service name: The rule applies to a specific service name. Only traces originating from this service’s root span will be considered.', }, { title: 'HTTP route', @@ -48,8 +48,7 @@ const LatencySampler: React.FC = ({ value, setValue }) => { placeholder: 'e.g. 1000', required: true, type: 'number', - tooltip: - 'Minimum latency threshold (ms): Specifies the minimum latency in milliseconds; traces with latency below this threshold are ignored.', + tooltip: 'Minimum latency threshold (ms): Specifies the minimum latency in milliseconds; traces with latency below this threshold are ignored.', }, { title: 'Fallback', @@ -63,6 +62,7 @@ const LatencySampler: React.FC = ({ value, setValue }) => { ]} value={mappedValue} onChange={handleChange} + errorMessage={errorMessage} /> ); }; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/pii-masking.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/pii-masking.tsx index 9343cbf97..3a719af67 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/pii-masking.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/pii-masking.tsx @@ -1,20 +1,28 @@ -import styled from 'styled-components'; +import React, { useEffect, useMemo, useState } from 'react'; import { safeJsonParse } from '@/utils'; import type { PiiMaskingSpec } from '@/types'; -import React, { useEffect, useMemo, useState } from 'react'; -import { Checkbox, FieldLabel } from '@/reuseable-components'; +import styled, { css } from 'styled-components'; +import { Checkbox, FieldError, FieldLabel } from '@/reuseable-components'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = PiiMaskingSpec; -const ListContainer = styled.div` +const ListContainer = styled.div<{ $hasError: boolean }>` display: flex; flex-direction: row; gap: 32px; + ${({ $hasError }) => + $hasError && + css` + border: 1px solid ${({ theme }) => theme.text.error}; + border-radius: 32px; + padding: 8px; + `} `; const strictPicklist = [ @@ -24,7 +32,7 @@ const strictPicklist = [ }, ]; -const PiiMasking: React.FC = ({ value, setValue }) => { +const PiiMasking: React.FC = ({ value, setValue, errorMessage }) => { const mappedValue = useMemo(() => safeJsonParse(value, { piiCategories: [] }).piiCategories, [value]); const [isLastSelection, setIsLastSelection] = useState(mappedValue.length === 1); @@ -56,11 +64,12 @@ const PiiMasking: React.FC = ({ value, setValue }) => { return (
- + {strictPicklist.map(({ id, label }) => ( handleChange(id, bool)} /> ))} + {!!errorMessage && {errorMessage}}
); }; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/probabilistic-sampler.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/probabilistic-sampler.tsx index 8952275cc..8e87c2cb4 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/probabilistic-sampler.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/probabilistic-sampler.tsx @@ -6,6 +6,7 @@ import type { ProbabilisticSamplerSpec } from '@/types'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = ProbabilisticSamplerSpec; @@ -13,7 +14,7 @@ type Parsed = ProbabilisticSamplerSpec; const MIN = 0, MAX = 100; -const ProbabilisticSampler: React.FC = ({ value, setValue }) => { +const ProbabilisticSampler: React.FC = ({ value, setValue, errorMessage }) => { const mappedValue = useMemo(() => safeJsonParse(value, { sampling_percentage: '0' }).sampling_percentage, [value]); const handleChange = (val: string) => { @@ -28,17 +29,7 @@ const ProbabilisticSampler: React.FC = ({ value, setValue }) => { setValue(str); }; - return ( - handleChange(v)} - /> - ); + return handleChange(v)} errorMessage={errorMessage} />; }; export default ProbabilisticSampler; diff --git a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/rename-attributes.tsx b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/rename-attributes.tsx index 2e7c4b3d9..414e8f8ee 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/custom-fields/rename-attributes.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/custom-fields/rename-attributes.tsx @@ -6,21 +6,19 @@ import { KeyValueInputsList } from '@/reuseable-components'; type Props = { value: string; setValue: (value: string) => void; + errorMessage?: string; }; type Parsed = RenameAttributesSpec; -const RenameAttributes: React.FC = ({ value, setValue }) => { - const mappedValue = useMemo( - () => Object.entries(safeJsonParse(value, { renames: {} }).renames).map(([k, v]) => ({ key: k, value: v })), - [value] - ); +const RenameAttributes: React.FC = ({ value, setValue, errorMessage }) => { + const mappedValue = useMemo(() => Object.entries(safeJsonParse(value, { renames: {} }).renames).map(([k, v]) => ({ key: k, value: v })), [value]); const handleChange = ( arr: { key: string; value: string; - }[] + }[], ) => { const payload: Parsed = { renames: {}, @@ -35,7 +33,7 @@ const RenameAttributes: React.FC = ({ value, setValue }) => { setValue(str); }; - return ; + return ; }; export default RenameAttributes; diff --git a/frontend/webapp/containers/main/actions/action-form-body/index.tsx b/frontend/webapp/containers/main/actions/action-form-body/index.tsx index cc55afe26..8ab6df68f 100644 --- a/frontend/webapp/containers/main/actions/action-form-body/index.tsx +++ b/frontend/webapp/containers/main/actions/action-form-body/index.tsx @@ -9,6 +9,7 @@ interface Props { isUpdate?: boolean; action: ActionOption; formData: ActionInput; + formErrors: Record; handleFormChange: (key: keyof ActionInput, val: any) => void; } @@ -23,7 +24,7 @@ const FieldTitle = styled(Text)` margin-bottom: 12px; `; -export const ActionFormBody: React.FC = ({ isUpdate, action, formData, handleFormChange }) => { +export const ActionFormBody: React.FC = ({ isUpdate, action, formData, formErrors, handleFormChange }) => { return ( {isUpdate && ( @@ -35,13 +36,28 @@ export const ActionFormBody: React.FC = ({ isUpdate, action, formData, ha {!isUpdate && } />} - handleFormChange('signals', value)} /> - - {!isUpdate && handleFormChange('name', value)} />} + handleFormChange('signals', value)} + errorMessage={formErrors['signals']} + /> + + {!isUpdate && ( + handleFormChange('name', value)} + errorMessage={formErrors['name']} + /> + )} - handleFormChange('details', val)} /> + handleFormChange('details', val)} errorMessage={formErrors['details']} /> -