Skip to content

Commit ab68964

Browse files
committed
#591 - load balancer
1 parent 0bd390f commit ab68964

7 files changed

Lines changed: 153 additions & 30 deletions

File tree

.github/workflows/tests.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
fail-fast: true
1818
matrix:
1919
php: [ 8.4 ]
20-
node-version: [ "20.x" ]
20+
node-version: [ "22.x" ]
2121

2222
steps:
2323
- uses: actions/checkout@v4
@@ -46,10 +46,13 @@ jobs:
4646
- name: Set up the .env file
4747
run: touch .env
4848

49+
- name: Generate Application Key
50+
run: php artisan key:generate
51+
4952
- name: Setup Node.js
5053
uses: actions/setup-node@v4
5154
with:
52-
node-version: "20.x"
55+
node-version: "22.x"
5356

5457
- name: Install NPM Dependencies
5558
run: npm install --force

app/Actions/Site/UpdateLoadBalancer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function update(Site $site, array $input): void
2222
foreach ($input['servers'] as $server) {
2323
$loadBalancerServer = new LoadBalancerServer([
2424
'load_balancer_id' => $site->id,
25-
'ip' => $server['server'],
25+
'ip' => $server['ip'],
2626
'port' => $server['port'],
2727
'weight' => $server['weight'],
2828
'backup' => (bool) $server['backup'],
@@ -43,7 +43,7 @@ public static function rules(Site $site): array
4343
'required',
4444
'array',
4545
],
46-
'servers.*.server' => [
46+
'servers.*.ip' => [
4747
'required',
4848
Rule::exists('servers', 'local_ip')
4949
->where('project_id', $site->project->id),

resources/js/pages/application/components/load-balancer.tsx

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import Container from '@/components/container';
66
import HeaderContainer from '@/components/header-container';
77
import Heading from '@/components/heading';
88
import { Button } from '@/components/ui/button';
9-
import { BookOpenIcon, LoaderCircleIcon } from 'lucide-react';
10-
import { FormEvent } from 'react';
9+
import { BookOpenIcon, LoaderCircleIcon, XIcon } from 'lucide-react';
10+
import React, { FormEvent } from 'react';
1111
import { LoadBalancerServer } from '@/types/load-balancer-server';
1212
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
1313
import { Form, FormField, FormFields } from '@/components/ui/form';
1414
import { Label } from '@/components/ui/label';
1515
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
1616
import InputError from '@/components/ui/input-error';
1717
import FormSuccessful from '@/components/form-successful';
18+
import ServerSelect from '@/pages/servers/components/server-select';
19+
import { Input } from '@/components/ui/input';
20+
import { Switch } from '@/components/ui/switch';
1821

1922
export default function LoadBalancer() {
2023
const page = usePage<{
@@ -26,26 +29,42 @@ export default function LoadBalancer() {
2629
const form = useForm<{
2730
method: 'round-robin' | 'least-connections' | 'ip-hash';
2831
servers: {
29-
server: string;
30-
port: string;
32+
load_balancer_id: number;
33+
ip: string;
34+
port: number;
3135
weight: string;
3236
backup: boolean;
3337
}[];
3438
}>({
3539
method: 'round-robin',
36-
servers: [],
40+
servers: page.props.loadBalancerServers,
3741
});
3842

43+
const addServer = () => {
44+
const newServer: LoadBalancerServer = {
45+
load_balancer_id: 0,
46+
ip: '',
47+
port: 80,
48+
weight: '100',
49+
backup: false,
50+
created_at: '',
51+
updated_at: '',
52+
};
53+
54+
form.setData('servers', [...form.data.servers, newServer]);
55+
};
56+
3957
const submit = (e: FormEvent) => {
4058
e.preventDefault();
4159
form.post(route('application.update-load-balancer', { server: page.props.server.id, site: page.props.site.id }), {
42-
onSuccess: () => {
43-
form.reset();
44-
},
4560
preserveScroll: true,
4661
});
4762
};
4863

64+
const getFieldError = (field: string): string | undefined => {
65+
return form.errors[field as keyof typeof form.errors];
66+
};
67+
4968
return (
5069
<ServerLayout>
5170
<Head title={`${page.props.site.domain} - ${page.props.server.name}`} />
@@ -90,6 +109,99 @@ export default function LoadBalancer() {
90109
</Select>
91110
<InputError message={form.errors.method} />
92111
</FormField>
112+
113+
{form.data.servers.map((item, index) => (
114+
<div key={`server-${index}`} className="relative rounded-md border border-dashed p-4">
115+
<XIcon
116+
className="text-muted-foreground hover:text-foreground absolute top-2 right-2 cursor-pointer"
117+
onClick={() => {
118+
const updatedServers = [...form.data.servers];
119+
updatedServers.splice(index, 1);
120+
form.setData('servers', updatedServers);
121+
}}
122+
/>
123+
<div className="grid grid-cols-1 items-start gap-6 md:grid-cols-2 lg:grid-cols-4">
124+
<FormField>
125+
<Label htmlFor={`server-${index}`}>Server</Label>
126+
<ServerSelect
127+
id={`server-${index}`}
128+
value={item.ip}
129+
valueBy="local_ip"
130+
onValueChange={(server) => {
131+
const updatedServers = [...form.data.servers];
132+
updatedServers[index] = {
133+
...updatedServers[index],
134+
ip: server ? server.local_ip || '' : '',
135+
};
136+
form.setData('servers', updatedServers);
137+
}}
138+
/>
139+
<InputError message={getFieldError(`servers.${index}.ip`)} />
140+
</FormField>
141+
142+
<FormField>
143+
<Label htmlFor={`port-${index}`}>Port</Label>
144+
<Input
145+
id={`port-${index}`}
146+
type="text"
147+
value={item.port || ''}
148+
onChange={(e) => {
149+
const updatedServers = [...form.data.servers];
150+
updatedServers[index] = {
151+
...updatedServers[index],
152+
port: parseInt(e.target.value, 10),
153+
};
154+
form.setData('servers', updatedServers);
155+
}}
156+
/>
157+
<InputError message={getFieldError(`servers.${index}.port`)} />
158+
</FormField>
159+
160+
<FormField>
161+
<Label htmlFor={`weight-${index}`}>Weight</Label>
162+
<Input
163+
id={`weight-${index}`}
164+
type="text"
165+
value={item.weight || '100'}
166+
onChange={(e) => {
167+
const updatedServers = [...form.data.servers];
168+
updatedServers[index] = {
169+
...updatedServers[index],
170+
weight: e.target.value,
171+
};
172+
form.setData('servers', updatedServers);
173+
}}
174+
/>
175+
<InputError message={getFieldError(`servers.${index}.weight`)} />
176+
</FormField>
177+
178+
<FormField>
179+
<Label htmlFor={`backup-${index}`}>Backup</Label>
180+
<Switch
181+
id={`backup-${index}`}
182+
checked={item.backup || false}
183+
onCheckedChange={(checked) => {
184+
const updatedServers = [...form.data.servers];
185+
updatedServers[index] = {
186+
...updatedServers[index],
187+
backup: checked,
188+
};
189+
form.setData('servers', updatedServers);
190+
}}
191+
className="mt-2"
192+
/>
193+
<InputError message={getFieldError(`servers.${index}.backup`)} />
194+
</FormField>
195+
</div>
196+
</div>
197+
))}
198+
199+
<FormField
200+
onClick={addServer}
201+
className="text-muted-foreground hover:text-foreground flex h-[92px] items-center justify-center rounded-md border border-dashed hover:cursor-pointer"
202+
>
203+
Add server to the load balancer
204+
</FormField>
93205
</FormFields>
94206
</Form>
95207
</CardContent>

resources/js/pages/scripts/components/execute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default function Execute({ script, children }: { script: Script; children
5555
<ServerSelect
5656
value={form.data.server}
5757
onValueChange={(value) => {
58-
form.setData('server', value.id.toString());
58+
form.setData('server', value ? value.id.toString() : '');
5959
setServer(value);
6060
}}
6161
/>

resources/js/pages/servers/components/server-select.tsx

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
88
import { cn } from '@/lib/utils';
99
import axios from 'axios';
1010

11-
export default function ServerSelect({ value, onValueChange }: { value: string; onValueChange: (selectedServer: Server) => void }) {
11+
export default function ServerSelect({
12+
value,
13+
valueBy = 'id',
14+
onValueChange,
15+
id,
16+
prefetch,
17+
}: {
18+
value: string;
19+
valueBy?: keyof Server;
20+
onValueChange: (selectedServer?: Server) => void;
21+
id?: string;
22+
prefetch?: boolean;
23+
}) {
1224
const [query, setQuery] = useState('');
1325
const [open, setOpen] = useState(false);
1426
const [selected, setSelected] = useState<string>(value);
@@ -27,7 +39,7 @@ export default function ServerSelect({ value, onValueChange }: { value: string;
2739
const response = await axios.get(route('servers.json', { query: query }));
2840
return response.data;
2941
},
30-
enabled: false,
42+
enabled: prefetch,
3143
});
3244

3345
const onOpenChange = (open: boolean) => {
@@ -47,12 +59,12 @@ export default function ServerSelect({ value, onValueChange }: { value: string;
4759
}
4860
}, [query, open, refetch]);
4961

50-
const selectedServer = servers.find((server) => server.id === parseInt(selected));
62+
const selectedServer = servers.find((server) => String(server[valueBy] as Server[keyof Server]) === selected);
5163

5264
return (
5365
<Popover open={open} onOpenChange={onOpenChange}>
5466
<PopoverTrigger asChild>
55-
<Button variant="outline" role="combobox" aria-expanded={open} className="w-full justify-between">
67+
<Button id={id} variant="outline" role="combobox" aria-expanded={open} className="w-full justify-between">
5668
{selectedServer ? selectedServer.name : 'Select server...'}
5769
<ChevronsUpDownIcon className="opacity-50" />
5870
</Button>
@@ -63,25 +75,21 @@ export default function ServerSelect({ value, onValueChange }: { value: string;
6375
<CommandList>
6476
<CommandEmpty>{isFetching ? 'Searching...' : query === '' ? 'Start typing to search servers' : 'No servers found.'}</CommandEmpty>
6577
<CommandGroup>
66-
{servers.map((server) => (
78+
{servers.map((server: Server) => (
6779
<CommandItem
6880
key={`server-select-${server.id}`}
69-
value={server.id.toString()}
81+
value={String(server[valueBy] as Server[keyof Server])}
7082
onSelect={(currentValue) => {
7183
const newSelected = currentValue === selected ? '' : currentValue;
7284
setSelected(newSelected);
7385
setOpen(false);
74-
if (newSelected) {
75-
const server = servers.find((s) => s.id.toString() === newSelected);
76-
if (server) {
77-
onValueChange(server);
78-
}
79-
}
86+
const server = servers.find((s) => String(s[valueBy] as Server[keyof Server]) === newSelected);
87+
onValueChange(server);
8088
}}
8189
className="truncate"
8290
>
8391
{server.name} ({server.ip})
84-
<CheckIcon className={cn('ml-auto', selected && parseInt(selected) === server.id ? 'opacity-100' : 'opacity-0')} />
92+
<CheckIcon className={cn('ml-auto', selected === String(server[valueBy] as Server[keyof Server]) ? 'opacity-100' : 'opacity-0')} />
8593
</CommandItem>
8694
))}
8795
</CommandGroup>

resources/js/pages/sites/components/create-site.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export default function CreateSite({ server, children }: { server?: Server; chil
104104
{server === undefined && (
105105
<FormField>
106106
<Label htmlFor="server">Server</Label>
107-
<ServerSelect value={form.data.server} onValueChange={(value) => form.setData('server', value.id.toString())} />
107+
<ServerSelect value={form.data.server} onValueChange={(value) => form.setData('server', value ? value.id.toString() : '')} />
108108
<InputError message={form.errors.server} />
109109
</FormField>
110110
)}

resources/js/types/load-balancer-server.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
export interface LoadBalancerServer {
22
load_balancer_id: number;
3-
ip: number;
3+
ip: string;
44
port: number;
5-
weight: boolean;
6-
backup: string;
5+
weight: string;
6+
backup: boolean;
77
created_at: string;
88
updated_at: string;
99

0 commit comments

Comments
 (0)