feat(search): implement global search functionality with caching and modal interface
This commit is contained in:
371
app/Livewire/GlobalSearch.php
Normal file
371
app/Livewire/GlobalSearch.php
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\Application;
|
||||||
|
use App\Models\Server;
|
||||||
|
use App\Models\Service;
|
||||||
|
use App\Models\StandaloneClickhouse;
|
||||||
|
use App\Models\StandaloneDragonfly;
|
||||||
|
use App\Models\StandaloneKeydb;
|
||||||
|
use App\Models\StandaloneMariadb;
|
||||||
|
use App\Models\StandaloneMongodb;
|
||||||
|
use App\Models\StandaloneMysql;
|
||||||
|
use App\Models\StandalonePostgresql;
|
||||||
|
use App\Models\StandaloneRedis;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class GlobalSearch extends Component
|
||||||
|
{
|
||||||
|
public $searchQuery = '';
|
||||||
|
|
||||||
|
public $isModalOpen = false;
|
||||||
|
|
||||||
|
public $searchResults = [];
|
||||||
|
|
||||||
|
public $allSearchableItems = [];
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
$this->searchQuery = '';
|
||||||
|
$this->isModalOpen = false;
|
||||||
|
$this->searchResults = [];
|
||||||
|
$this->allSearchableItems = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function openSearchModal()
|
||||||
|
{
|
||||||
|
$this->isModalOpen = true;
|
||||||
|
$this->loadSearchableItems();
|
||||||
|
$this->dispatch('search-modal-opened');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function closeSearchModal()
|
||||||
|
{
|
||||||
|
$this->isModalOpen = false;
|
||||||
|
$this->searchQuery = '';
|
||||||
|
$this->searchResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCacheKey($teamId)
|
||||||
|
{
|
||||||
|
return 'global_search_items_'.$teamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clearTeamCache($teamId)
|
||||||
|
{
|
||||||
|
Cache::forget(self::getCacheKey($teamId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatedSearchQuery()
|
||||||
|
{
|
||||||
|
$this->search();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadSearchableItems()
|
||||||
|
{
|
||||||
|
// Try to get from Redis cache first
|
||||||
|
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
|
||||||
|
|
||||||
|
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
|
||||||
|
$items = collect();
|
||||||
|
$team = auth()->user()->currentTeam();
|
||||||
|
|
||||||
|
// Get all applications
|
||||||
|
$applications = Application::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($app) {
|
||||||
|
// Collect all FQDNs from the application
|
||||||
|
$fqdns = collect([]);
|
||||||
|
|
||||||
|
// For regular applications
|
||||||
|
if ($app->fqdn) {
|
||||||
|
$fqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For docker compose based applications
|
||||||
|
if ($app->build_pack === 'dockercompose' && $app->docker_compose_domains) {
|
||||||
|
try {
|
||||||
|
$composeDomains = json_decode($app->docker_compose_domains, true);
|
||||||
|
if (is_array($composeDomains)) {
|
||||||
|
foreach ($composeDomains as $serviceName => $domains) {
|
||||||
|
if (is_array($domains)) {
|
||||||
|
$fqdns = $fqdns->merge($domains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Ignore JSON parsing errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fqdnsString = $fqdns->implode(' ');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $app->id,
|
||||||
|
'name' => $app->name,
|
||||||
|
'type' => 'application',
|
||||||
|
'uuid' => $app->uuid,
|
||||||
|
'description' => $app->description,
|
||||||
|
'link' => $app->link(),
|
||||||
|
'project' => $app->environment->project->name ?? null,
|
||||||
|
'environment' => $app->environment->name ?? null,
|
||||||
|
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
|
||||||
|
'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all services
|
||||||
|
$services = Service::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project', 'applications'])
|
||||||
|
->get()
|
||||||
|
->map(function ($service) {
|
||||||
|
// Collect all FQDNs from service applications
|
||||||
|
$fqdns = collect([]);
|
||||||
|
foreach ($service->applications as $app) {
|
||||||
|
if ($app->fqdn) {
|
||||||
|
$appFqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
|
||||||
|
$fqdns = $fqdns->merge($appFqdns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$fqdnsString = $fqdns->implode(' ');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $service->id,
|
||||||
|
'name' => $service->name,
|
||||||
|
'type' => 'service',
|
||||||
|
'uuid' => $service->uuid,
|
||||||
|
'description' => $service->description,
|
||||||
|
'link' => $service->link(),
|
||||||
|
'project' => $service->environment->project->name ?? null,
|
||||||
|
'environment' => $service->environment->name ?? null,
|
||||||
|
'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
|
||||||
|
'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all standalone databases
|
||||||
|
$databases = collect();
|
||||||
|
|
||||||
|
// PostgreSQL
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandalonePostgresql::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'postgresql',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' postgresql '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// MySQL
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneMysql::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'mysql',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' mysql '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// MariaDB
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneMariadb::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'mariadb',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' mariadb '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// MongoDB
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneMongodb::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'mongodb',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' mongodb '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Redis
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneRedis::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'redis',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' redis '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// KeyDB
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneKeydb::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'keydb',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' keydb '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dragonfly
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneDragonfly::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'dragonfly',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' dragonfly '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clickhouse
|
||||||
|
$databases = $databases->merge(
|
||||||
|
StandaloneClickhouse::ownedByCurrentTeam()
|
||||||
|
->with(['environment.project'])
|
||||||
|
->get()
|
||||||
|
->map(function ($db) {
|
||||||
|
return [
|
||||||
|
'id' => $db->id,
|
||||||
|
'name' => $db->name,
|
||||||
|
'type' => 'database',
|
||||||
|
'subtype' => 'clickhouse',
|
||||||
|
'uuid' => $db->uuid,
|
||||||
|
'description' => $db->description,
|
||||||
|
'link' => $db->link(),
|
||||||
|
'project' => $db->environment->project->name ?? null,
|
||||||
|
'environment' => $db->environment->name ?? null,
|
||||||
|
'search_text' => strtolower($db->name.' clickhouse '.$db->description),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all servers
|
||||||
|
$servers = Server::ownedByCurrentTeam()
|
||||||
|
->get()
|
||||||
|
->map(function ($server) {
|
||||||
|
return [
|
||||||
|
'id' => $server->id,
|
||||||
|
'name' => $server->name,
|
||||||
|
'type' => 'server',
|
||||||
|
'uuid' => $server->uuid,
|
||||||
|
'description' => $server->description,
|
||||||
|
'link' => $server->url(),
|
||||||
|
'project' => null,
|
||||||
|
'environment' => null,
|
||||||
|
'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge all collections
|
||||||
|
$items = $items->merge($applications)
|
||||||
|
->merge($services)
|
||||||
|
->merge($databases)
|
||||||
|
->merge($servers);
|
||||||
|
|
||||||
|
return $items->toArray();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function search()
|
||||||
|
{
|
||||||
|
if (strlen($this->searchQuery) < 2) {
|
||||||
|
$this->searchResults = [];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = strtolower($this->searchQuery);
|
||||||
|
|
||||||
|
// Case-insensitive search in the items
|
||||||
|
$this->searchResults = collect($this->allSearchableItems)
|
||||||
|
->filter(function ($item) use ($query) {
|
||||||
|
return str_contains($item['search_text'], $query);
|
||||||
|
})
|
||||||
|
->take(20)
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.global-search');
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ namespace App\Models;
|
|||||||
|
|
||||||
use App\Enums\ApplicationDeploymentStatus;
|
use App\Enums\ApplicationDeploymentStatus;
|
||||||
use App\Services\ConfigurationGenerator;
|
use App\Services\ConfigurationGenerator;
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasConfiguration;
|
use App\Traits\HasConfiguration;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
@@ -110,7 +111,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
|
|
||||||
class Application extends BaseModel
|
class Application extends BaseModel
|
||||||
{
|
{
|
||||||
use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
private static $parserVersion = '5';
|
private static $parserVersion = '5';
|
||||||
|
|
||||||
@@ -123,66 +124,6 @@ class Application extends BaseModel
|
|||||||
'http_basic_auth_password' => 'encrypted',
|
'http_basic_auth_password' => 'encrypted',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function customNetworkAliases(): Attribute
|
|
||||||
{
|
|
||||||
return Attribute::make(
|
|
||||||
set: function ($value) {
|
|
||||||
if (is_null($value) || $value === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's already a JSON string, decode it
|
|
||||||
if (is_string($value) && $this->isJson($value)) {
|
|
||||||
$value = json_decode($value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a string but not JSON, treat it as a comma-separated list
|
|
||||||
if (is_string($value) && ! is_array($value)) {
|
|
||||||
$value = explode(',', $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = collect($value)
|
|
||||||
->map(function ($alias) {
|
|
||||||
if (is_string($alias)) {
|
|
||||||
return str_replace(' ', '-', trim($alias));
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
->filter()
|
|
||||||
->unique() // Remove duplicate values
|
|
||||||
->values()
|
|
||||||
->toArray();
|
|
||||||
|
|
||||||
return empty($value) ? null : json_encode($value);
|
|
||||||
},
|
|
||||||
get: function ($value) {
|
|
||||||
if (is_null($value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_string($value) && $this->isJson($value)) {
|
|
||||||
return json_decode($value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_array($value) ? $value : [];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string is a valid JSON
|
|
||||||
*/
|
|
||||||
private function isJson($string)
|
|
||||||
{
|
|
||||||
if (! is_string($string)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
json_decode($string);
|
|
||||||
|
|
||||||
return json_last_error() === JSON_ERROR_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function booted()
|
protected static function booted()
|
||||||
{
|
{
|
||||||
static::addGlobalScope('withRelations', function ($builder) {
|
static::addGlobalScope('withRelations', function ($builder) {
|
||||||
@@ -250,6 +191,66 @@ class Application extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function customNetworkAliases(): Attribute
|
||||||
|
{
|
||||||
|
return Attribute::make(
|
||||||
|
set: function ($value) {
|
||||||
|
if (is_null($value) || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already a JSON string, decode it
|
||||||
|
if (is_string($value) && $this->isJson($value)) {
|
||||||
|
$value = json_decode($value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a string but not JSON, treat it as a comma-separated list
|
||||||
|
if (is_string($value) && ! is_array($value)) {
|
||||||
|
$value = explode(',', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = collect($value)
|
||||||
|
->map(function ($alias) {
|
||||||
|
if (is_string($alias)) {
|
||||||
|
return str_replace(' ', '-', trim($alias));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
->filter()
|
||||||
|
->unique() // Remove duplicate values
|
||||||
|
->values()
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return empty($value) ? null : json_encode($value);
|
||||||
|
},
|
||||||
|
get: function ($value) {
|
||||||
|
if (is_null($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && $this->isJson($value)) {
|
||||||
|
return json_decode($value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($value) ? $value : [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is a valid JSON
|
||||||
|
*/
|
||||||
|
private function isJson($string)
|
||||||
|
{
|
||||||
|
if (! is_string($string)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
json_decode($string);
|
||||||
|
|
||||||
|
return json_last_error() === JSON_ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||||
{
|
{
|
||||||
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
|
||||||
|
@@ -13,6 +13,7 @@ use App\Jobs\RegenerateSslCertJob;
|
|||||||
use App\Notifications\Server\Reachable;
|
use App\Notifications\Server\Reachable;
|
||||||
use App\Notifications\Server\Unreachable;
|
use App\Notifications\Server\Unreachable;
|
||||||
use App\Services\ConfigurationRepository;
|
use App\Services\ConfigurationRepository;
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
@@ -55,7 +56,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
|
|
||||||
class Server extends BaseModel
|
class Server extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
||||||
|
|
||||||
public static $batch_counter = 0;
|
public static $batch_counter = 0;
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Enums\ProcessStatus;
|
use App\Enums\ProcessStatus;
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -41,7 +42,7 @@ use Visus\Cuid2\Cuid2;
|
|||||||
)]
|
)]
|
||||||
class Service extends BaseModel
|
class Service extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
private static $parserVersion = '5';
|
private static $parserVersion = '5';
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneClickhouse extends BaseModel
|
class StandaloneClickhouse extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -43,6 +44,11 @@ class StandaloneClickhouse extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneDragonfly extends BaseModel
|
class StandaloneDragonfly extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -43,6 +44,11 @@ class StandaloneDragonfly extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneKeydb extends BaseModel
|
class StandaloneKeydb extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -43,6 +44,11 @@ class StandaloneKeydb extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneMariadb extends BaseModel
|
class StandaloneMariadb extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ class StandaloneMariadb extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneMongodb extends BaseModel
|
class StandaloneMongodb extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -46,6 +47,11 @@ class StandaloneMongodb extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneMysql extends BaseModel
|
class StandaloneMysql extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ class StandaloneMysql extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandalonePostgresql extends BaseModel
|
class StandalonePostgresql extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ class StandalonePostgresql extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
public function workdir()
|
public function workdir()
|
||||||
{
|
{
|
||||||
return database_configuration_dir()."/{$this->uuid}";
|
return database_configuration_dir()."/{$this->uuid}";
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Traits\ClearsGlobalSearchCache;
|
||||||
use App\Traits\HasSafeStringAttribute;
|
use App\Traits\HasSafeStringAttribute;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
@@ -9,7 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
|||||||
|
|
||||||
class StandaloneRedis extends BaseModel
|
class StandaloneRedis extends BaseModel
|
||||||
{
|
{
|
||||||
use HasFactory, HasSafeStringAttribute, SoftDeletes;
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
@@ -45,6 +46,11 @@ class StandaloneRedis extends BaseModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function ownedByCurrentTeam()
|
||||||
|
{
|
||||||
|
return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||||
|
}
|
||||||
|
|
||||||
protected function serverStatus(): Attribute
|
protected function serverStatus(): Attribute
|
||||||
{
|
{
|
||||||
return Attribute::make(
|
return Attribute::make(
|
||||||
|
53
app/Traits/ClearsGlobalSearchCache.php
Normal file
53
app/Traits/ClearsGlobalSearchCache.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
use App\Livewire\GlobalSearch;
|
||||||
|
|
||||||
|
trait ClearsGlobalSearchCache
|
||||||
|
{
|
||||||
|
protected static function bootClearsGlobalSearchCache()
|
||||||
|
{
|
||||||
|
static::saved(function ($model) {
|
||||||
|
// Clear search cache when model is saved
|
||||||
|
$teamId = $model->getTeamIdForCache();
|
||||||
|
if (filled($teamId)) {
|
||||||
|
GlobalSearch::clearTeamCache($teamId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static::created(function ($model) {
|
||||||
|
// Clear search cache when model is created
|
||||||
|
$teamId = $model->getTeamIdForCache();
|
||||||
|
if (filled($teamId)) {
|
||||||
|
GlobalSearch::clearTeamCache($teamId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static::deleted(function ($model) {
|
||||||
|
// Clear search cache when model is deleted
|
||||||
|
$teamId = $model->getTeamIdForCache();
|
||||||
|
if (filled($teamId)) {
|
||||||
|
GlobalSearch::clearTeamCache($teamId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTeamIdForCache()
|
||||||
|
{
|
||||||
|
// For database models, team is accessed through environment.project.team
|
||||||
|
if (method_exists($this, 'team')) {
|
||||||
|
$team = $this->team();
|
||||||
|
if (filled($team)) {
|
||||||
|
return is_object($team) ? $team->id : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For models with direct team_id property
|
||||||
|
if (property_exists($this, 'team_id') || isset($this->team_id)) {
|
||||||
|
return $this->team_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@@ -59,20 +59,20 @@
|
|||||||
if (this.zoom === '90') {
|
if (this.zoom === '90') {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
html {
|
|
||||||
font-size: 93.75%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--vh: 1vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
html {
|
html {
|
||||||
font-size: 87.5%;
|
font-size: 93.75%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
`;
|
:root {
|
||||||
|
--vh: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
html {
|
||||||
|
font-size: 87.5%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +82,9 @@
|
|||||||
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
|
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||||
<x-version />
|
<x-version />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<livewire:global-search />
|
||||||
|
</div>
|
||||||
<livewire:settings-dropdown />
|
<livewire:settings-dropdown />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 pt-2 pb-7">
|
<div class="px-2 pt-2 pb-7">
|
||||||
|
236
resources/views/livewire/global-search.blade.php
Normal file
236
resources/views/livewire/global-search.blade.php
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
<div x-data="{
|
||||||
|
modalOpen: false,
|
||||||
|
selectedIndex: -1,
|
||||||
|
openModal() {
|
||||||
|
this.modalOpen = true;
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
@this.openSearchModal();
|
||||||
|
},
|
||||||
|
closeModal() {
|
||||||
|
this.modalOpen = false;
|
||||||
|
this.selectedIndex = -1;
|
||||||
|
@this.closeSearchModal();
|
||||||
|
},
|
||||||
|
navigateResults(direction) {
|
||||||
|
const results = document.querySelectorAll('.search-result-item');
|
||||||
|
if (results.length === 0) return;
|
||||||
|
|
||||||
|
if (direction === 'down') {
|
||||||
|
this.selectedIndex = Math.min(this.selectedIndex + 1, results.length - 1);
|
||||||
|
} else if (direction === 'up') {
|
||||||
|
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedIndex >= 0 && this.selectedIndex < results.length) {
|
||||||
|
results[this.selectedIndex].focus();
|
||||||
|
results[this.selectedIndex].scrollIntoView({ block: 'nearest' });
|
||||||
|
} else if (this.selectedIndex === -1) {
|
||||||
|
this.$refs.searchInput?.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
// Listen for / key press globally
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) && !this.modalOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.openModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for Cmd+K or Ctrl+K globally
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.modalOpen) {
|
||||||
|
this.closeModal();
|
||||||
|
} else {
|
||||||
|
this.openModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for Escape key to close modal
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && this.modalOpen) {
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for arrow keys when modal is open
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (!this.modalOpen) return;
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.navigateResults('down');
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
this.navigateResults('up');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<!-- Search bar in navbar -->
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button @click="openModal()" type="button" title="Search (Press / or ⌘K)"
|
||||||
|
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
<kbd
|
||||||
|
class="px-1 py-0.5 text-xs font-semibold text-neutral-500 dark:text-neutral-400 bg-neutral-200 dark:bg-coolgray-200 rounded">/</kbd>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal overlay -->
|
||||||
|
<template x-teleport="body">
|
||||||
|
<div x-show="modalOpen" x-cloak
|
||||||
|
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen">
|
||||||
|
<div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs">
|
||||||
|
</div>
|
||||||
|
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||||
|
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave="ease-in duration-100"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||||
|
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300"
|
||||||
|
@click.stop>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center pb-3">
|
||||||
|
<h3 class="pr-8 text-2xl font-bold">Search</h3>
|
||||||
|
<button @click="closeModal()"
|
||||||
|
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||||
|
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-auto">
|
||||||
|
<input type="text" wire:model.live.debounce.500ms="searchQuery"
|
||||||
|
placeholder="Type to search for applications, services, databases, and servers..."
|
||||||
|
x-ref="searchInput" x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })" class="w-full input mb-4" />
|
||||||
|
|
||||||
|
<!-- Search results -->
|
||||||
|
<div class="relative min-h-[330px] max-h-[400px] overflow-y-auto scrollbar">
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div wire:loading.flex wire:target="searchQuery"
|
||||||
|
class="min-h-[330px] items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<svg class="animate-spin mx-auto h-8 w-8 text-neutral-400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10"
|
||||||
|
stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Searching...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results content - hidden while loading -->
|
||||||
|
<div wire:loading.remove wire:target="searchQuery">
|
||||||
|
@if (strlen($searchQuery) >= 2 && count($searchResults) > 0)
|
||||||
|
<div class="space-y-1 my-4 pb-4">
|
||||||
|
@foreach ($searchResults as $index => $result)
|
||||||
|
<a href="{{ $result['link'] ?? '#' }}"
|
||||||
|
class="search-result-item block p-3 mx-1 hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:ring-1 focus:ring-coollabs focus:bg-neutral-100 dark:focus:bg-coolgray-200 ">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium text-neutral-900 dark:text-white">
|
||||||
|
{{ $result['name'] }}
|
||||||
|
</span>
|
||||||
|
@if ($result['type'] === 'server')
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
|
||||||
|
Server
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@if (!empty($result['project']) && !empty($result['environment']))
|
||||||
|
<span
|
||||||
|
class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||||
|
{{ $result['project'] }} / {{ $result['environment'] }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if ($result['type'] === 'application')
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
|
||||||
|
Application
|
||||||
|
</span>
|
||||||
|
@elseif ($result['type'] === 'service')
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
|
||||||
|
Service
|
||||||
|
</span>
|
||||||
|
@elseif ($result['type'] === 'database')
|
||||||
|
<span
|
||||||
|
class="px-2 py-0.5 text-xs rounded bg-coolgray-100 text-white">
|
||||||
|
{{ ucfirst($result['subtype'] ?? 'Database') }}
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if (!empty($result['description']))
|
||||||
|
<div
|
||||||
|
class="text-sm text-neutral-600 dark:text-neutral-400 mt-0.5">
|
||||||
|
{{ Str::limit($result['description'], 100) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="shrink-0 ml-2 h-4 w-4 text-neutral-400" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0)
|
||||||
|
<div class="flex items-center justify-center min-h-[330px]">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
No results found for "<strong>{{ $searchQuery }}</strong>"
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">
|
||||||
|
Try different keywords or check the spelling
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif (strlen($searchQuery) > 0 && strlen($searchQuery) < 2)
|
||||||
|
<div class="flex items-center justify-center min-h-[330px]">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Type at least 2 characters to search
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex items-center justify-center min-h-[330px]">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
Start typing to search
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-neutral-500 dark:text-neutral-500 mt-2">
|
||||||
|
Search for applications, services, databases, and servers
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
Reference in New Issue
Block a user