- redesign the dashboard
- add search bar
- Mobile-friendly design
This commit is contained in:
Saeed Vaziry 2024-03-21 15:57:57 +01:00 committed by GitHub
parent 7949165648
commit d3aaf2a6fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1175 additions and 1009 deletions

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers;
use App\Models\Server;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SearchController extends Controller
{
/**
* @throws ValidationException
*/
public function search(Request $request): JsonResponse
{
$this->validate($request, [
'q' => 'required',
]);
$servers = Server::query()
->where(function ($query) use ($request) {
$query->where('name', 'like', '%'.$request->input('q').'%')
->orWhere('ip', 'like', '%'.$request->input('q').'%');
})
->get();
$sites = Site::query()
->where('domain', 'like', '%'.$request->input('q').'%')
->get();
$result = [];
/** @var Server $server */
foreach ($servers as $server) {
$result[] = [
'type' => 'server',
'url' => route('servers.show', ['server' => $server]),
'text' => $server->name,
'project' => $server->project->name,
];
}
/** @var Site $site */
foreach ($sites as $site) {
$result[] = [
'type' => 'site',
'url' => route('servers.sites.show', ['server' => $site->server, 'site' => $site]),
'text' => $site->domain,
'project' => $site->server->project->name,
];
}
return response()->json([
'results' => $result,
]);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Http;
use App\Http\Middleware\HandleSSHErrors;
use App\Http\Middleware\SelectCurrentProject;
use App\Http\Middleware\ServerIsReadyMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
@ -66,5 +67,6 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'server-is-ready' => ServerIsReadyMiddleware::class,
'handle-ssh-errors' => HandleSSHErrors::class,
'select-current-project' => SelectCurrentProject::class,
];
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SelectCurrentProject
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
/** @var Server $server */
$server = $request->route('server');
/** @var User $user */
$user = $request->user();
if ($server->project_id != $user->current_project_id) {
$user->current_project_id = $server->project_id;
$user->save();
}
return $next($request);
}
}

175
package-lock.json generated
View File

@ -9,7 +9,7 @@
"@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.6.0",
"flowbite": "^2.3.0",
"htmx.org": "^1.9.10",
"laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2",
@ -564,12 +564,6 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"node_modules/autoprefixer": {
"version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@ -603,17 +597,6 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -753,18 +736,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -792,15 +763,6 @@
"node": ">=4"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -926,38 +888,14 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"node_modules/flowbite": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.3.0.tgz",
"integrity": "sha512-pm3JRo8OIJHGfFYWgaGpPv8E+UdWy0Z3gEAGufw+G/1dusaU/P1zoBLiQpf2/+bYAi+GBQtPVG86KYlV0W+AFQ==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
"@popperjs/core": "^2.9.3",
"mini-svg-data-uri": "^1.4.3"
}
},
"node_modules/fraction.js": {
@ -1220,27 +1158,6 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
@ -1620,12 +1537,6 @@
}
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"node_modules/pusher-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-4.4.0.tgz",
@ -2392,12 +2303,6 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true
},
"autoprefixer": {
"version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@ -2412,17 +2317,6 @@
"postcss-value-parser": "^4.2.0"
}
},
"axios": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
"integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
"dev": true,
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2511,15 +2405,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
},
"commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@ -2538,12 +2423,6 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true
},
"didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@ -2649,21 +2528,14 @@
"to-regex-range": "^5.0.1"
}
},
"follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"dev": true
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"flowbite": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.3.0.tgz",
"integrity": "sha512-pm3JRo8OIJHGfFYWgaGpPv8E+UdWy0Z3gEAGufw+G/1dusaU/P1zoBLiQpf2/+bYAi+GBQtPVG86KYlV0W+AFQ==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
"@popperjs/core": "^2.9.3",
"mini-svg-data-uri": "^1.4.3"
}
},
"fraction.js": {
@ -2864,21 +2736,6 @@
"picomatch": "^2.3.1"
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": {
"mime-db": "1.52.0"
}
},
"mini-svg-data-uri": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
@ -3072,12 +2929,6 @@
"dev": true,
"requires": {}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"dev": true
},
"pusher-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-4.4.0.tgz",

View File

@ -11,7 +11,7 @@
"@tailwindcss/typography": "^0.5.9",
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.6.0",
"flowbite": "^2.3.0",
"htmx.org": "^1.9.10",
"laravel-echo": "^1.15.0",
"laravel-vite-plugin": "^0.7.2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"resources/css/app.css": {
"file": "assets/app-e2e337dc.css",
"file": "assets/app-986a0fb7.css",
"isEntry": true,
"src": "resources/css/app.css"
},
@ -12,7 +12,7 @@
"css": [
"assets/app-a1ae07b3.css"
],
"file": "assets/app-f7ac9558.js",
"file": "assets/app-e823d2ab.js",
"isEntry": true,
"src": "resources/js/app.js"
}

View File

@ -6,3 +6,7 @@
[x-cloak] {
display: none !important;
}
body {
@apply text-gray-700 dark:text-gray-300;
}

View File

@ -1,3 +1,5 @@
import 'flowbite';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

View File

@ -1,15 +0,0 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
import toastr from 'toastr';
window.toastr = toastr;
window.toastr.options = {
"debug": false,
"positionClass": "toast-bottom-right",
"preventDuplicates": true,
}

View File

@ -11,14 +11,17 @@
</x-card-header>
<x-live id="live-deployments">
<x-table>
<tr>
<x-thead>
<x-tr>
<x-th>{{ __("Commit") }}</x-th>
<x-th>{{ __("Date") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
</x-thead>
<x-tbody>
@foreach ($deployments as $deployment)
<tr>
<x-tr>
<x-td class="truncate">
<a
href="{{ $deployment->commit_data["url"] }}"
@ -48,8 +51,9 @@ class="block max-w-[500px] truncate font-semibold text-primary-600"
<x-heroicon name="o-eye" class="h-5 w-5" />
</x-icon-button>
</x-td>
</tr>
</x-tr>
@endforeach
</x-tbody>
</x-table>
</x-live>
<div class="mt-5">

View File

@ -5,12 +5,12 @@
{{ __("Here you can manage your application") }}
</x-slot>
<x-slot name="aside">
<div class="flex items-center">
<div class="mr-2">
<div class="flex flex-col items-end lg:flex-row lg:items-center">
<div class="mb-2 lg:mb-0 lg:mr-2">
@include("application.deploy")
</div>
@if ($site->source_control_id)
<div class="mr-2">
<div class="mb-2 lg:mb-0 lg:mr-2">
@include("application.auto-deployment")
</div>
@endif

View File

@ -1,3 +1,10 @@
<div {!! $attributes->merge(["class" => "py-12 max-w-7xl mx-auto px-6"]) !!}>
@php
$class = "mx-auto px-4 sm:px-6 lg:px-8";
if (! str($attributes->get("class"))->contains("max-w-")) {
$class .= " max-w-7xl";
}
@endphp
<div {!! $attributes->merge(["class" => $class]) !!}>
{{ $slot }}
</div>

View File

@ -1,5 +1,5 @@
<button
{{ $attributes->merge(["type" => "submit", "class" => "inline-flex w-max items-center justify-center rounded-md border border-transparent bg-red-600 px-4 py-1 h-9 font-semibold capitalize text-white transition hover:bg-red-500 focus:border-red-700 focus:outline-none focus:ring focus:ring-red-200 active:bg-red-600 disabled:opacity-25"]) }}
{{ $attributes->merge(["type" => "submit", "class" => "inline-flex w-max min-w-max items-center justify-center rounded-md border border-transparent bg-red-600 px-4 py-1 h-9 font-semibold capitalize text-white transition hover:bg-red-500 focus:border-red-700 focus:outline-none focus:ring focus:ring-red-200 active:bg-red-600 disabled:opacity-25"]) }}
>
{{ $slot }}
</button>

View File

@ -1,5 +1,11 @@
<a
{{ $attributes->merge(["class" => "block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-700 transition duration-150 ease-in-out flex items-center justify-start"]) }}
>
@props(["active" => false])
@php
$class = $active
? "block flex w-full items-center justify-start px-4 py-2 text-left text-sm leading-5 text-primary-500 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:hover:bg-gray-800/50 dark:focus:bg-gray-700"
: "block flex w-full items-center justify-start px-4 py-2 text-left text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800/50 dark:focus:bg-gray-700";
@endphp
<a {{ $attributes->merge(["class" => $class]) }}>
{{ $slot }}
</a>

View File

@ -1,4 +1,4 @@
@props(["align" => "right", "width" => "48", "contentClasses" => "bg-white py-1 dark:bg-gray-800"])
@props(["open" => false, "align" => "right", "width" => "48", "contentClasses" => "list-none divide-y divide-gray-100 rounded-md border border-gray-100 bg-white py-1 text-base shadow dark:divide-gray-600 dark:border-gray-600 dark:bg-gray-700"])
@php
switch ($align) {
@ -24,7 +24,7 @@
}
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div class="relative" x-data="{ open: @js($open) }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"
/>
</svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@ -0,0 +1,10 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>

After

Width:  |  Height:  |  Size: 255 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m7.875 14.25 1.214 1.942a2.25 2.25 0 0 0 1.908 1.058h2.006c.776 0 1.497-.4 1.908-1.058l1.214-1.942M2.41 9h4.636a2.25 2.25 0 0 1 1.872 1.002l.164.246a2.25 2.25 0 0 0 1.872 1.002h2.092a2.25 2.25 0 0 0 1.872-1.002l.164-.246A2.25 2.25 0 0 1 16.954 9h4.636M2.41 9a2.25 2.25 0 0 0-.16.832V12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 12V9.832c0-.287-.055-.57-.16-.832M2.41 9a2.25 2.25 0 0 1 .382-.632l3.285-3.832a2.25 2.25 0 0 1 1.708-.786h8.43c.657 0 1.281.287 1.709.786l3.284 3.832c.163.19.291.404.382.632M4.5 20.25h15A2.25 2.25 0 0 0 21.75 18v-2.625c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125V18a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 908 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
{{ $attributes }}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@ -0,0 +1 @@
<div class="my-3 border-b border-gray-200 dark:border-gray-700"></div>

View File

@ -4,7 +4,7 @@
@php
$class =
"inline-flex h-9 w-max items-center justify-center rounded-md border border-transparent bg-primary-600 px-4 py-1 font-semibold text-white outline-0 transition hover:bg-primary-700 focus:border-primary-300 focus:border-primary-700 focus:ring focus:ring-primary-200 focus:ring-opacity-50 active:bg-primary-700 disabled:opacity-25 dark:focus:border-primary-700 dark:focus:ring-primary-700 dark:focus:ring-opacity-40";
"inline-flex h-9 w-max min-w-max items-center justify-center rounded-md border border-transparent bg-primary-600 px-4 py-1 font-semibold text-white outline-0 transition hover:bg-primary-700 focus:border-primary-300 focus:border-primary-700 focus:ring focus:ring-primary-200 focus:ring-opacity-50 active:bg-primary-700 disabled:opacity-25 dark:focus:border-primary-700 dark:focus:ring-primary-700 dark:focus:ring-opacity-40";
@endphp
@if (isset($href))

View File

@ -7,7 +7,7 @@
@php
$class =
"inline-flex h-9 items-center rounded-md border border-gray-300 bg-white px-4 py-1 font-semibold text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800";
"inline-flex h-9 min-w-max items-center rounded-md border border-gray-300 bg-white px-4 py-1 font-semibold text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800";
@endphp
@if (isset($href))

View File

@ -5,8 +5,8 @@
@php
$classes =
$active ?? false
? "text-md flex h-10 items-center rounded-md bg-gray-900 px-4 py-3 font-semibold text-primary-500 transition transition-all duration-100 duration-150 ease-in-out"
: "text-md flex h-10 items-center rounded-md px-4 py-3 font-semibold text-gray-500 transition transition-all duration-100 duration-150 ease-in-out hover:bg-gray-900";
? "group flex items-center rounded-md bg-primary-700 p-2 text-white"
: "group flex items-center rounded-md p-2 text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700";
@endphp
<a {{ $attributes->merge(["class" => $classes]) }}>

View File

@ -5,8 +5,8 @@
@php
$classes =
$active ?? false
? "flex h-10 items-center justify-start rounded-lg bg-primary-50 px-3 py-2 font-semibold text-primary-500 hover:bg-gray-100 dark:bg-primary-500 dark:bg-opacity-20 dark:hover:bg-gray-800"
: "flex h-10 items-center justify-start rounded-lg px-3 py-2 font-semibold text-gray-800 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800";
? "flex h-10 items-center justify-start rounded-lg bg-primary-50 px-3 py-2 font-semibold text-primary-500 dark:bg-primary-500 dark:bg-opacity-20"
: "flex h-10 items-center justify-start rounded-lg px-3 py-2 font-semibold text-gray-800 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700";
@endphp
<a {{ $attributes->merge(["class" => $classes]) }}>

View File

@ -1,5 +1,5 @@
<div
{!! $attributes->merge(["class" => "inline-block min-w-full overflow-x-auto rounded-md bg-white align-middle border border-gray-200 dark:border-gray-700 dark:bg-gray-800"]) !!}
{!! $attributes->merge(["class" => "inline-block min-w-full max-w-full overflow-x-auto rounded-md bg-white align-middle border border-gray-200 dark:border-gray-700 dark:bg-gray-800"]) !!}
>
<table class="min-w-full">
{{ $slot }}

View File

@ -0,0 +1,3 @@
<tbody {!! $attributes->merge(["class" => ""]) !!}>
{{ $slot }}
</tbody>

View File

@ -0,0 +1,5 @@
<thead
{!! $attributes->merge(["class" => "text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 border-b border-gray-200 dark:border-gray-600"]) !!}
>
{{ $slot }}
</thead>

View File

@ -0,0 +1,5 @@
<tr
{!! $attributes->merge(["class" => "dark:text-white bg-white border-b last:border-b-0 dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/70"]) !!}
>
{{ $slot }}
</tr>

View File

@ -21,7 +21,7 @@ class="ml-1"
<x-live id="live-backup-files">
@if (count($files) > 0)
<x-table class="mt-5">
<tr>
<x-tr>
<x-th>{{ __("Name") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
{{-- <x-th>{{ __("Size") }}</x-th> --}}
@ -29,9 +29,9 @@ class="ml-1"
<x-th>{{ __("Restored") }}</x-th>
<x-th>{{ __("Restored To") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
@foreach ($files as $file)
<tr>
<x-tr>
<x-td>{{ $file->name }}</x-td>
<x-td>
<x-datetime :value="$file->created_at" />
@ -71,7 +71,7 @@ class="ml-1"
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
</x-td>
</tr>
</x-tr>
@endforeach
</x-table>
<div class="mt-5">

View File

@ -13,15 +13,15 @@
<x-live id="live-backups">
@if (count($backups) > 0)
<x-table>
<tr>
<x-tr>
<x-th>{{ __("Database") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Storage") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
@foreach ($backups as $backup)
<tr>
<x-tr>
<x-td>{{ $backup->database->name }}</x-td>
<x-td>
<x-datetime :value="$backup->created_at" />
@ -44,7 +44,7 @@
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
</x-td>
</tr>
</x-tr>
@endforeach
</x-table>
@else

View File

@ -16,14 +16,14 @@
<x-live id="live-databases">
@if (count($databases) > 0)
<x-table>
<tr>
<x-tr>
<x-th>{{ __("Name") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
@foreach ($databases as $database)
<tr>
<x-tr>
<x-td>{{ $database->name }}</x-td>
<x-td>
<x-datetime :value="$database->created_at" />
@ -40,7 +40,7 @@
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
</x-td>
</tr>
</x-tr>
@endforeach
</x-table>
@else

View File

@ -20,7 +20,7 @@
<x-live id="live-database-users">
@if (count($databaseUsers) > 0)
<x-table>
<tr>
<x-tr>
<x-th>{{ __("Username") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th class="flex items-center">
@ -29,9 +29,9 @@
</x-th>
<x-th>{{ __("Status") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
@foreach ($databaseUsers as $databaseUser)
<tr>
<x-tr>
<x-td>{{ $databaseUser->username }}</x-td>
<x-td>
<x-datetime :value="$databaseUser->created_at" />
@ -63,7 +63,7 @@
<x-heroicon name="o-trash" class="h-5 w-5" />
</x-icon-button>
</x-td>
</tr>
</x-tr>
@endforeach
</x-table>
@else

View File

@ -29,182 +29,37 @@
@vite(["resources/css/app.css", "resources/js/app.js"])
</head>
<body
class="min-h-screen min-w-max bg-gray-100 font-sans antialiased dark:bg-gray-900 dark:text-gray-300"
x-data=""
x-cloak
>
<div class="flex min-h-screen">
<div
class="left-0 top-0 min-h-screen w-64 flex-none bg-gray-800 p-3 dark:border-r-2 dark:border-gray-800 dark:bg-gray-800/50"
>
<div class="block h-16">
<div class="flex items-center justify-start text-2xl font-extrabold text-white">
<x-application-logo class="h-7 w-7 rounded-md" />
<span class="ml-1">Deploy</span>
</div>
</div>
<body class="bg-gray-50 font-sans antialiased dark:bg-gray-900 dark:text-gray-300" x-data="" x-cloak>
@include("layouts.partials.search")
<div class="mb-5">
<div class="text-sm font-semibold uppercase text-gray-300">
{{ __("Projects") }}
</div>
<div class="mt-2">
@include("layouts.partials.project-select", ["project" => auth()->user()->currentProject])
</div>
<div class="mt-5 text-sm font-semibold uppercase text-gray-300">
{{ __("Servers") }}
</div>
<div class="mt-2">
@include("layouts.partials.server-select", ["server" => $server ?? null])
</div>
@if (isset($server))
<div class="mt-3 space-y-1">
<x-sidebar-link
:href="route('servers.show', ['server' => $server])"
:active="request()->routeIs('servers.show')"
>
<x-heroicon name="o-home" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Overview") }}
</span>
</x-sidebar-link>
@if ($server->webserver())
<x-sidebar-link
:href="route('servers.sites', ['server' => $server])"
:active="request()->routeIs('servers.sites') || request()->is('servers/*/sites/*')"
>
<x-heroicon name="o-globe-alt" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Sites") }}
</span>
</x-sidebar-link>
@endif
@if ($server->database())
<x-sidebar-link
:href="route('servers.databases', ['server' => $server])"
:active="request()->routeIs('servers.databases') ||
request()->routeIs('servers.databases.backups')"
>
<x-heroicon name="o-circle-stack" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Databases") }}
</span>
</x-sidebar-link>
@endif
@if ($server->php())
<x-sidebar-link
:href="route('servers.php', ['server' => $server])"
:active="request()->routeIs('servers.php')"
>
<x-heroicon name="o-code-bracket" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("PHP") }}
</span>
</x-sidebar-link>
@endif
@if ($server->firewall())
<x-sidebar-link
:href="route('servers.firewall', ['server' => $server])"
:active="request()->routeIs('servers.firewall')"
>
<x-heroicon name="o-fire" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Firewall") }}
</span>
</x-sidebar-link>
@endif
<x-sidebar-link
:href="route('servers.cronjobs', ['server' => $server])"
:active="request()->routeIs('servers.cronjobs')"
>
<x-heroicon name="o-clock" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Cronjobs") }}
</span>
</x-sidebar-link>
<x-sidebar-link
:href="route('servers.ssh-keys', ['server' => $server])"
:active="request()->routeIs('servers.ssh-keys')"
>
<x-heroicon name="o-key" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("SSH Keys") }}
</span>
</x-sidebar-link>
<x-sidebar-link
:href="route('servers.services', ['server' => $server])"
:active="request()->routeIs('servers.services')"
>
<x-heroicon name="o-cog-6-tooth" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Services") }}
</span>
</x-sidebar-link>
<x-sidebar-link
:href="route('servers.settings', ['server' => $server])"
:active="request()->routeIs('servers.settings')"
>
<x-heroicon name="o-wrench-screwdriver" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Settings") }}
</span>
</x-sidebar-link>
<x-sidebar-link
:href="route('servers.logs', ['server' => $server])"
:active="request()->routeIs('servers.logs')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-6 w-6" />
<span class="ml-2 text-gray-50">
{{ __("Logs") }}
</span>
</x-sidebar-link>
</div>
@endif
</div>
</div>
@if (isset($sidebar))
<div
class="min-h-screen w-64 flex-none border-r border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
>
{{ $sidebar }}
</div>
@endif
<div class="flex min-h-screen flex-grow flex-col">
@include("layouts.navigation")
<!-- Page Heading -->
@include("layouts.sidebar")
<div class="mt-[64px] w-full"></div>
<div class="sm:ml-64">
@if (isset($header))
<header class="border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
<div class="mx-auto flex h-20 w-full max-w-full items-center justify-between px-8">
<header class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div class="mx-auto flex h-20 w-full max-w-full items-center justify-between px-5">
{{ $header }}
</div>
</header>
@endif
@if (isset($header2))
<header class="border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
<div class="mx-auto max-w-full px-8 py-6">
<header class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
<div class="mx-auto max-w-full px-5 py-6">
{{ $header2 }}
</div>
</header>
@endif
<!-- Page Content -->
<main class="px-8">
<div class="px-4 py-10">
{{ $slot }}
</main>
</div>
</div>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (

View File

@ -1,14 +1,79 @@
<nav x-data="{ open: false }" class="h-16 border-b border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
<div class="mx-auto max-w-full px-4 sm:px-6 lg:px-8">
<div class="flex h-16 items-center justify-between">
<div class="flex"></div>
<nav
class="fixed top-0 z-50 flex h-[64px] w-full items-center border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="w-full px-3 py-3 lg:px-5 lg:pl-3">
<div class="flex items-center justify-between">
<div class="flex items-center justify-start">
<button
data-drawer-target="logo-sidebar"
data-drawer-toggle="logo-sidebar"
aria-controls="logo-sidebar"
type="button"
class="inline-flex items-center rounded-md p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600 sm:hidden"
>
<span class="sr-only">Open sidebar</span>
<x-heroicon name="o-bars-3-center-left" class="h-6 w-6" />
</button>
<a href="/" class="ms-2 flex md:me-24">
<div class="flex items-center justify-start text-3xl font-extrabold">
<x-application-logo class="h-9 w-9 rounded-md" />
<span class="ml-1 hidden sm:block">Deploy</span>
</div>
</a>
<div class="h-[64px] w-1 border-r border-gray-200 px-3 dark:border-gray-700 md:px-0"></div>
<div class="ml-5 cursor-pointer" x-data="">
<div
class="flex w-full items-center rounded-md border border-gray-200 bg-gray-100 px-4 py-2 text-sm text-gray-900 focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:focus:ring-gray-600"
@click="$dispatch('open-search')"
>
<x-heroicon name="o-magnifying-glass" class="h-4 w-4" />
<span class="ml-2 hidden lg:block">Press / to Search</span>
</div>
</div>
</div>
<div class="flex items-center">
<div class="mr-3">
@include("layouts.partials.color-scheme")
</div>
<x-user-dropdown />
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button
class="flex rounded-full text-sm focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
>
<x-heroicon name="o-cog-6-tooth" class="h-8 w-8 rounded-full" />
</button>
</x-slot>
<x-slot name="content">
<div class="px-4 py-3" role="none">
<p class="text-sm text-gray-900 dark:text-white" role="none">
{{ auth()->user()->name }}
</p>
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-300" role="none">
{{ auth()->user()->email }}
</p>
</div>
<x-dropdown-link :href="route('profile')">
{{ __("Profile") }}
</x-dropdown-link>
<x-dropdown-link :href="route('projects')">
{{ __("Projects") }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route("logout") }}">
@csrf
<x-dropdown-link
:href="route('logout')"
onclick="event.preventDefault();this.closest('form').submit();"
>
{{ __("Log Out") }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
</div>
</div>

View File

@ -1,124 +1,35 @@
<div x-data="projectCombobox()">
<div class="relative">
<div data-tooltip="Project" class="cursor-pointer">
<x-dropdown align="left">
<x-slot:trigger>
<div>
<div
@click="open = !open"
class="text-md z-0 flex h-10 w-full cursor-pointer items-center rounded-md bg-gray-900 px-4 py-3 pr-10 leading-5 text-gray-100 focus:ring-1 focus:ring-gray-700"
x-text="selected.name ?? 'Select Project'"
></div>
<button type="button" @click="open = !open" class="absolute inset-y-0 right-0 z-0 flex items-center pr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
class="h-5 w-5 text-gray-400"
class="block w-full rounded-md border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
>
<path
fill-rule="evenodd"
d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
clip-rule="evenodd"
></path>
</svg>
{{ auth()->user()->currentProject?->name ?? __("Select Project") }}
</div>
<button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2">
<x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" />
</button>
<div
x-show="open"
@click.away="open = false"
class="absolute z-10 mt-1 w-full overflow-auto rounded-md bg-white pb-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-700 sm:text-sm"
>
<div class="relative p-2">
<input
x-model="query"
@input="filterProjectsAndOpen"
placeholder="Filter"
class="dark:focus:ring-800 w-full rounded-md bg-gray-200 py-2 pl-3 pr-10 text-sm leading-5 focus:ring-1 focus:ring-gray-400 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
<div class="relative max-h-[350px] overflow-y-auto">
<template x-for="(project, index) in filteredProjects" :key="index">
<div
@click="selectProject(project); open = false"
:class="project.id === selected.id ? 'cursor-default bg-primary-600 text-white' : 'cursor-pointer'"
class="relative select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white"
>
<span class="block truncate" x-text="project.name"></span>
<template x-if="project.id === selected.id">
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
class="h-5 w-5"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
></path>
</svg>
</x-slot>
<x-slot:content>
@foreach (auth()->user()->projects as $project)
<x-dropdown-link class="relative" :href="route('projects.switch', ['project' => $project])">
<span class="block truncate">{{ ucfirst($project->name) }}</span>
@if ($project->id == auth()->user()->current_project_id)
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-primary-600">
<x-heroicon name="o-check" class="h-5 w-5" />
</span>
</template>
</div>
</template>
</div>
<div
x-show="filteredProjects.length === 0"
class="relative block cursor-default select-none truncate px-4 py-2 text-gray-700 dark:text-white"
>
No projects found!
</div>
<div class="py-1">
<hr class="border-gray-300 dark:border-gray-600" />
</div>
<div>
<a
href="{{ route("projects") }}"
class="relative block cursor-pointer select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white"
>
<span class="block truncate">Projects List</span>
</a>
</div>
<div>
<a
href="{{ route("projects", ["create" => true]) }}"
class="relative block cursor-pointer select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white"
>
<span class="block truncate">Create a Project</span>
</a>
</div>
</div>
</div>
</div>
@endif
</x-dropdown-link>
@endforeach
<script>
function projectCombobox() {
const projects = @json(auth()->user()->projects()->select('id', 'name')->get());
return {
open: false,
query: '',
projects: projects,
selected: @if(isset($project)) @json($project->only('id', 'name')) @else {} @endif,
filteredProjects: projects,
selectProject(project) {
if (this.selected.id !== project.id) {
this.selected = project;
window.location.href = '{{ url('/settings/projects/switch') }}/' + project.id
}
},
filterProjectsAndOpen() {
if (this.query === '') {
this.filteredProjects = this.projects;
this.open = false;
} else {
this.filteredProjects = this.projects.filter((project) =>
project.name
.toLowerCase()
.replace(/\s+/g, '')
.includes(this.query.toLowerCase().replace(/\s+/g, ''))
);
this.open = true;
}
},
};
}
</script>
<x-dropdown-link href="{{ route('projects') }}">
{{ __("Projects List") }}
</x-dropdown-link>
<x-dropdown-link href="{{ route('projects', ['create' => 'open']) }}">
{{ __("Create a Project") }}
</x-dropdown-link>
</x-slot>
</x-dropdown>
</div>

View File

@ -0,0 +1,193 @@
<div
x-data="{
open: false,
isTyping: false,
focused: false,
searchQuery: '',
isLoading: false,
searchResult: [],
noResults: false,
showResults: false,
keyUp: false,
focusedItem: null,
init() {
document.onkeydown = (e) => {
if (e.key === '/') {
if (
! document.activeElement.type ||
document.activeElement.type === 'undefined'
) {
this.openSearch()
}
}
if (e.key === 'Escape') {
this.close()
} else {
this.changeFocus(e.code)
}
}
$watch(
'searchQuery',
this.debounce(() => {
this.isTyping = false
}, 700),
)
$watch('isTyping', (value) => {
if (! value && this.keyUp && this.searchQuery.length > 0) {
this.search(this.searchQuery)
}
if (! value && this.searchQuery.length === 0) {
this.showResults = false
this.searchResult = []
}
})
},
close() {
this.open = false
this.isLoading = false
this.focused = false
this.focusedItem = null
document.body.classList.remove('overflow-y-hidden')
},
openSearch() {
this.open = true
document.body.classList.add('overflow-y-hidden')
setTimeout(() => {
this.$refs.input.select()
}, 50)
},
search(searchQuery) {
this.showResults = true
this.noResults = false
this.isLoading = true
fetch('/search?q=' + searchQuery)
.then((response) => response.json())
.then((data) => {
this.searchResult = data.results
if (this.searchResult.length === 0) {
this.noResults = true
}
this.keyUp = false
})
.catch((error) => {
console.error('Error:', error)
})
.then(() => {
this.isLoading = false
})
},
keyEntered(e) {
this.keyUp = true
if (e.code === 'Escape') {
this.close()
}
this.changeFocus(e.code)
},
changeFocus(type) {
if (type === 'Backspace' && this.open && ! this.focused) {
setTimeout(() => {
this.$refs.input.select()
}, 100)
}
if (type === 'ArrowDown') {
if (this.searchResult.length > 0) {
if (this.focusedItem !== null) {
if (this.focusedItem < this.searchResult.length - 1) {
this.focusedItem++
}
} else {
this.focusedItem = 0
}
document.getElementById(`result-${this.focusedItem}`).focus()
}
}
if (type === 'ArrowUp') {
if (this.searchResult.length > 0) {
if (this.focusedItem !== null) {
if (this.focusedItem > 0) {
this.focusedItem--
}
document
.getElementById(`result-${this.focusedItem}`)
.focus()
}
}
}
},
debounce(func, wait, immediate) {
let timeout
return function () {
const context = this,
args = arguments
const later = function () {
timeout = null
if (! immediate) func.apply(context, args)
}
const callNow = immediate && ! timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(context, args)
}
},
}"
@open-search.window="openSearch"
>
<div x-show="open" class="absolute bottom-0 left-0 right-0 top-0 flex max-w-full items-start justify-center">
<div
x-on:click="close"
class="fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] items-center bg-gray-500 opacity-75 dark:bg-gray-900"
></div>
<div class="absolute z-[1000] mt-20 lg:scale-110">
<div class="w-[500px]">
<x-text-input
id="search-input"
x-ref="input"
@focus="focused = true"
@blur="focused = false"
type="text"
class="w-full"
@input="isTyping = true"
x-model="searchQuery"
placeholder="Search something..."
@keyup="keyEntered"
autocomplete="off"
></x-text-input>
<div
x-show="showResults"
class="list-none divide-y divide-gray-100 rounded-md bg-white py-1 text-base shadow ring-1 ring-black ring-opacity-5 dark:divide-gray-700/50 dark:bg-gray-800"
>
<div x-show="isLoading" class="w-full px-3 py-2 text-sm text-gray-800 dark:text-gray-300">
Please wait
</div>
<div
x-show="!isLoading && noResults"
class="w-full px-3 py-2 text-sm text-gray-800 dark:text-gray-300"
>
No results
</div>
<template x-for="(item, index) in searchResult">
<button
x-bind:id="`result-${index}`"
class="flex w-full items-center justify-between p-3 text-left text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-700/50 dark:focus:bg-gray-700"
x-on:click="window.location.href = item.url"
>
<div class="font-semibold text-primary-500" x-text="item.text"></div>
<div class="flex items-center">
<span
class="mr-1 rounded-xl bg-gray-100 px-2 text-gray-500 dark:bg-gray-700 dark:text-gray-400"
x-text="item.project"
data-tooltip="Project"
></span>
<span
class="rounded-xl bg-primary-100 px-2 text-primary-500 dark:bg-primary-500 dark:bg-opacity-10"
x-text="item.type"
data-tooltip="Type"
></span>
</div>
</button>
</template>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,124 +1,35 @@
<div x-data="serverCombobox()">
<div class="relative">
<div data-tooltip="Servers" class="cursor-pointer">
<x-dropdown width="full">
<x-slot:trigger>
<div>
<div
@click="open = !open"
class="text-md z-0 flex h-10 w-full cursor-pointer items-center rounded-md bg-gray-900 px-4 py-3 pr-10 leading-5 text-gray-100 focus:ring-1 focus:ring-gray-700"
x-text="selected.name ?? 'Select Server'"
></div>
<button type="button" @click="open = !open" class="absolute inset-y-0 right-0 z-0 flex items-center pr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
class="h-5 w-5 text-gray-400"
class="block w-full rounded-md border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
>
<path
fill-rule="evenodd"
d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z"
clip-rule="evenodd"
></path>
</svg>
{{ isset($server) ? $server->name : "Select Server" }}
</div>
<button type="button" class="absolute inset-y-0 right-0 flex items-center pr-2">
<x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" />
</button>
<div
x-show="open"
@click.away="open = false"
class="absolute z-10 mt-1 w-full overflow-auto rounded-md bg-white pb-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-700 sm:text-sm"
>
<div class="relative p-2">
<input
x-model="query"
@input="filterServersAndOpen"
placeholder="Filter"
class="dark:focus:ring-800 w-full rounded-md bg-gray-200 py-2 pl-3 pr-10 text-sm leading-5 focus:ring-1 focus:ring-gray-400 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
<div class="relative max-h-[350px] overflow-y-auto">
<template x-for="(server, index) in filteredServers" :key="index">
<div
@click="selectServer(server); open = false"
:class="server.id === selected.id ? 'cursor-default bg-primary-600 text-white' : 'cursor-pointer'"
class="relative select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white"
>
<span class="block truncate" x-text="server.name"></span>
<template x-if="server.id === selected.id">
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-white">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
class="h-5 w-5"
>
<path
fill-rule="evenodd"
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
clip-rule="evenodd"
></path>
</svg>
</x-slot>
<x-slot:content>
@foreach (auth()->user()->currentProject->servers as $s)
<x-dropdown-link class="relative" :href="route('servers.show', ['server' => $s])">
<span class="block truncate">{{ ucfirst($s->name) }}</span>
@if (isset($server) && $server->id == $s->id)
<span class="absolute inset-y-0 right-0 flex items-center pr-3 text-primary-600">
<x-heroicon name="o-check" class="h-5 w-5" />
</span>
</template>
</div>
</template>
</div>
<div
x-show="filteredServers.length === 0"
class="relative block cursor-default select-none truncate px-4 py-2 text-gray-700 dark:text-white"
>
No servers found!
</div>
<div class="py-1">
<hr class="border-gray-300 dark:border-gray-600" />
</div>
<div>
<a
href="{{ route("servers") }}"
class="@if(request()->routeIs('servers')) cursor-default bg-primary-600 text-white @else cursor-pointer @endif relative block select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white"
>
<span class="block truncate">Servers List</span>
</a>
</div>
<div>
<a
href="{{ route("servers.create") }}"
class="@if(request()->routeIs('servers.create')) cursor-default bg-primary-600 text-white @else cursor-pointer @endif relative block select-none px-4 py-2 text-gray-700 hover:bg-primary-600 hover:text-white dark:text-white"
>
<span class="block truncate">Create a Server</span>
</a>
</div>
</div>
</div>
</div>
@endif
</x-dropdown-link>
@endforeach
<script>
function serverCombobox() {
const servers = @json(auth()->user()->currentProject->servers()->select('id', 'name')->get());
return {
open: false,
query: '',
servers: servers,
selected: @if(isset($server)) @json($server->only('id', 'name')) @else {} @endif,
filteredServers: servers,
selectServer(server) {
if (this.selected.id !== server.id) {
this.selected = server;
window.location.href = '{{ url('/servers/') }}/' + server.id
}
},
filterServersAndOpen() {
if (this.query === '') {
this.filteredServers = this.servers;
this.open = false;
} else {
this.filteredServers = this.servers.filter((server) =>
server.name
.toLowerCase()
.replace(/\s+/g, '')
.includes(this.query.toLowerCase().replace(/\s+/g, ''))
);
this.open = true;
}
},
};
}
</script>
<x-dropdown-link href="{{ route('servers') }}">
{{ __("Servers List") }}
</x-dropdown-link>
<x-dropdown-link href="{{ route('servers.create') }}">
{{ __("Create a Server") }}
</x-dropdown-link>
</x-slot>
</x-dropdown>
</div>

View File

@ -3,147 +3,6 @@
<x-slot name="pageTitle">{{ $pageTitle }}</x-slot>
@endif
<x-slot name="sidebar">
<div class="flex h-16 items-center justify-center border-b border-gray-200 px-3 py-2 dark:border-gray-800">
<div class="w-full">
<span>Account Settings</span>
</div>
</div>
<div class="space-y-2 p-3">
<x-secondary-sidebar-link :href="route('profile')" :active="request()->routeIs('profile')">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
{{ __("Profile") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link :href="route('projects')" :active="request()->routeIs('projects')">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m7.875 14.25 1.214 1.942a2.25 2.25 0 0 0 1.908 1.058h2.006c.776 0 1.497-.4 1.908-1.058l1.214-1.942M2.41 9h4.636a2.25 2.25 0 0 1 1.872 1.002l.164.246a2.25 2.25 0 0 0 1.872 1.002h2.092a2.25 2.25 0 0 0 1.872-1.002l.164-.246A2.25 2.25 0 0 1 16.954 9h4.636M2.41 9a2.25 2.25 0 0 0-.16.832V12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 12V9.832c0-.287-.055-.57-.16-.832M2.41 9a2.25 2.25 0 0 1 .382-.632l3.285-3.832a2.25 2.25 0 0 1 1.708-.786h8.43c.657 0 1.281.287 1.709.786l3.284 3.832c.163.19.291.404.382.632M4.5 20.25h15A2.25 2.25 0 0 0 21.75 18v-2.625c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125V18a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
{{ __("Projects") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link
:href="route('server-providers')"
:active="request()->routeIs('server-providers')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
/>
</svg>
{{ __("Server Providers") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link
:href="route('source-controls')"
:active="request()->routeIs('source-controls')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
{{ __("Source Controls") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link
:href="route('storage-providers')"
:active="request()->routeIs('storage-providers')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"
/>
</svg>
{{ __("Storage Providers") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link
:href="route('notification-channels')"
:active="request()->routeIs('notification-channels')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0"
/>
</svg>
{{ __("Notification Channels") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link :href="route('ssh-keys')" :active="request()->routeIs('ssh-keys')">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-6 w-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
{{ __("SSH Keys") }}
</x-secondary-sidebar-link>
</div>
</x-slot>
<x-container class="flex">
<div class="w-full">
{{ $slot }}

View File

@ -0,0 +1,192 @@
<aside
id="logo-sidebar"
class="fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full border-r border-gray-200 bg-white pt-20 transition-transform dark:border-gray-700 dark:bg-gray-800 sm:translate-x-0"
aria-label="Sidebar"
>
<div class="h-full overflow-y-auto bg-white px-3 pb-4 dark:bg-gray-800">
<ul class="space-y-2 font-medium">
<li x-data="">
@include("layouts.partials.project-select")
</li>
<x-hr />
<li>
@include("layouts.partials.server-select")
</li>
<x-hr />
@if (isset($server))
<li>
<x-sidebar-link
:href="route('servers.show', ['server' => $server])"
:active="request()->routeIs('servers.show')"
>
<x-heroicon name="o-home" class="h-6 w-6" />
<span class="ml-2">Overview</span>
</x-sidebar-link>
</li>
@if ($server->webserver())
<li>
<x-sidebar-link
:href="route('servers.sites', ['server' => $server])"
:active="request()->routeIs('servers.sites') || request()->is('servers/*/sites/*')"
>
<x-heroicon name="o-globe-alt" class="h-6 w-6" />
<span class="ml-2">
{{ __("Sites") }}
</span>
</x-sidebar-link>
</li>
@endif
@if ($server->database())
<li>
<x-sidebar-link
:href="route('servers.databases', ['server' => $server])"
:active="request()->routeIs('servers.databases') ||
request()->routeIs('servers.databases.backups')"
>
<x-heroicon name="o-circle-stack" class="h-6 w-6" />
<span class="ml-2">
{{ __("Databases") }}
</span>
</x-sidebar-link>
</li>
@endif
@if ($server->php())
<li>
<x-sidebar-link
:href="route('servers.php', ['server' => $server])"
:active="request()->routeIs('servers.php')"
>
<x-heroicon name="o-code-bracket" class="h-6 w-6" />
<span class="ml-2">
{{ __("PHP") }}
</span>
</x-sidebar-link>
</li>
@endif
@if ($server->firewall())
<li>
<x-sidebar-link
:href="route('servers.firewall', ['server' => $server])"
:active="request()->routeIs('servers.firewall')"
>
<x-heroicon name="o-fire" class="h-6 w-6" />
<span class="ml-2">
{{ __("Firewall") }}
</span>
</x-sidebar-link>
</li>
@endif
<li>
<x-sidebar-link
:href="route('servers.cronjobs', ['server' => $server])"
:active="request()->routeIs('servers.cronjobs')"
>
<x-heroicon name="o-clock" class="h-6 w-6" />
<span class="ml-2">
{{ __("Cronjobs") }}
</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link
:href="route('servers.ssh-keys', ['server' => $server])"
:active="request()->routeIs('servers.ssh-keys')"
>
<x-heroicon name="o-key" class="h-6 w-6" />
<span class="ml-2">
{{ __("Server SSH Keys") }}
</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link
:href="route('servers.services', ['server' => $server])"
:active="request()->routeIs('servers.services')"
>
<x-heroicon name="o-cog-6-tooth" class="h-6 w-6" />
<span class="ml-2">
{{ __("Services") }}
</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link
:href="route('servers.settings', ['server' => $server])"
:active="request()->routeIs('servers.settings')"
>
<x-heroicon name="o-wrench-screwdriver" class="h-6 w-6" />
<span class="ml-2">
{{ __("Settings") }}
</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link
:href="route('servers.logs', ['server' => $server])"
:active="request()->routeIs('servers.logs')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-6 w-6" />
<span class="ml-2">
{{ __("Logs") }}
</span>
</x-sidebar-link>
</li>
<x-hr />
@endif
<li>
<x-sidebar-link :href="route('profile')" :active="request()->routeIs('profile')">
<x-heroicon name="o-user-circle" class="h-6 w-6" />
<span class="ml-2">Profile</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link :href="route('projects')" :active="request()->routeIs('projects')">
<x-heroicon name="o-inbox-stack" class="h-6 w-6" />
<span class="ml-2">Projects</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link :href="route('server-providers')" :active="request()->routeIs('server-providers')">
<x-heroicon name="o-server-stack" class="h-6 w-6" />
<span class="ml-2">Server Providers</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link :href="route('source-controls')" :active="request()->routeIs('source-controls')">
<x-heroicon name="o-code-bracket" class="h-6 w-6" />
<span class="ml-2">Source Controls</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link :href="route('storage-providers')" :active="request()->routeIs('storage-providers')">
<x-heroicon name="o-circle-stack" class="h-6 w-6" />
<span class="ml-2">Storage Providers</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link
:href="route('notification-channels')"
:active="request()->routeIs('notification-channels')"
>
<x-heroicon name="o-bell" class="h-6 w-6" />
<span class="ml-2">Notification Channels</span>
</x-sidebar-link>
</li>
<li>
<x-sidebar-link :href="route('ssh-keys')" :active="request()->routeIs('ssh-keys')">
<x-heroicon name="o-key" class="h-6 w-6" />
<span class="ml-2">SSH Keys</span>
</x-sidebar-link>
</li>
</ul>
</div>
</aside>

View File

@ -8,11 +8,111 @@
@endif
<x-slot name="header">
<h2 class="text-lg font-semibold">
<a href="{{ $site->getUrl() }}" target="_blank">
{{ $site->domain }}
</a>
</h2>
<div class="hidden md:flex md:items-center md:justify-start">
<x-tab-item
class="mr-1"
:href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.show')"
>
<x-heroicon name="o-globe-alt" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">Application</span>
</x-tab-item>
@if ($site->hasFeature(SiteFeature::SSL))
<x-tab-item
class="mr-1"
:href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.ssl')"
>
<x-heroicon name="o-lock-closed" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">SSL</span>
</x-tab-item>
@endif
@if ($site->hasFeature(SiteFeature::QUEUES))
<x-tab-item
class="mr-1"
:href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.queues')"
>
<x-heroicon name="o-queue-list" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">Queues</span>
</x-tab-item>
@endif
<x-tab-item
class="mr-1"
:href="route('servers.sites.settings', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.settings')"
>
<x-heroicon name="o-cog-6-tooth" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">Settings</span>
</x-tab-item>
<x-tab-item
class="mr-1"
:href="route('servers.sites.logs', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.logs')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-5 w-5" />
<span class="ml-2 hidden xl:block">Logs</span>
</x-tab-item>
</div>
<div class="md:hidden">
<x-dropdown align="left">
<x-slot name="trigger">
<div
class="flex w-full cursor-pointer items-center rounded-md border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500"
>
Select
<button type="button" class="ml-2">
<x-heroicon name="o-chevron-down" class="h-4 w-4 text-gray-400" />
</button>
</div>
</x-slot>
<x-slot name="content">
<x-dropdown-link
:href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.show')"
>
<x-heroicon name="o-globe-alt" class="h-5 w-5" />
<span class="ml-2">Application</span>
</x-dropdown-link>
@if ($site->hasFeature(SiteFeature::SSL))
<x-dropdown-link
:href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.ssl')"
>
<x-heroicon name="o-lock-closed" class="h-5 w-5" />
<span class="ml-2">SSL</span>
</x-dropdown-link>
@endif
@if ($site->hasFeature(SiteFeature::QUEUES))
<x-dropdown-link
:href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.queues')"
>
<x-heroicon name="o-queue-list" class="h-5 w-5" />
<span class="ml-2">Queues</span>
</x-dropdown-link>
@endif
<x-dropdown-link
:href="route('servers.sites.settings', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.settings')"
>
<x-heroicon name="o-cog-6-tooth" class="h-5 w-5" />
<span class="ml-2">Settings</span>
</x-dropdown-link>
<x-dropdown-link
:href="route('servers.sites.logs', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.logs')"
>
<x-heroicon name="o-square-3-stack-3d" class="h-5 w-5" />
<span class="ml-2">Logs</span>
</x-dropdown-link>
</x-slot>
</x-dropdown>
</div>
<div class="flex items-end">
<div class="flex h-20 flex-col items-end justify-center">
<div class="flex items-center">
@ -70,57 +170,6 @@ class="h-4 w-4 font-bold text-primary-600 dark:text-white"
</div>
</x-slot>
<x-slot name="sidebar">
<div class="flex h-16 items-center justify-center border-b border-gray-200 px-3 py-2 dark:border-gray-800">
<div class="w-full">
@include("layouts.partials.site-select", ["server" => $site->server, "site" => $site])
</div>
</div>
<div class="space-y-2 p-3">
<x-secondary-sidebar-link
:href="route('servers.sites.show', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.show')"
>
<x-heroicon name="o-globe-alt" class="mr-2 h-5 w-5" />
{{ __("Application") }}
</x-secondary-sidebar-link>
@if ($site->hasFeature(SiteFeature::SSL))
<x-secondary-sidebar-link
:href="route('servers.sites.ssl', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.ssl')"
>
<x-heroicon name="o-lock-closed" class="mr-2 h-5 w-5" />
{{ __("SSL") }}
</x-secondary-sidebar-link>
@endif
@if ($site->hasFeature(SiteFeature::QUEUES))
<x-secondary-sidebar-link
:href="route('servers.sites.queues', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.queues')"
>
<x-heroicon name="o-queue-list" class="mr-2 h-5 w-5" />
{{ __("Queues") }}
</x-secondary-sidebar-link>
@endif
<x-secondary-sidebar-link
:href="route('servers.sites.settings', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.settings')"
>
<x-heroicon name="o-cog-6-tooth" class="mr-2 h-5 w-5" />
{{ __("Settings") }}
</x-secondary-sidebar-link>
<x-secondary-sidebar-link
:href="route('servers.sites.logs', ['server' => $site->server, 'site' => $site])"
:active="request()->routeIs('servers.sites.logs')"
>
<x-heroicon name="o-square-3-stack-3d" class="mr-2 h-5 w-5" />
{{ __("Logs") }}
</x-secondary-sidebar-link>
</div>
</x-slot>
<x-container class="flex">
<div class="w-full space-y-10">
{{ $slot }}

View File

@ -18,13 +18,13 @@
</x-card-header>
<x-live id="live-server-logs">
<x-table>
<tr>
<x-tr>
<x-th>{{ __("Event") }}</x-th>
<x-th>{{ __("Date") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
@foreach ($logs as $log)
<tr>
<x-tr>
<x-td>{{ $log->type }}</x-td>
<x-td>
<x-datetime :value="$log->created_at" />
@ -39,7 +39,7 @@
<x-heroicon name="o-eye" class="h-5 w-5" />
</x-icon-button>
</x-td>
</tr>
</x-tr>
@endforeach
</x-table>
@if ($logs instanceof \Illuminate\Pagination\LengthAwarePaginator)

View File

@ -5,11 +5,11 @@
{{ __("Add or modify your ssh keys") }}
</x-slot>
<x-slot name="aside">
<div class="flex items-center">
<div>
<div class="flex flex-col items-end lg:flex-row lg:items-center">
<div class="mb-2 lg:mb-0 lg:mr-2">
@include("server-ssh-keys.partials.add-new-key")
</div>
<div class="ml-2">
<div>
@include("server-ssh-keys.partials.add-existing-key")
</div>
</div>

View File

@ -12,14 +12,14 @@
<div x-data="" class="space-y-3">
@if (count($ssls) > 0)
<x-table>
<tr>
<x-tr>
<x-th>{{ __("Type") }}</x-th>
<x-th>{{ __("Created") }}</x-th>
<x-th>{{ __("Expires at") }}</x-th>
<x-th></x-th>
</tr>
</x-tr>
@foreach ($ssls as $ssl)
<tr>
<x-tr>
<x-td>{{ $ssl->type }}</x-td>
<x-td>
<x-datetime :value="$ssl->created_at" />
@ -39,7 +39,7 @@
</div>
</div>
</x-td>
</tr>
</x-tr>
@endforeach
</x-table>
@else

View File

@ -22,6 +22,8 @@
Route::get('/', [ServerController::class, 'index'])->name('servers');
Route::get('/create', [ServerController::class, 'create'])->name('servers.create');
Route::post('/create', [ServerController::class, 'store'])->name('servers.create');
Route::middleware('select-current-project')->group(function () {
Route::get('/{server}', [ServerController::class, 'show'])->name('servers.show');
Route::delete('/{server}', [ServerController::class, 'delete'])->name('servers.delete');
@ -136,3 +138,4 @@
Route::get('/', [ServerLogController::class, 'index'])->name('servers.logs');
Route::get('/{serverLog}', [ServerLogController::class, 'show'])->name('servers.logs.show');
});
});

View File

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\SearchController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
@ -12,4 +13,6 @@
Route::prefix('/servers')->group(function () {
require __DIR__.'/server.php';
});
Route::get('/search', [SearchController::class, 'search'])->name('search');
});

View File

@ -1,14 +1,17 @@
const defaultTheme = require('tailwindcss/defaultTheme');
const colors = require("tailwindcss/colors");
import defaultTheme from 'tailwindcss/defaultTheme';
import forms from '@tailwindcss/forms';
import colors from "tailwindcss/colors";
import flowbite from 'flowbite/plugin';
/** @type {import('tailwindcss').Config} */
module.exports = {
export default {
darkMode: 'class',
content: [
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
'./resources/js/**/*.vue',
"./node_modules/flowbite/**/*.js"
],
theme: {
@ -16,16 +19,23 @@ module.exports = {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
colors: {
gray: colors.slate,
primary: colors.indigo
primary: colors.indigo,
green: colors.emerald,
},
variants: {
extend: {
border: ['last'],
}
},
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
forms,
flowbite({
charts: true
})
],
};

View File

@ -0,0 +1,58 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SearchTest extends TestCase
{
use RefreshDatabase;
public function test_search_server(): void
{
$this->actingAs($this->user);
$this->get(route('search', ['q' => $this->server->name]))
->assertOk()
->assertJson([
'results' => [
[
'type' => 'server',
'url' => route('servers.show', ['server' => $this->site->server]),
'text' => $this->server->name,
'project' => $this->server->project->name,
],
],
]);
}
public function test_search_site(): void
{
$this->actingAs($this->user);
$this->get(route('search', ['q' => $this->site->domain]))
->assertOk()
->assertJson([
'results' => [
[
'type' => 'site',
'url' => route('servers.sites.show', ['server' => $this->site->server, 'site' => $this->site]),
'text' => $this->site->domain,
'project' => $this->site->server->project->name,
],
],
]);
}
public function test_search_has_no_results(): void
{
$this->actingAs($this->user);
$this->get(route('search', ['q' => 'nothing-will-found']))
->assertOk()
->assertJson([
'results' => [],
]);
}
}

View File

@ -31,6 +31,7 @@ public function setUp(): void
config()->set('filesystems.disks.key-pairs.root', storage_path('app/key-pairs-test'));
$this->user = User::factory()->create();
$this->user->createDefaultProject();
\App\Models\NotificationChannel::factory()->create([
'provider' => NotificationChannel::EMAIL,
@ -60,6 +61,7 @@ private function setupServer(): void
{
$this->server = Server::factory()->create([
'user_id' => $this->user->id,
'project_id' => $this->user->current_project_id,
]);
$keys = $this->server->sshKey();