From d371f0ed28f14d7700b668db364a369d4c19d30b Mon Sep 17 00:00:00 2001
From: ShadowArcanist <162910371+ShadowArcanist@users.noreply.github.com>
Date: Thu, 23 Apr 2026 16:13:09 +0530
Subject: [PATCH 1/2] feat(destination): show resources that are deployed on
the destination
---
app/Livewire/Destination/Show.php | 62 +++++++++++++++++++
app/Models/StandaloneDocker.php | 5 +-
.../views/livewire/destination/show.blade.php | 20 ++++++
3 files changed, 86 insertions(+), 1 deletion(-)
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index 9d55d7462..11afbbccc 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -24,6 +24,8 @@ class Show extends Component
#[Validate(['string', 'required'])]
public string $serverIp;
+ public array $resources = [];
+
public function mount(string $destination_uuid)
{
try {
@@ -33,11 +35,71 @@ class Show extends Component
}
$this->destination = $destination;
$this->syncData();
+ $this->loadResources();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
+ public function loadResources(): void
+ {
+ if ($this->destination->getMorphClass() !== \App\Models\StandaloneDocker::class) {
+ return;
+ }
+
+ $this->resources = $this->collectResources([
+ $this->destination->applications,
+ $this->destination->services,
+ $this->destination->postgresqls,
+ $this->destination->redis,
+ $this->destination->mongodbs,
+ $this->destination->mysqls,
+ $this->destination->mariadbs,
+ $this->destination->keydbs,
+ $this->destination->dragonflies,
+ $this->destination->clickhouses,
+ ]);
+ }
+
+ protected function collectResources(array $groups): array
+ {
+ $rows = [];
+ foreach ($groups as $group) {
+ foreach ($group as $resource) {
+ $rows[] = $this->resourceRow($resource);
+ }
+ }
+
+ return $rows;
+ }
+
+ protected function resourceRow($resource): array
+ {
+ $type = match (true) {
+ $resource instanceof \App\Models\Application => 'application',
+ $resource instanceof \App\Models\Service => 'service',
+ default => 'database',
+ };
+ $environment = $resource->environment;
+ $project = $environment?->project;
+ $routeName = "project.{$type}.configuration";
+ $url = ($project && $environment)
+ ? route($routeName, [
+ 'project_uuid' => $project->uuid,
+ 'environment_uuid' => $environment->uuid,
+ "{$type}_uuid" => $resource->uuid,
+ ])
+ : null;
+
+ return [
+ 'type' => $type,
+ 'name' => $resource->name,
+ 'project' => $project?->name,
+ 'environment' => $environment?->name,
+ 'url' => $url,
+ ];
+ }
+
public function syncData(bool $toModel = false)
{
if ($toModel) {
diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php
index d6b4d1a1c..d12a15a7c 100644
--- a/app/Models/StandaloneDocker.php
+++ b/app/Models/StandaloneDocker.php
@@ -134,8 +134,11 @@ class StandaloneDocker extends BaseModel
$mongodbs = $this->mongodbs;
$mysqls = $this->mysqls;
$mariadbs = $this->mariadbs;
+ $keydbs = $this->keydbs;
+ $dragonflies = $this->dragonflies;
+ $clickhouses = $this->clickhouses;
- return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
+ return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
}
public function attachedTo()
diff --git a/resources/views/livewire/destination/show.blade.php b/resources/views/livewire/destination/show.blade.php
index 27260e920..1bb179823 100644
--- a/resources/views/livewire/destination/show.blade.php
+++ b/resources/views/livewire/destination/show.blade.php
@@ -28,4 +28,24 @@
@endif
+
+ @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
+
+
Resources
+
Applications, services, and databases deployed to this network.
+ @if (count($resources) === 0)
+
No resources are using this destination.
+ @else
+
+ @endif
+
+ @endif
From 13e94a499d9c5d88834e9ad59fb2e566e369ad02 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 3 Jun 2026 11:24:36 +0200
Subject: [PATCH 2/2] feat(destinations): split Docker resources into separate
page
Move standalone Docker destination resource listings out of the general
settings view and add a searchable resources page with team-scoped tests.
---
app/Livewire/Destination/Resources.php | 125 ++++++++++++++++++
app/Livewire/Destination/Show.php | 62 ---------
app/Models/StandaloneDocker.php | 3 +-
.../livewire/destination/navbar.blade.php | 14 ++
.../livewire/destination/resources.blade.php | 53 ++++++++
.../views/livewire/destination/show.blade.php | 24 +---
routes/web.php | 2 +
tests/Feature/TeamScopedDestinationTest.php | 77 +++++++++++
8 files changed, 276 insertions(+), 84 deletions(-)
create mode 100644 app/Livewire/Destination/Resources.php
create mode 100644 resources/views/livewire/destination/navbar.blade.php
create mode 100644 resources/views/livewire/destination/resources.blade.php
diff --git a/app/Livewire/Destination/Resources.php b/app/Livewire/Destination/Resources.php
new file mode 100644
index 000000000..c71010411
--- /dev/null
+++ b/app/Livewire/Destination/Resources.php
@@ -0,0 +1,125 @@
+route('destination.index');
+ }
+ if (! $destination instanceof StandaloneDocker) {
+ return redirect()->route('destination.show', ['destination_uuid' => $destination->uuid]);
+ }
+
+ $this->destination = $destination;
+ $this->loadResources();
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ /**
+ * Load applications, services, and database resources deployed to the standalone Docker destination.
+ *
+ * @return void Populates the resources property for display.
+ */
+ public function loadResources(): void
+ {
+ $this->resources = $this->collectResources([
+ $this->destination->applications,
+ $this->destination->services,
+ $this->destination->postgresqls,
+ $this->destination->redis,
+ $this->destination->mongodbs,
+ $this->destination->mysqls,
+ $this->destination->mariadbs,
+ $this->destination->keydbs,
+ $this->destination->dragonflies,
+ $this->destination->clickhouses,
+ ]);
+ }
+
+ /**
+ * @param array> $groups
+ * @return array
+ */
+ protected function collectResources(array $groups): array
+ {
+ $rows = [];
+ foreach ($groups as $group) {
+ foreach ($group as $resource) {
+ $rows[] = $this->resourceRow($resource);
+ }
+ }
+
+ return $rows;
+ }
+
+ /**
+ * @param Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource
+ * @return array{uuid:string,type:string,name:string,project:string|null,environment:string|null,url:string|null,search:string}
+ */
+ protected function resourceRow(BaseModel $resource): array
+ {
+ $type = match (true) {
+ $resource instanceof Application => 'application',
+ $resource instanceof Service => 'service',
+ default => 'database',
+ };
+ $environment = $resource->environment;
+ $project = $environment?->project;
+ $routeName = "project.{$type}.configuration";
+ $url = ($project && $environment)
+ ? route($routeName, [
+ 'project_uuid' => $project->uuid,
+ 'environment_uuid' => $environment->uuid,
+ "{$type}_uuid" => $resource->uuid,
+ ])
+ : null;
+
+ return [
+ 'uuid' => $resource->uuid,
+ 'type' => $type,
+ 'name' => $resource->name,
+ 'project' => $project?->name,
+ 'environment' => $environment?->name,
+ 'url' => $url,
+ 'search' => strtolower(implode(' ', array_filter([
+ $type,
+ $resource->name,
+ $project?->name,
+ $environment?->name,
+ ]))),
+ ];
+ }
+
+ public function render(): View
+ {
+ return view('livewire.destination.resources');
+ }
+}
diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php
index 11afbbccc..9d55d7462 100644
--- a/app/Livewire/Destination/Show.php
+++ b/app/Livewire/Destination/Show.php
@@ -24,8 +24,6 @@ class Show extends Component
#[Validate(['string', 'required'])]
public string $serverIp;
- public array $resources = [];
-
public function mount(string $destination_uuid)
{
try {
@@ -35,71 +33,11 @@ class Show extends Component
}
$this->destination = $destination;
$this->syncData();
- $this->loadResources();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
- public function loadResources(): void
- {
- if ($this->destination->getMorphClass() !== \App\Models\StandaloneDocker::class) {
- return;
- }
-
- $this->resources = $this->collectResources([
- $this->destination->applications,
- $this->destination->services,
- $this->destination->postgresqls,
- $this->destination->redis,
- $this->destination->mongodbs,
- $this->destination->mysqls,
- $this->destination->mariadbs,
- $this->destination->keydbs,
- $this->destination->dragonflies,
- $this->destination->clickhouses,
- ]);
- }
-
- protected function collectResources(array $groups): array
- {
- $rows = [];
- foreach ($groups as $group) {
- foreach ($group as $resource) {
- $rows[] = $this->resourceRow($resource);
- }
- }
-
- return $rows;
- }
-
- protected function resourceRow($resource): array
- {
- $type = match (true) {
- $resource instanceof \App\Models\Application => 'application',
- $resource instanceof \App\Models\Service => 'service',
- default => 'database',
- };
- $environment = $resource->environment;
- $project = $environment?->project;
- $routeName = "project.{$type}.configuration";
- $url = ($project && $environment)
- ? route($routeName, [
- 'project_uuid' => $project->uuid,
- 'environment_uuid' => $environment->uuid,
- "{$type}_uuid" => $resource->uuid,
- ])
- : null;
-
- return [
- 'type' => $type,
- 'name' => $resource->name,
- 'project' => $project?->name,
- 'environment' => $environment?->name,
- 'url' => $url,
- ];
- }
-
public function syncData(bool $toModel = false)
{
if ($toModel) {
diff --git a/app/Models/StandaloneDocker.php b/app/Models/StandaloneDocker.php
index d12a15a7c..1c5cfd342 100644
--- a/app/Models/StandaloneDocker.php
+++ b/app/Models/StandaloneDocker.php
@@ -5,6 +5,7 @@ namespace App\Models;
use App\Jobs\ConnectProxyToNetworksJob;
use App\Support\ValidationPatterns;
use App\Traits\HasSafeStringAttribute;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class StandaloneDocker extends BaseModel
@@ -127,7 +128,7 @@ class StandaloneDocker extends BaseModel
return $this->morphMany(Service::class, 'destination');
}
- public function databases()
+ public function databases(): Collection
{
$postgresqls = $this->postgresqls;
$redis = $this->redis;
diff --git a/resources/views/livewire/destination/navbar.blade.php b/resources/views/livewire/destination/navbar.blade.php
new file mode 100644
index 000000000..4585e57a9
--- /dev/null
+++ b/resources/views/livewire/destination/navbar.blade.php
@@ -0,0 +1,14 @@
+@if ($destination->getMorphClass() === 'App\\Models\\StandaloneDocker')
+
+@endif
diff --git a/resources/views/livewire/destination/resources.blade.php b/resources/views/livewire/destination/resources.blade.php
new file mode 100644
index 000000000..8883cc021
--- /dev/null
+++ b/resources/views/livewire/destination/resources.blade.php
@@ -0,0 +1,53 @@
+
+
+
Destination
+
+
Resources deployed to this Docker network.
+
+ @include('livewire.destination.navbar', ['destination' => $destination])
+
+
+ @if (count($resources) === 0)
+
No resources are using this destination.
+ @else
+
+
+
+
+
+
+
+ | Project |
+ Environment |
+ Name |
+ Type |
+
+
+
+ @foreach ($resources as $row)
+
+ | {{ $row['project'] }} |
+ {{ $row['environment'] }} |
+
+ @if ($row['url'])
+
+ {{ $row['name'] }}
+
+
+ @else
+ {{ $row['name'] }}
+ @endif
+ |
+ {{ ucfirst($row['type']) }} |
+
+ @endforeach
+
+
+
+
+
+ @endif
+
+
diff --git a/resources/views/livewire/destination/show.blade.php b/resources/views/livewire/destination/show.blade.php
index 1bb179823..77b7209b7 100644
--- a/resources/views/livewire/destination/show.blade.php
+++ b/resources/views/livewire/destination/show.blade.php
@@ -20,7 +20,9 @@
@endif
-
+ @include('livewire.destination.navbar', ['destination' => $destination])
+
+
@if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
@@ -28,24 +30,4 @@
@endif
-
- @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker')
-
-
Resources
-
Applications, services, and databases deployed to this network.
- @if (count($resources) === 0)
-
No resources are using this destination.
- @else
-
- @endif
-
- @endif
diff --git a/routes/web.php b/routes/web.php
index 3dab7b4fe..f77277699 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -7,6 +7,7 @@ use App\Livewire\Admin\Index as AdminIndex;
use App\Livewire\Boarding\Index as BoardingIndex;
use App\Livewire\Dashboard;
use App\Livewire\Destination\Index as DestinationIndex;
+use App\Livewire\Destination\Resources as DestinationResources;
use App\Livewire\Destination\Show as DestinationShow;
use App\Livewire\ForcePasswordReset;
use App\Livewire\Notifications\Discord as NotificationDiscord;
@@ -304,6 +305,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
});
Route::get('/destinations', DestinationIndex::class)->name('destination.index');
Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show');
+ Route::get('/destination/{destination_uuid}/resources', DestinationResources::class)->name('destination.resources');
// Route::get('/security', fn () => view('security.index'))->name('security.index');
Route::get('/security/private-key', SecurityPrivateKeyIndex::class)->name('security.private-key.index');
diff --git a/tests/Feature/TeamScopedDestinationTest.php b/tests/Feature/TeamScopedDestinationTest.php
index bdac0251d..943676ba4 100644
--- a/tests/Feature/TeamScopedDestinationTest.php
+++ b/tests/Feature/TeamScopedDestinationTest.php
@@ -1,5 +1,6 @@
get('destination'))->toBeNull();
$component->assertRedirect(route('destination.index'));
});
+
+ test('general page links to separate resources page without rendering the resources table', function () {
+ Livewire::test(DestinationShow::class, ['destination_uuid' => $this->destinationA->uuid])
+ ->assertSee('General')
+ ->assertSee('Resources')
+ ->assertDontSee('Search resources...')
+ ->assertDontSee('No resources are using this destination.');
+ });
+
+ test('mount with own standalone destination lists deployed resources', function () {
+ Application::factory()->create([
+ 'name' => 'application-on-destination',
+ 'environment_id' => $this->environmentA->id,
+ 'destination_id' => $this->destinationA->id,
+ 'destination_type' => StandaloneDocker::class,
+ ]);
+ Service::factory()->create([
+ 'name' => 'service-on-destination',
+ 'environment_id' => $this->environmentA->id,
+ 'destination_id' => $this->destinationA->id,
+ 'destination_type' => StandaloneDocker::class,
+ ]);
+ StandalonePostgresql::withoutEvents(fn () => StandalonePostgresql::create([
+ 'uuid' => fake()->uuid(),
+ 'name' => 'database-on-destination',
+ 'postgres_password' => 'password',
+ 'environment_id' => $this->environmentA->id,
+ 'destination_id' => $this->destinationA->id,
+ 'destination_type' => StandaloneDocker::class,
+ ]));
+
+ Livewire::test(DestinationResources::class, ['destination_uuid' => $this->destinationA->uuid])
+ ->assertSee('Search resources...')
+ ->assertSee('Project')
+ ->assertSee('Environment')
+ ->assertSee('Name')
+ ->assertSee('Type')
+ ->assertSee('application-on-destination')
+ ->assertSee('service-on-destination')
+ ->assertSee('database-on-destination')
+ ->assertSee($this->projectA->name)
+ ->assertSee($this->environmentA->name);
+ });
+
+ test('mount with own standalone destination shows empty state without resources', function () {
+ Livewire::test(DestinationResources::class, ['destination_uuid' => $this->destinationA->uuid])
+ ->assertSee('No resources are using this destination.');
+ });
+
+ test('mount with own standalone destination does not list another team resources', function () {
+ Application::factory()->create([
+ 'name' => 'other-team-application',
+ 'environment_id' => $this->environmentB->id,
+ 'destination_id' => $this->destinationB->id,
+ 'destination_type' => StandaloneDocker::class,
+ ]);
+
+ Livewire::test(DestinationResources::class, ['destination_uuid' => $this->destinationA->uuid])
+ ->assertDontSee('other-team-application');
+ });
+
+ test('resource without project renders as non-clickable row', function () {
+ StandalonePostgresql::withoutEvents(fn () => StandalonePostgresql::create([
+ 'uuid' => fake()->uuid(),
+ 'name' => 'database-without-project',
+ 'postgres_password' => 'password',
+ 'environment_id' => null,
+ 'destination_id' => $this->destinationA->id,
+ 'destination_type' => StandaloneDocker::class,
+ ]));
+
+ $component = Livewire::test(DestinationResources::class, ['destination_uuid' => $this->destinationA->uuid])
+ ->assertSee('database-without-project');
+
+ expect($component->html())->not->toContain('href=""');
+ });
});