Skip to content

Commit

Permalink
(ui): create views for server list / new server form
Browse files Browse the repository at this point in the history
  • Loading branch information
camwhit-e committed Nov 2, 2024
1 parent d022eaf commit edd6641
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 20 deletions.
28 changes: 28 additions & 0 deletions app/Http/Controllers/ServersController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Influx\Http\Controllers;

use Inertia\Inertia;
use Inertia\Response;
use Influx\Models\Server;
use Illuminate\Http\Request;

class ServersController extends Controller
{
/**
* ServersController constructor.
*/
public function __construct()
{
}

/**
* Display the table holding all servers.
*/
public function index(Request $request): Response
{
return Inertia::render('Servers/Index', [
'servers' => Server::all(),
]);
}
}
30 changes: 16 additions & 14 deletions resources/js/Layouts/AuthenticatedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import ApplicationLogo from '@/Components/ApplicationLogo';
import Dropdown from '@/Components/Dropdown';
import NavLink from '@/Components/NavLink';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink';
import { ComputerDesktopIcon, UsersIcon } from '@heroicons/react/24/outline';
import { ChevronDownIcon, ComputerDesktopIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline';
import { Head, Link, usePage } from '@inertiajs/react';
import { PropsWithChildren, ReactNode, useState } from 'react';

Expand Down Expand Up @@ -46,6 +46,13 @@ export default function Authenticated({
<UsersIcon className={'mr-2 h-5 w-5'} />
Accounts
</NavLink>
<NavLink
href={route('servers.index')}
active={route().current('servers.*')}
>
<ServerStackIcon className={'mr-2 h-5 w-5'} />
Servers
</NavLink>
</div>
</div>

Expand All @@ -59,19 +66,7 @@ export default function Authenticated({
className="inline-flex items-center rounded-md border border-transparent px-3 py-2 text-sm font-medium leading-4 text-gray-500 transition duration-150 ease-in-out hover:text-gray-700 focus:outline-none dark:text-gray-400 dark:hover:text-gray-300"
>
{user.name}

<svg
className="-me-0.5 ms-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
<ChevronDownIcon className={'w-4 h-4 mt-1 ml-1'} />
</button>
</span>
</Dropdown.Trigger>
Expand Down Expand Up @@ -158,6 +153,13 @@ export default function Authenticated({
<UsersIcon />
Accounts
</ResponsiveNavLink>
<ResponsiveNavLink
href={route('servers.index')}
active={route().current('servers.*')}
>
<ServerStackIcon />
Servers
</ResponsiveNavLink>
</div>

<div className="border-t border-gray-200 pb-1 pt-4 dark:border-gray-600">
Expand Down
95 changes: 95 additions & 0 deletions resources/js/Pages/Servers/Index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import PrimaryButton from '@/Components/PrimaryButton';
import { Body, Head } from '@/Components/Table';
import Authenticated from '@/Layouts/AuthenticatedLayout';
import { PageProps, Server } from '@/types';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { Link } from '@inertiajs/react';
import Avatar from 'boring-avatars';
import { useState } from 'react';
import New from './New';

export default function Index({ servers }: PageProps<{ servers: Server[] }>) {
const [filter, setFilter] = useState<string>('');
const [showForm, setShowForm] = useState(false);

if (showForm) {
return <New />;
};

return (
<Authenticated title={'All Servers'}>
<div className={'mb-4 flex justify-between'}>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center ps-3">
<MagnifyingGlassIcon className={'h-5 text-white'} />
</div>
<input
type="text"
className="block w-full rounded-lg border border-zinc-700 bg-zinc-800 p-2.5 ps-10 text-sm text-white focus:border-green-500 focus:ring-green-500 dark:placeholder-gray-400 dark:focus:border-green-500 dark:focus:ring-green-500"
placeholder="Search servers"
onChange={(e) => {
if (e.currentTarget.value.length > 2) {
setFilter(e.currentTarget.value);
} else {
setFilter('');
}
}}
required
/>
</div>
<div className={'ml-auto'}>
<PrimaryButton onClick={() => setShowForm(true)}>New Server</PrimaryButton>
</div>
</div>
<table className="w-full text-left text-sm text-gray-400">
<Head columns={['Name', 'Position', 'Status', 'Action']} />
<Body>
{servers
.filter((x) => x.name.startsWith(filter))
.map((server) => (
<tr
className="duration-250 border-b border-gray-800 bg-zinc-800 transition dark:hover:bg-zinc-800/80"
key={server.name}
>
<th
scope="row"
className="flex items-center whitespace-nowrap px-6 py-4"
>
<Avatar
variant={'beam'}
name={server.name}
className={'h-10 w-10'}
/>
<div className="ps-3">
<div className="text-base font-semibold">
{server.name}
</div>
<div className="font-normal text-gray-500">
{server.address}
</div>
</div>
</th>
<td className="px-6 py-4">
{server.public ? 'Public' : 'Private'}
</td>
<td className="px-6 py-4">
<div className="flex items-center">
<div className="me-2 h-2.5 w-2.5 rounded-full bg-green-500"></div>{' '}
Online
</div>
</td>
<td className="px-6 py-4">
<Link
href={`/servers/${server.id}`}
className="font-medium text-green-600 hover:underline dark:text-green-500"
>
Edit
</Link>
</td>
</tr>
))}
</Body>
</table>
</Authenticated>
);
}
24 changes: 24 additions & 0 deletions resources/js/Pages/Servers/New.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Card from '@/Components/Card';
import Authenticated from '@/Layouts/AuthenticatedLayout';
import { ServerStackIcon } from '@heroicons/react/24/outline';
import ServerForm from './ServerForm';

export default function New() {
return (
<Authenticated title={'New Server'}>
<Card
header={'Create new server'}
description={'Add a new server to the management interface.'}
>
<div className={'grid gap-6 lg:grid-cols-3'}>
<div className={'grid items-center justify-center'}>
<div>
<ServerStackIcon className={'h-24 text-green-600'} />
</div>
</div>
<ServerForm />
</div>
</Card>
</Authenticated>
);
}
109 changes: 109 additions & 0 deletions resources/js/Pages/Servers/ServerForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import PrimaryButton from '@/Components/PrimaryButton';
import { Server } from '@/types';
import { router } from '@inertiajs/react';
import { useState } from 'react';

export default function ServerForm({ server }: { server?: Server }) {
const [values, setValues] = useState({
name: server?.name ?? '',
address: server?.address ?? '',
});

function handleChange(e: {
target: { id: string; value: string | boolean };
}) {
const key = e.target.id;
const value = e.target.value;
setValues((values) => ({
...values,
[key]: value,
}));
}

function handleSubmit(e: { preventDefault: () => void }) {
e.preventDefault();

if (server) {
router.put(`/servers/${server!.id}`, values);
} else {
router.post('/servers/new', values);
}
}

return (
<form
className="lg:col-span-2"
autoComplete={'off'}
onSubmit={handleSubmit}
>
<div className="group relative z-0 mb-5 w-full">
<input
type="text"
name="name"
id="name"
onChange={handleChange}
className="peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent px-0 py-2.5 text-sm text-gray-900 focus:border-green-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" "
defaultValue={server?.name}
required
/>
<label className="absolute top-3 -z-10 origin-[0] -translate-y-6 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:start-0 peer-focus:-translate-y-6 peer-focus:scale-75 peer-focus:font-medium peer-focus:text-green-600 dark:text-gray-400 peer-focus:dark:text-green-500">
Server name
</label>
</div>
<div className="group relative z-0 mb-5 w-full">
<input
type="text"
name="address"
id="address"
onChange={handleChange}
className="peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent px-0 py-2.5 text-sm text-gray-900 focus:border-green-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" "
defaultValue={server?.address}
required
/>
<label className="absolute top-3 -z-10 origin-[0] -translate-y-6 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:start-0 peer-focus:-translate-y-6 peer-focus:scale-75 peer-focus:font-medium peer-focus:text-green-600 dark:text-gray-400 peer-focus:dark:text-green-500">
Server IP/FQDN Address
</label>
</div>
<div className="grid md:grid-cols-2 md:gap-6">
<div className="group relative z-0 mb-5 w-full">
<input
type="text"
name="owner_id"
id="owner_id"
defaultValue={server?.ownerId}
className="peer block w-full appearance-none border-0 border-b-2 border-gray-300 bg-transparent px-0 py-2.5 text-sm text-gray-900 focus:border-green-600 focus:outline-none focus:ring-0 dark:border-gray-600 dark:text-white dark:focus:border-green-500"
placeholder=" "
onChange={handleChange}
required
/>
<label
htmlFor="name"
className="absolute top-3 -z-10 origin-[0] -translate-y-6 scale-75 transform text-sm text-gray-500 duration-300 peer-placeholder-shown:translate-y-0 peer-placeholder-shown:scale-100 peer-focus:start-0 peer-focus:-translate-y-6 peer-focus:scale-75 peer-focus:font-medium peer-focus:text-green-600 rtl:peer-focus:translate-x-1/4 dark:text-gray-400 peer-focus:dark:text-green-500"
>
Server Owner
</label>
</div>
<div className="mb-4 flex items-center">
<input
id="public"
type="checkbox"
defaultChecked={server?.public}
onChange={handleChange}
className="h-4 w-4 rounded border-gray-300 bg-gray-100 text-green-600 focus:ring-2 focus:ring-green-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-green-600 dark:focus:ring-offset-gray-800"
/>
<label
htmlFor="superuser"
className="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Allow this server to be viewed publicly
</label>
</div>
</div>
<div className={'text-right'}>
<PrimaryButton type={'submit'}>{server ? 'Update' : 'Create'} Server</PrimaryButton>
</div>
</form>
);
}
1 change: 0 additions & 1 deletion resources/js/Pages/Users/UserForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import DangerButton from '@/Components/DangerButton';
import PrimaryButton from '@/Components/PrimaryButton';
import { User } from '@/types';
import { router } from '@inertiajs/react';
Expand Down
8 changes: 8 additions & 0 deletions resources/js/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ export interface User {
email_verified_at?: string;
}

export interface Server {
id: number;
name: string;
address: string;
ownerId: number;
public: boolean;
}

export type PageProps<
T extends Record<string, unknown> = Record<string, unknown>,
> = T & {
Expand Down
13 changes: 8 additions & 5 deletions routes/web.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<?php

use Inertia\Inertia;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Application;
use Influx\Http\Controllers\UsersController;
use Influx\Http\Controllers\ServersController;
use Influx\Http\Controllers\ProfileController;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});

Expand All @@ -30,6 +29,10 @@
Route::delete('/{user:id}', [UsersController::class, 'delete'])->name('users.delete');
});

Route::prefix('/servers')->group(function () {
Route::get('/', [ServersController::class, 'index'])->name('servers.index');
});

Route::prefix('/profile')->group(function () {
Route::get('/', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/', [ProfileController::class, 'update'])->name('profile.update');
Expand Down

0 comments on commit edd6641

Please sign in to comment.