mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 18:52:49 +00:00
Add a /dashboard/canvas page that renders every team server with its resources and their running or queued deployments, backed by a new ServerCanvasController::dashboard endpoint and a per-server usage endpoint for zone headers. The dashboard remembers the list/canvas choice and each section (projects, servers) its own table/grid view. - Show Deploying/Queued pills on canvas cards and a foldable recent deployments panel; add shared CanvasControls component - Allow backup downloads from the server host via the S3 download storage (PrepareFileDownloadJob accepts a null container) - Extract backup toggle/run actions into ManagesDatabaseBackup and surface them in backup execution headers; merge S3 and retention sections into the general backup settings - Move deployments indicator to the top bar and add a main nav resource menu with submenus; simplify breadcrumbs and settings layouts - Add shared variables overview canvas and scheduled task modals - Add feature tests for dashboard canvas, view modes, navbar submenus, backup S3 downloads and scheduled task modals
212 lines
9.5 KiB
PHP
212 lines
9.5 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Project\ProjectCanvas;
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Project;
|
|
use App\Models\ResourceLink;
|
|
use App\Models\Server;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config([
|
|
'cache.default' => 'array',
|
|
'session.driver' => 'array',
|
|
'queue.default' => 'sync',
|
|
'app.maintenance.driver' => 'file',
|
|
]);
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0], ['is_api_enabled' => true]));
|
|
Server::flushIdentityMap();
|
|
|
|
$this->team = Team::factory()->create(['show_boarding' => false]);
|
|
$this->admin = User::factory()->create();
|
|
$this->team->members()->attach($this->admin->id, ['role' => 'admin']);
|
|
|
|
$this->server = Server::factory()->create(['team_id' => $this->team->id, 'name' => 'web-server']);
|
|
$this->project = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'Shop']);
|
|
$this->staging = Environment::factory()->create(['project_id' => $this->project->id, 'name' => 'staging']);
|
|
$this->production = $this->project->environments()->where('name', 'production')->firstOrFail();
|
|
});
|
|
|
|
function projectCanvasApp(Environment $environment, Server $server, string $name): Application
|
|
{
|
|
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
|
|
|
return Application::factory()->create([
|
|
'name' => $name,
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
}
|
|
|
|
function projectCanvasSession(object $test, User $user, Team $team): object
|
|
{
|
|
return $test->actingAs($user)->withSession(['currentTeam' => $team])
|
|
->withHeader('X-Canvas-Team', (string) $team->id);
|
|
}
|
|
|
|
test('the project canvas shows the resources of every environment, grouped by environment', function () {
|
|
$app = projectCanvasApp($this->staging, $this->server, 'storefront');
|
|
$api = projectCanvasApp($this->staging, $this->server, 'orders-api');
|
|
$live = projectCanvasApp($this->production, $this->server, 'storefront-live');
|
|
$link = ResourceLink::create([
|
|
'environment_id' => $this->staging->id,
|
|
'source_type' => $app->getMorphClass(), 'source_id' => $app->id,
|
|
'target_type' => $api->getMorphClass(), 'target_id' => $api->id,
|
|
]);
|
|
$this->production->forceFill(['canvas_positions' => [$live->uuid => ['x' => 40, 'y' => 80]]])->save();
|
|
|
|
// A resource of another project of the same team stays out.
|
|
$other = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $this->team->id])->id]);
|
|
projectCanvasApp($other, $this->server, 'elsewhere');
|
|
|
|
$response = projectCanvasSession($this, $this->admin, $this->team)
|
|
->getJson("/canvas/api/projects/{$this->project->uuid}/canvas")
|
|
->assertOk()
|
|
->assertJsonPath('title', 'Shop')
|
|
->assertJsonPath('can_edit', true)
|
|
->assertJsonPath('can_move', false)
|
|
->assertJsonPath('layout', 'server')
|
|
->assertJsonPath('links.0.id', $link->uuid)
|
|
// Auto-placed: a position saved on one environment canvas does not fit next to the other environments.
|
|
->assertJsonPath('positions', []);
|
|
|
|
expect(collect($response->json('zones'))->pluck('name')->all())->toBe(['web-server'])
|
|
->and(collect($response->json('nodes'))->mapWithKeys(fn ($node) => [$node['name'] => $node['context']])->sortKeys()->all())->toBe([
|
|
'orders-api' => 'staging',
|
|
'storefront' => 'staging',
|
|
'storefront-live' => 'production',
|
|
])
|
|
// The canvas saves moves and links on the environment canvas of each card.
|
|
->and(collect($response->json('nodes'))->firstWhere('name', 'storefront-live')['environment'])->toBe(['uuid' => $this->production->uuid, 'name' => 'production']);
|
|
});
|
|
|
|
test('members see the project canvases but cannot edit them', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
|
|
projectCanvasSession($this, $member, $this->team)
|
|
->getJson("/canvas/api/projects/{$this->project->uuid}/canvas")
|
|
->assertOk()
|
|
->assertJsonPath('can_edit', false);
|
|
projectCanvasSession($this, $member, $this->team)
|
|
->getJson('/canvas/api/projects/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('can_edit', false);
|
|
});
|
|
|
|
test('the project canvas is out of reach from another team', function () {
|
|
$otherTeam = Team::factory()->create(['show_boarding' => false]);
|
|
$outsider = User::factory()->create();
|
|
$otherTeam->members()->attach($outsider->id, ['role' => 'owner']);
|
|
$this->team->members()->attach($outsider->id, ['role' => 'member']);
|
|
|
|
// Session switched to the other team: this team's project is out of reach.
|
|
projectCanvasSession($this, $outsider, $otherTeam)
|
|
->getJson("/canvas/api/projects/{$this->project->uuid}/canvas")
|
|
->assertNotFound();
|
|
projectCanvasSession($this, $outsider, $otherTeam)
|
|
->get(route('project.canvas', ['project_uuid' => $this->project->uuid]))
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('the project page reopens the canvas when the user left it on the canvas view', function () {
|
|
$parameters = ['project_uuid' => $this->project->uuid];
|
|
|
|
projectCanvasSession($this, $this->admin, $this->team)
|
|
->withoutVite()
|
|
->get(route('project.canvas', $parameters))
|
|
->assertOk()
|
|
->assertSee('data-endpoint="/projects/'.$this->project->uuid.'/canvas"', false)
|
|
->assertCookie("coolify_{$this->admin->id}_project_view", 'canvas');
|
|
|
|
projectCanvasSession($this, $this->admin, $this->team)
|
|
->withCookie("coolify_{$this->admin->id}_project_view", 'canvas')
|
|
->get(route('project.show', $parameters))
|
|
->assertRedirect(route('project.canvas', $parameters));
|
|
|
|
// The table / grid links on the canvas pick the list view again.
|
|
projectCanvasSession($this, $this->admin, $this->team)
|
|
->withCookie("coolify_{$this->admin->id}_project_view", 'canvas')
|
|
->get(route('project.show', [...$parameters, 'view' => 'grid']))
|
|
->assertOk()
|
|
->assertCookie("coolify_{$this->admin->id}_project_view", 'grid');
|
|
});
|
|
|
|
test('the projects canvas shows every project of the team as a zone, grouped by environment', function () {
|
|
$app = projectCanvasApp($this->staging, $this->server, 'storefront');
|
|
$blog = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'Blog']);
|
|
$post = projectCanvasApp($blog->environments()->firstOrFail(), $this->server, 'wordpress');
|
|
|
|
// Another team's project stays out.
|
|
$otherTeam = Team::factory()->create();
|
|
$otherServer = Server::factory()->create(['team_id' => $otherTeam->id]);
|
|
$foreign = Project::factory()->create(['team_id' => $otherTeam->id, 'name' => 'Foreign']);
|
|
projectCanvasApp($foreign->environments()->firstOrFail(), $otherServer, 'secret-app');
|
|
|
|
$response = projectCanvasSession($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/projects/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('title', 'Projects')
|
|
->assertJsonPath('can_edit', true)
|
|
->assertJsonPath('layout', 'server');
|
|
|
|
expect(collect($response->json('zones'))->pluck('name')->all())->toBe(['Blog', 'Shop'])
|
|
->and(collect($response->json('nodes'))->mapWithKeys(fn ($node) => [$node['name'] => [$node['zone'], $node['context']]])->sortKeys()->all())->toBe([
|
|
'storefront' => [$this->project->uuid, 'staging'],
|
|
'wordpress' => [$blog->uuid, 'production'],
|
|
]);
|
|
});
|
|
|
|
test('the projects page reopens the canvas when the user left it on the canvas view', function () {
|
|
projectCanvasSession($this, $this->admin, $this->team)
|
|
->get(route('project.canvas.index'))
|
|
->assertOk()
|
|
->assertCookie("coolify_{$this->admin->id}_projects_view", 'canvas');
|
|
|
|
projectCanvasSession($this, $this->admin, $this->team)
|
|
->withCookie("coolify_{$this->admin->id}_projects_view", 'canvas')
|
|
->get(route('project.index'))
|
|
->assertRedirect(route('project.canvas.index'));
|
|
|
|
projectCanvasSession($this, $this->admin, $this->team)
|
|
->withCookie("coolify_{$this->admin->id}_projects_view", 'canvas')
|
|
->get(route('project.index', ['view' => 'table']))
|
|
->assertOk()
|
|
->assertCookie("coolify_{$this->admin->id}_projects_view", 'table');
|
|
});
|
|
|
|
test('an admin creates an environment from the project canvas', function () {
|
|
$this->actingAs($this->admin);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
Livewire::test(ProjectCanvas::class, ['project_uuid' => $this->project->uuid])
|
|
->set('name', 'testing')
|
|
->call('submit')
|
|
->assertRedirect();
|
|
|
|
expect($this->project->environments()->where('name', 'testing')->exists())->toBeTrue();
|
|
});
|
|
|
|
test('a member cannot create an environment from the project canvas', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$this->actingAs($member);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
Livewire::test(ProjectCanvas::class, ['project_uuid' => $this->project->uuid])
|
|
->set('name', 'testing')
|
|
->call('submit')
|
|
->assertDispatched('error');
|
|
|
|
expect($this->project->environments()->where('name', 'testing')->exists())->toBeFalse();
|
|
});
|