diff --git a/app/Actions/Database/CreateDatabase.php b/app/Actions/Database/CreateDatabase.php index d2438bc6..844b27bd 100755 --- a/app/Actions/Database/CreateDatabase.php +++ b/app/Actions/Database/CreateDatabase.php @@ -6,6 +6,7 @@ use App\Models\Database; use App\Models\Server; use App\Models\Service; +use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -16,6 +17,8 @@ class CreateDatabase */ public function create(Server $server, array $input): Database { + Validator::make($input, self::rules($server, $input))->validate(); + $database = new Database([ 'server_id' => $server->id, 'charset' => $input['charset'], diff --git a/app/Http/Controllers/API/DatabaseController.php b/app/Http/Controllers/API/DatabaseController.php index 9ec54d86..a49d268c 100644 --- a/app/Http/Controllers/API/DatabaseController.php +++ b/app/Http/Controllers/API/DatabaseController.php @@ -50,8 +50,6 @@ public function create(Request $request, Project $project, Server $server): Data $this->validateRoute($project, $server); - $this->validate($request, CreateDatabase::rules($server, $request->input())); - $database = app(CreateDatabase::class)->create($server, $request->all()); return new DatabaseResource($database); diff --git a/app/Http/Controllers/API/ProjectController.php b/app/Http/Controllers/API/ProjectController.php index 1b4ff4eb..13e85d12 100644 --- a/app/Http/Controllers/API/ProjectController.php +++ b/app/Http/Controllers/API/ProjectController.php @@ -87,7 +87,9 @@ public function delete(Project $project): Response /** @var User $user */ $user = auth()->user(); - app(DeleteProject::class)->delete($user, $project); + app(DeleteProject::class)->delete($user, $project, [ + 'name' => $project->name, + ]); return response()->noContent(); } diff --git a/app/Http/Controllers/DatabaseController.php b/app/Http/Controllers/DatabaseController.php new file mode 100644 index 00000000..3b2a2d07 --- /dev/null +++ b/app/Http/Controllers/DatabaseController.php @@ -0,0 +1,96 @@ +authorize('viewAny', [Database::class, $server]); + + return Inertia::render('databases/index', [ + 'databases' => DatabaseResource::collection($server->databases()->simplePaginate(config('web.pagination_size'))), + ]); + } + + #[Get('/charsets', name: 'databases.charsets')] + public function charsets(Server $server): JsonResponse + { + $this->authorize('view', $server); + + $charsets = []; + foreach ($server->database()->type_data['charsets'] as $charset => $value) { + $charsets[] = $charset; + } + + return response()->json($charsets); + } + + #[Get('/collations/{charset?}', name: 'databases.collations')] + public function collations(Server $server, ?string $charset = null): JsonResponse + { + $this->authorize('view', $server); + + if (! $charset) { + $charset = $server->database()->type_data['defaultCharset'] ?? null; + } + + $charsets = $server->database()->type_data['charsets'] ?? []; + + return response()->json(data_get($charsets, $charset.'.list', data_get($charsets, $charset.'.default', []))); + } + + #[Post('/', name: 'databases.store')] + public function store(Request $request, Server $server): RedirectResponse + { + $this->authorize('create', [Database::class, $server]); + + app(CreateDatabase::class)->create($server, $request->all()); + + return back() + ->with('success', 'Database created successfully.'); + } + + #[Patch('/sync', name: 'databases.sync')] + public function sync(Server $server): RedirectResponse + { + $this->authorize('create', [Database::class, $server]); + + app(SyncDatabases::class)->sync($server); + + return back() + ->with('success', 'Databases synced successfully.'); + } + + #[Delete('/{database}', name: 'databases.destroy')] + public function destroy(Server $server, Database $database): RedirectResponse + { + $this->authorize('delete', [$database, $server]); + + app(DeleteDatabase::class)->delete($server, $database); + + return back() + ->with('success', 'Database deleted successfully.'); + } +} diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php index bfbc6be4..6bd1dd57 100644 --- a/app/Http/Controllers/ServerController.php +++ b/app/Http/Controllers/ServerController.php @@ -10,6 +10,7 @@ use App\Models\ServerProvider; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\URL; use Illuminate\Validation\Rule; use Inertia\Inertia; use Inertia\Response; @@ -57,8 +58,7 @@ public function show(Server $server): Response $this->authorize('view', $server); return Inertia::render('servers/show', [ - 'server' => ServerResource::make($server), - 'logs' => ServerLogResource::collection($server->logs()->latest()->paginate(config('web.pagination_size'))), + 'logs' => ServerLogResource::collection($server->logs()->latest()->simplePaginate(config('web.pagination_size'))), ]); } @@ -67,6 +67,18 @@ public function switch(Server $server): RedirectResponse { $this->authorize('view', $server); + $previousUrl = URL::previous(); + $previousRequest = Request::create($previousUrl); + $previousRoute = app('router')->getRoutes()->match($previousRequest); + + if ($previousRoute->hasParameter('server')) { + if (count($previousRoute->parameters()) > 1) { + return redirect()->route('servers.show', ['server' => $server->id]); + } + + return redirect()->route($previousRoute->getName(), ['server' => $server]); + } + return redirect()->route('servers.show', ['server' => $server->id]); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index e6309bf9..636a17bb 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -51,8 +51,14 @@ public function share(Request $request): array $servers = ServerResource::collection($user->currentProject?->servers); } + $data = []; + if ($request->route('server')) { + $data['server'] = ServerResource::make($request->route('server')); + } + return [ ...parent::share($request), + ...$data, 'name' => config('app.name'), 'quote' => ['message' => trim($message), 'author' => trim($author)], 'auth' => [ diff --git a/app/Http/Resources/DatabaseResource.php b/app/Http/Resources/DatabaseResource.php index fefb381c..890b09ff 100644 --- a/app/Http/Resources/DatabaseResource.php +++ b/app/Http/Resources/DatabaseResource.php @@ -18,7 +18,10 @@ public function toArray(Request $request): array 'id' => $this->id, 'server_id' => $this->server_id, 'name' => $this->name, + 'collation' => $this->collation, + 'charset' => $this->charset, 'status' => $this->status, + 'status_color' => $this::$statusColors[$this->status] ?? 'gray', 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; diff --git a/app/Models/Database.php b/app/Models/Database.php index 89f5726d..cc236fc2 100755 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -4,6 +4,7 @@ use App\Enums\DatabaseStatus; use Carbon\Carbon; +use Database\Factories\DatabaseFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -21,7 +22,7 @@ */ class Database extends AbstractModel { - /** @use HasFactory<\Database\Factories\DatabaseFactory> */ + /** @use HasFactory */ use HasFactory; use SoftDeletes; diff --git a/config/web.php b/config/web.php index 45133d06..b4460b36 100644 --- a/config/web.php +++ b/config/web.php @@ -1,7 +1,7 @@ 20, + 'pagination_size' => 10, 'controllers' => [ 'servers' => \App\Http\Controllers\ServerController::class, diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 46f60e91..a32f0994 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -9,12 +9,16 @@ import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubItem, } from '@/components/ui/sidebar'; import { type NavItem } from '@/types'; import { Link } from '@inertiajs/react'; import { BookOpen, CogIcon, Folder, ServerIcon } from 'lucide-react'; import AppLogo from './app-logo'; import { Icon } from '@/components/icon'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { useState } from 'react'; const mainNavItems: NavItem[] = [ { @@ -43,6 +47,8 @@ const footerNavItems: NavItem[] = [ ]; export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?: NavItem[]; secondNavTitle?: string }) { + const [open, setOpen] = useState(); + return ( {/* This is the first sidebar */} @@ -65,10 +71,10 @@ export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems? {mainNavItems.map((item) => ( - + @@ -85,7 +91,7 @@ export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems? {footerNavItems.map((item) => ( - + {item.icon && } @@ -113,16 +119,60 @@ export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems? - {secondNavItems.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} + {secondNavItems.map((item) => { + const isActive = item.onlyActivePath ? window.location.href === item.href : window.location.href.startsWith(item.href); + + if (item.children && item.children.length > 0) { + return ( + setOpen(item)} + className="group/collapsible" + > + + + + {item.icon && } + {item.title} + + + + + {item.children.map((childItem) => ( + + + + {childItem.title} + + + + ))} + + + + + ); + } + + return ( + + + + {item.icon && } + {item.title} + + + + ); + })} diff --git a/resources/js/components/header-container.tsx b/resources/js/components/header-container.tsx new file mode 100644 index 00000000..21452fbf --- /dev/null +++ b/resources/js/components/header-container.tsx @@ -0,0 +1,5 @@ +import { ReactNode } from 'react'; + +export default function HeaderContainer({ children }: { children: ReactNode }) { + return
{children}
; +} diff --git a/resources/js/components/user-select.tsx b/resources/js/components/user-select.tsx index b9c59bd7..52061210 100644 --- a/resources/js/components/user-select.tsx +++ b/resources/js/components/user-select.tsx @@ -1,5 +1,5 @@ import { User } from '@/types/user'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'; @@ -13,6 +13,13 @@ export default function UserSelect({ onChange }: { onChange: (selectedUser: User const [open, setOpen] = useState(false); const [value, setValue] = useState(); + const onOpenChange = (open: boolean) => { + setOpen(open); + if (open) { + fetchUsers(); + } + }; + const fetchUsers = async () => { const response = await axios.get(route('users.json', { query: query })); @@ -24,12 +31,8 @@ export default function UserSelect({ onChange }: { onChange: (selectedUser: User setUsers([]); }; - useEffect(() => { - fetchUsers(); - }, [query]); - return ( - + + + + + + + ); +} + +export const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Name', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return ( +
+ + {row.getValue('name')} +
+ ); + }, + }, + { + accessorKey: 'charset', + header: 'Charset', + enableColumnFilter: true, + enableSorting: true, + }, + { + accessorKey: 'collation', + header: 'Collation', + enableColumnFilter: true, + enableSorting: true, + }, + { + accessorKey: 'created_at', + header: 'Created at', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return ; + }, + }, + { + id: 'actions', + enableColumnFilter: false, + enableSorting: false, + cell: ({ row }) => { + return ( +
+ + + + + + + + +
+ ); + }, + }, +]; diff --git a/resources/js/pages/databases/components/create-database.tsx b/resources/js/pages/databases/components/create-database.tsx new file mode 100644 index 00000000..b8353bb9 --- /dev/null +++ b/resources/js/pages/databases/components/create-database.tsx @@ -0,0 +1,192 @@ +import { Server } from '@/types/server'; +import React, { FormEvent, ReactNode, useState } from 'react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { useForm } from '@inertiajs/react'; +import { Button } from '@/components/ui/button'; +import { LoaderCircle } from 'lucide-react'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import InputError from '@/components/ui/input-error'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import axios from 'axios'; +import { Checkbox } from '@/components/ui/checkbox'; + +type CreateForm = { + name: string; + charset: string; + collation: string; + user: boolean; + username: string; + password: string; + remote: boolean; + host: string; +}; + +export default function CreateDatabase({ server, children }: { server: Server; children: ReactNode }) { + const [open, setOpen] = useState(false); + const [charsets, setCharsets] = useState([]); + const [collations, setCollations] = useState([]); + + const fetchCharsets = async () => { + axios.get(route('databases.charsets', server.id)).then((response) => { + setCharsets(response.data); + }); + }; + + const form = useForm({ + name: '', + charset: '', + collation: '', + user: false, + username: '', + password: '', + remote: false, + host: '', + }); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.post(route('databases.store', server.id), { + onSuccess: () => { + form.reset(); + setOpen(false); + }, + }); + }; + + const handleOpenChange = (open: boolean) => { + setOpen(open); + if (open && charsets.length === 0) { + fetchCharsets(); + } + }; + + const handleCharsetChange = (value: string) => { + form.setData('collation', ''); + form.setData('charset', value); + axios.get(route('databases.collations', { server: server.id, charset: value })).then((response) => { + setCollations(response.data); + }); + }; + + return ( + + {children} + + + Create database + Create new database + +
+ + + + form.setData('name', e.target.value)} /> + + + + + + + + + + + + + +
+ form.setData('user', !form.data.user)} /> + +
+ +
+ {form.data.user && ( + <> + + + form.setData('username', e.target.value)} + /> + + + + + form.setData('password', e.target.value)} + /> + + + +
+ form.setData('remote', !form.data.remote)} /> + +
+ +
+ {form.data.remote && ( + + + form.setData('host', e.target.value)} /> + + + )} + + )} +
+
+ + + + + + +
+
+ ); +} diff --git a/resources/js/pages/databases/components/sync-databases.tsx b/resources/js/pages/databases/components/sync-databases.tsx new file mode 100644 index 00000000..e0025670 --- /dev/null +++ b/resources/js/pages/databases/components/sync-databases.tsx @@ -0,0 +1,45 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Server } from '@/types/server'; +import { Button } from '@/components/ui/button'; +import { LoaderCircleIcon, RefreshCwIcon } from 'lucide-react'; +import { useForm } from '@inertiajs/react'; +import { useState } from 'react'; + +export default function SyncDatabases({ server }: { server: Server }) { + const [open, setOpen] = useState(false); + const form = useForm(); + + const submit = () => { + form.patch(route('databases.sync', server.id), { + onSuccess: () => { + setOpen(false); + }, + }); + }; + return ( + + + + + + + Sync Databases + Sync databases from the server to Vito. + +

Are you sure you want to sync the databases from the server to Vito?

+ + + + + + +
+
+ ); +} diff --git a/resources/js/pages/databases/index.tsx b/resources/js/pages/databases/index.tsx new file mode 100644 index 00000000..a0e51758 --- /dev/null +++ b/resources/js/pages/databases/index.tsx @@ -0,0 +1,54 @@ +import { Head, usePage } from '@inertiajs/react'; +import { Server } from '@/types/server'; +import type { Database } from '@/types/database'; +import Container from '@/components/container'; +import HeaderContainer from '@/components/header-container'; +import Heading from '@/components/heading'; +import CreateDatabase from '@/pages/databases/components/create-database'; +import { Button } from '@/components/ui/button'; +import ServerLayout from '@/layouts/server/layout'; +import { DataTable } from '@/components/data-table'; +import { columns } from '@/pages/databases/components/columns'; +import React from 'react'; +import { BookOpenIcon, PlusIcon } from 'lucide-react'; +import SyncDatabases from '@/pages/databases/components/sync-databases'; + +type Page = { + server: Server; + databases: { + data: Database[]; + }; +}; + +export default function Databases() { + const page = usePage(); + + return ( + + + + + + +
+ + + + + + + +
+ + + + + + ); +} diff --git a/resources/js/pages/notification-channels/components/connect-notification-channel.tsx b/resources/js/pages/notification-channels/components/connect-notification-channel.tsx index f301a744..3d396dcf 100644 --- a/resources/js/pages/notification-channels/components/connect-notification-channel.tsx +++ b/resources/js/pages/notification-channels/components/connect-notification-channel.tsx @@ -11,7 +11,7 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { useForm, usePage } from '@inertiajs/react'; -import { FormEventHandler, ReactNode, useEffect, useState } from 'react'; +import { FormEventHandler, ReactNode, useState } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import InputError from '@/components/ui/input-error'; @@ -42,7 +42,7 @@ export default function ConnectNotificationChannel({ const page = usePage(); const form = useForm>({ - provider: 'email', + provider: defaultProvider || 'email', name: '', global: false, }); @@ -59,10 +59,6 @@ export default function ConnectNotificationChannel({ }); }; - useEffect(() => { - form.setData('provider', defaultProvider ?? 'email'); - }, [defaultProvider]); - return ( {children} diff --git a/resources/js/pages/server-providers/components/connect-server-provider.tsx b/resources/js/pages/server-providers/components/connect-server-provider.tsx index 01a3b28f..fa9526c1 100644 --- a/resources/js/pages/server-providers/components/connect-server-provider.tsx +++ b/resources/js/pages/server-providers/components/connect-server-provider.tsx @@ -11,7 +11,7 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { useForm, usePage } from '@inertiajs/react'; -import { FormEventHandler, ReactNode, useEffect, useState } from 'react'; +import { FormEventHandler, ReactNode, useState } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import InputError from '@/components/ui/input-error'; @@ -42,7 +42,7 @@ export default function ConnectServerProvider({ const page = usePage(); const form = useForm>({ - provider: 'aws', + provider: defaultProvider || 'aws', name: '', global: false, }); @@ -59,10 +59,6 @@ export default function ConnectServerProvider({ }); }; - useEffect(() => { - form.setData('provider', defaultProvider ?? 'aws'); - }, [defaultProvider]); - return ( {children} @@ -87,11 +83,14 @@ export default function ConnectServerProvider({ - {providers.map((provider) => ( - - {provider} - - ))} + {providers.map( + (provider) => + provider !== 'custom' && ( + + {provider} + + ), + )} diff --git a/resources/js/pages/servers/components/header.tsx b/resources/js/pages/servers/components/header.tsx index 93599c83..dd3dbe0e 100644 --- a/resources/js/pages/servers/components/header.tsx +++ b/resources/js/pages/servers/components/header.tsx @@ -31,8 +31,10 @@ export default function ServerHeader({ server }: { server: Server }) { - {server.provider} - Server Provider +
+ {server.provider} + Server Provider +
@@ -55,7 +57,7 @@ export default function ServerHeader({ server }: { server: Server }) {
-
%{server.progress}
+
%{parseInt(server.progress || '0')}
Installation Progress diff --git a/resources/js/pages/servers/index.tsx b/resources/js/pages/servers/index.tsx index 964663a0..b3bc6b09 100644 --- a/resources/js/pages/servers/index.tsx +++ b/resources/js/pages/servers/index.tsx @@ -11,6 +11,7 @@ import Container from '@/components/container'; import { Button } from '@/components/ui/button'; import React from 'react'; import Layout from '@/layouts/app/layout'; +import { PlusIcon } from 'lucide-react'; type Response = { servers: { @@ -31,7 +32,10 @@ export default function Servers() {
- +
diff --git a/resources/js/pages/servers/installing.tsx b/resources/js/pages/servers/installing.tsx index 923e924a..6d90c79e 100644 --- a/resources/js/pages/servers/installing.tsx +++ b/resources/js/pages/servers/installing.tsx @@ -4,6 +4,7 @@ import Container from '@/components/container'; import { DataTable } from '@/components/data-table'; import { columns } from '@/pages/server-logs/components/columns'; import { usePage } from '@inertiajs/react'; +import Heading from '@/components/heading'; export default function InstallingServer() { const page = usePage<{ @@ -15,6 +16,7 @@ export default function InstallingServer() { return ( + {' '} ); diff --git a/resources/js/pages/servers/show.tsx b/resources/js/pages/servers/show.tsx index c97bf967..d1a98148 100644 --- a/resources/js/pages/servers/show.tsx +++ b/resources/js/pages/servers/show.tsx @@ -24,7 +24,7 @@ export default function ShowServer() { const page = usePage(); return ( - + {['installing', 'installation_failed'].includes(page.props.server.status) ? : } diff --git a/resources/js/pages/source-controls/components/connect-source-control.tsx b/resources/js/pages/source-controls/components/connect-source-control.tsx index a54bb278..ebaa3019 100644 --- a/resources/js/pages/source-controls/components/connect-source-control.tsx +++ b/resources/js/pages/source-controls/components/connect-source-control.tsx @@ -11,7 +11,7 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { useForm, usePage } from '@inertiajs/react'; -import { FormEventHandler, ReactNode, useEffect, useState } from 'react'; +import { FormEventHandler, ReactNode, useState } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import InputError from '@/components/ui/input-error'; @@ -42,7 +42,7 @@ export default function ConnectSourceControl({ const page = usePage(); const form = useForm>({ - provider: 'github', + provider: defaultProvider || 'github', name: '', global: false, }); @@ -59,10 +59,6 @@ export default function ConnectSourceControl({ }); }; - useEffect(() => { - form.setData('provider', defaultProvider ?? 'github'); - }, [defaultProvider]); - return ( {children} diff --git a/resources/js/pages/storage-providers/components/connect-storage-provider.tsx b/resources/js/pages/storage-providers/components/connect-storage-provider.tsx index 4edc6a1a..0106d092 100644 --- a/resources/js/pages/storage-providers/components/connect-storage-provider.tsx +++ b/resources/js/pages/storage-providers/components/connect-storage-provider.tsx @@ -11,7 +11,7 @@ import { DialogTrigger, } from '@/components/ui/dialog'; import { useForm, usePage } from '@inertiajs/react'; -import { FormEventHandler, ReactNode, useEffect, useState } from 'react'; +import { FormEventHandler, ReactNode, useState } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import InputError from '@/components/ui/input-error'; @@ -42,7 +42,7 @@ export default function ConnectStorageProvider({ const page = usePage(); const form = useForm>({ - provider: 's3', + provider: defaultProvider || 's3', name: '', global: false, }); @@ -59,10 +59,6 @@ export default function ConnectStorageProvider({ }); }; - useEffect(() => { - form.setData('provider', defaultProvider ?? 's3'); - }, [defaultProvider]); - return ( {children} diff --git a/resources/js/types/database.d.ts b/resources/js/types/database.d.ts new file mode 100644 index 00000000..56aad9f5 --- /dev/null +++ b/resources/js/types/database.d.ts @@ -0,0 +1,13 @@ +export interface Database { + id: number; + server_id: number; + name: string; + collation: string; + charset: string; + status: string; + status_color: string; + created_at: string; + updated_at: string; + + [key: string]: unknown; +} diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 768ce80f..94ee369b 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -23,9 +23,10 @@ export interface NavGroup { export interface NavItem { title: string; href: string; - activePath?: string; + onlyActivePath?: string; icon?: LucideIcon | null; isActive?: boolean; + children?: NavItem[]; } export interface Configs { diff --git a/tests/Feature/DatabaseTest.php b/tests/Feature/DatabaseTest.php index b997a826..ba9bde61 100644 --- a/tests/Feature/DatabaseTest.php +++ b/tests/Feature/DatabaseTest.php @@ -6,10 +6,8 @@ use App\Enums\DatabaseUserStatus; use App\Facades\SSH; use App\Models\Database; -use App\Web\Pages\Servers\Databases\Index; -use App\Web\Pages\Servers\Databases\Widgets\DatabasesList; use Illuminate\Foundation\Testing\RefreshDatabase; -use Livewire\Livewire; +use Inertia\Testing\AssertableInertia; use Tests\TestCase; class DatabaseTest extends TestCase @@ -22,15 +20,11 @@ public function test_create_database(): void SSH::fake(); - Livewire::test(Index::class, [ - 'server' => $this->server, - ]) - ->callAction('create', [ - 'name' => 'database', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - ]) - ->assertSuccessful(); + $this->post(route('databases.store', $this->server), [ + 'name' => 'database', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + ])->assertSessionDoesntHaveErrors(); $this->assertDatabaseHas('databases', [ 'name' => 'database', @@ -44,20 +38,16 @@ public function test_create_database_with_user(): void SSH::fake(); - Livewire::test(Index::class, [ - 'server' => $this->server, - ]) - ->callAction('create', [ - 'name' => 'database', - 'charset' => 'utf8mb4', - 'collation' => 'utf8mb4_unicode_ci', - 'user' => true, - 'username' => 'user', - 'password' => 'password', - 'remote' => true, - 'host' => '%', - ]) - ->assertSuccessful(); + $this->post(route('databases.store', $this->server), [ + 'name' => 'database', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'user' => true, + 'username' => 'user', + 'password' => 'password', + 'remote' => true, + 'host' => '%', + ])->assertSessionDoesntHaveErrors(); $this->assertDatabaseHas('databases', [ 'name' => 'database', @@ -76,14 +66,13 @@ public function test_see_databases_list(): void { $this->actingAs($this->user); - /** @var Database $database */ - $database = Database::factory()->create([ + Database::factory()->create([ 'server_id' => $this->server, ]); - $this->get(Index::getUrl(['server' => $this->server])) + $this->get(route('databases', $this->server)) ->assertSuccessful() - ->assertSee($database->name); + ->assertInertia(fn (AssertableInertia $page) => $page->component('databases/index')); } public function test_delete_database(): void @@ -97,11 +86,10 @@ public function test_delete_database(): void 'server_id' => $this->server, ]); - Livewire::test(DatabasesList::class, [ + $this->delete(route('databases.destroy', [ 'server' => $this->server, - ]) - ->callTableAction('delete', $database->id) - ->assertSuccessful(); + 'database' => $database, + ]))->assertSessionDoesntHaveErrors(); $this->assertSoftDeleted('databases', [ 'id' => $database->id, @@ -114,10 +102,7 @@ public function test_sync_databases(): void SSH::fake(); - Livewire::test(Index::class, [ - 'server' => $this->server, - ]) - ->callAction('sync') - ->assertSuccessful(); + $this->patch(route('databases.sync', $this->server)) + ->assertSessionDoesntHaveErrors(); } } diff --git a/tsconfig.json b/tsconfig.json index eaa96a31..f113eb24 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,122 +1,136 @@ { - "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ - // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ - // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Language and Environment */ + "target": "ESNext" + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "ESNext" /* Specify what module code is generated. */, - // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ - // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ - // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ - // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ - // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ - // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ - // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ - // "resolveJsonModule": true, /* Enable importing .json files. */ - // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ - // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* Modules */ + "module": "ESNext" + /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "bundler" + /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* JavaScript Support */ + "allowJs": true + /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - "noEmit": true /* Disable emitting files from a compilation. */, - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ - // "outDir": "./", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true + /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - /* Interop Constraints */ - "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, - // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ - // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Interop Constraints */ + "isolatedModules": true + /* Ensure that each file can be safely transpiled without relying on other imports. */, + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true + /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true + /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true + /* Enable all strict type-checking options. */, + "noImplicitAny": true + /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, - // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ - // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ - // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */, - "baseUrl": ".", - "paths": { - "@/*": ["./resources/js/*"], - "ziggy-js": ["./vendor/tightenco/ziggy"] - }, - "jsx": "react-jsx", + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true + /* Skip type checking all .d.ts files. */, + "baseUrl": ".", + "paths": { + "@/*": [ + "./resources/js/*" + ], + "ziggy-js": [ + "./vendor/tightenco/ziggy" + ] }, - "include": [ - "resources/js/**/*.ts", - "resources/js/**/*.d.ts", - "resources/js/**/*.tsx", - ] + "jsx": "react-jsx" + }, + "include": [ + "resources/js/**/*.ts", + "resources/js/**/*.d.ts", + "resources/js/**/*.tsx" + ] }