diff --git a/package-lock.json b/package-lock.json index 569d3e07..c15f8026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/themes": "^3.0.5", "allotment": "^1.20.2", "electron-squirrel-startup": "^1.0.1", + "immer": "^10.1.1", "lodash-es": "^4.17.21", "node-forge": "^1.3.1", "prettier": "^3.3.2", @@ -6399,6 +6400,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "dev": true, diff --git a/package.json b/package.json index 40841fcf..b8bf67e1 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@radix-ui/themes": "^3.0.5", "allotment": "^1.20.2", "electron-squirrel-startup": "^1.0.1", + "immer": "^10.1.1", "lodash-es": "^4.17.21", "node-forge": "^1.3.1", "prettier": "^3.3.2", diff --git a/src/hooks/useGeneratorStore.ts b/src/hooks/useGeneratorStore.ts new file mode 100644 index 00000000..f0d627d8 --- /dev/null +++ b/src/hooks/useGeneratorStore.ts @@ -0,0 +1,37 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' + +import { GroupedProxyData } from '@/types' +import { TestRule } from '@/types/rules' + +interface GeneratorState { + recording: GroupedProxyData + rules: TestRule[] + requestFilters: string[] + setRecording: (recording: GroupedProxyData) => void + addRequestFilter: (filter: string) => void +} + +export const useGeneratorStore = create()( + immer((set) => ({ + recording: {}, + rules: [ + { + type: 'customCode', + filter: { path: '' }, + snippet: 'console.log("Hello, world!")', + placement: 'before', + }, + ], + requestFilters: [], + + addRequestFilter: (filter: string) => + set((state) => { + state.requestFilters.push(filter) + }), + setRecording: (recording: GroupedProxyData) => + set((state) => { + state.recording = recording + }), + })) +) diff --git a/src/hooks/useListenProxyData.ts b/src/hooks/useListenProxyData.ts index e593661b..5a86705c 100644 --- a/src/hooks/useListenProxyData.ts +++ b/src/hooks/useListenProxyData.ts @@ -1,9 +1,9 @@ -import { ProxyData } from '@/types' -import { mergeRequestsById } from '@/views/Recorder/Recorder.utils' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef } from 'react' + +import { useRecorderStore } from './useRecorderStore' export function useListenProxyData(group?: string) { - const [proxyData, setProxyData] = useState([]) + const { addRequest } = useRecorderStore() const groupRef = useRef(group) useEffect(() => { @@ -14,14 +14,7 @@ export function useListenProxyData(group?: string) { useEffect(() => { return window.studio.proxy.onProxyData((data) => { - setProxyData((prev) => { - return mergeRequestsById(prev, { ...data, group: groupRef.current }) - }) + addRequest(data, groupRef.current ?? 'default') }) - }, []) - - return { - proxyData, - resetProxyData: () => setProxyData([]), - } + }, [addRequest]) } diff --git a/src/hooks/useRecorderStore.ts b/src/hooks/useRecorderStore.ts new file mode 100644 index 00000000..2d27bd99 --- /dev/null +++ b/src/hooks/useRecorderStore.ts @@ -0,0 +1,41 @@ +import { create } from 'zustand' +import { immer } from 'zustand/middleware/immer' + +import { ProxyData } from '@/types' +import { mergeRequestsById } from '@/views/Recorder/Recorder.utils' + +interface RecorderState { + isRecording: boolean + proxyData: ProxyData[] + addRequest: (request: ProxyData, currentGroup: string) => void + setIsRecording: (isRecording: boolean) => void + setProxyData: (proxyData: ProxyData[]) => void + resetProxyData: () => void +} + +export const useRecorderStore = create()( + immer((set) => ({ + isRecording: false, + proxyData: [], + + addRequest: (request: ProxyData, currentGroup: string) => + set((state) => { + state.proxyData = mergeRequestsById(state.proxyData, { + ...request, + group: currentGroup, + }) + }), + setIsRecording: (isRecording: boolean) => + set((state) => { + state.isRecording = isRecording + }), + setProxyData: (proxyData: ProxyData[]) => + set((state) => { + state.proxyData = proxyData + }), + resetProxyData: () => + set((state) => { + state.proxyData = [] + }), + })) +) diff --git a/src/views/Generator/Generator.tsx b/src/views/Generator/Generator.tsx index 3ffc30a1..43c135a9 100644 --- a/src/views/Generator/Generator.tsx +++ b/src/views/Generator/Generator.tsx @@ -1,40 +1,27 @@ -import { useState } from 'react' +import { Allotment } from 'allotment' import { Box, Button } from '@radix-ui/themes' -import { GroupedProxyData } from '@/types' import { exportScript, saveScript } from './Generator.utils' import { PageHeading } from '@/components/Layout/PageHeading' import { harToGroupedProxyData } from '@/utils/harToProxyData' import { GeneratorDrawer } from './GeneratorDrawer' -import { Allotment } from 'allotment' import { GeneratorSidebar } from './GeneratorSidebar' +import { useGeneratorStore } from '@/hooks/useGeneratorStore' export function Generator() { - const [requests, setRequests] = useState({}) - const [filter, setFilter] = useState('') - const hasRecording = Object.entries(requests).length > 0 + const { recording, requestFilters, rules, setRecording } = useGeneratorStore() + const hasRecording = Object.entries(recording).length > 0 const handleImport = async () => { const har = await window.studio.har.openFile() if (!har) return const groupedProxyData = harToGroupedProxyData(har) - setRequests(groupedProxyData) + setRecording(groupedProxyData) } const handleExport = async () => { - const script = await exportScript( - requests, - [ - { - type: 'customCode', - filter: { path: '' }, - snippet: 'console.log("Hello, world!")', - placement: 'before', - }, - ], - [filter] - ) + const script = await exportScript(recording, rules, requestFilters) saveScript(script) } @@ -50,16 +37,16 @@ export function Generator() { - + Rules: - - + + - + diff --git a/src/views/Generator/GeneratorDrawer.tsx b/src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx similarity index 56% rename from src/views/Generator/GeneratorDrawer.tsx rename to src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx index 9a6c4c19..a46a74e3 100644 --- a/src/views/Generator/GeneratorDrawer.tsx +++ b/src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx @@ -1,15 +1,7 @@ -import * as Label from '@radix-ui/react-label' -import { Box, Flex, Tabs, TextField } from '@radix-ui/themes' +import { Box, Tabs } from '@radix-ui/themes' +import { RequestFilters } from './RequestFilters' -interface GeneratorDrawerProps { - filter: string - onFilterChange: (filters: string) => void -} - -export function GeneratorDrawer({ - filter, - onFilterChange, -}: GeneratorDrawerProps) { +export function GeneratorDrawer() { return ( @@ -27,18 +19,7 @@ export function GeneratorDrawer({ Think time content Test data content - - - Allow requests containing - - onFilterChange(e.target.value)} - placeholder="Type part of the request URL to filter requests" - /> - + diff --git a/src/views/Generator/GeneratorDrawer/RequestFilters.tsx b/src/views/Generator/GeneratorDrawer/RequestFilters.tsx new file mode 100644 index 00000000..54240272 --- /dev/null +++ b/src/views/Generator/GeneratorDrawer/RequestFilters.tsx @@ -0,0 +1,39 @@ +import * as Label from '@radix-ui/react-label' +import { Box, Button, Flex, TextField } from '@radix-ui/themes' +import { useState } from 'react' + +import { useGeneratorStore } from '@/hooks/useGeneratorStore' + +export function RequestFilters() { + const [newFilter, setNewFilter] = useState('') + const { requestFilters, addRequestFilter } = useGeneratorStore() + + return ( + + + + Allow requests containing + + setNewFilter(e.target.value)} + placeholder="Type part of the request URL to filter requests" + /> + + +
    + {requestFilters.map((filter) => ( +
  • {filter}
  • + ))} +
+
+ ) +} diff --git a/src/views/Generator/GeneratorDrawer/index.ts b/src/views/Generator/GeneratorDrawer/index.ts new file mode 100644 index 00000000..cfe3d3d5 --- /dev/null +++ b/src/views/Generator/GeneratorDrawer/index.ts @@ -0,0 +1 @@ +export * from './GeneratorDrawer' diff --git a/src/views/Recorder/Recorder.tsx b/src/views/Recorder/Recorder.tsx index 6baf7209..6f61962c 100644 --- a/src/views/Recorder/Recorder.tsx +++ b/src/views/Recorder/Recorder.tsx @@ -1,33 +1,25 @@ import { useState } from 'react' import { Flex, Heading, ScrollArea } from '@radix-ui/themes' +import { groupBy } from 'lodash-es' + import { WebLogView } from '@/components/WebLogView' import { GroupForm } from './GroupForm' -import { proxyDataToHar } from '@/utils/proxyDataToHar' import { DebugControls } from './DebugControls' -import { RecordingButton } from './RecordingButton' -import { SaveHarDialog } from './SaveHarDialog' +import { RecordingControls } from './RecordingButton' import { useListenProxyData } from '@/hooks/useListenProxyData' import { PageHeading } from '@/components/Layout/PageHeading' -import { groupBy } from 'lodash-es' +import { useRecorderStore } from '@/hooks/useRecorderStore' export function Recorder() { + const { proxyData } = useRecorderStore() const [group, setGroup] = useState('Default') - const [showHarSaveDialog, setShowHarSaveDialog] = useState(false) - const { proxyData, resetProxyData } = useListenProxyData(group) + useListenProxyData(group) const groupedProxyData = groupBy(proxyData, 'group') - function saveHarToFile() { - const har = proxyDataToHar(groupedProxyData) - window.studio.har.saveFile(JSON.stringify(har, null, 4)) - } - return ( <> - setShowHarSaveDialog(true)} - onStart={resetProxyData} - /> + @@ -40,11 +32,6 @@ export function Recorder() { - ) } diff --git a/src/views/Recorder/RecordingButton.tsx b/src/views/Recorder/RecordingButton.tsx index 0c1c4b08..e15c183b 100644 --- a/src/views/Recorder/RecordingButton.tsx +++ b/src/views/Recorder/RecordingButton.tsx @@ -1,42 +1,68 @@ import { useState } from 'react' +import { useNavigate } from 'react-router-dom' import { Button, Spinner } from '@radix-ui/themes' import { PlayIcon, StopIcon } from '@radix-ui/react-icons' + import { startRecording, stopRecording } from './Recorder.utils' +import { useRecorderStore } from '@/hooks/useRecorderStore' +import { proxyDataToHar } from '@/utils/proxyDataToHar' +import { GroupedProxyData } from '@/types' +import { useGeneratorStore } from '@/hooks/useGeneratorStore' -export function RecordingButton({ - onStop, - onStart, +export function RecordingControls({ + requests, }: { - onStop?: () => void - onStart?: () => void + requests: GroupedProxyData }) { - const [recording, setRecording] = useState(false) + const { isRecording, setIsRecording, resetProxyData } = useRecorderStore() + const isEmpty = Object.keys(requests).length === 0 + const { setRecording } = useGeneratorStore() + const navigate = useNavigate() const [isLoading, setIsLoading] = useState(false) function handleStartRecording() { - onStart?.() + resetProxyData() setIsLoading(true) startRecording().then(() => { setIsLoading(false) - setRecording(true) + setIsRecording(true) }) } function handleStopRecording() { stopRecording() - setRecording(false) - onStop?.() + setIsRecording(false) + } + + function handleSave() { + const har = proxyDataToHar(requests) + window.studio.har.saveFile(JSON.stringify(har, null, 4)) + } + + function handleCreateTestGenerator() { + setRecording(requests) + navigate('/generator') } return ( - + <> + + {!isEmpty && ( + <> + + + + )} + ) } diff --git a/src/views/Recorder/SaveHarDialog.tsx b/src/views/Recorder/SaveHarDialog.tsx deleted file mode 100644 index 48d03680..00000000 --- a/src/views/Recorder/SaveHarDialog.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { AlertDialog, Button, Flex } from '@radix-ui/themes' - -export function SaveHarDialog({ - open, - onOpenChange, - onConfirm, -}: { - open: boolean - onOpenChange: (val: boolean) => void - onConfirm: () => void -}) { - // AlertDialog.Action & AlertDialog.Cancel are swapped to - // save the file when hitting the "enter" key - return ( - - - Save HAR file - - Would you like to save the current recording as a HAR file? - - - - - - - - - - - - - ) -} diff --git a/src/views/Validator/Validator.tsx b/src/views/Validator/Validator.tsx index abb7aea2..869548f5 100644 --- a/src/views/Validator/Validator.tsx +++ b/src/views/Validator/Validator.tsx @@ -2,6 +2,7 @@ import { PageHeading } from '@/components/Layout/PageHeading' import { LogView } from '@/components/LogView' import { WebLogView } from '@/components/WebLogView' import { useListenProxyData } from '@/hooks/useListenProxyData' +import { useRecorderStore } from '@/hooks/useRecorderStore' import { K6Log } from '@/types' import { Button, Flex, Heading, ScrollArea, Spinner } from '@radix-ui/themes' import { groupBy } from 'lodash-es' @@ -11,7 +12,8 @@ export function Validator() { const [scriptPath, setScriptPath] = useState() const [isRunning, setIsRunning] = useState(false) const [logs, setLogs] = useState([]) - const { proxyData, resetProxyData } = useListenProxyData() + useListenProxyData() + const { proxyData, resetProxyData } = useRecorderStore() const groupedProxyData = groupBy( proxyData,