From e9d5be3305a82e3edde2b8617cf3d23c0ec24bce Mon Sep 17 00:00:00 2001 From: Edgar Fisher Date: Tue, 22 Oct 2024 17:09:15 +0300 Subject: [PATCH] feat: Add parameterization rule UI (#257) --- src/components/Form/ControlledSelect.tsx | 13 +- src/globalStyles.ts | 5 + src/rules/rules.ts | 2 +- src/store/generator/selectors.ts | 2 +- src/utils/rules.ts | 2 +- src/views/Generator/NewRuleMenu.tsx | 9 ++ .../RuleEditor/CorrelationEditor.tsx | 8 +- .../Generator/RuleEditor/CustomCodeEditor.tsx | 2 +- .../Generator/RuleEditor/FilterField.tsx | 6 +- .../RuleEditor/ParameterizationEditor.tsx | 27 ++++ .../ParameterizationValueEditor.tsx | 115 ++++++++++++++++++ src/views/Generator/RuleEditor/RuleEditor.tsx | 11 +- .../Generator/RuleEditor/SelectorField.tsx | 19 +-- .../RulePreview/ParameterizationPreview.tsx | 79 ++++++++++++ .../Generator/RulePreview/RulePreview.tsx | 5 +- .../Generator/TestOptions/VariablesEditor.tsx | 109 ++++++++++++----- .../TestRuleContainer/SortableRuleList.tsx | 36 +++--- .../TestRuleContainer/TestRule/TestRule.tsx | 12 +- .../TestRule/TestRuleInlineContent.tsx | 14 ++- .../TestRule/TestRuleSelector.tsx | 99 ++++++++++++--- .../TestRule/TestRuleTypeBadge.tsx | 2 +- 21 files changed, 474 insertions(+), 103 deletions(-) create mode 100644 src/views/Generator/RuleEditor/ParameterizationEditor.tsx create mode 100644 src/views/Generator/RuleEditor/ParameterizationValueEditor.tsx create mode 100644 src/views/Generator/RulePreview/ParameterizationPreview.tsx diff --git a/src/components/Form/ControlledSelect.tsx b/src/components/Form/ControlledSelect.tsx index 46eb540f..81042c0f 100644 --- a/src/components/Form/ControlledSelect.tsx +++ b/src/components/Form/ControlledSelect.tsx @@ -1,13 +1,15 @@ import { Select } from '@radix-ui/themes' +import { ReactNode } from 'react' import { Control, Controller, FieldValues, Path } from 'react-hook-form' -type Option = { label: string; value: string } +type Option = { label: ReactNode; value: string; disabled?: boolean } interface ControlledSelectProps { name: Path control: Control options: O[] selectProps?: Select.RootProps + contentProps?: Select.ContentProps onChange?: (value: O['value']) => void } @@ -16,6 +18,7 @@ export function ControlledSelect({ control, options, selectProps = {}, + contentProps = {}, onChange, }: ControlledSelectProps) { return ( @@ -33,9 +36,13 @@ export function ControlledSelect({ id={name} css={{ width: '100%' }} /> - + {options.map((option) => ( - + {option.label} ))} diff --git a/src/globalStyles.ts b/src/globalStyles.ts index 0299916d..4f2d7949 100644 --- a/src/globalStyles.ts +++ b/src/globalStyles.ts @@ -20,4 +20,9 @@ export const globalStyles = css` .rt-ScrollAreaScrollbar { z-index: 2; } + + /* Allow to truncate text in Select options */ + .rt-SelectItem > span:not(.rt-SelectItemIndicator) { + width: 100%; + } ` diff --git a/src/rules/rules.ts b/src/rules/rules.ts index 5a2e55ad..8cc5b27e 100644 --- a/src/rules/rules.ts +++ b/src/rules/rules.ts @@ -24,7 +24,7 @@ export function applyRules(recording: ProxyData[], rules: TestRule[]) { return { requestSnippetSchemas, ruleInstances } } -export function createRuleInstance( +function createRuleInstance( rule: T, idGenerator: Generator ) { diff --git a/src/store/generator/selectors.ts b/src/store/generator/selectors.ts index ca14a0b5..49a3a0b7 100644 --- a/src/store/generator/selectors.ts +++ b/src/store/generator/selectors.ts @@ -17,7 +17,7 @@ export function selectSelectedRule(state: GeneratorStore) { export function selectIsRulePreviewable(state: GeneratorStore) { const rule = selectSelectedRule(state) - return rule?.type === 'correlation' + return ['correlation', 'parameterization'].includes(rule?.type ?? '') } export function selectHasRecording(state: GeneratorStore) { diff --git a/src/utils/rules.ts b/src/utils/rules.ts index ac3cea62..41a1c02a 100644 --- a/src/utils/rules.ts +++ b/src/utils/rules.ts @@ -35,7 +35,7 @@ export function createEmptyRule(type: TestRule['type']): TestRule { begin: '', end: '', }, - value: { type: 'variable', variableName: '' }, + value: { type: 'string', value: '' }, } case 'verification': return { diff --git a/src/views/Generator/NewRuleMenu.tsx b/src/views/Generator/NewRuleMenu.tsx index 06cf2017..4908f23b 100644 --- a/src/views/Generator/NewRuleMenu.tsx +++ b/src/views/Generator/NewRuleMenu.tsx @@ -40,6 +40,15 @@ export function NewRuleMenu(props: ComponentProps) { Correlation + + { + createRule('parameterization') + }} + > + Parameterization + + { diff --git a/src/views/Generator/RuleEditor/CorrelationEditor.tsx b/src/views/Generator/RuleEditor/CorrelationEditor.tsx index 5f480446..0d2728b1 100644 --- a/src/views/Generator/RuleEditor/CorrelationEditor.tsx +++ b/src/views/Generator/RuleEditor/CorrelationEditor.tsx @@ -30,8 +30,8 @@ export function CorrelationEditor() { Extraction value for correlation. - - + + diff --git a/src/views/Generator/RuleEditor/CustomCodeEditor.tsx b/src/views/Generator/RuleEditor/CustomCodeEditor.tsx index 0ae5a4ee..91c5c48f 100644 --- a/src/views/Generator/RuleEditor/CustomCodeEditor.tsx +++ b/src/views/Generator/RuleEditor/CustomCodeEditor.tsx @@ -18,7 +18,7 @@ export function CustomCodeEditor() { return ( - + () - const fieldName = `${path}.path` as const + const fieldName = `${field}.path` as const return ( + + Parameterization + + + + Replace request data with variables or custom values. + + + + + + + + + + + + ) +} diff --git a/src/views/Generator/RuleEditor/ParameterizationValueEditor.tsx b/src/views/Generator/RuleEditor/ParameterizationValueEditor.tsx new file mode 100644 index 00000000..decf6431 --- /dev/null +++ b/src/views/Generator/RuleEditor/ParameterizationValueEditor.tsx @@ -0,0 +1,115 @@ +import { Code, Flex, Text, TextField } from '@radix-ui/themes' +import { ControlledSelect, FieldGroup } from '@/components/Form' +import { ParameterizationRule } from '@/types/rules' +import { useFormContext } from 'react-hook-form' +import { useGeneratorStore } from '@/store/generator' +import { useMemo } from 'react' +import { exhaustive } from '@/utils/typescript' + +export function ParamaterizationValueEditor() { + const { + control, + formState: { errors }, + } = useFormContext() + + const variablesExist = useGeneratorStore( + (state) => state.variables.length > 0 + ) + + const variablesLabel = variablesExist + ? 'Variables' + : 'Variables (add in test options)' + + const VALUE_TYPE_OPTIONS = [ + { value: 'string', label: 'Text value' }, + { value: 'variable', label: variablesLabel, disabled: !variablesExist }, + ] + + return ( + <> + + + + + + ) +} + +function ValueTypeSwitch() { + const { + watch, + register, + formState: { errors }, + } = useFormContext() + + const type = watch('value.type') + + switch (type) { + case 'string': + return ( + + + + ) + case 'variable': + return + + case 'array': + case 'customCode': + return null + + default: + return exhaustive(type) + } +} + +function VariableSelect() { + const variables = useGeneratorStore((store) => store.variables) + + const options = useMemo(() => { + return variables.map((variable) => ({ + value: variable.name, + label: ( + + + {variable.name} + + + {variable.value} + + + ), + })) + }, [variables]) + + const { + control, + watch, + formState: { errors }, + } = useFormContext() + + const variableName = watch('value.variableName') + + return ( + + + + ) +} diff --git a/src/views/Generator/RuleEditor/RuleEditor.tsx b/src/views/Generator/RuleEditor/RuleEditor.tsx index 77c7c476..63feb664 100644 --- a/src/views/Generator/RuleEditor/RuleEditor.tsx +++ b/src/views/Generator/RuleEditor/RuleEditor.tsx @@ -10,6 +10,7 @@ import { CorrelationEditor } from './CorrelationEditor' import { CustomCodeEditor } from './CustomCodeEditor' import { TestRule } from '@/types/rules' import { TestRuleSchema } from '@/schemas/rules' +import { ParameterizationEditor } from './ParameterizationEditor' export function RuleEditorSwitch() { const { watch } = useFormContext() @@ -21,14 +22,8 @@ export function RuleEditorSwitch() { case 'customCode': return case 'parameterization': - return ( - - - - - Not implemented yet - - ) + return + case 'verification': return ( diff --git a/src/views/Generator/RuleEditor/SelectorField.tsx b/src/views/Generator/RuleEditor/SelectorField.tsx index 0ef5f769..d87999a4 100644 --- a/src/views/Generator/RuleEditor/SelectorField.tsx +++ b/src/views/Generator/RuleEditor/SelectorField.tsx @@ -29,16 +29,23 @@ const allowedTypes: Record< url: [typeOptions.beginEnd, typeOptions.regex], } -export function SelectorField({ type }: { type: 'extractor' | 'replacer' }) { +export function SelectorField({ + field, +}: { + field: 'extractor.selector' | 'replacer.selector' | 'selector' +}) { const { watch, control, setValue, formState: { errors }, } = useFormContext() - const field = `${type}.selector` as const const selector = watch(field) + if (!selector) { + return null + } + const handleFromChange = (value: Selector['from']) => { // When "from" changes reset type to the first allowed type if the current type is not allowed if (!allowedTypes[value].find(({ value }) => value === selector.type)) { @@ -102,25 +109,23 @@ export function SelectorField({ type }: { type: 'extractor' | 'replacer' }) { - + ) } function SelectorContent({ selector, - type, + field, }: { selector: Selector - type: 'extractor' | 'replacer' + field: 'extractor.selector' | 'replacer.selector' | 'selector' }) { const { register, formState: { errors }, } = useFormContext() - const field = `${type}.selector` as const - switch (selector.type) { case 'json': return ( diff --git a/src/views/Generator/RulePreview/ParameterizationPreview.tsx b/src/views/Generator/RulePreview/ParameterizationPreview.tsx new file mode 100644 index 00000000..74f359cc --- /dev/null +++ b/src/views/Generator/RulePreview/ParameterizationPreview.tsx @@ -0,0 +1,79 @@ +import { WebLogView } from '@/components/WebLogView' +import { applyRules } from '@/rules/rules' +import { useGeneratorStore, selectFilteredRequests } from '@/store/generator' +import { ProxyData } from '@/types' +import { ParameterizationRule } from '@/types/rules' +import { Box, Callout, Heading, ScrollArea } from '@radix-ui/themes' +import { useMemo, useState } from 'react' +import { requestsReplacedToProxyData } from './CorrelationPreview' +import { Details } from '@/components/WebLogView/Details' +import { Allotment } from 'allotment' + +export function ParameterizationPreview({ + rule, +}: { + rule: ParameterizationRule +}) { + const [selectedRequest, setSelectedRequest] = useState(null) + const requests = useGeneratorStore(selectFilteredRequests) + const rules = useGeneratorStore((state) => state.rules) + + const preview = useMemo(() => { + const preceedingAndSelectedRule = rules.slice(0, rules.indexOf(rule) + 1) + const { ruleInstances } = applyRules(requests, preceedingAndSelectedRule) + + const selectedRuleInstance = ruleInstances.find( + (ruleInstance) => ruleInstance.rule.id === rule.id + ) + + if ( + !selectedRuleInstance || + selectedRuleInstance.type !== 'parameterization' + ) { + return null + } + + return selectedRuleInstance.state + }, [rules, requests, rule]) + + if (preview?.requestsReplaced.length === 0) { + return ( + + + No requests matched + + + ) + } + + return ( + + + + + {preview && preview.requestsReplaced.length > 0 && ( + <> + + Requests matched + + + + )} + + + + +
+ + + ) +} diff --git a/src/views/Generator/RulePreview/RulePreview.tsx b/src/views/Generator/RulePreview/RulePreview.tsx index 4f3906eb..6166a5c9 100644 --- a/src/views/Generator/RulePreview/RulePreview.tsx +++ b/src/views/Generator/RulePreview/RulePreview.tsx @@ -1,6 +1,7 @@ import { CorrelationPreview } from './CorrelationPreview' import { exhaustive } from '@/utils/typescript' import { selectSelectedRule, useGeneratorStore } from '@/store/generator' +import { ParameterizationPreview } from './ParameterizationPreview' export function RulePreview() { const rule = useGeneratorStore(selectSelectedRule) @@ -13,8 +14,10 @@ export function RulePreview() { case 'correlation': return - case 'customCode': case 'parameterization': + return + + case 'customCode': case 'verification': return null diff --git a/src/views/Generator/TestOptions/VariablesEditor.tsx b/src/views/Generator/TestOptions/VariablesEditor.tsx index 53d343e2..93f5f3f4 100644 --- a/src/views/Generator/TestOptions/VariablesEditor.tsx +++ b/src/views/Generator/TestOptions/VariablesEditor.tsx @@ -1,8 +1,22 @@ import { useCallback, useEffect } from 'react' import { TrashIcon } from '@radix-ui/react-icons' -import { Button, IconButton, TextField, Text, Code } from '@radix-ui/themes' +import { + Button, + IconButton, + TextField, + Text, + Code, + Tooltip, +} from '@radix-ui/themes' import { useGeneratorStore } from '@/store/generator' -import { useForm, useFieldArray } from 'react-hook-form' +import { + useForm, + useFieldArray, + FieldArrayWithId, + UseFormRegister, + FieldErrors, + UseFieldArrayRemove, +} from 'react-hook-form' import { TestData } from '@/types/testData' import { zodResolver } from '@hookform/resolvers/zod' import { TestDataSchema } from '@/schemas/testData' @@ -69,37 +83,14 @@ export function VariablesEditor() { {fields.map((field, index) => ( - - - - - - - - - - - - - remove(index)}> - - - - + ))} @@ -113,3 +104,55 @@ export function VariablesEditor() { ) } + +function VariableRow({ + field, + index, + errors, + register, + remove, +}: { + field: FieldArrayWithId + index: number + register: UseFormRegister + errors: FieldErrors + remove: UseFieldArrayRemove +}) { + const isVariableInUse = useGeneratorStore((state) => + state.rules.some( + (rule) => + rule.type === 'parameterization' && + rule.value.type === 'variable' && + rule.value.variableName === field.name + ) + ) + + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/src/views/Generator/TestRuleContainer/SortableRuleList.tsx b/src/views/Generator/TestRuleContainer/SortableRuleList.tsx index d2f3d38a..7dbae5ba 100644 --- a/src/views/Generator/TestRuleContainer/SortableRuleList.tsx +++ b/src/views/Generator/TestRuleContainer/SortableRuleList.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Flex } from '@radix-ui/themes' +import { Flex, Grid } from '@radix-ui/themes' import { DndContext, closestCenter, @@ -58,6 +58,8 @@ export function SortableRuleList({ setActive(rules.find((rule) => rule.id === event.active.id) || null) } + const gridColumns = 'auto auto 1fr auto' + return ( - {rules.map((rule) => ( - { - setSelectedRuleId(rule.id) - }} - key={rule.id} - /> - ))} - - {active ? ( + + {rules.map((rule) => ( { + setSelectedRuleId(rule.id) + }} + key={rule.id} /> + ))} + + + {active ? ( + + + ) : null} diff --git a/src/views/Generator/TestRuleContainer/TestRule/TestRule.tsx b/src/views/Generator/TestRuleContainer/TestRule/TestRule.tsx index 808dd28d..0ca1fe6d 100644 --- a/src/views/Generator/TestRuleContainer/TestRule/TestRule.tsx +++ b/src/views/Generator/TestRuleContainer/TestRule/TestRule.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { Flex, IconButton } from '@radix-ui/themes' +import { Flex, Grid, IconButton } from '@radix-ui/themes' import { DragHandleDots2Icon } from '@radix-ui/react-icons' import type { TestRule } from '@/types/rules' @@ -48,7 +48,7 @@ export function TestRuleItem({ : undefined return ( - - + + + - + ) } diff --git a/src/views/Generator/TestRuleContainer/TestRule/TestRuleInlineContent.tsx b/src/views/Generator/TestRuleContainer/TestRule/TestRuleInlineContent.tsx index 18c6c434..b6f78a3c 100644 --- a/src/views/Generator/TestRuleContainer/TestRule/TestRuleInlineContent.tsx +++ b/src/views/Generator/TestRuleContainer/TestRule/TestRuleInlineContent.tsx @@ -3,6 +3,7 @@ import { CustomCodeRule, TestRule, VerificationRule, + ParameterizationRule, } from '@/types/rules' import { TestRuleFilter } from './TestRuleFilter' import { Badge, Tooltip } from '@radix-ui/themes' @@ -26,7 +27,7 @@ export function TestRuleInlineContent({ rule }: TestRuleInlineContentProps) { case 'customCode': return case 'parameterization': - return null + return case 'verification': return default: @@ -51,7 +52,16 @@ function CorrelationContent({ rule }: { rule: CorrelationRule }) { return ( <> - + + + ) +} + +function ParameterizationContent({ rule }: { rule: ParameterizationRule }) { + return ( + <> + + ) } diff --git a/src/views/Generator/TestRuleContainer/TestRule/TestRuleSelector.tsx b/src/views/Generator/TestRuleContainer/TestRule/TestRuleSelector.tsx index 82d2f945..bd163014 100644 --- a/src/views/Generator/TestRuleContainer/TestRule/TestRuleSelector.tsx +++ b/src/views/Generator/TestRuleContainer/TestRule/TestRuleSelector.tsx @@ -1,28 +1,22 @@ -import { Badge, Code, Tooltip } from '@radix-ui/themes' +import { Badge, Strong, Tooltip } from '@radix-ui/themes' import { css } from '@emotion/react' -import { Selector } from '@/types/rules' +import { CorrelationRule, ParameterizationRule, Selector } from '@/types/rules' import { exhaustive } from '@/utils/typescript' import { useOverflowCheck } from '@/hooks/useOverflowCheck' import { useRef } from 'react' +import { Link1Icon } from '@radix-ui/react-icons' interface TestRuleSelectorProps { - selector: Selector + rule: CorrelationRule | ParameterizationRule } -export function TestRuleSelector({ selector }: TestRuleSelectorProps) { +export function TestRuleSelector({ rule }: TestRuleSelectorProps) { const ref = useRef(null) const hasEllipsis = useOverflowCheck(ref) return ( - - from {selector.from} - - } - hidden={!hasEllipsis} - > + } hidden={!hasEllipsis}> - from {selector.from} + ) } -function SelectorContent({ selector }: { selector: Selector }) { +function SelectorContent({ + rule, +}: { + rule: CorrelationRule | ParameterizationRule +}) { + switch (rule.type) { + case 'correlation': + return + case 'parameterization': + return + default: + return exhaustive(rule) + } +} + +function CorrelationSelectorContetent({ rule }: { rule: CorrelationRule }) { + return ( + <> + Correlate from{' '} + {rule.extractor.selector.from} + + ) +} + +function ParameterizationSelectorContent({ + rule, +}: { + rule: ParameterizationRule +}) { + return ( + <> + Replace in {rule.selector.from}{' '} + with + + ) +} + +function SelectorLabel({ selector }: { selector: Selector }) { switch (selector.type) { case 'json': - return {selector.path} + return ( + + {'{ }'} {stringFallback(selector.path)} + + ) case 'begin-end': return ( <> - between {selector.begin} and{' '} - {selector.end} + between {stringFallback(selector.begin)} and{' '} + {stringFallback(selector.end)} ) case 'regex': - return {selector.regex} + return ( + <> + (.*) {stringFallback(selector.regex)} + + ) default: return exhaustive(selector) } } + +function ParameterizationValue({ rule }: { rule: ParameterizationRule }) { + switch (rule.value.type) { + case 'string': + return {stringFallback(rule.value.value)} + case 'variable': + return ( + + {' '} + {rule.value.variableName} + + ) + case 'array': + case 'customCode': + return null + default: + return exhaustive(rule.value) + } +} + +function stringFallback(value: string, fallback = '_') { + return value === '' ? fallback : value +} diff --git a/src/views/Generator/TestRuleContainer/TestRule/TestRuleTypeBadge.tsx b/src/views/Generator/TestRuleContainer/TestRule/TestRuleTypeBadge.tsx index 5355e6c6..5523a397 100644 --- a/src/views/Generator/TestRuleContainer/TestRule/TestRuleTypeBadge.tsx +++ b/src/views/Generator/TestRuleContainer/TestRule/TestRuleTypeBadge.tsx @@ -18,7 +18,7 @@ export function TestRuleTypeBadge({ rule }: TestRuleTypeBadgeProps) { color="gray" css={css` text-transform: uppercase; - min-width: 100px; + white-space: nowrap; `} > {label}