From ae91b2ae9c3507630cbd390d670b6fa700c86c89 Mon Sep 17 00:00:00 2001
From: Edgar Fisher <fisher.edgar@gmail.com>
Date: Wed, 19 Mar 2025 10:07:45 +0200
Subject: [PATCH 1/3] feat: Add correlation label

---
 src/components/TextWithTooltip.tsx            |  16 +++
 src/components/WebLogView/Row.tsx             | 136 +++++++++++-------
 src/components/WebLogView/SearchResults.tsx   |   4 +-
 src/rules/correlation.ts                      |   4 -
 .../Generator/GeneratorTabs/RequestList.tsx   |   4 +-
 .../Generator/GeneratorTabs/RequestRow.tsx    | 136 ++++++++++++++----
 .../Generator/GeneratorTabs/RequestTable.tsx  |   8 +-
 7 files changed, 211 insertions(+), 97 deletions(-)
 create mode 100644 src/components/TextWithTooltip.tsx

diff --git a/src/components/TextWithTooltip.tsx b/src/components/TextWithTooltip.tsx
new file mode 100644
index 00000000..483c90d3
--- /dev/null
+++ b/src/components/TextWithTooltip.tsx
@@ -0,0 +1,16 @@
+import { useOverflowCheck } from '@/hooks/useOverflowCheck'
+import { Tooltip, Text, TextProps } from '@radix-ui/themes'
+import { useRef } from 'react'
+
+export function TextWithTooltip({ children, ...props }: TextProps) {
+  const ref = useRef<HTMLElement>(null)
+  const isEllipsisActive = useOverflowCheck(ref)
+
+  return (
+    <Tooltip content={children} hidden={!isEllipsisActive} avoidCollisions>
+      <Text {...props} truncate ref={ref}>
+        {children}
+      </Text>
+    </Tooltip>
+  )
+}
diff --git a/src/components/WebLogView/Row.tsx b/src/components/WebLogView/Row.tsx
index 5b6179f9..ec7c5d6c 100644
--- a/src/components/WebLogView/Row.tsx
+++ b/src/components/WebLogView/Row.tsx
@@ -14,30 +14,22 @@ export interface RowProps {
   isSelected?: boolean
   onSelectRequest: (data: ProxyDataWithMatches) => void
   filter?: string
-  className?: string
 }
 
-export function Row({
-  data,
-  onSelectRequest,
-  isSelected,
-  filter,
-  className,
-}: RowProps) {
+export function Row({ data, onSelectRequest, isSelected, filter }: RowProps) {
   return (
     <>
-      <Table.Row
-        onClick={() => onSelectRequest(data)}
-        className={className}
-        css={{
-          backgroundColor: isSelected ? 'var(--accent-3)' : 'transparent',
-          '&:hover': {
-            backgroundColor: isSelected ? 'var(--accent-3)' : 'var(--accent-2)',
-          },
-        }}
+      <TableRow
+        data={data}
+        onSelectRequest={onSelectRequest}
+        isSelected={isSelected}
       >
-        <Cells data={data} isSelected={isSelected} />
-      </Table.Row>
+        <MethodCell data={data} isSelected={isSelected} />
+        <StatusCell data={data} />
+        <RequestTypeCell data={data} />
+        <HostCell data={data} />
+        <PathCell data={data} />
+      </TableRow>
 
       <SearchResults
         data={data}
@@ -49,7 +41,28 @@ export function Row({
   )
 }
 
-export function Cells({
+export function TableRow({
+  data,
+  onSelectRequest,
+  isSelected,
+  children,
+}: RowProps & { children: React.ReactNode }) {
+  return (
+    <Table.Row
+      onClick={() => onSelectRequest(data)}
+      css={{
+        backgroundColor: isSelected ? 'var(--accent-3)' : 'transparent',
+        '&:hover': {
+          backgroundColor: isSelected ? 'var(--accent-3)' : 'var(--accent-2)',
+        },
+      }}
+    >
+      {children}
+    </Table.Row>
+  )
+}
+
+export function MethodCell({
   data,
   isSelected,
 }: {
@@ -57,43 +70,58 @@ export function Cells({
   isSelected?: boolean
 }) {
   return (
-    <>
-      <Table.Cell
+    <Table.Cell
+      css={{
+        cursor: 'var(--cursor-button)',
+        padding: '0',
+        display: 'flex',
+        alignItems: 'center',
+      }}
+    >
+      <Box
         css={{
-          cursor: 'var(--cursor-button)',
-          padding: '0',
-          display: 'flex',
-          alignItems: 'center',
+          width: '3px',
+          backgroundColor: isSelected ? 'var(--accent-9)' : 'transparent',
+          height: 'var(--table-cell-min-height)',
+          marginRight: 'var(--space-2)',
         }}
-      >
-        <Box
-          css={{
-            width: '3px',
-            backgroundColor: isSelected ? 'var(--accent-9)' : 'transparent',
-            height: 'var(--table-cell-min-height)',
-            marginRight: 'var(--space-2)',
-          }}
+      />
+      <MethodBadge method={data.request.method}>
+        <HighlightedText text={data.request.method} matches={data.matches} />
+      </MethodBadge>
+    </Table.Cell>
+  )
+}
+
+export function StatusCell({ data }: { data: ProxyDataWithMatches }) {
+  return (
+    <Table.Cell>
+      <ResponseStatusBadge status={data.response?.statusCode}>
+        <HighlightedText
+          text={data.response?.statusCode.toString() ?? '-'}
+          matches={data.matches}
         />
-        <MethodBadge method={data.request.method}>
-          <HighlightedText text={data.request.method} matches={data.matches} />
-        </MethodBadge>
-      </Table.Cell>
+      </ResponseStatusBadge>
+    </Table.Cell>
+  )
+}
 
-      <Table.Cell>
-        <ResponseStatusBadge status={data.response?.statusCode}>
-          <HighlightedText
-            text={data.response?.statusCode.toString() ?? '-'}
-            matches={data.matches}
-          />
-        </ResponseStatusBadge>
-      </Table.Cell>
-      <TableCellWithTooltip>{getRequestType(data)}</TableCellWithTooltip>
-      <TableCellWithTooltip>
-        <HighlightedText text={data.request.host} matches={data.matches} />
-      </TableCellWithTooltip>
-      <TableCellWithTooltip>
-        <HighlightedText text={data.request.path} matches={data.matches} />
-      </TableCellWithTooltip>
-    </>
+export function RequestTypeCell({ data }: { data: ProxyDataWithMatches }) {
+  return <TableCellWithTooltip>{getRequestType(data)}</TableCellWithTooltip>
+}
+
+export function HostCell({ data }: { data: ProxyDataWithMatches }) {
+  return (
+    <TableCellWithTooltip>
+      <HighlightedText text={data.request.host} matches={data.matches} />
+    </TableCellWithTooltip>
+  )
+}
+
+export function PathCell({ data }: { data: ProxyDataWithMatches }) {
+  return (
+    <TableCellWithTooltip>
+      <HighlightedText text={data.request.path} matches={data.matches} />
+    </TableCellWithTooltip>
   )
 }
diff --git a/src/components/WebLogView/SearchResults.tsx b/src/components/WebLogView/SearchResults.tsx
index 1c58aace..c04d234b 100644
--- a/src/components/WebLogView/SearchResults.tsx
+++ b/src/components/WebLogView/SearchResults.tsx
@@ -39,12 +39,10 @@ export function SearchResults({
   data,
   onSelectRequest,
   filter,
-  colSpan = 5,
 }: {
   data: ProxyDataWithMatches
   onSelectRequest: (data: ProxyDataWithMatches) => void
   filter?: string
-  colSpan?: number
 }) {
   const { setTab: setRequestTab } = useRequestDetailsTab()
   const { setTab: setResponseTab } = useResponseDetailsTab()
@@ -105,7 +103,7 @@ export function SearchResults({
 
   return (
     <Table.Row>
-      <Table.Cell colSpan={colSpan}>
+      <Table.Cell colSpan={5}>
         {visibleResults.map((result, excerptIndex) => (
           <Flex
             key={excerptIndex}
diff --git a/src/rules/correlation.ts b/src/rules/correlation.ts
index 06f09ac3..0c1e3a8a 100644
--- a/src/rules/correlation.ts
+++ b/src/rules/correlation.ts
@@ -143,10 +143,6 @@ function applyRule({
       ],
 
       count: state.count + 1,
-      matchedRequestIds: [
-        ...state.matchedRequestIds,
-        requestSnippetSchema.data.id,
-      ],
     })
 
     return {
diff --git a/src/views/Generator/GeneratorTabs/RequestList.tsx b/src/views/Generator/GeneratorTabs/RequestList.tsx
index 3b83a8e7..26dd48da 100644
--- a/src/views/Generator/GeneratorTabs/RequestList.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestList.tsx
@@ -98,9 +98,7 @@ export function RequestList({
             ListComponent={(props) => (
               <RequestTable
                 {...props}
-                highlightedRequestIds={
-                  selectedRuleInstance?.state?.matchedRequestIds
-                }
+                selectedRuleInstance={selectedRuleInstance}
               />
             )}
           />
diff --git a/src/views/Generator/GeneratorTabs/RequestRow.tsx b/src/views/Generator/GeneratorTabs/RequestRow.tsx
index 536e6125..d11a90b2 100644
--- a/src/views/Generator/GeneratorTabs/RequestRow.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestRow.tsx
@@ -1,6 +1,18 @@
-import { Cells, RowProps } from '@/components/WebLogView'
+import { HighlightedText } from '@/components/HighlightedText'
+import { Table } from '@/components/Table'
+import { TextWithTooltip } from '@/components/TextWithTooltip'
+import {
+  HostCell,
+  MethodCell,
+  RequestTypeCell,
+  RowProps,
+  StatusCell,
+  TableRow,
+} from '@/components/WebLogView'
 import { SearchResults } from '@/components/WebLogView/SearchResults'
-import { Badge, Flex, Strong, Table } from '@radix-ui/themes'
+import { ProxyData } from '@/types'
+import { RuleInstance } from '@/types/rules'
+import { Badge, Flex, Strong } from '@radix-ui/themes'
 import { useMemo } from 'react'
 
 export function RequestRow({
@@ -8,45 +20,111 @@ export function RequestRow({
   onSelectRequest,
   isSelected,
   filter,
-  highlightedRequestIds,
-}: RowProps & { highlightedRequestIds?: string[] }) {
-  const isMatch = useMemo(() => {
-    if (!highlightedRequestIds) {
-      return false
-    }
-
-    return highlightedRequestIds.includes(data.id)
-  }, [highlightedRequestIds, data.id])
+  selectedRuleInstance,
+}: RowProps & { selectedRuleInstance?: RuleInstance }) {
   return (
     <>
-      <Table.Row
-        onClick={() => onSelectRequest(data)}
-        css={{
-          backgroundColor: isSelected ? 'var(--accent-3)' : 'transparent',
-          '&:hover': {
-            backgroundColor: isSelected ? 'var(--accent-3)' : 'var(--accent-2)',
-          },
-        }}
+      <TableRow
+        data={data}
+        onSelectRequest={onSelectRequest}
+        isSelected={isSelected}
       >
-        <Cells data={data} isSelected={isSelected} />
+        <MethodCell data={data} isSelected={isSelected} />
+        <StatusCell data={data} />
+        <RequestTypeCell data={data} />
+        <HostCell data={data} />
+
         <Table.Cell css={{ padding: 0 }}>
-          {isMatch && (
-            <Flex justify="end" align="center" height="100%" pr="2">
-              <Badge color="green" size="1">
-                <Strong>Match</Strong>
-              </Badge>
-            </Flex>
-          )}
+          <Flex justify="between" align="center" height="100%" gap="1">
+            <TextWithTooltip size="1">
+              <HighlightedText
+                text={data.request.path}
+                matches={data.matches}
+              />
+            </TextWithTooltip>
+            <RuleBadges
+              selectedRuleInstance={selectedRuleInstance}
+              data={data}
+            />
+          </Flex>
         </Table.Cell>
-      </Table.Row>
+      </TableRow>
 
       <SearchResults
         data={data}
         key={data.id}
         onSelectRequest={onSelectRequest}
         filter={filter}
-        colSpan={6}
       />
     </>
   )
 }
+
+function RuleBadges({
+  selectedRuleInstance,
+  data,
+}: {
+  selectedRuleInstance?: RuleInstance
+  data: ProxyData
+}) {
+  if (!selectedRuleInstance) {
+    return null
+  }
+
+  return (
+    <Flex justify="end" align="center" height="100%" pr="2" gap="2">
+      <ExtractorBadge selectedRuleInstance={selectedRuleInstance} data={data} />
+      <MatchBadge selectedRuleInstance={selectedRuleInstance} data={data} />
+    </Flex>
+  )
+}
+
+function MatchBadge({
+  selectedRuleInstance,
+  data,
+}: {
+  selectedRuleInstance: RuleInstance
+  data: ProxyData
+}) {
+  const isMatch = useMemo(() => {
+    return selectedRuleInstance.state.matchedRequestIds.includes(data.id)
+  }, [selectedRuleInstance, data.id])
+
+  if (!isMatch) {
+    return null
+  }
+
+  return (
+    <Badge color="green" size="1">
+      <Strong>Match</Strong>
+    </Badge>
+  )
+}
+
+function ExtractorBadge({
+  selectedRuleInstance,
+  data,
+}: {
+  selectedRuleInstance: RuleInstance
+  data: ProxyData
+}) {
+  const isExtractor = useMemo(() => {
+    if (selectedRuleInstance.type !== 'correlation') {
+      return false
+    }
+
+    return selectedRuleInstance.state.responsesExtracted.some(
+      (request) => request.id === data.id
+    )
+  }, [selectedRuleInstance, data.id])
+
+  if (!isExtractor) {
+    return null
+  }
+
+  return (
+    <Badge color="blue" size="1">
+      <Strong>Value extracted</Strong>
+    </Badge>
+  )
+}
diff --git a/src/views/Generator/GeneratorTabs/RequestTable.tsx b/src/views/Generator/GeneratorTabs/RequestTable.tsx
index dedeb1b5..e1b6221f 100644
--- a/src/views/Generator/GeneratorTabs/RequestTable.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestTable.tsx
@@ -2,14 +2,15 @@ import { Table } from '@/components/Table'
 
 import { RequestListProps } from '@/components/WebLogView'
 import { RequestRow } from './RequestRow'
+import { RuleInstance } from '@/types/rules'
 
 export function RequestTable({
   requests,
   selectedRequestId,
   onSelectRequest,
   filter,
-  highlightedRequestIds,
-}: RequestListProps & { highlightedRequestIds?: string[] }) {
+  selectedRuleInstance,
+}: RequestListProps & { selectedRuleInstance?: RuleInstance }) {
   return (
     <Table.Root size="1" layout="fixed">
       <Table.Header css={{ textWrap: 'nowrap' }}>
@@ -19,7 +20,6 @@ export function RequestTable({
           <Table.ColumnHeaderCell width="50px">Type</Table.ColumnHeaderCell>
           <Table.ColumnHeaderCell width="20%">Host</Table.ColumnHeaderCell>
           <Table.ColumnHeaderCell width="80%">Path</Table.ColumnHeaderCell>
-          <Table.ColumnHeaderCell width="55px" />
         </Table.Row>
       </Table.Header>
       <Table.Body>
@@ -30,7 +30,7 @@ export function RequestTable({
             isSelected={selectedRequestId === data.id}
             onSelectRequest={onSelectRequest}
             filter={filter}
-            highlightedRequestIds={highlightedRequestIds}
+            selectedRuleInstance={selectedRuleInstance}
           />
         ))}
       </Table.Body>

From 4edd8aede4eef4098ac7424ce824e9e2a5c505c7 Mon Sep 17 00:00:00 2001
From: Edgar Fisher <fisher.edgar@gmail.com>
Date: Wed, 19 Mar 2025 10:16:55 +0200
Subject: [PATCH 2/3] refactor: move request list components into subfolder

---
 .../Header.tsx}                               |  2 +-
 .../{ => RequestList}/RequestList.tsx         |  4 +-
 .../{ => RequestList}/RequestList.utils.tsx   |  2 +-
 .../{ => RequestList}/RequestRow.tsx          | 74 +------------------
 .../{ => RequestList}/RequestTable.tsx        |  0
 .../GeneratorTabs/RequestList/RuleBadges.tsx  | 73 ++++++++++++++++++
 .../GeneratorTabs/RequestList/index.ts        |  1 +
 7 files changed, 80 insertions(+), 76 deletions(-)
 rename src/views/Generator/GeneratorTabs/{RequestListHeader.tsx => RequestList/Header.tsx} (98%)
 rename src/views/Generator/GeneratorTabs/{ => RequestList}/RequestList.tsx (97%)
 rename src/views/Generator/GeneratorTabs/{ => RequestList}/RequestList.utils.tsx (97%)
 rename src/views/Generator/GeneratorTabs/{ => RequestList}/RequestRow.tsx (50%)
 rename src/views/Generator/GeneratorTabs/{ => RequestList}/RequestTable.tsx (100%)
 create mode 100644 src/views/Generator/GeneratorTabs/RequestList/RuleBadges.tsx
 create mode 100644 src/views/Generator/GeneratorTabs/RequestList/index.ts

diff --git a/src/views/Generator/GeneratorTabs/RequestListHeader.tsx b/src/views/Generator/GeneratorTabs/RequestList/Header.tsx
similarity index 98%
rename from src/views/Generator/GeneratorTabs/RequestListHeader.tsx
rename to src/views/Generator/GeneratorTabs/RequestList/Header.tsx
index dedb7a72..3b5208ab 100644
--- a/src/views/Generator/GeneratorTabs/RequestListHeader.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestList/Header.tsx
@@ -4,7 +4,7 @@ import { useGeneratorStore } from '@/store/generator'
 import { getFileNameWithoutExtension } from '@/utils/file'
 import { RecorderIcon } from '@/components/icons'
 
-export function RequestListHeader({
+export function Header({
   filter,
   setFilter,
   filterAllData,
diff --git a/src/views/Generator/GeneratorTabs/RequestList.tsx b/src/views/Generator/GeneratorTabs/RequestList/RequestList.tsx
similarity index 97%
rename from src/views/Generator/GeneratorTabs/RequestList.tsx
rename to src/views/Generator/GeneratorTabs/RequestList/RequestList.tsx
index 26dd48da..50d63bf6 100644
--- a/src/views/Generator/GeneratorTabs/RequestList.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestList/RequestList.tsx
@@ -9,8 +9,8 @@ import { useStudioUIStore } from '@/store/ui'
 import { useGeneratorStore } from '@/store/generator'
 import { EmptyMessage } from '@/components/EmptyMessage'
 import { validateRecording } from './RequestList.utils'
-import { RequestListHeader } from './RequestListHeader'
 import { useApplyRules } from '@/store/hooks/useApplyRules'
+import { Header } from './Header'
 import { RequestTable } from './RequestTable'
 
 interface RequestListProps {
@@ -71,7 +71,7 @@ export function RequestList({
   return (
     <Flex direction="column" height="100%">
       {!recordingError && (
-        <RequestListHeader
+        <Header
           filter={filter}
           setFilter={setFilter}
           filterAllData={filterAllData}
diff --git a/src/views/Generator/GeneratorTabs/RequestList.utils.tsx b/src/views/Generator/GeneratorTabs/RequestList/RequestList.utils.tsx
similarity index 97%
rename from src/views/Generator/GeneratorTabs/RequestList.utils.tsx
rename to src/views/Generator/GeneratorTabs/RequestList/RequestList.utils.tsx
index 4abefca8..8d176019 100644
--- a/src/views/Generator/GeneratorTabs/RequestList.utils.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestList/RequestList.utils.tsx
@@ -3,7 +3,7 @@ import { ProxyData } from '@/types'
 import { GlobeIcon } from '@radix-ui/react-icons'
 import { Button } from '@radix-ui/themes'
 import { ComponentProps } from 'react'
-import { RecordingSelector } from '../RecordingSelector'
+import { RecordingSelector } from '../../RecordingSelector'
 
 export function validateRecording({
   allowlist,
diff --git a/src/views/Generator/GeneratorTabs/RequestRow.tsx b/src/views/Generator/GeneratorTabs/RequestList/RequestRow.tsx
similarity index 50%
rename from src/views/Generator/GeneratorTabs/RequestRow.tsx
rename to src/views/Generator/GeneratorTabs/RequestList/RequestRow.tsx
index d11a90b2..642ce0d3 100644
--- a/src/views/Generator/GeneratorTabs/RequestRow.tsx
+++ b/src/views/Generator/GeneratorTabs/RequestList/RequestRow.tsx
@@ -10,10 +10,9 @@ import {
   TableRow,
 } from '@/components/WebLogView'
 import { SearchResults } from '@/components/WebLogView/SearchResults'
-import { ProxyData } from '@/types'
 import { RuleInstance } from '@/types/rules'
-import { Badge, Flex, Strong } from '@radix-ui/themes'
-import { useMemo } from 'react'
+import { Flex } from '@radix-ui/themes'
+import { RuleBadges } from './RuleBadges'
 
 export function RequestRow({
   data,
@@ -59,72 +58,3 @@ export function RequestRow({
     </>
   )
 }
-
-function RuleBadges({
-  selectedRuleInstance,
-  data,
-}: {
-  selectedRuleInstance?: RuleInstance
-  data: ProxyData
-}) {
-  if (!selectedRuleInstance) {
-    return null
-  }
-
-  return (
-    <Flex justify="end" align="center" height="100%" pr="2" gap="2">
-      <ExtractorBadge selectedRuleInstance={selectedRuleInstance} data={data} />
-      <MatchBadge selectedRuleInstance={selectedRuleInstance} data={data} />
-    </Flex>
-  )
-}
-
-function MatchBadge({
-  selectedRuleInstance,
-  data,
-}: {
-  selectedRuleInstance: RuleInstance
-  data: ProxyData
-}) {
-  const isMatch = useMemo(() => {
-    return selectedRuleInstance.state.matchedRequestIds.includes(data.id)
-  }, [selectedRuleInstance, data.id])
-
-  if (!isMatch) {
-    return null
-  }
-
-  return (
-    <Badge color="green" size="1">
-      <Strong>Match</Strong>
-    </Badge>
-  )
-}
-
-function ExtractorBadge({
-  selectedRuleInstance,
-  data,
-}: {
-  selectedRuleInstance: RuleInstance
-  data: ProxyData
-}) {
-  const isExtractor = useMemo(() => {
-    if (selectedRuleInstance.type !== 'correlation') {
-      return false
-    }
-
-    return selectedRuleInstance.state.responsesExtracted.some(
-      (request) => request.id === data.id
-    )
-  }, [selectedRuleInstance, data.id])
-
-  if (!isExtractor) {
-    return null
-  }
-
-  return (
-    <Badge color="blue" size="1">
-      <Strong>Value extracted</Strong>
-    </Badge>
-  )
-}
diff --git a/src/views/Generator/GeneratorTabs/RequestTable.tsx b/src/views/Generator/GeneratorTabs/RequestList/RequestTable.tsx
similarity index 100%
rename from src/views/Generator/GeneratorTabs/RequestTable.tsx
rename to src/views/Generator/GeneratorTabs/RequestList/RequestTable.tsx
diff --git a/src/views/Generator/GeneratorTabs/RequestList/RuleBadges.tsx b/src/views/Generator/GeneratorTabs/RequestList/RuleBadges.tsx
new file mode 100644
index 00000000..23754243
--- /dev/null
+++ b/src/views/Generator/GeneratorTabs/RequestList/RuleBadges.tsx
@@ -0,0 +1,73 @@
+import { ProxyData } from '@/types'
+import { RuleInstance } from '@/types/rules'
+import { Badge, Flex, Strong } from '@radix-ui/themes'
+import { useMemo } from 'react'
+
+export function RuleBadges({
+  selectedRuleInstance,
+  data,
+}: {
+  selectedRuleInstance?: RuleInstance
+  data: ProxyData
+}) {
+  if (!selectedRuleInstance) {
+    return null
+  }
+
+  return (
+    <Flex justify="end" align="center" height="100%" pr="2" gap="2">
+      <ExtractorBadge selectedRuleInstance={selectedRuleInstance} data={data} />
+      <MatchBadge selectedRuleInstance={selectedRuleInstance} data={data} />
+    </Flex>
+  )
+}
+
+function MatchBadge({
+  selectedRuleInstance,
+  data,
+}: {
+  selectedRuleInstance: RuleInstance
+  data: ProxyData
+}) {
+  const isMatch = useMemo(() => {
+    return selectedRuleInstance.state.matchedRequestIds.includes(data.id)
+  }, [selectedRuleInstance, data.id])
+
+  if (!isMatch) {
+    return null
+  }
+
+  return (
+    <Badge color="green" size="1">
+      <Strong>Match</Strong>
+    </Badge>
+  )
+}
+
+function ExtractorBadge({
+  selectedRuleInstance,
+  data,
+}: {
+  selectedRuleInstance: RuleInstance
+  data: ProxyData
+}) {
+  const isExtractor = useMemo(() => {
+    if (selectedRuleInstance.type !== 'correlation') {
+      return false
+    }
+
+    return selectedRuleInstance.state.responsesExtracted.some(
+      (request) => request.id === data.id
+    )
+  }, [selectedRuleInstance, data.id])
+
+  if (!isExtractor) {
+    return null
+  }
+
+  return (
+    <Badge color="blue" size="1">
+      <Strong>Value extracted</Strong>
+    </Badge>
+  )
+}
diff --git a/src/views/Generator/GeneratorTabs/RequestList/index.ts b/src/views/Generator/GeneratorTabs/RequestList/index.ts
new file mode 100644
index 00000000..20dcb480
--- /dev/null
+++ b/src/views/Generator/GeneratorTabs/RequestList/index.ts
@@ -0,0 +1 @@
+export * from './RequestList'

From 159ded2c108025975b3632be6390231fcd46355b Mon Sep 17 00:00:00 2001
From: Edgar Fisher <fisher.edgar@gmail.com>
Date: Wed, 19 Mar 2025 10:45:54 +0200
Subject: [PATCH 3/3] fix: correlation form crashing when rule is disabled

---
 .../RuleEditor/CorrelationEditor.tsx          | 51 +++++++++++--------
 1 file changed, 30 insertions(+), 21 deletions(-)

diff --git a/src/views/Generator/RuleEditor/CorrelationEditor.tsx b/src/views/Generator/RuleEditor/CorrelationEditor.tsx
index 52ba0d85..1cd4da08 100644
--- a/src/views/Generator/RuleEditor/CorrelationEditor.tsx
+++ b/src/views/Generator/RuleEditor/CorrelationEditor.tsx
@@ -10,7 +10,7 @@ import {
   Tooltip,
 } from '@radix-ui/themes'
 
-import { TestRule } from '@/types/rules'
+import { RuleInstance, TestRule } from '@/types/rules'
 import { FilterField } from './FilterField'
 import { SelectorField } from './SelectorField'
 import { Label } from '@/components/Label'
@@ -19,7 +19,6 @@ import { FieldGroup } from '@/components/Form'
 import { ControlledRadioGroup } from '@/components/Form/ControllerRadioGroup'
 import { InfoCircledIcon } from '@radix-ui/react-icons'
 import { useApplyRules } from '@/store/hooks/useApplyRules'
-import invariant from 'tiny-invariant'
 
 const EXTRACTION_MODE_OPTIONS = [
   { value: 'single', label: 'First match' },
@@ -29,11 +28,6 @@ const EXTRACTION_MODE_OPTIONS = [
 export function CorrelationEditor() {
   const { selectedRuleInstance } = useApplyRules()
 
-  invariant(
-    selectedRuleInstance?.type === 'correlation',
-    'Selected rule instance is not a correlation rule'
-  )
-
   const {
     setValue,
     watch,
@@ -41,7 +35,6 @@ export function CorrelationEditor() {
     formState: { errors },
   } = useFormContext<TestRule>()
 
-  const { extractedValue } = selectedRuleInstance.state
   const replacer = watch('replacer')
 
   const isCustomReplacerSelector = !!replacer?.selector
@@ -88,19 +81,7 @@ export function CorrelationEditor() {
             }
           />
         </FieldGroup>
-        {extractedValue && (
-          <Text size="2">
-            <Text color="gray">Extracted value:</Text>{' '}
-            <pre>
-              <Code>{JSON.stringify(extractedValue, null, 2)}</Code>
-            </pre>
-          </Text>
-        )}
-        {!extractedValue && (
-          <Text size="2" color="gray">
-            The rule does not match any requests
-          </Text>
-        )}
+        <ExtractedValue selectedRuleInstance={selectedRuleInstance} />
       </Box>
       <Separator orientation="vertical" size="4" decorative />
       <Box>
@@ -145,5 +126,33 @@ export function CorrelationEditor() {
   )
 }
 
+function ExtractedValue({
+  selectedRuleInstance,
+}: {
+  selectedRuleInstance?: RuleInstance
+}) {
+  if (selectedRuleInstance?.type !== 'correlation') {
+    return null
+  }
+
+  const extractedValue = selectedRuleInstance?.state?.extractedValue
+
+  if (!extractedValue) {
+    return (
+      <Text size="2" color="gray">
+        The rule does not match any requests
+      </Text>
+    )
+  }
+  return (
+    <Text size="2">
+      <Text color="gray">Extracted value:</Text>{' '}
+      <pre>
+        <Code>{JSON.stringify(extractedValue, null, 2)}</Code>
+      </pre>
+    </Text>
+  )
+}
+
 const replacerTooltip =
   'By default, the correlation rule will replace all occurrences of the extracted value in the requests. Enable this option to fine tune your selection.'