-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Recording selector draft #36
Changes from 1 commit
6f794bd
33d22fe
d346e06
9f102f0
56cdc3a
7304ae4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ export const useGeneratorStore = create<GeneratorState>()( | |
...createRulesSlice(set, get, store), | ||
...createVariablesSlice(set, get, store), | ||
...createThinkTimeSlice(set, get, store), | ||
name: `generator_${Date()}`, | ||
name: generateNewName(), | ||
setName: (name: string) => | ||
set((state) => { | ||
state.name = name | ||
|
@@ -30,6 +30,7 @@ export const useGeneratorStore = create<GeneratorState>()( | |
resetRecording: () => | ||
set((state) => { | ||
state.requests = [] | ||
state.recordingPath = '' | ||
state.filteredRequests = [] | ||
state.allowList = [] | ||
}), | ||
|
@@ -58,3 +59,8 @@ export const useGeneratorStore = create<GeneratorState>()( | |
}), | ||
})) | ||
) | ||
|
||
function generateNewName() { | ||
const date = new Date().toISOString().split('T')[0] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't split it, so that if you are working on multiple ones you can save them with a different name on the same day 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops, you're right! A fix is incoming in a bit 👀 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed it, but used the US format 🤔. We should probably use the same format we use in GCk6 for test names There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think format is a 🔥 topic, using US format would add unneeded confusion 🤔 Maybe the ideal is to use the common syntax of You could argue that the format you are using is more readable, so I'm not completely sure. I would still slightly prefer the format that is readable out of the box for both US & Europe, but maybe we can gather more feedback. Remembering that this is just the generator file name, I don't think we have a strong need for keeping the format used in test names 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That's fair
I'd rather use a format we use in other parts of k6 offerings, for consistency reasons
As of now, it's not actually a part of the filename, but I did think about adding it to the filename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Llandy3d I don't have a strong opinion actually, let's discuss it some time next week and pick one format 👍 |
||
return `Generator ${date}` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { useEffect } from 'react' | ||
|
||
const defaultTitle = 'k6 studio' | ||
|
||
export function useWindowTitle(title: string) { | ||
going-confetti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
useEffect(() => { | ||
document.title = `${defaultTitle} - ${title}` | ||
|
||
return () => { | ||
document.title = defaultTitle | ||
} | ||
}, [title]) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,12 +29,11 @@ export function AllowList() { | |
|
||
return ( | ||
<> | ||
<Button | ||
onClick={() => setShowAllowListDialog(true)} | ||
disabled={requests.length === 0} | ||
> | ||
Allowed hosts [{allowList.length}/{hosts.length}] | ||
</Button> | ||
{requests.length > 0 && ( | ||
<Button onClick={() => setShowAllowListDialog(true)}> | ||
Allowed hosts [{allowList.length}/{hosts.length}] | ||
</Button> | ||
)} | ||
Comment on lines
+32
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed until we have the recording |
||
{/* Radix does not unmount dialog on close, this is need to clear local state */} | ||
{showAllowListDialog && ( | ||
<AllowListDialog | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,39 +4,28 @@ import { useEffect } from 'react' | |
|
||
import { exportScript, saveScript, saveGenerator } from './Generator.utils' | ||
import { PageHeading } from '@/components/Layout/PageHeading' | ||
import { harToProxyData } from '@/utils/harToProxyData' | ||
import { GeneratorDrawer } from './GeneratorDrawer' | ||
import { GeneratorSidebar } from './GeneratorSidebar' | ||
import { useGeneratorStore } from '@/hooks/useGeneratorStore' | ||
import { useGeneratorStore, useHasRecording } from '@/hooks/useGeneratorStore' | ||
import { TestRuleContainer } from './TestRuleContainer' | ||
import { AllowList } from './AllowList/AllowList' | ||
import { RecordingSelector } from './RecordingSelector' | ||
import { useWindowTitle } from '@/hooks/useWindowTitle' | ||
|
||
export function Generator() { | ||
const { | ||
rules, | ||
setRecording, | ||
resetRecording, | ||
filteredRequests, | ||
setRecordingPath, | ||
} = useGeneratorStore() | ||
|
||
const hasRecording = filteredRequests.length > 0 | ||
const rules = useGeneratorStore((store) => store.rules) | ||
const name = useGeneratorStore((store) => store.name) | ||
const resetRecording = useGeneratorStore((store) => store.resetRecording) | ||
const filteredRequests = useGeneratorStore((store) => store.filteredRequests) | ||
const hasRecording = useHasRecording() | ||
useWindowTitle(name) | ||
|
||
useEffect(() => { | ||
return () => { | ||
resetRecording() | ||
} | ||
}, [resetRecording]) | ||
|
||
const handleImport = async () => { | ||
const harFile = await window.studio.har.openFile() | ||
if (!harFile) return | ||
|
||
const proxyData = harToProxyData(harFile.content) | ||
setRecording(proxyData) | ||
setRecordingPath(harFile.path) | ||
} | ||
|
||
const handleExport = async () => { | ||
const script = await exportScript(filteredRequests, rules) | ||
|
||
|
@@ -46,12 +35,10 @@ export function Generator() { | |
return ( | ||
<> | ||
<PageHeading text="Generator"> | ||
<Button onClick={saveGenerator}>Save</Button> | ||
<Button onClick={handleImport}>Import HAR</Button> | ||
<RecordingSelector /> | ||
<AllowList /> | ||
<Button onClick={handleExport} disabled={!hasRecording}> | ||
Export script | ||
</Button> | ||
{hasRecording && <Button onClick={saveGenerator}>Save</Button>} | ||
{hasRecording && <Button onClick={handleExport}>Export script</Button>} | ||
Comment on lines
-49
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took the liberty of rearranging the buttons to have the most important ones on the right + hiding save, export, and allow list until there's an actual recording. |
||
</PageHeading> | ||
<Allotment defaultSizes={[3, 1]}> | ||
<Allotment.Pane minSize={400}> | ||
|
@@ -64,7 +51,7 @@ export function Generator() { | |
</Allotment.Pane> | ||
</Allotment> | ||
</Allotment.Pane> | ||
<Allotment.Pane minSize={300}> | ||
<Allotment.Pane minSize={300} visible={hasRecording}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar reasoning as with the header buttons: no need to show the sidebar if it's empty |
||
<GeneratorSidebar requests={filteredRequests} /> | ||
</Allotment.Pane> | ||
</Allotment> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { useGeneratorStore, useHasRecording } from '@/hooks/useGeneratorStore' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some unused imports in this file, maybe we could add this plugin to clear imports automatically (we already run eslint --fix on commit) |
||
import { harToProxyData } from '@/utils/harToProxyData' | ||
import { css } from '@emotion/react' | ||
import { ArrowDownIcon, CaretDownIcon } from '@radix-ui/react-icons' | ||
import { Button, Flex, IconButton, Popover, Text } from '@radix-ui/themes' | ||
import { startRecording } from '../Recorder/Recorder.utils' | ||
import { Link } from 'react-router-dom' | ||
|
||
export function RecordingSelector() { | ||
const recordingPath = useGeneratorStore((store) => store.recordingPath) | ||
const setRecording = useGeneratorStore((store) => store.setRecording) | ||
const setRecordingPath = useGeneratorStore((store) => store.setRecordingPath) | ||
const hasRecording = useHasRecording() | ||
|
||
const fileName = recordingPath?.split('/').pop() | ||
|
||
const handleImport = async () => { | ||
const harFile = await window.studio.har.openFile() | ||
if (!harFile) return | ||
|
||
const proxyData = harToProxyData(harFile.content) | ||
setRecording(proxyData) | ||
setRecordingPath(harFile.path) | ||
} | ||
|
||
return ( | ||
<Flex align="center" gap="2"> | ||
<Text>{fileName || 'Select recording'}</Text> | ||
<Popover.Root> | ||
<Popover.Trigger> | ||
<IconButton variant="outline"> | ||
<CaretDownIcon /> | ||
</IconButton> | ||
</Popover.Trigger> | ||
<Popover.Content size="1" align="end"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will have a scrollable/searchable list of saved recordings once we implement filesystem integration and all that |
||
<Flex direction="column" gap="2"> | ||
<Text>You don{"'"}t have saved recordings</Text> | ||
<Flex gap="2"> | ||
<Popover.Close> | ||
<Button variant="outline" asChild> | ||
<Link to="/recorder">Start recording</Link> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, just redirects to the recorder page, but should also start a new recording in future |
||
</Button> | ||
</Popover.Close> | ||
<Popover.Close> | ||
<Button onClick={handleImport}>Import HAR</Button> | ||
</Popover.Close> | ||
</Flex> | ||
</Flex> | ||
</Popover.Content> | ||
</Popover.Root> | ||
</Flex> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@e-fisher what's your favorite way of defining reusable selectors? Maybe the should be callbacks that we then import and pass to
useGeneratorStore
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pmndrs/zustand#387
would this approach work well ? Seems like a clean way to have a file defining selectors, importing them and using with the

useGeneratorStore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! That's what I meant by
callbacks that we then import and pass to useGeneratorStore
, I should've been more specificThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably vote for the callback approach just because it's a bit more explicit -
useHasRecording - hook named this way could be doing anything
useGeneratorStore(selectHasRecording) - indicates that value is read from store