feat(canvas): add interactive resource views and team avatars (#12160)

This commit is contained in:
Andras Bacsai
2026-10-14 09:28:10 +02:00
committed by GitHub
parent 448dc8ea0a
commit 33ee09fa87
146 changed files with 12076 additions and 367 deletions
+18
View File
@@ -6,6 +6,7 @@ use App\Traits\Auditable;
use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -33,6 +34,18 @@ class Environment extends BaseModel
'uuid',
];
/**
* Moving cards on the environment canvas is layout, not a change worth an audit event.
*/
protected array $auditExclude = ['canvas_positions'];
protected function casts(): array
{
return [
'canvas_positions' => 'array',
];
}
protected static function booted()
{
static::deleting(function ($environment) {
@@ -148,6 +161,11 @@ class Environment extends BaseModel
return $this->hasMany(Service::class);
}
public function resourceLinks(): HasMany
{
return $this->hasMany(ResourceLink::class);
}
protected function customizeName($value)
{
return str($value)->lower()->trim()->replace('/', '-')->toString();
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* A visual-only link between two resources of one environment on the environment canvas.
*/
class ResourceLink extends BaseModel
{
protected $fillable = [
'environment_id',
'source_type',
'source_id',
'target_type',
'target_id',
'label',
'created_by',
];
public function environment(): BelongsTo
{
return $this->belongsTo(Environment::class);
}
public function source(): MorphTo
{
return $this->morphTo();
}
public function target(): MorphTo
{
return $this->morphTo();
}
}
+9
View File
@@ -255,6 +255,15 @@ class Service extends BaseModel
return 'starting:unhealthy';
}
return $this->containerStatus();
}
/**
* The aggregate status of the stored container statuses, without the running start check.
* Callers that check starting services in bulk use it to avoid a query per service.
*/
public function containerStatus(): ?string
{
$applications = $this->applications;
$databases = $this->databases;
-8
View File
@@ -4,7 +4,6 @@ namespace App\Models;
use App\Traits\Auditable;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Support\Facades\DB;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -36,13 +35,6 @@ class Tag extends BaseModel
return Tag::whereTeamId(currentTeam()->id)->orderBy('name');
}
public function deleteIfOrphaned(): void
{
if (DB::table('taggables')->where('tag_id', $this->id)->doesntExist()) {
$this->delete();
}
}
public function applications()
{
return $this->morphedByMany(Application::class, 'taggable');
+5
View File
@@ -10,6 +10,7 @@ use App\Notifications\Channels\SendsEmail;
use App\Notifications\Channels\SendsPushover;
use App\Notifications\Channels\SendsSlack;
use App\Notifications\Server\Unreachable;
use App\Services\TeamAvatarStorageService;
use App\Traits\Auditable;
use App\Traits\HasNotificationSettings;
use App\Traits\HasSafeStringAttribute;
@@ -123,6 +124,10 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
$s3->delete();
}
});
static::deleted(function (Team $team) {
app(TeamAvatarStorageService::class)->deleteTeamFile($team);
});
}
public static function serverLimitReached(?Team $team = null)