Skip to content

Commit

Permalink
feat: URL prompt for starting recording from provided URL
Browse files Browse the repository at this point in the history
  • Loading branch information
going-confetti committed Oct 18, 2024
1 parent a622a55 commit 2a7bf4e
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 69 deletions.
6 changes: 5 additions & 1 deletion src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ function getBrowserPath() {
return recorder.browserPath as string
}

export const launchBrowser = async (browserWindow: BrowserWindow) => {
export const launchBrowser = async (
browserWindow: BrowserWindow,
url?: string
) => {
const path = getBrowserPath()
console.info(`browser path: ${path}`)

Expand Down Expand Up @@ -67,6 +70,7 @@ export const launchBrowser = async (browserWindow: BrowserWindow) => {
`--proxy-server=http://localhost:${appSettings.proxy.port}`,
`--ignore-certificate-errors-spki-list=${certificateSPKI}`,
disableChromeOptimizations,
url ?? '',
],
onExit: sendBrowserClosedEvent,
})
Expand Down
1 change: 1 addition & 0 deletions src/components/Layout/PageHeading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function PageHeading({
css={css`
background-color: var(--gray-2);
border-bottom: 1px solid var(--gray-4);
min-height: 49px;
`}
>
<Flex maxWidth="50%" flexGrow="1" gap="1" align="center">
Expand Down
5 changes: 1 addition & 4 deletions src/components/Layout/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@ export function Sidebar({ isExpanded, onCollapseSidebar }: SidebarProps) {
variant="ghost"
size="1"
>
<Link
to={getRoutePath('recorder')}
state={{ autoStart: true }}
>
<Link to={getRoutePath('recorder')}>
<PlusIcon />
</Link>
</IconButton>
Expand Down
4 changes: 2 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,13 +224,13 @@ const waitForProxy = async (): Promise<void> => {
}

// Browser
ipcMain.handle('browser:start', async (event) => {
ipcMain.handle('browser:start', async (event, url?: string) => {
console.info('browser:start event received')

await waitForProxy()

const browserWindow = browserWindowFromEvent(event)
currentBrowserProcess = await launchBrowser(browserWindow)
currentBrowserProcess = await launchBrowser(browserWindow, url)
console.info('browser started')
})

Expand Down
4 changes: 2 additions & 2 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const proxy = {
} as const

const browser = {
launchBrowser: (): Promise<void> => {
return ipcRenderer.invoke('browser:start')
launchBrowser: (url?: string): Promise<void> => {
return ipcRenderer.invoke('browser:start', url)
},
stopBrowser: () => {
ipcRenderer.send('browser:stop')
Expand Down
96 changes: 96 additions & 0 deletions src/views/Recorder/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useEffect, useRef } from 'react'
import { css } from '@emotion/react'
import { zodResolver } from '@hookform/resolvers/zod'
import { DiscIcon } from '@radix-ui/react-icons'
import { Button, Flex, Heading, Text, TextField } from '@radix-ui/themes'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

import { FieldGroup } from '@/components/Form'

interface EmptyStateProps {
isLoading: boolean
onStart: (url?: string) => void
}

const RecorderEmptyStateSchema = z.object({
url: z.string(),
})

type RecorderEmptyStateFields = z.infer<typeof RecorderEmptyStateSchema>

export function EmptyState({ isLoading, onStart }: EmptyStateProps) {
const inputRef = useRef<HTMLInputElement | null>(null)

const {
register,
handleSubmit,
formState: { errors },
} = useForm<RecorderEmptyStateFields>({
resolver: zodResolver(RecorderEmptyStateSchema),
defaultValues: {
url: '',
},
shouldFocusError: false,
})

const { ref, ...inputProps } = register('url')

const onSubmit = ({ url }: RecorderEmptyStateFields) => {
onStart(url)
}

useEffect(() => {
window.requestAnimationFrame(() => {
inputRef.current?.focus()
})
}, [])

return (
<Flex direction="column" align="center" gap="2">
<Heading
size="8"
css={css`
font-weight: 400;
`}
>
Record your user flow
</Heading>
<Text color="gray" size="1">
Once you begin recording, requests will appear in this area
</Text>
<form
onSubmit={handleSubmit(onSubmit)}
css={css`
margin-top: var(--space-6);
`}
>
<FieldGroup
name="url"
label="Target URL"
hint="Provide the URL of the service you want to test"
hintType="text"
errors={errors}
width="460px"
>
<TextField.Root
ref={(e) => {
ref(e)
inputRef.current = e
}}
{...inputProps}
placeholder="e.g. test.k6.io"
css={css`
flex-grow: 1;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
`}
/>
</FieldGroup>
<Button disabled={isLoading} type="submit">
<DiscIcon /> Start recording
</Button>
</form>
</Flex>
)
}
96 changes: 40 additions & 56 deletions src/views/Recorder/Recorder.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useBlocker, useLocation, useNavigate } from 'react-router-dom'
import { Box, Button, Flex, Text } from '@radix-ui/themes'
import { DiscIcon, PlusCircledIcon, StopIcon } from '@radix-ui/react-icons'
import { useBlocker, useNavigate } from 'react-router-dom'
import { Box, Button, Flex } from '@radix-ui/themes'
import { PlusCircledIcon, StopIcon } from '@radix-ui/react-icons'
import { Allotment } from 'allotment'
import log from 'electron-log/renderer'

import { View } from '@/components/Layout/View'
import { RequestsSection } from './RequestsSection'
Expand All @@ -22,7 +23,7 @@ import { useToast } from '@/store/ui/useToast'
import TextSpinner from '@/components/TextSpinner/TextSpinner'
import { DEFAULT_GROUP_NAME } from '@/constants'
import { ButtonWithTooltip } from '@/components/ButtonWithTooltip'
import log from 'electron-log/renderer'
import { EmptyState } from './EmptyState'

const INITIAL_GROUPS: Group[] = [
{
Expand All @@ -47,32 +48,32 @@ export function Recorder() {
const debouncedProxyData = useDebouncedProxyData(proxyData)

const navigate = useNavigate()
const { state } = useLocation()
const blocker = useBlocker(
recorderState === 'starting' || recorderState === 'recording'
)

const autoStart = Boolean(state?.autoStart)

const isLoading = recorderState === 'starting' || recorderState === 'saving'

const handleStartRecording = useCallback(async () => {
try {
resetProxyData()
setRecorderState('starting')

await startRecording()

setRecorderState('recording')
} catch (error) {
setRecorderState('idle')
showToast({
title: 'Failed to start recording',
status: 'error',
})
log.error(error)
}
}, [resetProxyData, showToast])
const handleStartRecording = useCallback(
async (url?: string) => {
try {
resetProxyData()
setRecorderState('starting')
console.log('url', url)
await startRecording(url)

setRecorderState('recording')
} catch (error) {
setRecorderState('idle')
showToast({
title: 'Failed to start recording',
status: 'error',
})
log.error(error)
}
},
[resetProxyData, showToast]
)

const validateAndSaveHarFile = useCallback(async () => {
try {
Expand Down Expand Up @@ -139,12 +140,6 @@ export function Recorder() {
setGroups(INITIAL_GROUPS)
}

useEffect(() => {
if (autoStart) {
handleStartRecording()
}
}, [autoStart, handleStartRecording])

useEffect(() => {
return window.studio.browser.onBrowserClosed(async () => {
const fileName = await validateAndSaveHarFile()
Expand Down Expand Up @@ -173,25 +168,18 @@ export function Recorder() {
<View
title="Recorder"
actions={
<>
{recorderState === 'idle' && (
<Button disabled={isLoading} onClick={handleStartRecording}>
<DiscIcon /> Start recording
recorderState !== 'idle' && (
<>
{isLoading && <TextSpinner text="Starting" />}
<Button
disabled={isLoading}
color="red"
onClick={handleStopRecording}
>
<StopIcon /> Stop recording
</Button>
)}
{recorderState !== 'idle' && (
<>
{isLoading && <TextSpinner text="Starting" />}
<Button
disabled={isLoading}
color="red"
onClick={handleStopRecording}
>
<StopIcon /> Stop recording
</Button>
</>
)}
</>
</>
)
}
>
<Allotment defaultSizes={[1, 1]}>
Expand All @@ -201,14 +189,10 @@ export function Recorder() {
<RequestsSection
proxyData={debouncedProxyData}
noRequestsMessage={
<>
<Text color="gray" size="1">
Once you start the recording, requests will appear here
</Text>
<Button disabled={isLoading} onClick={handleStartRecording}>
<DiscIcon /> Start recording
</Button>
</>
<EmptyState
isLoading={isLoading}
onStart={handleStartRecording}
/>
}
showNoRequestsMessage={recorderState === 'idle'}
selectedRequestId={selectedRequest?.id}
Expand Down
4 changes: 2 additions & 2 deletions src/views/Recorder/Recorder.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ function getRequestSignature(request: Request) {
}

// TODO: add error and timeout handling
export async function startRecording() {
export async function startRecording(url?: string) {
// Kill previous browser window
window.studio.browser.stopBrowser()

return window.studio.browser.launchBrowser()
return window.studio.browser.launchBrowser(url)
}

export function stopRecording() {
Expand Down
4 changes: 2 additions & 2 deletions src/views/RecordingPreviewer/RecordingPreviewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function RecordingPreviewer() {

const handleDiscard = async () => {
await window.studio.ui.deleteFile(fileName)
navigate(getRoutePath('recorder'), { state: { autoStart: true } })
navigate(getRoutePath('recorder'))
}

return (
Expand All @@ -80,7 +80,7 @@ export function RecordingPreviewer() {
<>
{isDiscardable && (
<Button onClick={handleDiscard} variant="outline" color="red">
Discard and start over
Discard
</Button>
)}

Expand Down

0 comments on commit 2a7bf4e

Please sign in to comment.