mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
feat(tags): connect resources to a tag from the tag page
Add a "Connect to resources" modal to the tag show page for small screens, where the tags canvas is hidden. A new toggleResource action attaches or detaches the tag on a team resource after an authorization check, then refreshes the tag's applications and services. Also improve responsive layouts: - Wrap section headings, project/environment toolbars and view toggles on narrow screens; keep modal headers from shrinking. - Move the environment settings link to an icon next to the environment name and link the project name back to its environments. - Group project icon, name and description into one "Project details" form with an unsaved-changes bar. - Show dashboard "view all" links for projects and servers only when there are more items than the dashboard limit.
This commit is contained in:
@@ -7,6 +7,7 @@ use App\Livewire\Concerns\CreatesTags;
|
||||
use App\Livewire\Concerns\RemembersCanvasView;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\Tag;
|
||||
use App\Services\TagCanvas;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
@@ -41,6 +42,9 @@ class Show extends Component
|
||||
#[Locked]
|
||||
public ?array $deploymentsPerTagPerServer = null;
|
||||
|
||||
/** The "Connect to resources" modal on small screens, where the tags canvas is not shown. */
|
||||
public bool $connectResourcesOpen = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (str($this->tagName)->isEmpty() && $this->prefersCanvasView('tags')) {
|
||||
@@ -123,9 +127,39 @@ class Show extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function openConnectResources(): void
|
||||
{
|
||||
$this->connectResourcesOpen = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tag without resources. Tag resources on the tags canvas or on the resource.
|
||||
* Adds the tag to a resource of the tag's team, or removes it when the resource already has it.
|
||||
*/
|
||||
public function toggleResource(string $uuid): void
|
||||
{
|
||||
try {
|
||||
$resource = app(TagCanvas::class)->resources($this->tag->team_id)->get($uuid);
|
||||
if (! $resource) {
|
||||
$this->dispatch('error', 'Resource not found.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->authorize('update', $resource);
|
||||
|
||||
if ($resource->tags->contains('id', $this->tag->id)) {
|
||||
$resource->tags()->detach($this->tag->id);
|
||||
} else {
|
||||
$resource->tags()->attach($this->tag->id);
|
||||
}
|
||||
|
||||
$this->applications = $this->tag->applications()->get();
|
||||
$this->services = $this->tag->services()->get();
|
||||
$this->getDeployments();
|
||||
} catch (\Exception $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the tag and removes it from every resource.
|
||||
*/
|
||||
@@ -158,6 +192,30 @@ class Show extends Component
|
||||
'resourceCount' => $applicationsCount + $servicesCount,
|
||||
];
|
||||
})->values()->toArray(),
|
||||
'connectableResources' => $this->connectableResources(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resources of the tag's team for the "Connect to resources" modal, by "Project / Environment".
|
||||
* Loaded only while the modal is open.
|
||||
*
|
||||
* @return Collection<string, Collection<int, array{uuid: string, name: string, tagged: bool, can_update: bool}>>
|
||||
*/
|
||||
private function connectableResources(): Collection
|
||||
{
|
||||
if (! $this->connectResourcesOpen || ! $this->tag) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return app(TagCanvas::class)->resources($this->tag->team_id)
|
||||
->groupBy(fn ($resource) => $resource->environment->project->name.' / '.$resource->environment->name)
|
||||
->sortKeys(SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->map(fn (Collection $resources) => $resources->map(fn ($resource) => [
|
||||
'uuid' => $resource->uuid,
|
||||
'name' => $resource->name,
|
||||
'tagged' => $resource->tags->contains('id', $this->tag->id),
|
||||
'can_update' => auth()->user()->can('update', $resource),
|
||||
])->values());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@props(['listUrl' => null, 'canvasUrl' => null, 'onCanvas' => false, 'views' => null])
|
||||
@php
|
||||
$idle = 'text-neutral-400 hover:bg-neutral-100 hover:text-black dark:text-fg dark:hover:bg-white/[0.06] dark:hover:text-fg';
|
||||
$canvasItem = 'relative flex h-7.5 items-center gap-1.5 rounded-md px-2.5 text-[12px] font-medium transition-colors';
|
||||
$canvasItem = 'relative flex h-7.5 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md px-2.5 text-[12px] font-medium transition-colors';
|
||||
$views ??= ['table' => ['Table view', 'unordered-list'], 'grid' => ['Grid view', 'grid']];
|
||||
// One list view (no table / grid choice): nothing to switch to on the list page.
|
||||
$singleList = count($views) === 1;
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
'sm:h-[40rem]' => $fixedHeight,
|
||||
])
|
||||
style="box-shadow: 0 0 0 1px var(--coollabs-hairline), var(--shadow-modal)">
|
||||
<header class="flex-wrap! sm:flex-nowrap!">
|
||||
<header class="shrink-0 flex-wrap! sm:flex-nowrap!">
|
||||
<div class="min-w-0 flex-1 py-0.5">
|
||||
<h3 class="truncate">{{ $title }}</h3>
|
||||
@if ($subtitle)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
puts a small control (such as a + button) right after the title.
|
||||
--}}
|
||||
<div {{ $attributes->merge(['class' => 'mb-3 min-w-0']) }}>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||
<div class="flex min-w-0 items-baseline gap-2">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<h2 class="min-w-0 truncate text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
@@ -29,7 +29,7 @@
|
||||
@endif
|
||||
</div>
|
||||
@isset($actions)
|
||||
<div class="flex shrink-0 items-center gap-3">
|
||||
<div class="ml-auto flex shrink-0 items-center gap-3">
|
||||
@if (filled($href))
|
||||
@include('components.section-heading-link')
|
||||
@endif
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
}
|
||||
}">
|
||||
<x-section-heading title="Projects"
|
||||
:href="route('project.index')">
|
||||
:href="$projects->count() > $dashboardItemLimit ? route('project.index') : null">
|
||||
{{-- Projects has no sidebar entry, so the dashboard is where new projects start. --}}
|
||||
@can('createAnyResource')
|
||||
<x-slot:titleAction>
|
||||
@@ -272,7 +272,7 @@
|
||||
}
|
||||
}">
|
||||
<x-section-heading title="Servers"
|
||||
:href="route('server.index')">
|
||||
:href="$servers->count() > $dashboardItemLimit ? route('server.index') : null">
|
||||
@can('createAnyResource')
|
||||
<x-slot:titleAction>
|
||||
<a href="{{ route('server.create') }}" {{ wireNavigate() }} class="{{ $titleIconButton }}"
|
||||
|
||||
@@ -7,7 +7,17 @@
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<section class="application-settings-section" x-data="{
|
||||
<form wire:submit="submit">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<section class="application-settings-section">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Project details</h2>
|
||||
<p>Icon, name, and description shown across the dashboard.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2" x-data="{
|
||||
preview: null,
|
||||
processing: false,
|
||||
uploadError: null,
|
||||
@@ -55,48 +65,38 @@
|
||||
}
|
||||
},
|
||||
}">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Project icon</h2>
|
||||
<p>Upload a JPG, PNG, or WebP image. It will appear in the projects list.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-settings-section-body flex items-center gap-4">
|
||||
<div class="flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-neutral-50 text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.04] dark:text-fg-dim">
|
||||
<img x-cloak x-show="preview" :src="preview" alt="Project icon preview" class="h-full w-full object-cover">
|
||||
@if ($project->icon_path)
|
||||
<img x-show="!preview" src="{{ project_icon_url($project) }}"
|
||||
alt="{{ $project->name }} icon" class="h-full w-full object-cover">
|
||||
@else
|
||||
<x-reicon x-show="!preview" name="projects" class="size-6" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input x-ref="iconInput" type="file" x-on:change="prepareIcon($event)" accept="image/jpeg,image/png,image/webp" class="hidden">
|
||||
<x-forms.button type="button" x-on:click="$refs.iconInput.click()" x-bind:disabled="processing">
|
||||
<span x-text="processing ? 'Uploading…' : 'Browse…'"></span>
|
||||
</x-forms.button>
|
||||
@if ($project->icon_path)
|
||||
<x-forms.button type="button" wire:click="removeIcon" x-bind:disabled="processing" isError>Remove</x-forms.button>
|
||||
@endif
|
||||
<div class="mb-2 text-sm font-medium leading-4">Icon</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-neutral-200 text-neutral-700 dark:bg-white/[0.1] dark:text-fg">
|
||||
<img x-cloak x-show="preview" :src="preview" alt="Project icon preview" class="h-full w-full object-cover">
|
||||
@if ($project->icon_path)
|
||||
<img x-show="!preview" src="{{ project_icon_url($project) }}"
|
||||
alt="{{ $project->name }} icon" class="h-full w-full object-cover">
|
||||
@else
|
||||
<x-reicon x-show="!preview" name="projects" class="size-6" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<input x-ref="iconInput" type="file" x-on:change="prepareIcon($event)"
|
||||
accept="image/jpeg,image/png,image/webp" class="hidden">
|
||||
<x-forms.button type="button" x-on:click="$refs.iconInput.click()"
|
||||
x-bind:disabled="processing">
|
||||
<span x-text="processing ? 'Uploading…' : 'Browse…'"></span>
|
||||
</x-forms.button>
|
||||
@if ($project->icon_path)
|
||||
<x-forms.button type="button" wire:click="removeIcon" x-bind:disabled="processing"
|
||||
isError>Remove</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 dark:text-fg-dim">JPG, PNG, or WebP. Shown in the projects list.</p>
|
||||
<p x-cloak x-show="uploadError" x-text="uploadError" class="text-xs text-red-500"></p>
|
||||
@error('icon')
|
||||
<p class="text-xs text-red-500">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p x-cloak x-show="uploadError" x-text="uploadError" class="text-xs text-red-500"></p>
|
||||
@error('icon') <p class="text-xs text-red-500">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form wire:submit="submit">
|
||||
<x-unsaved-bar action="submit" />
|
||||
<section class="application-settings-section">
|
||||
<div class="application-settings-section-header">
|
||||
<div>
|
||||
<h2>Project details</h2>
|
||||
<p>Name and describe this project across the dashboard.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="application-settings-section-body grid gap-4 sm:grid-cols-2">
|
||||
<x-forms.input label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-table.dropdown panel-class="w-52!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
<div x-data="resourceIndex()" class="w-full">
|
||||
<header class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $environment->name }}</h1>
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $environment->name }}</h1>
|
||||
@can('update', $project)
|
||||
<a href="{{ route('project.environment.edit', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
title="Environment settings"
|
||||
aria-label="Open settings for {{ $environment->name }}">
|
||||
<x-reicon name="settings" class="size-4" />
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
<span x-text="`${resources.length} ${resources.length === 1 ? 'resource' : 'resources'}`"></span>
|
||||
in {{ $project->name }}
|
||||
in <a href="{{ route('project.show', ['project_uuid' => $project->uuid]) }}" {{ wireNavigate() }}
|
||||
class="font-medium text-neutral-700 underline-offset-2 hover:text-black hover:underline dark:text-fg dark:hover:text-white"
|
||||
title="Show all environments in {{ $project->name }}">{{ $project->name }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex w-fit shrink-0 items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:shrink-0">
|
||||
@if ($environment->isEmpty())
|
||||
{{-- The list toolbar (with its canvas switch) is hidden when there are no resources. --}}
|
||||
<x-canvas-view-toggle :views="['list' => ['List view', 'unordered-list']]"
|
||||
@@ -25,16 +38,6 @@
|
||||
<x-reicon name="variables" class="size-3.5" />
|
||||
Shared variables
|
||||
</a>
|
||||
@can('update', $project)
|
||||
<a href="{{ route('project.environment.edit', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="button whitespace-nowrap"
|
||||
title="Environment settings"
|
||||
aria-label="Open settings for {{ $environment->name }}">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
Settings
|
||||
</a>
|
||||
@endcan
|
||||
@can('createAnyResource')
|
||||
<a href="{{ route('project.resource.create', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
@@ -79,7 +82,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-table.dropdown panel-class="w-64! overflow-hidden! p-0!" :multiselectable="true">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button max-w-64"
|
||||
|
||||
@@ -5,7 +5,18 @@
|
||||
<div x-data="projectEnvironments()" class="w-full">
|
||||
<header class="mb-5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $project->name }}</h1>
|
||||
<div class="flex min-w-0 items-center gap-1.5">
|
||||
<h1 class="truncate text-[24px]! leading-7! font-semibold! tracking-tight!">{{ $project->name }}</h1>
|
||||
@can('update', $project)
|
||||
<a href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="flex size-7 shrink-0 items-center justify-center rounded-md text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-black dark:text-fg-faint dark:hover:bg-white/[0.06] dark:hover:text-fg"
|
||||
title="Project settings"
|
||||
aria-label="Open settings for {{ $project->name }}">
|
||||
<x-reicon name="settings" class="size-4" />
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
<p class="mt-1 text-[13px] text-neutral-500 dark:text-fg-dim">
|
||||
<span
|
||||
x-text="`${environments.length} ${environments.length === 1 ? 'environment' : 'environments'}`"></span>
|
||||
@@ -15,15 +26,6 @@
|
||||
|
||||
@can('update', $project)
|
||||
<div class="flex w-fit shrink-0 items-center gap-2">
|
||||
<a href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}"
|
||||
{{ wireNavigate() }}
|
||||
class="button"
|
||||
title="Project settings"
|
||||
aria-label="Open settings for {{ $project->name }}">
|
||||
<x-reicon name="settings" class="size-3.5" />
|
||||
Settings
|
||||
</a>
|
||||
|
||||
<x-modal-input title="New Environment">
|
||||
<x-slot:content>
|
||||
<button type="button"
|
||||
@@ -58,7 +60,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-table.dropdown panel-class="w-48!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-table.dropdown panel-class="w-52!">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="button" aria-haspopup="listbox" :aria-expanded="open">
|
||||
@@ -280,10 +280,44 @@
|
||||
<x-application.settings-section :title="$tag->name"
|
||||
description="Use this webhook to deploy every resource with this tag.">
|
||||
<x-slot:actions>
|
||||
<a href="{{ app(App\Services\TagCanvas::class)->connectLink($tag) }}" class="button whitespace-nowrap"
|
||||
title="Add this tag to resources, and see which resources have it">
|
||||
<x-reicon name="link" class="size-3.5" />
|
||||
Connect to resources
|
||||
{{-- Two ways to connect: a list in a modal, or the canvas (wide screens only). --}}
|
||||
<x-modal-input title="Connect to resources" subtitle="Add this tag to resources, or remove it."
|
||||
wireOpen="connectResourcesOpen" :wireIgnore="false" :contentClicks="false">
|
||||
<x-slot:content>
|
||||
<button type="button" class="button whitespace-nowrap" wire:click="openConnectResources">
|
||||
<x-reicon name="link" class="size-3.5" />
|
||||
Connect to resources
|
||||
</button>
|
||||
</x-slot:content>
|
||||
@forelse ($connectableResources as $context => $resources)
|
||||
<div wire:key="connect-group-{{ $context }}" class="mb-4 last:mb-0">
|
||||
<p class="mb-1.5 truncate text-[11px] font-medium text-neutral-500 dark:text-fg-faint">{{ $context }}</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
@foreach ($resources as $resource)
|
||||
<label wire:key="connect-resource-{{ $resource['uuid'] }}" @class([
|
||||
'flex items-center gap-2.5 rounded-md border border-neutral-200 px-3 py-2 text-[13px] dark:border-white/[0.08]',
|
||||
'cursor-pointer' => $resource['can_update'],
|
||||
'opacity-60' => ! $resource['can_update'],
|
||||
])>
|
||||
<input type="checkbox" @checked($resource['tagged']) @disabled(! $resource['can_update'])
|
||||
wire:click="toggleResource(@js($resource['uuid']))" wire:loading.attr="disabled"
|
||||
class="size-4 shrink-0 rounded-sm border-neutral-300 text-accent focus:ring-accent dark:border-white/20 dark:bg-transparent" />
|
||||
<span class="min-w-0 truncate">{{ $resource['name'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div wire:loading.remove wire:target="openConnectResources">
|
||||
<x-empty title="No resources yet" description="Create a resource in a project first." icon-name="tags" size="sm" />
|
||||
</div>
|
||||
@endforelse
|
||||
</x-modal-input>
|
||||
<a href="{{ app(App\Services\TagCanvas::class)->connectLink($tag) }}"
|
||||
class="button hidden whitespace-nowrap md:inline-flex"
|
||||
title="Add this tag to resources on the canvas, and see which resources have it">
|
||||
<x-reicon name="deploy-canvas" class="size-3.5" />
|
||||
Connect in canvas
|
||||
</a>
|
||||
@can('delete', $tag)
|
||||
<x-modal-confirmation title="Delete this tag?" isErrorButton buttonTitle="Delete"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -42,3 +43,13 @@ it('remembers the view of each dashboard section on its own', function () {
|
||||
->assertSee("viewMode: 'grid'", false)
|
||||
->assertSee("viewMode: 'table'", false);
|
||||
});
|
||||
|
||||
it('links to all projects only when the dashboard hides some of them', function () {
|
||||
Project::factory()->count(8)->create(['team_id' => $this->team->id]);
|
||||
|
||||
Livewire::test(Dashboard::class)->assertDontSee(route('project.index'), false);
|
||||
|
||||
Project::factory()->create(['team_id' => $this->team->id]);
|
||||
|
||||
Livewire::test(Dashboard::class)->assertSee(route('project.index'), false);
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ test('project show page header links to project settings', function () {
|
||||
$editUrl = route('project.edit', ['project_uuid' => $this->project->uuid]);
|
||||
|
||||
Livewire::test(ProjectShow::class, ['project_uuid' => $this->project->uuid])
|
||||
->assertSee('Settings')
|
||||
->assertSee('Open settings for Test Project', false)
|
||||
->assertSee('Project settings', false)
|
||||
->assertSeeHtml($editUrl);
|
||||
});
|
||||
@@ -51,7 +51,7 @@ test('environment resources page header links to environment settings', function
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
]))
|
||||
->assertSuccessful()
|
||||
->assertSee('Settings')
|
||||
->assertSee('Open settings for '.$this->environment->name, false)
|
||||
->assertSee('Environment settings', false)
|
||||
->assertSee($editUrl, false);
|
||||
});
|
||||
|
||||
@@ -98,3 +98,72 @@ it('redirects to the overview when the requested tag does not exist', function (
|
||||
Livewire::test(Show::class, ['tagName' => 'missing-tag'])
|
||||
->assertRedirect(route('tags.show'));
|
||||
});
|
||||
|
||||
it('lists the team resources in the connect modal and toggles the tag on a resource', function () {
|
||||
$tag = Tag::create(['name' => 'hello', 'team_id' => $this->team->id]);
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'shop']);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id, 'name' => 'prod']);
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'name' => 'untagged-app',
|
||||
]);
|
||||
|
||||
$component = Livewire::test(Show::class, ['tagName' => 'hello'])
|
||||
->assertDontSee('shop / prod')
|
||||
->call('openConnectResources')
|
||||
->assertSee('shop / prod')
|
||||
->assertSee('untagged-app')
|
||||
->call('toggleResource', $application->uuid);
|
||||
|
||||
expect($application->tags()->whereKey($tag->id)->exists())->toBeTrue();
|
||||
expect($component->get('applications')->pluck('uuid')->all())->toBe([$application->uuid]);
|
||||
|
||||
$component->call('toggleResource', $application->uuid);
|
||||
|
||||
expect($application->tags()->whereKey($tag->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not tag a resource of another team from the connect modal', function () {
|
||||
$tag = Tag::create(['name' => 'hello', 'team_id' => $this->team->id]);
|
||||
$otherTeam = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $otherTeam->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||
$project = Project::factory()->create(['team_id' => $otherTeam->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
Livewire::test(Show::class, ['tagName' => 'hello'])
|
||||
->call('toggleResource', $application->uuid)
|
||||
->assertDispatched('error');
|
||||
|
||||
expect($application->tags()->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('does not let a member change tags from the connect modal', function () {
|
||||
$tag = Tag::create(['name' => 'hello', 'team_id' => $this->team->id]);
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$destination = StandaloneDocker::where('server_id', $server->id)->first();
|
||||
$project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
]);
|
||||
$member = User::factory()->create();
|
||||
$member->teams()->attach($this->team, ['role' => 'member']);
|
||||
$this->actingAs($member);
|
||||
|
||||
Livewire::test(Show::class, ['tagName' => 'hello'])
|
||||
->call('toggleResource', $application->uuid);
|
||||
|
||||
expect($application->tags()->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user