Skip to content

Commit e04518b

Browse files
saeedvaziryCopilot
andauthored
Add search to server select (#911)
* Add search to server select * Update resources/js/pages/servers/components/server-select.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3de5105 commit e04518b

File tree

8 files changed

+278
-175
lines changed

8 files changed

+278
-175
lines changed

app/Actions/Server/GetServers.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace App\Actions\Server;
4+
5+
use App\Models\Project;
6+
use Illuminate\Support\Collection;
7+
use Illuminate\Support\Facades\Validator;
8+
9+
class GetServers
10+
{
11+
public function get(Project $project, array $input, int $perPage = 10): Collection
12+
{
13+
$validated = $this->validate($input);
14+
15+
$serversQuery = $project->servers();
16+
17+
if (! empty($validated['query'])) {
18+
$serversQuery->where('name', 'like', "%{$validated['query']}%");
19+
}
20+
21+
$page = $validated['page'] ?? 1;
22+
23+
return $serversQuery
24+
->skip(($page - 1) * $perPage)
25+
->take($perPage)
26+
->get();
27+
}
28+
29+
private function validate(array $input): array
30+
{
31+
return Validator::make($input, [
32+
'query' => [
33+
'nullable',
34+
'string',
35+
],
36+
'page' => [
37+
'nullable',
38+
'integer',
39+
'min:1',
40+
],
41+
])->validate();
42+
}
43+
}

app/Http/Controllers/ServerController.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Http\Controllers;
44

55
use App\Actions\Server\CreateServer;
6+
use App\Actions\Server\GetServers;
67
use App\Actions\Server\RebootServer;
78
use App\Actions\Server\TransferServer;
89
use App\Actions\Server\Update;
@@ -56,16 +57,7 @@ public function json(Request $request): ResourceCollection
5657

5758
$this->authorize('viewAny', [Server::class, $project]);
5859

59-
$this->validate($request, [
60-
'query' => [
61-
'nullable',
62-
'string',
63-
],
64-
]);
65-
66-
$servers = $project->servers()->where('name', 'like', "%{$request->input('query')}%")
67-
->take(10)
68-
->get();
60+
$servers = app(GetServers::class)->get($project, $request->input(), 10);
6961

7062
return ServerResource::collection($servers);
7163
}

app/Http/Middleware/HandleInertiaRequests.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,6 @@ public function share(Request $request): array
5757
return $this->share($request);
5858
}
5959

60-
// servers
61-
$servers = [];
62-
if ($user && $currentProject && $user->can('viewAny', [Server::class, $currentProject])) {
63-
// TODO: limit servers
64-
$servers = ServerResource::collection($currentProject->servers);
65-
}
66-
6760
$data = [];
6861
if ($request->route('server')) {
6962
/** @var Server $server */
@@ -101,7 +94,6 @@ public function share(Request $request): array
10194
'currentProject' => ProjectResource::make($currentProject),
10295
] : null,
10396
'public_key_text' => __('servers.create.public_key_text', ['public_key' => get_public_key_content()]),
104-
'project_servers' => $servers,
10597
'configs' => [
10698
'operating_systems' => config('core.operating_systems'),
10799
'colors' => config('core.colors'),

resources/js/components/project-select.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type Project } from '@/types/project';
2-
import { useState, useEffect, useRef, useCallback } from 'react';
2+
import { useState, useEffect, useRef } from 'react';
33
import { useInfiniteQuery } from '@tanstack/react-query';
44
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
55
import { Button } from '@/components/ui/button';
@@ -18,7 +18,6 @@ interface ProjectSelectProps {
1818
open?: boolean;
1919
onOpenChange?: (open: boolean) => void;
2020
footer?: ReactNode;
21-
onRefetch?: (refetch: () => void) => void;
2221
}
2322

2423
export function ProjectSelect({
@@ -30,7 +29,6 @@ export function ProjectSelect({
3029
open: controlledOpen,
3130
onOpenChange: controlledOnOpenChange,
3231
footer,
33-
onRefetch,
3432
}: ProjectSelectProps) {
3533
const [internalOpen, setInternalOpen] = useState(false);
3634
const [query, setQuery] = useState('');
@@ -52,6 +50,9 @@ export function ProjectSelect({
5250
refetchOnWindowFocus: false,
5351
initialPageParam: 1,
5452
getNextPageParam: (lastPage, allPages) => {
53+
if (!lastPage || !Array.isArray(lastPage)) {
54+
return undefined;
55+
}
5556
return lastPage.length === 10 ? allPages.length + 1 : undefined;
5657
},
5758
});
@@ -60,24 +61,12 @@ export function ProjectSelect({
6061
const selectedProject = projects.find((project) => project.id.toString() === value);
6162
const refetchRef = useRef<(() => void) | null>(null);
6263

63-
const safeRefetch = useCallback(() => {
64-
if (refetchRef.current) {
65-
refetchRef.current();
66-
}
67-
}, []);
68-
6964
useEffect(() => {
7065
if (refetch) {
7166
refetchRef.current = refetch;
7267
}
7368
}, [refetch]);
7469

75-
useEffect(() => {
76-
if (onRefetch && open) {
77-
onRefetch(safeRefetch);
78-
}
79-
}, [onRefetch, open, safeRefetch]);
80-
8170
useEffect(() => {
8271
if (!open || !hasNextPage) return;
8372

resources/js/components/project-switch.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,13 @@ export function ProjectSwitch() {
1616
const [open, setOpen] = useState(false);
1717
const [projectFormOpen, setProjectFormOpen] = useState(false);
1818
const [selected, setSelected] = useState<string>(auth.currentProject?.id?.toString() ?? '');
19-
const [refetchFn, setRefetchFn] = useState<(() => void) | null>(null);
2019
const initials = useInitials();
2120
const form = useForm();
2221

2322
useEffect(() => {
2423
setSelected(auth.currentProject?.id?.toString() ?? '');
2524
}, [auth.currentProject?.id]);
2625

27-
useEffect(() => {
28-
if (!projectFormOpen && open && refetchFn) {
29-
refetchFn();
30-
}
31-
}, [projectFormOpen, open, refetchFn]);
32-
3326
const handleProjectChange = (value: string, project: Project) => {
3427
setSelected(value);
3528
setOpen(false);
@@ -67,15 +60,7 @@ export function ProjectSwitch() {
6760

6861
return (
6962
<div className="flex items-center">
70-
<ProjectSelect
71-
value={selected}
72-
onValueChange={handleProjectChange}
73-
trigger={trigger}
74-
open={open}
75-
onOpenChange={setOpen}
76-
footer={footer}
77-
onRefetch={setRefetchFn}
78-
/>
63+
<ProjectSelect value={selected} onValueChange={handleProjectChange} trigger={trigger} open={open} onOpenChange={setOpen} footer={footer} />
7964
</div>
8065
);
8166
}
Lines changed: 64 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,84 @@
1+
import { type SharedData } from '@/types';
2+
import { type Server } from '@/types/server';
13
import { useForm, usePage } from '@inertiajs/react';
2-
import { useState } from 'react';
3-
import {
4-
DropdownMenu,
5-
DropdownMenuCheckboxItem,
6-
DropdownMenuContent,
7-
DropdownMenuItem,
8-
DropdownMenuSeparator,
9-
DropdownMenuTrigger,
10-
} from '@/components/ui/dropdown-menu';
4+
import { useState, useEffect } from 'react';
115
import { Button } from '@/components/ui/button';
126
import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react';
137
import { useInitials } from '@/hooks/use-initials';
148
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
15-
import { type Server } from '@/types/server';
16-
import type { SharedData } from '@/types';
179
import CreateServer from '@/pages/servers/components/create-server';
10+
import ServerSelect from '@/pages/servers/components/server-select';
11+
import { CommandGroup, CommandItem } from '@/components/ui/command';
1812

1913
export function ServerSwitch() {
2014
const page = usePage<SharedData>();
21-
const [selectedServer, setSelectedServer] = useState(page.props.server || null);
15+
const [open, setOpen] = useState(false);
16+
const [serverFormOpen, setServerFormOpen] = useState(false);
17+
const [selected, setSelected] = useState<string>(page.props.server?.id?.toString() ?? '');
2218
const initials = useInitials();
2319
const form = useForm();
2420

25-
const handleServerChange = (server: Server) => {
26-
setSelectedServer(server);
21+
useEffect(() => {
22+
setSelected(page.props.server?.id?.toString() ?? '');
23+
}, [page.props.server?.id]);
24+
25+
const handleServerChange = (value: string, server: Server) => {
26+
setSelected(value);
27+
setOpen(false);
2728
form.post(route('servers.switch', { server: server.id }));
2829
};
2930

30-
return (
31-
<div className="flex items-center">
32-
<DropdownMenu modal={false}>
33-
<DropdownMenuTrigger asChild>
34-
<Button variant="ghost" className="px-1!">
35-
{selectedServer && (
36-
<>
37-
<Avatar className="size-6 rounded-sm">
38-
<AvatarFallback className="rounded-sm">{initials(selectedServer?.name ?? '')}</AvatarFallback>
39-
</Avatar>
40-
<span className="hidden lg:flex">{selectedServer?.name}</span>
41-
</>
42-
)}
31+
const footer = (
32+
<CommandGroup>
33+
<CreateServer defaultOpen={serverFormOpen} onOpenChange={setServerFormOpen}>
34+
<CommandItem
35+
value="create-server"
36+
onSelect={() => {
37+
setServerFormOpen(true);
38+
}}
39+
className="gap-0"
40+
>
41+
<div className="flex items-center">
42+
<PlusIcon size={5} />
43+
<span className="ml-2">Create new server</span>
44+
</div>
45+
</CommandItem>
46+
</CreateServer>
47+
</CommandGroup>
48+
);
4349

44-
{!selectedServer && (
45-
<>
46-
<Avatar className="size-6 rounded-sm">
47-
<AvatarFallback className="rounded-sm">S</AvatarFallback>
48-
</Avatar>
49-
<span className="hidden lg:flex">Select a server</span>
50-
</>
51-
)}
50+
const trigger = (
51+
<Button variant="ghost" className="px-1!">
52+
{page.props.server ? (
53+
<>
54+
<Avatar className="size-6 rounded-sm">
55+
<AvatarFallback className="rounded-sm">{initials(page.props.server?.name ?? '')}</AvatarFallback>
56+
</Avatar>
57+
<span className="hidden lg:flex">{page.props.server?.name}</span>
58+
</>
59+
) : (
60+
<>
61+
<Avatar className="size-6 rounded-sm">
62+
<AvatarFallback className="rounded-sm">S</AvatarFallback>
63+
</Avatar>
64+
<span className="hidden lg:flex">Select a server</span>
65+
</>
66+
)}
67+
<ChevronsUpDownIcon size={5} />
68+
</Button>
69+
);
5270

53-
<ChevronsUpDownIcon size={5} />
54-
</Button>
55-
</DropdownMenuTrigger>
56-
<DropdownMenuContent className="w-56" align="start">
57-
{page.props.project_servers.length > 0 ? (
58-
page.props.project_servers.map((server) => (
59-
<DropdownMenuCheckboxItem
60-
key={`server-${server.id.toString()}`}
61-
checked={selectedServer?.id === server.id}
62-
onCheckedChange={() => handleServerChange(server)}
63-
>
64-
{server.name}
65-
</DropdownMenuCheckboxItem>
66-
))
67-
) : (
68-
<DropdownMenuItem disabled>No servers</DropdownMenuItem>
69-
)}
70-
<DropdownMenuSeparator />
71-
<CreateServer>
72-
<DropdownMenuItem className="gap-0" onSelect={(e) => e.preventDefault()}>
73-
<div className="flex items-center">
74-
<PlusIcon size={5} />
75-
<span className="ml-2">Create new server</span>
76-
</div>
77-
</DropdownMenuItem>
78-
</CreateServer>
79-
</DropdownMenuContent>
80-
</DropdownMenu>
71+
return (
72+
<div className="flex items-center">
73+
<ServerSelect
74+
value={selected}
75+
onValueChangeAdvanced={handleServerChange}
76+
trigger={trigger}
77+
open={open}
78+
onOpenChange={setOpen}
79+
footer={footer}
80+
showIp={false}
81+
/>
8182
</div>
8283
);
8384
}

0 commit comments

Comments
 (0)