mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-28 02:06:37 -04:00
feat(global-search): integrate projects and environments into global search functionality
- Added retrieval and mapping of projects and environments to the global search results. - Enhanced search result structure to include resource counts and descriptions for projects and environments. - Updated the UI to reflect the new search capabilities, improving user experience when searching for resources.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
@@ -335,11 +337,81 @@ class GlobalSearch extends Component
|
||||
];
|
||||
});
|
||||
|
||||
// Get all projects
|
||||
$projects = Project::ownedByCurrentTeam()
|
||||
->withCount(['environments', 'applications', 'services'])
|
||||
->get()
|
||||
->map(function ($project) {
|
||||
$resourceCount = $project->applications_count + $project->services_count;
|
||||
$resourceSummary = $resourceCount > 0
|
||||
? "{$resourceCount} resource".($resourceCount !== 1 ? 's' : '')
|
||||
: 'No resources';
|
||||
|
||||
return [
|
||||
'id' => $project->id,
|
||||
'name' => $project->name,
|
||||
'type' => 'project',
|
||||
'uuid' => $project->uuid,
|
||||
'description' => $project->description,
|
||||
'link' => $project->navigateTo(),
|
||||
'project' => null,
|
||||
'environment' => null,
|
||||
'resource_count' => $resourceSummary,
|
||||
'environment_count' => $project->environments_count,
|
||||
'search_text' => strtolower($project->name.' '.$project->description.' project'),
|
||||
];
|
||||
});
|
||||
|
||||
// Get all environments
|
||||
$environments = Environment::query()
|
||||
->whereHas('project', function ($query) {
|
||||
$query->where('team_id', auth()->user()->currentTeam()->id);
|
||||
})
|
||||
->with('project')
|
||||
->withCount(['applications', 'services'])
|
||||
->get()
|
||||
->map(function ($environment) {
|
||||
$resourceCount = $environment->applications_count + $environment->services_count;
|
||||
$resourceSummary = $resourceCount > 0
|
||||
? "{$resourceCount} resource".($resourceCount !== 1 ? 's' : '')
|
||||
: 'No resources';
|
||||
|
||||
// Build description with project context
|
||||
$descriptionParts = [];
|
||||
if ($environment->project) {
|
||||
$descriptionParts[] = "Project: {$environment->project->name}";
|
||||
}
|
||||
if ($environment->description) {
|
||||
$descriptionParts[] = $environment->description;
|
||||
}
|
||||
if (empty($descriptionParts)) {
|
||||
$descriptionParts[] = $resourceSummary;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $environment->id,
|
||||
'name' => $environment->name,
|
||||
'type' => 'environment',
|
||||
'uuid' => $environment->uuid,
|
||||
'description' => implode(' • ', $descriptionParts),
|
||||
'link' => route('project.resource.index', [
|
||||
'project_uuid' => $environment->project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
]),
|
||||
'project' => $environment->project->name ?? null,
|
||||
'environment' => null,
|
||||
'resource_count' => $resourceSummary,
|
||||
'search_text' => strtolower($environment->name.' '.$environment->description.' '.$environment->project->name.' environment'),
|
||||
];
|
||||
});
|
||||
|
||||
// Merge all collections
|
||||
$items = $items->merge($applications)
|
||||
->merge($services)
|
||||
->merge($databases)
|
||||
->merge($servers);
|
||||
->merge($servers)
|
||||
->merge($projects)
|
||||
->merge($environments);
|
||||
|
||||
return $items->toArray();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
@@ -19,6 +20,7 @@ use OpenApi\Attributes as OA;
|
||||
)]
|
||||
class Environment extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache;
|
||||
use HasSafeStringAttribute;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -24,6 +25,7 @@ use Visus\Cuid2\Cuid2;
|
||||
)]
|
||||
class Project extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache;
|
||||
use HasSafeStringAttribute;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -10,77 +10,119 @@ trait ClearsGlobalSearchCache
|
||||
protected static function bootClearsGlobalSearchCache()
|
||||
{
|
||||
static::saving(function ($model) {
|
||||
// Only clear cache if searchable fields are being changed
|
||||
if ($model->hasSearchableChanges()) {
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
try {
|
||||
// Only clear cache if searchable fields are being changed
|
||||
if ($model->hasSearchableChanges()) {
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently fail cache clearing - don't break the save operation
|
||||
ray('Failed to clear global search cache on saving: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
static::created(function ($model) {
|
||||
// Always clear cache when model is created
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
try {
|
||||
// Always clear cache when model is created
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently fail cache clearing - don't break the create operation
|
||||
ray('Failed to clear global search cache on creation: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
static::deleted(function ($model) {
|
||||
// Always clear cache when model is deleted
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
try {
|
||||
// Always clear cache when model is deleted
|
||||
$teamId = $model->getTeamIdForCache();
|
||||
if (filled($teamId)) {
|
||||
GlobalSearch::clearTeamCache($teamId);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Silently fail cache clearing - don't break the delete operation
|
||||
ray('Failed to clear global search cache on deletion: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function hasSearchableChanges(): bool
|
||||
{
|
||||
// Define searchable fields based on model type
|
||||
$searchableFields = ['name', 'description'];
|
||||
try {
|
||||
// Define searchable fields based on model type
|
||||
$searchableFields = ['name', 'description'];
|
||||
|
||||
// Add model-specific searchable fields
|
||||
if ($this instanceof \App\Models\Application) {
|
||||
$searchableFields[] = 'fqdn';
|
||||
$searchableFields[] = 'docker_compose_domains';
|
||||
} elseif ($this instanceof \App\Models\Server) {
|
||||
$searchableFields[] = 'ip';
|
||||
} elseif ($this instanceof \App\Models\Service) {
|
||||
// Services don't have direct fqdn, but name and description are covered
|
||||
}
|
||||
// Database models only have name and description as searchable
|
||||
|
||||
// Check if any searchable field is dirty
|
||||
foreach ($searchableFields as $field) {
|
||||
if ($this->isDirty($field)) {
|
||||
return true;
|
||||
// Add model-specific searchable fields
|
||||
if ($this instanceof \App\Models\Application) {
|
||||
$searchableFields[] = 'fqdn';
|
||||
$searchableFields[] = 'docker_compose_domains';
|
||||
} elseif ($this instanceof \App\Models\Server) {
|
||||
$searchableFields[] = 'ip';
|
||||
} elseif ($this instanceof \App\Models\Service) {
|
||||
// Services don't have direct fqdn, but name and description are covered
|
||||
} elseif ($this instanceof \App\Models\Project || $this instanceof \App\Models\Environment) {
|
||||
// Projects and environments only have name and description as searchable
|
||||
}
|
||||
}
|
||||
// Database models only have name and description as searchable
|
||||
|
||||
return false;
|
||||
// Check if any searchable field is dirty
|
||||
foreach ($searchableFields as $field) {
|
||||
// Check if attribute exists before checking if dirty
|
||||
if (array_key_exists($field, $this->getAttributes()) && $this->isDirty($field)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
// If checking changes fails, assume changes exist to be safe
|
||||
ray('Failed to check searchable changes: '.$e->getMessage());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getTeamIdForCache()
|
||||
{
|
||||
// For database models, team is accessed through environment.project.team
|
||||
if (method_exists($this, 'team')) {
|
||||
if ($this instanceof \App\Models\Server) {
|
||||
$team = $this->team;
|
||||
} else {
|
||||
$team = $this->team();
|
||||
try {
|
||||
// For Project models (has direct team_id)
|
||||
if ($this instanceof \App\Models\Project) {
|
||||
return $this->team_id ?? null;
|
||||
}
|
||||
if (filled($team)) {
|
||||
return is_object($team) ? $team->id : null;
|
||||
|
||||
// For Environment models (get team_id through project)
|
||||
if ($this instanceof \App\Models\Environment) {
|
||||
return $this->project?->team_id;
|
||||
}
|
||||
}
|
||||
|
||||
// For models with direct team_id property
|
||||
if (property_exists($this, 'team_id') || isset($this->team_id)) {
|
||||
return $this->team_id;
|
||||
}
|
||||
// For database models, team is accessed through environment.project.team
|
||||
if (method_exists($this, 'team')) {
|
||||
if ($this instanceof \App\Models\Server) {
|
||||
$team = $this->team;
|
||||
} else {
|
||||
$team = $this->team();
|
||||
}
|
||||
if (filled($team)) {
|
||||
return is_object($team) ? $team->id : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// For models with direct team_id property
|
||||
if (property_exists($this, 'team_id') || isset($this->team_id)) {
|
||||
return $this->team_id ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Throwable $e) {
|
||||
// If we can't determine team ID, return null
|
||||
ray('Failed to get team ID for cache: '.$e->getMessage());
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user