Skip to content

Commit aee6cf9

Browse files
committed
Merge branch 'main' into feat/configurable-verification-rule
2 parents b1553d1 + 9260004 commit aee6cf9

File tree

22 files changed

+193
-57
lines changed

22 files changed

+193
-57
lines changed

forge.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ function getPlatformSpecificResources() {
2525

2626
const config: ForgeConfig = {
2727
packagerConfig: {
28-
name: 'Grafana k6 Studio',
2928
executableName: 'k6-studio',
3029
icon: './resources/icons/logo',
3130
asar: true,

src/codegen/codegen.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ describe('Code generation', () => {
277277
from: 'body',
278278
path: 'user_id',
279279
},
280+
extractionMode: 'single',
280281
},
281282
},
282283
{
@@ -290,6 +291,7 @@ describe('Code generation', () => {
290291
from: 'headers',
291292
regex: 'project_id=(.*)$',
292293
},
294+
extractionMode: 'single',
293295
},
294296
},
295297
]

src/components/Form/ControllerRadioGroup.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RadioGroup } from '@radix-ui/themes'
1+
import { Flex, RadioGroup } from '@radix-ui/themes'
22
import { Control, Controller, FieldValues, Path } from 'react-hook-form'
33

44
type Option = { label: string; value: string }
@@ -7,6 +7,7 @@ interface ControlledRadioGroupProps<T extends FieldValues, O extends Option> {
77
name: Path<T>
88
control: Control<T>
99
options: O[]
10+
direction?: 'column' | 'row'
1011
radioGroupProps?: RadioGroup.RootProps
1112
onChange?: (value: O['value']) => void
1213
}
@@ -15,6 +16,7 @@ export function ControlledRadioGroup<T extends FieldValues, O extends Option>({
1516
name,
1617
control,
1718
options,
19+
direction = 'column',
1820
radioGroupProps,
1921
onChange,
2022
}: ControlledRadioGroupProps<T, O>) {
@@ -27,12 +29,18 @@ export function ControlledRadioGroup<T extends FieldValues, O extends Option>({
2729
{...radioGroupProps}
2830
value={field.value}
2931
onValueChange={onChange ?? field.onChange}
32+
asChild
3033
>
31-
{options.map((option) => (
32-
<RadioGroup.Item key={option.value} value={option.value}>
33-
{option.label}
34-
</RadioGroup.Item>
35-
))}
34+
<Flex
35+
direction={direction}
36+
gap={direction === 'column' ? undefined : '2'}
37+
>
38+
{options.map((option) => (
39+
<RadioGroup.Item key={option.value} value={option.value}>
40+
{option.label}
41+
</RadioGroup.Item>
42+
))}
43+
</Flex>
3644
</RadioGroup.Root>
3745
)}
3846
/>

src/components/Layout/ActivityBar/SettingsButton.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ import { GearIcon } from '@radix-ui/react-icons'
33
import { Tooltip, IconButton } from '@radix-ui/themes'
44

55
export function SettingsButton() {
6-
const setIsOpen = useStudioUIStore((state) => state.setIsSettingsDialogOpen)
6+
const openSettingsDialog = useStudioUIStore(
7+
(state) => state.openSettingsDialog
8+
)
79

810
return (
911
<Tooltip content="Settings" side="right">
1012
<IconButton
1113
area-label="Settings"
1214
color="gray"
1315
variant="ghost"
14-
onClick={() => setIsOpen(true)}
16+
onClick={() => openSettingsDialog()}
1517
>
1618
<GearIcon />
1719
</IconButton>

src/components/Settings/SettingsDialog.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod'
33
import { Box, Button, Dialog, Flex, ScrollArea, Tabs } from '@radix-ui/themes'
44
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
55
import { findIndex, sortBy } from 'lodash-es'
6-
import { useState } from 'react'
6+
import { useEffect, useState } from 'react'
77
import { FormProvider, useForm } from 'react-hook-form'
88

99
import { ProxySettings } from './ProxySettings'
@@ -16,8 +16,13 @@ import { AppearanceSettings } from './AppearanceSettings'
1616
import { LogsSettings } from './LogsSettings'
1717
import { useStudioUIStore } from '@/store/ui'
1818
import { useSaveSettings, useSettings } from '@/hooks/useSettings'
19+
import { SettingsTabValue } from './types'
1920

20-
const tabs = [
21+
const tabs: Array<{
22+
label: string
23+
value: SettingsTabValue
24+
component: () => React.ReactNode
25+
}> = [
2126
{ label: 'Proxy', value: 'proxy', component: ProxySettings },
2227
{ label: 'Recorder', value: 'recorder', component: RecorderSettings },
2328
{
@@ -39,12 +44,19 @@ const tabs = [
3944

4045
export const SettingsDialog = () => {
4146
const { data: settings } = useSettings()
42-
const isOpen = useStudioUIStore((state) => state.isSettingsDialogOpen)
43-
const setIsOpen = useStudioUIStore((state) => state.setIsSettingsDialogOpen)
47+
const {
48+
isSettingsDialogOpen: isOpen,
49+
closeSettingsDialog,
50+
selectedSettingsTab,
51+
} = useStudioUIStore((state) => state)
4452
const { mutateAsync: saveSettings, isPending } = useSaveSettings(() => {
45-
setIsOpen(false)
53+
closeSettingsDialog()
4654
})
47-
const [selectedTab, setSelectedTab] = useState('proxy')
55+
const [selectedTab, setSelectedTab] = useState(selectedSettingsTab)
56+
57+
useEffect(() => {
58+
setSelectedTab(selectedSettingsTab)
59+
}, [isOpen, selectedSettingsTab])
4860

4961
const formMethods = useForm<AppSettings>({
5062
resolver: zodResolver(AppSettingsSchema),
@@ -61,15 +73,16 @@ export const SettingsDialog = () => {
6173
const onInvalid = (errors: Record<string, unknown>) => {
6274
// Sort tabs by the order they appear in the UI
6375
const tabsWithError = sortBy(Object.keys(errors), (key) =>
64-
findIndex(tabs, { value: key })
76+
findIndex(tabs, { value: key as SettingsTabValue })
6577
)
66-
setSelectedTab(tabsWithError[0] || 'proxy')
78+
setSelectedTab((tabsWithError[0] as SettingsTabValue) || 'proxy')
6779
}
6880

6981
const handleOpenChange = () => {
7082
reset(settings)
71-
setSelectedTab('proxy')
72-
setIsOpen(!isOpen)
83+
if (isOpen) {
84+
closeSettingsDialog()
85+
}
7386
}
7487

7588
return (
@@ -89,7 +102,7 @@ export const SettingsDialog = () => {
89102
<FormProvider {...formMethods}>
90103
<Tabs.Root
91104
value={selectedTab}
92-
onValueChange={(value) => setSelectedTab(value)}
105+
onValueChange={(value) => setSelectedTab(value as SettingsTabValue)}
93106
css={css`
94107
height: 100%;
95108
display: flex;

src/components/Settings/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type SettingsTabValue =
2+
| 'proxy'
3+
| 'recorder'
4+
| 'usageReport'
5+
| 'appearance'
6+
| 'logs'

src/handlers/auth/states.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
SignInResult,
88
Stack,
99
} from '@/types/auth'
10-
import { safeStorage, shell } from 'electron'
10+
import { shell } from 'electron'
1111
import { waitFor } from '../utils'
1212
import { fetchPersonalToken } from '@/services/k6'
1313
import { getProfileData, saveProfileData } from './fs'
@@ -243,10 +243,6 @@ export class SignInStateMachine extends EventEmitter<StateEventMap> {
243243
}
244244
}
245245

246-
const encryptedToken = safeStorage
247-
.encryptString(apiTokenResponse.token)
248-
.toString('base64')
249-
250246
const profileData = await getProfileData()
251247

252248
const stackInfo: StackInfo = {
@@ -263,7 +259,7 @@ export class SignInStateMachine extends EventEmitter<StateEventMap> {
263259
version: '1.0',
264260
tokens: {
265261
...profileData.tokens,
266-
[stack.id]: encryptedToken,
262+
[stack.id]: apiTokenResponse.token,
267263
},
268264
profiles: {
269265
...profileData.profiles,

src/rules/correlation.ts

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ function applyRule({
124124
)
125125

126126
if (extractedValue && correlationExtractionSnippet) {
127-
// Skip extraction and bump count if value is already extracted
128-
if (state.extractedValue) {
127+
// Skip extraction and bump count if value is already extracted and we are in single extraction mode
128+
if (state.extractedValue && rule.extractor.extractionMode === 'single') {
129129
setState({
130130
count: state.count + 1,
131131
})
@@ -858,7 +858,7 @@ correlation_vars['correlation_1'] = resp.json().user_id`
858858
).toStrictEqual(expectedResult)
859859
})
860860

861-
it('extracts only first correlation match', () => {
861+
it('extracts only first correlation match in single mode', () => {
862862
const recording = [
863863
createProxyData({
864864
response: createResponse({
@@ -887,6 +887,7 @@ correlation_vars['correlation_1'] = resp.json().user_id`
887887
from: 'body',
888888
path: 'user_id',
889889
},
890+
extractionMode: 'single',
890891
},
891892
}
892893

@@ -902,10 +903,61 @@ correlation_vars['correlation_1'] = resp.json().user_id`
902903
expect(requestSnippets[0]?.after[0]?.replace(/\s/g, '')).toBe(
903904
`correlation_vars['correlation_0']=resp.json().user_id`
904905
)
905-
906+
expect(ruleInstance.state.extractedValue).toBe('444')
906907
expect(requestSnippets[1]?.after).toEqual([])
907908
})
908909

910+
it('extracts multiple correlation match in multiple mode', () => {
911+
const recording = [
912+
createProxyData({
913+
response: createResponse({
914+
content: JSON.stringify({ user_id: '444' }),
915+
}),
916+
}),
917+
createProxyData({
918+
request: createRequest({
919+
url: 'http://test.k6.io/api/v1/login?user_id=444',
920+
}),
921+
response: createResponse({
922+
content: JSON.stringify({ user_id: '777' }),
923+
}),
924+
}),
925+
]
926+
const sequentialIdGenerator = generateSequentialInt()
927+
928+
const rule: CorrelationRule = {
929+
type: 'correlation',
930+
id: '1',
931+
enabled: true,
932+
extractor: {
933+
filter: { path: '' },
934+
selector: {
935+
type: 'json',
936+
from: 'body',
937+
path: 'user_id',
938+
},
939+
extractionMode: 'multiple',
940+
},
941+
}
942+
943+
const ruleInstance = createCorrelationRuleInstance(
944+
rule,
945+
sequentialIdGenerator
946+
)
947+
948+
const requestSnippets = recording.map((data) =>
949+
ruleInstance.apply({ data, before: [], after: [] })
950+
)
951+
952+
expect(requestSnippets[0]?.after[0]?.replace(/\s/g, '')).toBe(
953+
`correlation_vars['correlation_0']=resp.json().user_id`
954+
)
955+
expect(requestSnippets[1]?.after[0]?.replace(/\s/g, '')).toBe(
956+
`correlation_vars['correlation_0']=resp.json().user_id`
957+
)
958+
expect(ruleInstance.state.extractedValue).toBe('777')
959+
})
960+
909961
it('does not apply replacer if filter does not match', () => {
910962
const recording = [
911963
createProxyData({
@@ -935,6 +987,7 @@ correlation_vars['correlation_1'] = resp.json().user_id`
935987
from: 'body',
936988
path: 'user_id',
937989
},
990+
extractionMode: 'single',
938991
},
939992

940993
replacer: {
@@ -990,6 +1043,7 @@ correlation_vars['correlation_1'] = resp.json().user_id`
9901043
from: 'body',
9911044
path: 'user_id',
9921045
},
1046+
extractionMode: 'single',
9931047
},
9941048

9951049
replacer: {

src/schemas/generator/v0/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ export function migrate(generator: GeneratorSchema): v1.GeneratorSchema {
3131
},
3232
testData: { ...generator.testData, files: [] },
3333
rules: generator.rules.map((rule): v1.GeneratorSchema['rules'][number] => {
34+
if (rule.type === 'correlation') {
35+
return {
36+
...rule,
37+
extractor: { ...rule.extractor, extractionMode: 'single' },
38+
}
39+
}
40+
3441
if (rule.type === 'verification') {
3542
return {
3643
...rule,
@@ -39,6 +46,7 @@ export function migrate(generator: GeneratorSchema): v1.GeneratorSchema {
3946
value: { type: 'recordedValue' },
4047
}
4148
}
49+
4250
return rule
4351
}),
4452

src/schemas/generator/v1/loadZone.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const LoadZoneSchema = z.object({
4848
(data) => {
4949
const totalPercentage = currentLoadZonePercentage(data)
5050
return {
51-
message: `Total percentage must be 100, currently ${totalPercentage}`,
51+
message: `Total percentage must be 100 (currently ${totalPercentage})`,
5252
path: ['root'],
5353
}
5454
}

0 commit comments

Comments
 (0)