Skip to content

Commit

Permalink
[GEN-1761]: manage form errors (#1878)
Browse files Browse the repository at this point in the history
  • Loading branch information
BenElferink authored Nov 28, 2024
1 parent d003e9e commit 82c9418
Show file tree
Hide file tree
Showing 30 changed files with 350 additions and 341 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ value, setValue }) => {
const AddClusterInfo: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(
() =>
safeJsonParse<Parsed>(value, { clusterAttributes: [] }).clusterAttributes.map((obj) => ({
Expand All @@ -33,7 +34,7 @@ const AddClusterInfo: React.FC<Props> = ({ value, setValue }) => {
setValue(str);
};

return <KeyValueInputsList title='Resource Attributes' required value={mappedValue} onChange={handleChange} />;
return <KeyValueInputsList title='Resource Attributes' value={mappedValue} onChange={handleChange} required errorMessage={errorMessage} />;
};

export default AddClusterInfo;
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ value, setValue }) => {
const DeleteAttributes: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(() => safeJsonParse<Parsed>(value, { attributeNamesToDelete: [] }).attributeNamesToDelete, [value]);

const handleChange = (arr: string[]) => {
Expand All @@ -23,7 +24,7 @@ const DeleteAttributes: React.FC<Props> = ({ value, setValue }) => {
setValue(str);
};

return <InputList title='Attributes to delete' required value={mappedValue} onChange={handleChange} />;
return <InputList title='Attributes to delete' value={mappedValue} onChange={handleChange} required errorMessage={errorMessage} />;
};

export default DeleteAttributes;
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type { ErrorSamplerSpec } from '@/types';
type Props = {
value: string;
setValue: (value: string) => void;
errorMessage?: string;
};

type Parsed = ErrorSamplerSpec;

const MIN = 0,
MAX = 100;

const ErrorSampler: React.FC<Props> = ({ value, setValue }) => {
const ErrorSampler: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(() => safeJsonParse<Parsed>(value, { fallback_sampling_ratio: 0 }).fallback_sampling_ratio, [value]);

const handleChange = (val: string) => {
Expand All @@ -28,17 +29,7 @@ const ErrorSampler: React.FC<Props> = ({ value, setValue }) => {
setValue(str);
};

return (
<Input
title='Fallback sampling ratio'
required
type='number'
min={MIN}
max={MAX}
value={mappedValue}
onChange={({ target: { value: v } }) => handleChange(v)}
/>
);
return <Input title='Fallback sampling ratio' required type='number' min={MIN} max={MAX} value={mappedValue} onChange={({ target: { value: v } }) => handleChange(v)} errorMessage={errorMessage} />;
};

export default ErrorSampler;
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentProps> | null;

Expand All @@ -31,12 +33,12 @@ const componentsMap: Record<ActionsType, ComponentType> = {
[ActionsType.LATENCY_SAMPLER]: LatencySampler,
};

const ActionCustomFields: React.FC<ActionCustomFieldsProps> = ({ actionType, value, setValue }) => {
const ActionCustomFields: React.FC<Props> = ({ actionType, value, setValue, errorMessage }) => {
if (!actionType) return null;

const Component = componentsMap[actionType];

return Component ? <Component value={value} setValue={setValue} /> : null;
return Component ? <Component value={value} setValue={setValue} errorMessage={errorMessage} /> : null;
};

export default ActionCustomFields;
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ value, setValue }) => {
const LatencySampler: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(() => safeJsonParse<Parsed>(value, { endpoints_filters: [] }).endpoints_filters, [value]);

const handleChange = (arr: Parsed['endpoints_filters']) => {
Expand All @@ -31,8 +32,7 @@ const LatencySampler: React.FC<Props> = ({ 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',
Expand All @@ -48,8 +48,7 @@ const LatencySampler: React.FC<Props> = ({ 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',
Expand All @@ -63,6 +62,7 @@ const LatencySampler: React.FC<Props> = ({ value, setValue }) => {
]}
value={mappedValue}
onChange={handleChange}
errorMessage={errorMessage}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -24,7 +32,7 @@ const strictPicklist = [
},
];

const PiiMasking: React.FC<Props> = ({ value, setValue }) => {
const PiiMasking: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(() => safeJsonParse<Parsed>(value, { piiCategories: [] }).piiCategories, [value]);
const [isLastSelection, setIsLastSelection] = useState(mappedValue.length === 1);

Expand Down Expand Up @@ -56,11 +64,12 @@ const PiiMasking: React.FC<Props> = ({ value, setValue }) => {
return (
<div>
<FieldLabel title='Attributes to mask' required />
<ListContainer>
<ListContainer $hasError={!!errorMessage}>
{strictPicklist.map(({ id, label }) => (
<Checkbox key={id} title={label} disabled={isLastSelection && mappedValue.includes(id)} initialValue={mappedValue.includes(id)} onChange={(bool) => handleChange(id, bool)} />
))}
</ListContainer>
{!!errorMessage && <FieldError>{errorMessage}</FieldError>}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type { ProbabilisticSamplerSpec } from '@/types';
type Props = {
value: string;
setValue: (value: string) => void;
errorMessage?: string;
};

type Parsed = ProbabilisticSamplerSpec;

const MIN = 0,
MAX = 100;

const ProbabilisticSampler: React.FC<Props> = ({ value, setValue }) => {
const ProbabilisticSampler: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(() => safeJsonParse<Parsed>(value, { sampling_percentage: '0' }).sampling_percentage, [value]);

const handleChange = (val: string) => {
Expand All @@ -28,17 +29,7 @@ const ProbabilisticSampler: React.FC<Props> = ({ value, setValue }) => {
setValue(str);
};

return (
<Input
title='Sampling percentage'
required
type='number'
min={MIN}
max={MAX}
value={mappedValue}
onChange={({ target: { value: v } }) => handleChange(v)}
/>
);
return <Input title='Sampling percentage' required type='number' min={MIN} max={MAX} value={mappedValue} onChange={({ target: { value: v } }) => handleChange(v)} errorMessage={errorMessage} />;
};

export default ProbabilisticSampler;
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({ value, setValue }) => {
const mappedValue = useMemo(
() => Object.entries(safeJsonParse<Parsed>(value, { renames: {} }).renames).map(([k, v]) => ({ key: k, value: v })),
[value]
);
const RenameAttributes: React.FC<Props> = ({ value, setValue, errorMessage }) => {
const mappedValue = useMemo(() => Object.entries(safeJsonParse<Parsed>(value, { renames: {} }).renames).map(([k, v]) => ({ key: k, value: v })), [value]);

const handleChange = (
arr: {
key: string;
value: string;
}[]
}[],
) => {
const payload: Parsed = {
renames: {},
Expand All @@ -35,7 +33,7 @@ const RenameAttributes: React.FC<Props> = ({ value, setValue }) => {
setValue(str);
};

return <KeyValueInputsList title='Attributes to rename' required value={mappedValue} onChange={handleChange} />;
return <KeyValueInputsList title='Attributes to rename' value={mappedValue} onChange={handleChange} required errorMessage={errorMessage} />;
};

export default RenameAttributes;
18 changes: 14 additions & 4 deletions frontend/webapp/containers/main/actions/action-form-body/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,26 @@ export const ActionFormBody: React.FC<Props> = ({ isUpdate, action, formData, fo

<MonitoringCheckboxes
title='Signals for Processing'
required
allowedSignals={action.allowedSignals}
selectedSignals={formData.signals}
selectedSignals={formData['signals']}
setSelectedSignals={(value) => handleFormChange('signals', value)}
errorMessage={formErrors['signals']}
/>

{!isUpdate && <Input title='Action name' placeholder='Use a name that describes the action' value={formData.name} onChange={({ target: { value } }) => handleFormChange('name', value)} />}
{!isUpdate && (
<Input
title='Action name'
placeholder='Use a name that describes the action'
value={formData['name']}
onChange={({ target: { value } }) => handleFormChange('name', value)}
errorMessage={formErrors['name']}
/>
)}

<ActionCustomFields actionType={action.type} value={formData.details} setValue={(val) => handleFormChange('details', val)} />
<ActionCustomFields actionType={action.type} value={formData['details']} setValue={(val) => handleFormChange('details', val)} errorMessage={formErrors['details']} />

<TextArea title='Notes' value={formData.notes} onChange={({ target: { value } }) => handleFormChange('notes', value)} />
<TextArea title='Notes' value={formData['notes']} onChange={({ target: { value } }) => handleFormChange('notes', value)} errorMessage={formErrors['notes']} />
</Container>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@ import { Dropdown, Input, TextArea, InputList, KeyValueInputsList } from '@/reus
interface Props {
fields: any[];
onChange: (name: string, value: any) => void;
formErrors: Record<string, string>;
}

export const DestinationDynamicFields: React.FC<Props> = ({ fields, onChange }) => {
export const DestinationDynamicFields: React.FC<Props> = ({ fields, onChange, formErrors }) => {
return fields?.map((field: any) => {
const { componentType, ...rest } = field;

switch (componentType) {
case INPUT_TYPES.INPUT:
return <Input key={field.name} {...rest} onChange={(e) => onChange(field.name, e.target.value)} />;
return <Input key={field.name} {...rest} onChange={(e) => onChange(field.name, e.target.value)} errorMessage={formErrors[field.name]} />;
case INPUT_TYPES.DROPDOWN:
return <Dropdown key={field.name} {...rest} onSelect={(option) => onChange(field.name, { id: option.id, value: option.value })} />;
return <Dropdown key={field.name} {...rest} onSelect={(option) => onChange(field.name, { id: option.id, value: option.value })} errorMessage={formErrors[field.name]} />;
case INPUT_TYPES.MULTI_INPUT:
return <InputList key={field.name} {...rest} onChange={(value: string[]) => onChange(field.name, JSON.stringify(value))} />;
return <InputList key={field.name} {...rest} onChange={(value: string[]) => onChange(field.name, JSON.stringify(value))} errorMessage={formErrors[field.name]} />;
case INPUT_TYPES.KEY_VALUE_PAIR:
return <KeyValueInputsList key={field.name} {...rest} onChange={(value) => onChange(field.name, JSON.stringify(value))} />;
return <KeyValueInputsList key={field.name} {...rest} onChange={(value) => onChange(field.name, JSON.stringify(value))} errorMessage={formErrors[field.name]} />;
case INPUT_TYPES.TEXTAREA:
return <TextArea key={field.name} {...rest} onChange={(e) => onChange(field.name, e.target.value)} />;
return <TextArea key={field.name} {...rest} onChange={(e) => onChange(field.name, e.target.value)} errorMessage={formErrors[field.name]} />;
default:
return null;
}
Expand Down
Loading

0 comments on commit 82c9418

Please sign in to comment.