From 9a15f76e7b961ea7ccf01c72fe6d98f0e8bf99f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Uladzimir=20Dzmitra=C4=8Dko=C5=AD?=
<3773351+going-confetti@users.noreply.github.com>
Date: Thu, 1 Aug 2024 11:58:42 +0200
Subject: [PATCH] feat: Route-based nav (#74)
---
src/App.tsx | 9 +-
src/browser.ts | 1 +
src/components/FileTree/File.tsx | 33 ++---
src/components/FileTree/FileList.tsx | 20 +--
src/components/FileTree/FileTree.tsx | 6 +-
src/components/Layout/Layout.tsx | 13 +-
.../Layout/Sidebar/Sidebar.hooks.ts | 9 ++
src/components/Layout/Sidebar/Sidebar.tsx | 13 +-
src/components/Layout/View.tsx | 23 ++++
src/components/WebLogView/WebLogView.tsx | 27 ++--
src/hooks/useAutoScroll.ts | 6 +-
src/hooks/useListenProxyData.ts | 26 ++--
src/main.ts | 74 ++++++-----
src/preload.ts | 25 +++-
src/store/generator/slices/rules.ts | 3 +-
.../slices/testOptions/loadProfile.ts | 16 +--
src/store/generator/useGeneratorStore.ts | 2 +-
src/store/recorder/index.ts | 2 -
src/store/recorder/selectors.ts | 6 -
src/store/recorder/useRecorderStore.ts | 46 -------
src/store/ui/useStudioUIStore.ts | 21 ++--
src/utils/file.ts | 3 +
src/utils/generator.ts | 42 +++++++
src/utils/proxyDataToHar.ts | 1 -
src/views/Generator/Generator.tsx | 45 ++++---
src/views/Generator/Generator.utils.ts | 10 +-
.../GeneratorDrawer/GeneratorDrawer.tsx | 2 +-
src/views/Generator/RecordingSelector.tsx | 4 +-
src/views/Home/Home.tsx | 30 ++++-
src/views/Home/NavigationCard.tsx | 9 +-
src/views/Recorder/Recorder.tsx | 113 ++++++++++++-----
src/views/Recorder/RecordingButton.tsx | 88 -------------
src/views/Recorder/RequestsSection.tsx | 34 +++++
.../RecordingPreviewer/RecordingPreviewer.tsx | 116 ++++++++++++++++++
src/views/RecordingPreviewer/index.ts | 1 +
src/views/Validator/LogsPaneContent.tsx | 26 ++++
src/views/Validator/RequestsPaneContent.tsx | 26 ++++
src/views/Validator/ScriptPaneContent.tsx | 17 +++
src/views/Validator/Validator.tsx | 115 ++++++++---------
src/views/Validator/ValidatorControls.tsx | 46 +++++++
40 files changed, 708 insertions(+), 401 deletions(-)
create mode 100644 src/components/Layout/View.tsx
delete mode 100644 src/store/recorder/index.ts
delete mode 100644 src/store/recorder/selectors.ts
delete mode 100644 src/store/recorder/useRecorderStore.ts
create mode 100644 src/utils/file.ts
create mode 100644 src/utils/generator.ts
delete mode 100644 src/views/Recorder/RecordingButton.tsx
create mode 100644 src/views/Recorder/RequestsSection.tsx
create mode 100644 src/views/RecordingPreviewer/RecordingPreviewer.tsx
create mode 100644 src/views/RecordingPreviewer/index.ts
create mode 100644 src/views/Validator/LogsPaneContent.tsx
create mode 100644 src/views/Validator/RequestsPaneContent.tsx
create mode 100644 src/views/Validator/ScriptPaneContent.tsx
create mode 100644 src/views/Validator/ValidatorControls.tsx
diff --git a/src/App.tsx b/src/App.tsx
index 06bfdf25..6c47f13f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -9,6 +9,7 @@ import { Home } from '@/views/Home'
import { Generator } from './views/Generator/Generator'
import { useTheme } from './hooks/useTheme'
import { globalStyles } from './globalStyles'
+import { RecordingPreviewer } from './views/RecordingPreviewer'
export function App() {
const theme = useTheme()
@@ -21,8 +22,12 @@ export function App() {
}>
} />
} />
- } />
- } />
+ }
+ />
+ } />
+ } />
diff --git a/src/browser.ts b/src/browser.ts
index a0eb5099..6b143738 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -45,6 +45,7 @@ export const launchBrowser = async () => {
'--no-first-run',
'--disable-background-networking',
'--disable-component-update',
+ '--disable-search-engine-choice-screen',
`--proxy-server=http://localhost:${proxyPort}`,
`--ignore-certificate-errors-spki-list=${certificateSPKI}`,
disableChromeOptimizations,
diff --git a/src/components/FileTree/File.tsx b/src/components/FileTree/File.tsx
index 5f9103df..c104de9b 100644
--- a/src/components/FileTree/File.tsx
+++ b/src/components/FileTree/File.tsx
@@ -1,20 +1,22 @@
import { css } from '@emotion/react'
import { Button, Tooltip } from '@radix-ui/themes'
+import { getFileNameFromPath } from '@/utils/file'
+import { Link } from 'react-router-dom'
+
interface FileProps {
path: string
- isSelected?: boolean
- onOpen?: (path: string) => void
+ viewPath: string
+ isSelected: boolean
}
-export function File({ path, isSelected, onOpen }: FileProps) {
- const fileName = path.split('/').pop()
+export function File({ path, viewPath, isSelected }: FileProps) {
+ const fileName = getFileNameFromPath(path)
return (
)
diff --git a/src/components/FileTree/FileList.tsx b/src/components/FileTree/FileList.tsx
index 6be67ea1..d011760e 100644
--- a/src/components/FileTree/FileList.tsx
+++ b/src/components/FileTree/FileList.tsx
@@ -1,23 +1,17 @@
import { css } from '@emotion/react'
import { Button } from '@radix-ui/themes'
import { File } from './File'
-import { useStudioUIStore } from '@/store/ui'
+import { useParams } from 'react-router-dom'
interface FileListProps {
files: string[]
+ viewPath: string
noFilesMessage: string
onOpenFile?: (path: string) => void
}
-export function FileList({ files, onOpenFile, noFilesMessage }: FileListProps) {
- const selectedFile = useStudioUIStore((state) => state.selectedFile)
- const setSelectedFile = useStudioUIStore((state) => state.setSelectedFile)
-
- const handleOpenFile = (path: string) => {
- if (!onOpenFile) return
- onOpenFile(path)
- setSelectedFile(path)
- }
+export function FileList({ files, noFilesMessage, viewPath }: FileListProps) {
+ const { path } = useParams()
if (files.length === 0) {
return (
@@ -50,11 +44,7 @@ export function FileList({ files, onOpenFile, noFilesMessage }: FileListProps) {
>
{files.map((file) => (
-
+
))}
diff --git a/src/components/FileTree/FileTree.tsx b/src/components/FileTree/FileTree.tsx
index 009d67a4..9317daaf 100644
--- a/src/components/FileTree/FileTree.tsx
+++ b/src/components/FileTree/FileTree.tsx
@@ -8,15 +8,15 @@ import { FileList } from './FileList'
interface FileTreeProps {
label: string
files: string[]
+ viewPath: string
noFilesMessage?: string
- onOpenFile?: (path: string) => void
}
export function FileTree({
label,
files,
+ viewPath,
noFilesMessage = 'No files found',
- onOpenFile,
}: FileTreeProps) {
const [open, setOpen] = useState(true)
@@ -37,7 +37,7 @@ export function FileTree({
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
index a2a2740c..a5d4074b 100644
--- a/src/components/Layout/Layout.tsx
+++ b/src/components/Layout/Layout.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex } from '@radix-ui/themes'
+import { Box } from '@radix-ui/themes'
import { css } from '@emotion/react'
import { Allotment } from 'allotment'
import { Outlet } from 'react-router-dom'
@@ -22,21 +22,14 @@ export function Layout() {
`}
>
-
+
-
-
-
+
{bottomDrawer.isOpen && (
diff --git a/src/components/Layout/Sidebar/Sidebar.hooks.ts b/src/components/Layout/Sidebar/Sidebar.hooks.ts
index da758d1e..11497e18 100644
--- a/src/components/Layout/Sidebar/Sidebar.hooks.ts
+++ b/src/components/Layout/Sidebar/Sidebar.hooks.ts
@@ -6,6 +6,7 @@ export function useFolderContent() {
const generators = useStudioUIStore((s) => s.generators)
const scripts = useStudioUIStore((s) => s.scripts)
const addFile = useStudioUIStore((s) => s.addFile)
+ const removeFile = useStudioUIStore((s) => s.removeFile)
const setFolderContent = useStudioUIStore((s) => s.setFolderContent)
useEffect(() => {
@@ -23,6 +24,14 @@ export function useFolderContent() {
[addFile]
)
+ useEffect(
+ () =>
+ window.studio.ui.onRemoveFile((path) => {
+ removeFile(path)
+ }),
+ [removeFile]
+ )
+
return {
recordings,
generators,
diff --git a/src/components/Layout/Sidebar/Sidebar.tsx b/src/components/Layout/Sidebar/Sidebar.tsx
index 9943b8dd..5f85e8d9 100644
--- a/src/components/Layout/Sidebar/Sidebar.tsx
+++ b/src/components/Layout/Sidebar/Sidebar.tsx
@@ -1,21 +1,14 @@
import { css } from '@emotion/react'
import { Box, Flex, Heading, IconButton, ScrollArea } from '@radix-ui/themes'
-import { Link, useNavigate } from 'react-router-dom'
+import { Link } from 'react-router-dom'
import { ThemeSwitcher } from '@/components/ThemeSwitcher'
import { FileTree } from '@/components/FileTree'
import { useFolderContent } from './Sidebar.hooks'
-import { loadGenerator } from '@/views/Generator/Generator.utils'
import K6Logo from '@/assets/logo.svg'
export function Sidebar() {
const { recordings, generators, scripts } = useFolderContent()
- const navigate = useNavigate()
-
- const handleOpenGenerator = (path: string) => {
- loadGenerator(path)
- navigate('/generator')
- }
return (
diff --git a/src/components/Layout/View.tsx b/src/components/Layout/View.tsx
new file mode 100644
index 00000000..8c936310
--- /dev/null
+++ b/src/components/Layout/View.tsx
@@ -0,0 +1,23 @@
+import { Box, Flex } from '@radix-ui/themes'
+import { PropsWithChildren, ReactNode } from 'react'
+import { PageHeading } from './PageHeading'
+
+interface ViewProps {
+ title: string
+ actions: ReactNode
+ loading?: boolean
+}
+
+export function View({
+ title,
+ actions,
+ loading = false,
+ children,
+}: PropsWithChildren) {
+ return (
+
+ {actions}
+ {loading ? Loading... : children}
+
+ )
+}
diff --git a/src/components/WebLogView/WebLogView.tsx b/src/components/WebLogView/WebLogView.tsx
index a496f297..b5758b99 100644
--- a/src/components/WebLogView/WebLogView.tsx
+++ b/src/components/WebLogView/WebLogView.tsx
@@ -7,13 +7,14 @@ import { isGroupedProxyData } from './WebLogView.utils'
import { Row } from './Row'
import { Group } from './Group'
-export function WebLogView({
- requests,
-}: {
+interface WebLogViewProps {
requests: ProxyData[] | GroupedProxyData
-}) {
+ noRequestsMessage?: string
+}
+
+export function WebLogView({ requests, noRequestsMessage }: WebLogViewProps) {
if (isEmpty(requests)) {
- return
+ return
}
if (isGroupedProxyData(requests)) {
@@ -31,7 +32,11 @@ export function WebLogView({
return
}
-function RequestList({ requests }: { requests: ProxyData[] }) {
+interface RequestListProps {
+ requests: ProxyData[]
+}
+
+function RequestList({ requests }: RequestListProps) {
return (
<>
{requests.map((data) => (
@@ -41,13 +46,19 @@ function RequestList({ requests }: { requests: ProxyData[] }) {
)
}
-function NoRequestsMessage() {
+interface NoRequestsMessageProps {
+ noRequestsMessage?: string
+}
+
+function NoRequestsMessage({
+ noRequestsMessage = 'Your requests will appear here.',
+}: NoRequestsMessageProps) {
return (
- Your requests will appear here.
+ {noRequestsMessage}
)
}
diff --git a/src/hooks/useAutoScroll.ts b/src/hooks/useAutoScroll.ts
index f3450fa5..6fed6fbd 100644
--- a/src/hooks/useAutoScroll.ts
+++ b/src/hooks/useAutoScroll.ts
@@ -1,13 +1,13 @@
import { useEffect, useRef } from 'react'
-export function useAutoScroll(items: unknown) {
+export function useAutoScroll(items: unknown, enabled = true) {
const bottomRef = useRef(null)
useEffect(() => {
- if (!bottomRef.current) return
+ if (!bottomRef.current || !enabled) return
bottomRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' })
- }, [items])
+ }, [items, enabled])
return bottomRef
}
diff --git a/src/hooks/useListenProxyData.ts b/src/hooks/useListenProxyData.ts
index 0c07fce2..47dc7c27 100644
--- a/src/hooks/useListenProxyData.ts
+++ b/src/hooks/useListenProxyData.ts
@@ -1,16 +1,15 @@
-import { useEffect, useRef } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
-import { useRecorderStore } from '@/store/recorder/useRecorderStore'
+import { ProxyData } from '@/types'
+import { mergeRequestsById } from '@/views/Recorder/Recorder.utils'
export function useListenProxyData(group?: string) {
- const { resetProxyData, addRequest } = useRecorderStore()
+ const [proxyData, setProxyData] = useState([])
const groupRef = useRef(group)
- useEffect(() => {
- return () => {
- resetProxyData()
- }
- }, [resetProxyData])
+ const resetProxyData = useCallback(() => {
+ setProxyData([])
+ }, [])
useEffect(() => {
// Create ref to avoid creating multiple listeners
@@ -20,7 +19,14 @@ export function useListenProxyData(group?: string) {
useEffect(() => {
return window.studio.proxy.onProxyData((data) => {
- addRequest(data, groupRef.current ?? 'default')
+ setProxyData((s) =>
+ mergeRequestsById(s, {
+ ...data,
+ group: groupRef.current ?? 'default',
+ })
+ )
})
- }, [addRequest])
+ }, [])
+
+ return { proxyData, resetProxyData }
}
diff --git a/src/main.ts b/src/main.ts
index a9271f55..8a99f731 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,5 @@
import { app, BrowserWindow, dialog, ipcMain, nativeTheme } from 'electron'
-import { open, writeFile } from 'fs/promises'
+import { open, writeFile, unlink } from 'fs/promises'
import { readdirSync } from 'fs'
import path from 'path'
import eventEmmitter from 'events'
@@ -178,6 +178,17 @@ ipcMain.handle('script:select', async (event) => {
}
})
+ipcMain.handle('script:open', async (_, filePath: string) => {
+ const fileHandle = await open(filePath, 'r')
+ try {
+ const script = await fileHandle?.readFile({ encoding: 'utf-8' })
+
+ return { path: filePath, content: script }
+ } finally {
+ await fileHandle?.close()
+ }
+})
+
ipcMain.handle('script:run', async (event, scriptPath: string) => {
console.info('script:run event received')
await waitForProxy()
@@ -216,20 +227,10 @@ ipcMain.on('script:save', async (event, script: string) => {
})
// HAR
-ipcMain.on('har:save', async (event, data) => {
- console.info('har:save event received')
- const browserWindow = browserWindowFromEvent(event)
-
- const dialogResult = await dialog.showSaveDialog(browserWindow, {
- message: 'Save HAR file of the recording',
- defaultPath: path.join(RECORDINGS_PATH, `${new Date().toISOString()}.har`),
- })
-
- if (dialogResult.canceled) {
- return
- }
-
- await writeFile(dialogResult.filePath, data)
+ipcMain.handle('har:save', async (_, data) => {
+ const fineName = `${new Date().toISOString()}.har`
+ await writeFile(path.join(RECORDINGS_PATH, fineName), data)
+ return path.join(RECORDINGS_PATH, fineName)
})
ipcMain.handle('har:open', async (event, filePath?: string) => {
@@ -243,6 +244,7 @@ ipcMain.handle('har:open', async (event, filePath?: string) => {
const dialogResult = await dialog.showOpenDialog(browserWindow, {
message: 'Open HAR file',
properties: ['openFile'],
+ defaultPath: RECORDINGS_PATH,
filters: [{ name: 'HAR', extensions: ['har'] }],
})
@@ -253,6 +255,11 @@ ipcMain.handle('har:open', async (event, filePath?: string) => {
return
})
+ipcMain.handle('har:delete', async (_, filePath: string) => {
+ console.info('har:delete event received')
+ return unlink(filePath)
+})
+
const loadHarFile = async (filePath: string) => {
const fileHandle = await open(filePath, 'r')
try {
@@ -265,22 +272,32 @@ const loadHarFile = async (filePath: string) => {
}
}
-ipcMain.on('generator:save', async (event, generatorFile: string) => {
- console.info('generator:save event received')
+// Generator
+ipcMain.handle(
+ 'generator:save',
+ async (event, generatorFile: string, fileName?: string) => {
+ console.info('generator:save event received')
- const browserWindow = browserWindowFromEvent(event)
- const dialogResult = await dialog.showSaveDialog(browserWindow, {
- message: 'Save Generator',
- defaultPath: path.join(GENERATORS_PATH, 'generator.json'),
- filters: [{ name: 'JSON', extensions: ['json'] }],
- })
+ if (fileName) {
+ await writeFile(path.join(GENERATORS_PATH, fileName), generatorFile)
+ return path.join(GENERATORS_PATH, fileName)
+ }
- if (dialogResult.canceled) {
- return
- }
+ const browserWindow = browserWindowFromEvent(event)
+ const dialogResult = await dialog.showSaveDialog(browserWindow, {
+ message: 'Save Generator',
+ defaultPath: path.join(GENERATORS_PATH, 'generator.json'),
+ filters: [{ name: 'JSON', extensions: ['json'] }],
+ })
- await writeFile(dialogResult.filePath, generatorFile)
-})
+ if (dialogResult.canceled) {
+ return
+ }
+
+ await writeFile(dialogResult.filePath, generatorFile)
+ return dialogResult.filePath
+ }
+)
ipcMain.handle('generator:open', async (event, path?: string) => {
console.info('generator:open event received')
@@ -293,6 +310,7 @@ ipcMain.handle('generator:open', async (event, path?: string) => {
const dialogResult = await dialog.showOpenDialog(browserWindow, {
message: 'Open Generator file',
properties: ['openFile'],
+ defaultPath: GENERATORS_PATH,
filters: [{ name: 'JSON', extensions: ['json'] }],
})
diff --git a/src/preload.ts b/src/preload.ts
index 44963c25..4eea71b4 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -47,6 +47,14 @@ const script = {
} | void> => {
return ipcRenderer.invoke('script:select')
},
+ openScript: (
+ filePath: string
+ ): Promise<{
+ path: string
+ content: string
+ }> => {
+ return ipcRenderer.invoke('script:open', filePath)
+ },
saveScript: (script: string) => {
ipcRenderer.send('script:save', script)
},
@@ -65,12 +73,15 @@ const script = {
} as const
const har = {
- saveFile: (data: string) => {
- ipcRenderer.send('har:save', data)
+ saveFile: (data: string): Promise => {
+ return ipcRenderer.invoke('har:save', data)
},
openFile: (filePath?: string): Promise => {
return ipcRenderer.invoke('har:open', filePath)
},
+ deleteFile: (filePath: string): Promise => {
+ return ipcRenderer.invoke('har:delete', filePath)
+ },
} as const
const ui = {
@@ -81,11 +92,17 @@ const ui = {
onAddFile: (callback: (path: string) => void) => {
return createListener('ui:add-file', callback)
},
+ onRemoveFile: (callback: (path: string) => void) => {
+ return createListener('ui:remove-file', callback)
+ },
} as const
const generator = {
- saveGenerator: (generatorFile: string) => {
- ipcRenderer.send('generator:save', generatorFile)
+ saveGenerator: (
+ generatorFile: string,
+ fileName?: string
+ ): Promise => {
+ return ipcRenderer.invoke('generator:save', generatorFile, fileName)
},
loadGenerator: (path?: string): Promise => {
return ipcRenderer.invoke('generator:open', path)
diff --git a/src/store/generator/slices/rules.ts b/src/store/generator/slices/rules.ts
index 6e02ded5..416b0e17 100644
--- a/src/store/generator/slices/rules.ts
+++ b/src/store/generator/slices/rules.ts
@@ -1,6 +1,5 @@
import { TestRule } from '@/types/rules'
import { ImmerStateCreator } from '@/utils/typescript'
-import { rules } from '../fixtures'
interface State {
rules: TestRule[]
@@ -19,7 +18,7 @@ interface Actions {
export type RulesSliceStore = State & Actions
export const createRulesSlice: ImmerStateCreator = (set) => ({
- rules,
+ rules: [],
selectedRuleId: null,
createRule: (type) =>
set((state) => {
diff --git a/src/store/generator/slices/testOptions/loadProfile.ts b/src/store/generator/slices/testOptions/loadProfile.ts
index c3d6977d..bc18adab 100644
--- a/src/store/generator/slices/testOptions/loadProfile.ts
+++ b/src/store/generator/slices/testOptions/loadProfile.ts
@@ -4,6 +4,7 @@ import {
RampingVUsOptions,
SharedIterationsOptions,
} from '@/types/testOptions'
+import { createStage, getInitialStages } from '@/utils/generator'
import { ImmerStateCreator } from '@/utils/typescript'
interface SharedIterationsState
@@ -124,18 +125,3 @@ export const createLoadProfileSlice: ImmerStateCreator = (
...createRampingSlice(...args),
...createSharedIterationsSlice(...args),
})
-
-// TODO: Add validation
-function createStage(
- target: number | string = '',
- duration = ''
-): RampingStage {
- return {
- target,
- duration,
- }
-}
-
-function getInitialStages() {
- return [createStage(20, '1m'), createStage(20, '3m30s'), createStage(0, '1m')]
-}
diff --git a/src/store/generator/useGeneratorStore.ts b/src/store/generator/useGeneratorStore.ts
index cb0f0d72..d5f1d30a 100644
--- a/src/store/generator/useGeneratorStore.ts
+++ b/src/store/generator/useGeneratorStore.ts
@@ -59,7 +59,7 @@ export const useGeneratorStore = create()(
// recording
state.requests = recording
state.recordingPath = recordingPath
- state.showAllowlistDialog = false
+ state.showAllowlistDialog = !!recordingPath && allowlist.length === 0
state.allowlist = allowlist
// rules
state.rules = rules
diff --git a/src/store/recorder/index.ts b/src/store/recorder/index.ts
deleted file mode 100644
index 446bad0e..00000000
--- a/src/store/recorder/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './useRecorderStore'
-export * from './selectors'
diff --git a/src/store/recorder/selectors.ts b/src/store/recorder/selectors.ts
deleted file mode 100644
index bf5a4c11..00000000
--- a/src/store/recorder/selectors.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { groupProxyData } from '@/utils/groups'
-import { RecorderStore } from './useRecorderStore'
-
-export function selectGroupedProxyData(state: RecorderStore) {
- return groupProxyData(state.proxyData)
-}
diff --git a/src/store/recorder/useRecorderStore.ts b/src/store/recorder/useRecorderStore.ts
deleted file mode 100644
index 2d7a5572..00000000
--- a/src/store/recorder/useRecorderStore.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { create } from 'zustand'
-import { immer } from 'zustand/middleware/immer'
-
-import { ProxyData } from '@/types'
-import { mergeRequestsById } from '@/views/Recorder/Recorder.utils'
-
-interface State {
- isRecording: boolean
- proxyData: ProxyData[]
-}
-
-interface Actions {
- addRequest: (request: ProxyData, currentGroup: string) => void
- setIsRecording: (isRecording: boolean) => void
- setProxyData: (proxyData: ProxyData[]) => void
- resetProxyData: () => void
-}
-
-export type RecorderStore = State & Actions
-
-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/store/ui/useStudioUIStore.ts b/src/store/ui/useStudioUIStore.ts
index e858b7c9..7903cd8b 100644
--- a/src/store/ui/useStudioUIStore.ts
+++ b/src/store/ui/useStudioUIStore.ts
@@ -2,13 +2,11 @@ import { FolderContent } from '@/types'
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
-interface State extends FolderContent {
- selectedFile: string | null
-}
+interface State extends FolderContent {}
interface Actions {
addFile: (path: string) => void
- setSelectedFile: (path: string | null) => void
+ removeFile: (path: string) => void
setFolderContent: (content: FolderContent) => void
}
@@ -19,7 +17,6 @@ export const useStudioUIStore = create()(
recordings: [],
generators: [],
scripts: [],
- selectedFile: null,
addFile: (path) =>
set((state) => {
@@ -35,9 +32,19 @@ export const useStudioUIStore = create()(
state.scripts.push(path)
}
}),
- setSelectedFile: (path) =>
+ removeFile: (path) =>
set((state) => {
- state.selectedFile = path
+ if (path.endsWith('.har')) {
+ state.recordings = state.recordings.filter((file) => file !== path)
+ }
+
+ if (path.endsWith('.json')) {
+ state.generators = state.generators.filter((file) => file !== path)
+ }
+
+ if (path.endsWith('.js')) {
+ state.scripts = state.scripts.filter((file) => file !== path)
+ }
}),
setFolderContent: ({ recordings, generators, scripts }) =>
set((state) => {
diff --git a/src/utils/file.ts b/src/utils/file.ts
new file mode 100644
index 00000000..8d6f6cb1
--- /dev/null
+++ b/src/utils/file.ts
@@ -0,0 +1,3 @@
+export function getFileNameFromPath(path: string) {
+ return path.split('/').pop() ?? ''
+}
diff --git a/src/utils/generator.ts b/src/utils/generator.ts
new file mode 100644
index 00000000..532bb66d
--- /dev/null
+++ b/src/utils/generator.ts
@@ -0,0 +1,42 @@
+import { GeneratorFileData } from '@/types/generator'
+import { RampingStage } from '@/types/testOptions'
+
+export function createNewGeneratorFile(recordingPath = ''): GeneratorFileData {
+ return {
+ name: 'New test generator',
+ version: '0',
+ recordingPath,
+ options: {
+ loadProfile: {
+ executor: 'ramping-vus',
+ stages: getInitialStages(),
+ },
+ thinkTime: {
+ sleepType: 'groups',
+ timing: {
+ type: 'fixed',
+ value: 1,
+ },
+ },
+ },
+ testData: {
+ variables: [],
+ },
+ rules: [],
+ allowlist: [],
+ }
+}
+
+export function createStage(
+ target: number | string = '',
+ duration = ''
+): RampingStage {
+ return {
+ target,
+ duration,
+ }
+}
+
+export function getInitialStages() {
+ return [createStage(20, '1m'), createStage(20, '3m30s'), createStage(0, '1m')]
+}
diff --git a/src/utils/proxyDataToHar.ts b/src/utils/proxyDataToHar.ts
index 01355ac1..ee55af75 100644
--- a/src/utils/proxyDataToHar.ts
+++ b/src/utils/proxyDataToHar.ts
@@ -16,7 +16,6 @@ function createLog(
pages: Page[],
entries: EntryWithOptionalResponse[]
): HarWithOptionalResponse['log'] {
- console.log(entries[0]?.response)
return {
version: '1.2',
creator: {
diff --git a/src/views/Generator/Generator.tsx b/src/views/Generator/Generator.tsx
index ae09ecc1..405d290b 100644
--- a/src/views/Generator/Generator.tsx
+++ b/src/views/Generator/Generator.tsx
@@ -1,8 +1,6 @@
import { Allotment } from 'allotment'
import { Button } from '@radix-ui/themes'
-import { useEffect } from 'react'
-import { PageHeading } from '@/components/Layout/PageHeading'
import { useSetWindowTitle } from '@/hooks/useSetWindowTitle'
import {
useGeneratorStore,
@@ -16,29 +14,46 @@ import { GeneratorSidebar } from './GeneratorSidebar'
import { TestRuleContainer } from './TestRuleContainer'
import { Allowlist } from './Allowlist'
import { RecordingSelector } from './RecordingSelector'
+import { View } from '@/components/Layout/View'
+import { useEffect, useState } from 'react'
+import { useParams } from 'react-router-dom'
export function Generator() {
const name = useGeneratorStore((store) => store.name)
- const resetRecording = useGeneratorStore((store) => store.resetRecording)
const filteredRequests = useGeneratorStore(selectFilteredRequests)
const hasRecording = useGeneratorStore(selectHasRecording)
+ const [isLoading, setIsLoading] = useState(false)
+ const { path } = useParams()
useSetWindowTitle(name)
useEffect(() => {
- return () => {
- resetRecording()
+ if (!path) {
+ return
}
- }, [resetRecording])
+
+ ;(async () => {
+ setIsLoading(true)
+ await loadGenerator(path)
+ setIsLoading(false)
+ })()
+ }, [path])
return (
- <>
-
-
-
-
-
- {hasRecording && }
-
+
+
+
+
+
+ {hasRecording && (
+
+ )}
+ >
+ }
+ loading={isLoading}
+ >
@@ -54,6 +69,6 @@ export function Generator() {
- >
+
)
}
diff --git a/src/views/Generator/Generator.utils.ts b/src/views/Generator/Generator.utils.ts
index 0cc37ee0..451fe9bb 100644
--- a/src/views/Generator/Generator.utils.ts
+++ b/src/views/Generator/Generator.utils.ts
@@ -42,7 +42,9 @@ export async function exportScript() {
export const saveGenerator = () => {
const generatorFile = selectGeneratorData(useGeneratorStore.getState())
- window.studio.generator.saveGenerator(JSON.stringify(generatorFile, null, 2))
+ return window.studio.generator.saveGenerator(
+ JSON.stringify(generatorFile, null, 2)
+ )
}
export const loadGenerator = async (path?: string) => {
@@ -60,9 +62,9 @@ export const loadGenerator = async (path?: string) => {
return
}
- const harFile = await window.studio.har.openFile(
- generatorFileData.data.recordingPath
- )
+ const harFile = generatorFileData.data.recordingPath
+ ? await window.studio.har.openFile(generatorFileData.data.recordingPath)
+ : undefined
// TODO: we need to better handle errors scenarios
const recording = harFile ? harToProxyData(harFile.content) : []
diff --git a/src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx b/src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx
index 78225dfc..5810ed8f 100644
--- a/src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx
+++ b/src/views/Generator/GeneratorDrawer/GeneratorDrawer.tsx
@@ -41,7 +41,7 @@ export function GeneratorDrawer() {
}
function TabNavLink({ path, label }: { path: string; label: string }) {
- const match = useMatch(`generator/${path}`)
+ const match = useMatch(`generator/:pathp/${path}`)
return (
diff --git a/src/views/Generator/RecordingSelector.tsx b/src/views/Generator/RecordingSelector.tsx
index 5dee5eb0..c4c80be3 100644
--- a/src/views/Generator/RecordingSelector.tsx
+++ b/src/views/Generator/RecordingSelector.tsx
@@ -1,4 +1,5 @@
import { useGeneratorStore } from '@/store/generator'
+import { getFileNameFromPath } from '@/utils/file'
import { harToProxyData } from '@/utils/harToProxyData'
import { CaretDownIcon } from '@radix-ui/react-icons'
import { Button, Flex, IconButton, Popover, Text } from '@radix-ui/themes'
@@ -7,8 +8,7 @@ import { Link } from 'react-router-dom'
export function RecordingSelector() {
const recordingPath = useGeneratorStore((store) => store.recordingPath)
const setRecording = useGeneratorStore((store) => store.setRecording)
-
- const fileName = recordingPath?.split('/').pop()
+ const fileName = getFileNameFromPath(recordingPath)
const handleImport = async () => {
const harFile = await window.studio.har.openFile()
diff --git a/src/views/Home/Home.tsx b/src/views/Home/Home.tsx
index 5e012078..809b3b56 100644
--- a/src/views/Home/Home.tsx
+++ b/src/views/Home/Home.tsx
@@ -1,7 +1,31 @@
import { Flex, Grid, Text } from '@radix-ui/themes'
import { NavigationCard } from './NavigationCard'
+import { createNewGeneratorFile } from '@/utils/generator'
+import { useNavigate } from 'react-router-dom'
export function Home() {
+ const navigate = useNavigate()
+
+ // TODO: offer to create a new generator or use an existing one
+ async function handleCreateTestGenerator() {
+ const newGenerator = createNewGeneratorFile()
+ const generatorPath = await window.studio.generator.saveGenerator(
+ JSON.stringify(newGenerator, null, 2),
+ `${new Date().toISOString()}.json`
+ )
+
+ navigate(`/generator/${encodeURIComponent(generatorPath)}`)
+ }
+
+ function handleNavigateToRecorder() {
+ navigate('/recorder')
+ }
+
+ // TODO: offer to select a script to validate
+ function handleNavigateToValidator() {
+ navigate('/validator')
+ }
+
return (
@@ -11,17 +35,17 @@ export function Home() {
diff --git a/src/views/Home/NavigationCard.tsx b/src/views/Home/NavigationCard.tsx
index bd7bb4f3..d3093d93 100644
--- a/src/views/Home/NavigationCard.tsx
+++ b/src/views/Home/NavigationCard.tsx
@@ -1,27 +1,26 @@
import { Card, Text } from '@radix-ui/themes'
-import { Link, To } from 'react-router-dom'
interface NavigationCardProps {
title: string
description: string
- to: To
+ onClick: () => void
}
export function NavigationCard({
title,
description,
- to,
+ onClick,
}: NavigationCardProps) {
return (
-
+
)
}
diff --git a/src/views/Recorder/Recorder.tsx b/src/views/Recorder/Recorder.tsx
index d71d1250..995c5f16 100644
--- a/src/views/Recorder/Recorder.tsx
+++ b/src/views/Recorder/Recorder.tsx
@@ -1,44 +1,95 @@
-import { useState } from 'react'
-import { Flex, Heading, ScrollArea } from '@radix-ui/themes'
-
-import { PageHeading } from '@/components/Layout/PageHeading'
-import { WebLogView } from '@/components/WebLogView'
-import { useListenProxyData } from '@/hooks/useListenProxyData'
-import { useRecorderStore, selectGroupedProxyData } from '@/store/recorder'
-import { useSetWindowTitle } from '@/hooks/useSetWindowTitle'
-import { useAutoScroll } from '@/hooks/useAutoScroll'
+import { useCallback, useEffect, useState } from 'react'
+import { useNavigate, useSearchParams } from 'react-router-dom'
+import { Button, Flex } from '@radix-ui/themes'
+import { PlayIcon, StopIcon } from '@radix-ui/react-icons'
import { GroupForm } from './GroupForm'
import { DebugControls } from './DebugControls'
-import { RecordingControls } from './RecordingButton'
+import { View } from '@/components/Layout/View'
+import { RequestsSection } from './RequestsSection'
+import { useSetWindowTitle } from '@/hooks/useSetWindowTitle'
+import { useListenProxyData } from '@/hooks/useListenProxyData'
+import { groupProxyData } from '@/utils/groups'
+import { startRecording, stopRecording } from './Recorder.utils'
+import { proxyDataToHar } from '@/utils/proxyDataToHar'
export function Recorder() {
const [group, setGroup] = useState('Default')
- const groupedProxyData = useRecorderStore(selectGroupedProxyData)
- const contentRef = useAutoScroll(groupedProxyData)
- useListenProxyData(group)
+ const { proxyData, resetProxyData } = useListenProxyData(group)
+ const groupedProxyData = groupProxyData(proxyData)
+ const [isLoading, setIsLoading] = useState(false)
+ const [isRecording, setIsRecording] = useState(false)
+
+ const navigate = useNavigate()
+ const [searchParams] = useSearchParams()
+ const autoStart = searchParams.get('autoStart') !== null
useSetWindowTitle('Recorder')
+ const handleStartRecording = useCallback(async () => {
+ resetProxyData()
+ setIsLoading(true)
+
+ await startRecording()
+
+ setIsLoading(false)
+ setIsRecording(true)
+ }, [resetProxyData])
+
+ async function handleStopRecording() {
+ stopRecording()
+ setIsRecording(false)
+
+ if (proxyData.length === 0) {
+ return
+ }
+
+ const har = proxyDataToHar(groupedProxyData)
+ const filePath = await window.studio.har.saveFile(
+ JSON.stringify(har, null, 4)
+ )
+
+ navigate(`/recording-previewer/${encodeURIComponent(filePath)}?discardable`)
+ }
+
+ useEffect(() => {
+ if (autoStart) {
+ handleStartRecording()
+ }
+ }, [autoStart, handleStartRecording])
+
return (
- <>
-
-
-
-
-
-
-
-
-
-
+
+ {isRecording ? (
+ <>
+ Stop recording
+ >
+ ) : (
+ <>
+ Start recording
+ >
+ )}
+
+ }
+ >
+
+
+
+
+
- Requests
-
-
-
-
-
- >
+
+
)
}
diff --git a/src/views/Recorder/RecordingButton.tsx b/src/views/Recorder/RecordingButton.tsx
deleted file mode 100644
index dfd571dc..00000000
--- a/src/views/Recorder/RecordingButton.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-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 '@/store/recorder'
-import { proxyDataToHar } from '@/utils/proxyDataToHar'
-import { useGeneratorStore } from '@/store/generator'
-import { groupProxyData } from '@/utils/groups'
-import { recordingToProxyData } from '@/utils/serializers/recording'
-
-export function RecordingControls() {
- const requests = useRecorderStore((state) => state.proxyData)
- const isRecording = useRecorderStore((state) => state.isRecording)
- const setIsRecording = useRecorderStore((state) => state.setIsRecording)
- const resetProxyData = useRecorderStore((state) => state.resetProxyData)
- const isEmpty = useRecorderStore((state) => state.proxyData.length === 0)
- const setGeneratorRecording = useGeneratorStore((state) => state.setRecording)
- const navigate = useNavigate()
- const [isLoading, setIsLoading] = useState(false)
-
- function handleStartRecording() {
- resetProxyData()
- setIsLoading(true)
- startRecording().then(() => {
- setIsLoading(false)
- setIsRecording(true)
- })
- }
-
- function handleStopRecording() {
- stopRecording()
- setIsRecording(false)
- }
-
- function handleSave() {
- const grouped = groupProxyData(requests)
- const har = proxyDataToHar(grouped)
- window.studio.har.saveFile(JSON.stringify(har, null, 4))
- }
-
- function handleCreateTestGenerator() {
- const proxyData = recordingToProxyData(requests)
-
- setGeneratorRecording(proxyData, '', true)
- navigate('/generator')
- }
-
- return (
- <>
-
- {!isRecording && !isEmpty && (
- <>
-
-
- >
- )}
- >
- )
-}
-
-function Icon({
- recording,
- loading,
-}: {
- recording: boolean
- loading: boolean
-}) {
- if (loading) {
- return
- }
-
- if (recording) {
- return
- }
-
- return
-}
diff --git a/src/views/Recorder/RequestsSection.tsx b/src/views/Recorder/RequestsSection.tsx
new file mode 100644
index 00000000..28ed93c0
--- /dev/null
+++ b/src/views/Recorder/RequestsSection.tsx
@@ -0,0 +1,34 @@
+import { WebLogView } from '@/components/WebLogView'
+import { useAutoScroll } from '@/hooks/useAutoScroll'
+import { GroupedProxyData } from '@/types'
+import { Flex, Heading, ScrollArea } from '@radix-ui/themes'
+
+interface RequestsSectionProps {
+ groupedProxyData: GroupedProxyData
+ autoScroll?: boolean
+ noRequestsMessage?: string
+}
+
+export function RequestsSection({
+ groupedProxyData,
+ noRequestsMessage,
+ autoScroll = false,
+}: RequestsSectionProps) {
+ const ref = useAutoScroll(groupedProxyData, autoScroll)
+
+ return (
+
+
+ Requests
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/views/RecordingPreviewer/RecordingPreviewer.tsx b/src/views/RecordingPreviewer/RecordingPreviewer.tsx
new file mode 100644
index 00000000..f35f045a
--- /dev/null
+++ b/src/views/RecordingPreviewer/RecordingPreviewer.tsx
@@ -0,0 +1,116 @@
+import { Button, DropdownMenu, IconButton } from '@radix-ui/themes'
+import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'
+import { useEffect, useState } from 'react'
+
+import { getFileNameFromPath } from '@/utils/file'
+import { View } from '@/components/Layout/View'
+import { RequestsSection } from '@/views/Recorder/RequestsSection'
+import { createNewGeneratorFile } from '@/utils/generator'
+import { GroupedProxyData } from '@/types'
+import { harToProxyData } from '@/utils/harToProxyData'
+import { groupProxyData } from '@/utils/groups'
+import { DotsVerticalIcon } from '@radix-ui/react-icons'
+
+export function RecordingPreviewer() {
+ const [groupedProxyData, setGroupedProxyData] = useState({})
+ const [isLoading, setIsLoading] = useState(false)
+ const { path } = useParams()
+ const navigate = useNavigate()
+ const [searchParams] = useSearchParams()
+ const isDiscardable = searchParams.get('discardable') !== null
+
+ useEffect(() => {
+ if (!path) {
+ navigate('/')
+ return
+ }
+
+ ;(async () => {
+ setIsLoading(true)
+ setGroupedProxyData({})
+ const har = await window.studio.har.openFile(path)
+ setIsLoading(false)
+
+ if (!har) {
+ return
+ }
+
+ setGroupedProxyData(groupProxyData(harToProxyData(har.content)))
+ })()
+
+ return () => {
+ setGroupedProxyData({})
+ }
+ }, [path, navigate])
+
+ async function handleDeleteRecording() {
+ if (!path) {
+ return
+ }
+
+ await window.studio.har.deleteFile(path)
+ navigate('/')
+ }
+
+ async function handleCreateTestGenerator() {
+ if (!path) {
+ return
+ }
+
+ const newGenerator = createNewGeneratorFile(path)
+ const generatorPath = await window.studio.generator.saveGenerator(
+ JSON.stringify(newGenerator, null, 2),
+ `${new Date().toISOString()}.json`
+ )
+
+ navigate(`/generator/${encodeURIComponent(generatorPath)}`)
+ }
+
+ async function handleDiscard() {
+ if (!path) {
+ return
+ }
+
+ await window.studio.har.deleteFile(path)
+ navigate('/recorder?autoStart')
+ }
+
+ return (
+
+ {isDiscardable && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ New recording
+
+
+ Delete
+
+
+
+ >
+ }
+ >
+
+
+ )
+}
diff --git a/src/views/RecordingPreviewer/index.ts b/src/views/RecordingPreviewer/index.ts
new file mode 100644
index 00000000..78517398
--- /dev/null
+++ b/src/views/RecordingPreviewer/index.ts
@@ -0,0 +1 @@
+export * from './RecordingPreviewer'
diff --git a/src/views/Validator/LogsPaneContent.tsx b/src/views/Validator/LogsPaneContent.tsx
new file mode 100644
index 00000000..848a21d3
--- /dev/null
+++ b/src/views/Validator/LogsPaneContent.tsx
@@ -0,0 +1,26 @@
+import { Flex, Heading, ScrollArea } from '@radix-ui/themes'
+
+import { LogView } from '@/components/LogView'
+import { K6Log } from '@/types'
+import { useAutoScroll } from '@/hooks/useAutoScroll'
+
+interface LogsPaneContentProps {
+ logs: K6Log[]
+}
+
+export function LogsPaneContent({ logs }: LogsPaneContentProps) {
+ const ref = useAutoScroll(logs)
+
+ return (
+
+
+ Logs
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/views/Validator/RequestsPaneContent.tsx b/src/views/Validator/RequestsPaneContent.tsx
new file mode 100644
index 00000000..1f92e1ba
--- /dev/null
+++ b/src/views/Validator/RequestsPaneContent.tsx
@@ -0,0 +1,26 @@
+import { Flex, Heading, ScrollArea } from '@radix-ui/themes'
+
+import { WebLogView } from '@/components/WebLogView'
+import { GroupedProxyData } from '@/types'
+import { useAutoScroll } from '@/hooks/useAutoScroll'
+
+interface RequestPaneContentProps {
+ requests: GroupedProxyData
+}
+
+export function RequestPaneContent({ requests }: RequestPaneContentProps) {
+ const ref = useAutoScroll(requests)
+
+ return (
+
+
+ Requests
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/views/Validator/ScriptPaneContent.tsx b/src/views/Validator/ScriptPaneContent.tsx
new file mode 100644
index 00000000..0d175139
--- /dev/null
+++ b/src/views/Validator/ScriptPaneContent.tsx
@@ -0,0 +1,17 @@
+import { ReadOnlyEditor } from '@/components/Monaco/ReadOnlyEditor'
+import { Flex, Heading } from '@radix-ui/themes'
+
+interface ScriptPaneContentProps {
+ script: string
+}
+
+export function ScriptPaneContent({ script }: ScriptPaneContentProps) {
+ return (
+
+
+ Script
+
+
+
+ )
+}
diff --git a/src/views/Validator/Validator.tsx b/src/views/Validator/Validator.tsx
index d4577a06..b24f6d6f 100644
--- a/src/views/Validator/Validator.tsx
+++ b/src/views/Validator/Validator.tsx
@@ -1,39 +1,53 @@
-import { PageHeading } from '@/components/Layout/PageHeading'
-import { LogView } from '@/components/LogView'
-import { WebLogView } from '@/components/WebLogView'
+import { useCallback, useEffect, useState } from 'react'
+import { useParams } from 'react-router-dom'
+import { Allotment } from 'allotment'
+
import { useListenProxyData } from '@/hooks/useListenProxyData'
-import { useRecorderStore } from '@/store/recorder'
import { useSetWindowTitle } from '@/hooks/useSetWindowTitle'
import { K6Log } from '@/types'
import { groupProxyData } from '@/utils/groups'
-import { Button, Flex, Heading, ScrollArea, Spinner } from '@radix-ui/themes'
-import { useEffect, useState } from 'react'
-import { Allotment } from 'allotment'
-import { ReadOnlyEditor } from '@/components/Monaco/ReadOnlyEditor'
-import { useAutoScroll } from '@/hooks/useAutoScroll'
+import { getFileNameFromPath } from '@/utils/file'
+import { LogsPaneContent } from './LogsPaneContent'
+import { ScriptPaneContent } from './ScriptPaneContent'
+import { RequestPaneContent } from './RequestsPaneContent'
+import { ValidatorControls } from './ValidatorControls'
+import { View } from '@/components/Layout/View'
export function Validator() {
+ const [isLoading, setIsLoading] = useState(false)
const [scriptPath, setScriptPath] = useState()
const [script, setScript] = useState('')
const [isRunning, setIsRunning] = useState(false)
const [logs, setLogs] = useState([])
- const proxyData = useRecorderStore((store) => store.proxyData)
- const resetProxyData = useRecorderStore((store) => store.resetProxyData)
+ const { path: scripPath } = useParams()
+ const fileName = getFileNameFromPath(scripPath ?? '')
- const requestsRef = useAutoScroll(proxyData)
- const logsRef = useAutoScroll(logs)
-
- useListenProxyData()
- useSetWindowTitle('Validator')
+ const { proxyData, resetProxyData } = useListenProxyData()
+ useSetWindowTitle(fileName || 'Validator')
const groupedProxyData = groupProxyData(proxyData)
- async function handleSelectScript() {
+ const handleSelectScript = useCallback(async () => {
const { path = '', content = '' } =
(await window.studio.script.showScriptSelectDialog()) || {}
setScriptPath(path)
setScript(content)
- }
+ }, [])
+
+ useEffect(() => {
+ if (!scripPath) {
+ return
+ }
+
+ ;(async () => {
+ setIsLoading(true)
+ const { path = '', content = '' } =
+ (await window.studio.script.openScript(scripPath)) || {}
+ setIsLoading(false)
+ setScriptPath(path)
+ setScript(content)
+ })()
+ }, [scripPath])
function handleRunScript() {
if (!scriptPath || !script) {
@@ -64,65 +78,34 @@ export function Validator() {
}, [])
return (
- <>
-
-
-
-
-
+
+ }
+ loading={isLoading}
+ >
-
-
- Requests
-
-
-
-
-
-
-
+
-
-
- Script
-
-
-
+
-
-
- Logs
-
-
-
-
-
-
-
+
- >
+
)
}
diff --git a/src/views/Validator/ValidatorControls.tsx b/src/views/Validator/ValidatorControls.tsx
new file mode 100644
index 00000000..7dddcee6
--- /dev/null
+++ b/src/views/Validator/ValidatorControls.tsx
@@ -0,0 +1,46 @@
+import { DotsVerticalIcon } from '@radix-ui/react-icons'
+import { Button, DropdownMenu, IconButton } from '@radix-ui/themes'
+
+interface ValidatorControlsProps {
+ isRunning: boolean
+ isScriptSelected: boolean
+ onRunScript: () => void
+ onSelectScript: () => void
+ onStopScript: () => void
+}
+
+export function ValidatorControls({
+ isRunning,
+ isScriptSelected,
+ onRunScript,
+ onSelectScript,
+ onStopScript,
+}: ValidatorControlsProps) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Select script
+
+
+ Delete
+
+
+
+ >
+ )
+}