Skip to content

Commit 7301d18

Browse files
internal(Load Zones): Ability to add load zones (#518)
1 parent 51a0cb6 commit 7301d18

File tree

9 files changed

+304
-17
lines changed

9 files changed

+304
-17
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"allotment": "^1.20.2",
8686
"chokidar": "^4.0.3",
8787
"constrained-editor-plugin": "^1.3.0",
88+
"country-flag-icons": "^1.5.18",
8889
"electron-log": "^5.2.0",
8990
"electron-squirrel-startup": "^1.0.1",
9091
"find-process": "^1.4.7",

src/schemas/generator/v1/loadZone.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const AvailableLoadZonesSchema = z.enum([
2727
export const LoadZoneItemSchema = z.object({
2828
id: z.string(),
2929
loadZone: AvailableLoadZonesSchema,
30-
percent: z.number().int().min(1).max(100),
30+
percent: z.number().int().min(1, { message: 'Invalid percentage' }).max(100),
3131
})
3232

3333
export const LoadZoneSchema = z.object({

src/store/generator/useGeneratorStore.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const useGeneratorStore = create<GeneratorStore>()(
3737
...createScriptDataSlice(set, ...rest),
3838
setGeneratorFile: (
3939
{
40-
options: { thinkTime, loadProfile, thresholds },
40+
options: { thinkTime, loadProfile, thresholds, cloud },
4141
testData: { variables, files },
4242
recordingPath,
4343
rules,
@@ -52,6 +52,7 @@ export const useGeneratorStore = create<GeneratorStore>()(
5252
// options
5353
state.sleepType = thinkTime.sleepType
5454
state.timing = thinkTime.timing
55+
state.loadZones = cloud.loadZones
5556
state.thresholds = thresholds
5657
state.executor = loadProfile.executor
5758
switch (loadProfile.executor) {

src/types/testOptions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ThresholdStatisticSchema,
1818
} from '@/schemas/generator'
1919
import {
20+
AvailableLoadZonesSchema,
2021
LoadZoneItemSchema,
2122
LoadZoneSchema,
2223
} from '@/schemas/generator/v1/loadZone'
@@ -42,5 +43,6 @@ export type ThresholdStatstic = z.infer<typeof ThresholdStatisticSchema>
4243

4344
export type LoadZoneData = z.infer<typeof LoadZoneSchema>
4445
export type LoadZoneItem = z.infer<typeof LoadZoneItemSchema>
46+
export type AvailableLoadZones = z.infer<typeof AvailableLoadZonesSchema>
4547

4648
export type TestOptions = z.infer<typeof TestOptionsSchema>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { FieldGroup, ControlledSelect } from '@/components/Form'
2+
import { Table } from '@/components/Table'
3+
import { LoadZoneData } from '@/types/testOptions'
4+
import {
5+
FieldArrayWithId,
6+
UseFieldArrayRemove,
7+
useFormContext,
8+
} from 'react-hook-form'
9+
import { IconButton, TextField } from '@radix-ui/themes'
10+
import { TrashIcon } from '@radix-ui/react-icons'
11+
import { LOAD_ZONES_REGIONS_OPTIONS } from './LoadZones.utils'
12+
13+
type LoadZoneRowProps = {
14+
index: number
15+
field: FieldArrayWithId<LoadZoneData, 'loadZones', 'id'>
16+
remove: UseFieldArrayRemove
17+
}
18+
19+
export function LoadZoneRow({ field, index, remove }: LoadZoneRowProps) {
20+
const {
21+
register,
22+
formState: { errors },
23+
control,
24+
watch,
25+
} = useFormContext<LoadZoneData>()
26+
27+
const { distribution, loadZones } = watch()
28+
29+
// Disable load zone options that are already in use
30+
const getLoadZoneOptions = () => {
31+
return LOAD_ZONES_REGIONS_OPTIONS.map((option) => ({
32+
...option,
33+
disabled: loadZones.some(
34+
(zone, i) => zone.loadZone === option.value && i !== index
35+
),
36+
}))
37+
}
38+
39+
return (
40+
<Table.Row key={field.id}>
41+
<Table.Cell>
42+
<FieldGroup errors={errors} name={`loadZones.${index}.loadZone`} mb="0">
43+
<ControlledSelect
44+
control={control}
45+
name={`loadZones.${index}.loadZone`}
46+
options={getLoadZoneOptions()}
47+
/>
48+
</FieldGroup>
49+
</Table.Cell>
50+
<Table.Cell>
51+
<FieldGroup errors={errors} name={`loadZones.${index}.percent`} mb="0">
52+
<TextField.Root
53+
type="number"
54+
placeholder="value"
55+
disabled={distribution === 'even'}
56+
{...register(`loadZones.${index}.percent`, { valueAsNumber: true })}
57+
>
58+
<TextField.Slot side="right">%</TextField.Slot>
59+
</TextField.Root>
60+
</FieldGroup>
61+
</Table.Cell>
62+
<Table.Cell>
63+
<IconButton onClick={() => remove(index)}>
64+
<TrashIcon width="18" height="18" />
65+
</IconButton>
66+
</Table.Cell>
67+
</Table.Row>
68+
)
69+
}

src/views/Generator/TestOptions/LoadZones/LoadZones.tsx

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,55 @@ import { LoadZoneSchema } from '@/schemas/generator/v1/loadZone'
33
import { useGeneratorStore } from '@/store/generator/useGeneratorStore'
44
import { LoadZoneData } from '@/types/testOptions'
55
import { zodResolver } from '@hookform/resolvers/zod'
6-
import { Text, Link as RadixLink, Button } from '@radix-ui/themes'
6+
import {
7+
Text,
8+
Link as RadixLink,
9+
Button,
10+
Switch,
11+
Flex,
12+
Callout,
13+
Tooltip,
14+
} from '@radix-ui/themes'
715
import { useCallback, useEffect } from 'react'
8-
import { FormProvider, useForm } from 'react-hook-form'
16+
import {
17+
FormProvider,
18+
useForm,
19+
useFieldArray,
20+
useFormContext,
21+
} from 'react-hook-form'
22+
import { LoadZoneRow } from './LoadZoneRow'
23+
import { FieldGroup } from '@/components/Form'
24+
import {
25+
findUnusedLoadZone,
26+
getRemainingPercentage,
27+
LOAD_ZONES_REGIONS_OPTIONS,
28+
} from './LoadZones.utils'
29+
import { Cross1Icon } from '@radix-ui/react-icons'
930

1031
export function LoadZones() {
11-
const { distribution, loadZones } = useGeneratorStore(
12-
(store) => store.loadZones
13-
)
32+
const loadZones = useGeneratorStore((store) => store.loadZones)
1433
const setLoadZones = useGeneratorStore((store) => store.setLoadZones)
1534

1635
const formMethods = useForm<LoadZoneData>({
1736
resolver: zodResolver(LoadZoneSchema),
1837
shouldFocusError: false,
19-
defaultValues: {
20-
distribution,
21-
loadZones,
22-
},
38+
defaultValues: loadZones,
39+
})
40+
41+
const {
42+
handleSubmit,
43+
watch,
44+
control,
45+
setValue,
46+
formState: { errors },
47+
} = formMethods
48+
49+
const { append, remove, fields } = useFieldArray<LoadZoneData>({
50+
control,
51+
name: 'loadZones',
2352
})
2453

25-
const { handleSubmit, watch } = formMethods
54+
const { distribution, loadZones: usedLoadZones } = watch()
2655

2756
const handleOpenDocs = (event: React.MouseEvent) => {
2857
event.preventDefault()
@@ -31,8 +60,14 @@ export function LoadZones() {
3160
)
3261
}
3362

34-
function handleAddLoadZone() {
35-
// TODO: Implement
63+
function handleAddLoadZone(event: React.MouseEvent) {
64+
event.preventDefault()
65+
66+
append({
67+
id: crypto.randomUUID(),
68+
loadZone: findUnusedLoadZone(usedLoadZones),
69+
percent: getRemainingPercentage(usedLoadZones),
70+
})
3671
}
3772

3873
const onSubmit = useCallback(
@@ -48,6 +83,20 @@ export function LoadZones() {
4883
return () => subscription.unsubscribe()
4984
}, [watch, handleSubmit, onSubmit])
5085

86+
// evenly distribute load zones if distribution is set to "even"
87+
useEffect(() => {
88+
if (distribution !== 'even') return
89+
90+
const basePercent = Math.floor(100 / fields.length)
91+
const remainder = 100 % fields.length
92+
93+
fields.forEach((_, index) => {
94+
// ensure only integers are used
95+
const percent = index < remainder ? basePercent + 1 : basePercent
96+
setValue(`loadZones.${index}.percent`, percent)
97+
})
98+
}, [distribution, fields, setValue])
99+
51100
return (
52101
<FormProvider {...formMethods}>
53102
<form onSubmit={handleSubmit(onSubmit)}>
@@ -59,6 +108,25 @@ export function LoadZones() {
59108
</RadixLink>
60109
.
61110
</Text>
111+
112+
<FieldGroup name="distribution" label="Distribution" errors={errors}>
113+
<Text size="2">
114+
<Flex gap="2" align="center">
115+
Even
116+
<Switch
117+
name="distribution"
118+
checked={distribution === 'manual'}
119+
onCheckedChange={(checked) => {
120+
setValue('distribution', checked ? 'manual' : 'even')
121+
}}
122+
/>
123+
Manual
124+
</Flex>
125+
</Text>
126+
</FieldGroup>
127+
128+
{errors.loadZones?.root && <LoadZonePercentageError />}
129+
62130
<Table.Root size="1" variant="surface" layout="fixed">
63131
<Table.Header>
64132
<Table.Row>
@@ -73,11 +141,31 @@ export function LoadZones() {
73141
</Table.Header>
74142

75143
<Table.Body>
144+
{fields.map((field, index) => (
145+
<LoadZoneRow
146+
key={field.id}
147+
field={field}
148+
index={index}
149+
remove={remove}
150+
/>
151+
))}
152+
76153
<Table.Row>
77154
<Table.RowHeaderCell colSpan={7} justify="center">
78-
<Button variant="ghost" onClick={handleAddLoadZone}>
79-
Add new load zone
80-
</Button>
155+
<Tooltip
156+
content="All available load zones are already in use"
157+
hidden={fields.length !== LOAD_ZONES_REGIONS_OPTIONS.length}
158+
>
159+
<Button
160+
variant="ghost"
161+
onClick={handleAddLoadZone}
162+
disabled={
163+
fields.length === LOAD_ZONES_REGIONS_OPTIONS.length
164+
}
165+
>
166+
Add new load zone
167+
</Button>
168+
</Tooltip>
81169
</Table.RowHeaderCell>
82170
</Table.Row>
83171
</Table.Body>
@@ -86,3 +174,19 @@ export function LoadZones() {
86174
</FormProvider>
87175
)
88176
}
177+
178+
function LoadZonePercentageError() {
179+
const {
180+
formState: { errors },
181+
} = useFormContext<LoadZoneData>()
182+
183+
return (
184+
<Callout.Root variant="soft" color="tomato" mb="3">
185+
<Callout.Icon>
186+
<Cross1Icon />
187+
</Callout.Icon>
188+
189+
<Callout.Text>{errors.loadZones?.root?.message}</Callout.Text>
190+
</Callout.Root>
191+
)
192+
}

0 commit comments

Comments
 (0)