mirror of
https://github.com/coollabsio/coolify.git
synced 2026-09-25 17:01:49 -04:00
fix(analytics): hide traffic UI when disabled and skip default-on (#11700)
This commit is contained in:
@@ -11,6 +11,7 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Attributes\Lazy;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -178,6 +179,23 @@ class Analytics extends Component
|
||||
$this->live = ! $this->live;
|
||||
}
|
||||
|
||||
#[On('trafficAnalyticsStateChanged')]
|
||||
public function refreshTrafficAnalyticsState(): void
|
||||
{
|
||||
if ($this->scopedServerUuid === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server = Server::ownedByCurrentTeam()->whereUuid($this->scopedServerUuid)->firstOrFail();
|
||||
$this->overview = null;
|
||||
$this->servers = $server->isTrafficAnalyticsEnabled() ? collect([$server]) : collect();
|
||||
|
||||
if ($this->servers->isNotEmpty()) {
|
||||
$this->refreshAppOptions();
|
||||
$this->loadData();
|
||||
}
|
||||
}
|
||||
|
||||
public function isLivePollable(): bool
|
||||
{
|
||||
return $this->live && $this->range === '24h';
|
||||
@@ -564,10 +582,18 @@ class Analytics extends Component
|
||||
return [$from->toIso8601ZuluString(), $to->toIso8601ZuluString()];
|
||||
}
|
||||
|
||||
public function placeholder(): View
|
||||
public function placeholder(array $params = []): View
|
||||
{
|
||||
$scopedServerUuid = $params['scopedServerUuid'] ?? null;
|
||||
$hideSkeleton = false;
|
||||
|
||||
if (is_string($scopedServerUuid)) {
|
||||
$server = Server::ownedByCurrentTeamCached()->firstWhere('uuid', $scopedServerUuid);
|
||||
$hideSkeleton = $server !== null && ! $server->isTrafficAnalyticsEnabled();
|
||||
}
|
||||
|
||||
// Rendered instantly; the Sentinel round-trips run in the deferred lazy-load request.
|
||||
return view('livewire.analytics-placeholder');
|
||||
return view('livewire.analytics-placeholder', compact('hideSkeleton'));
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -221,8 +221,17 @@ class Analytics extends Component
|
||||
return [$from->toIso8601ZuluString(), $to->toIso8601ZuluString()];
|
||||
}
|
||||
|
||||
public function placeholder(): View
|
||||
public function placeholder(array $params = []): View
|
||||
{
|
||||
$application = $params['application'] ?? null;
|
||||
|
||||
if ($application instanceof Application && ! $application->destination?->server?->isTrafficAnalyticsEnabled()) {
|
||||
$this->application = $application;
|
||||
$this->enabled = false;
|
||||
|
||||
return view('livewire.project.application.analytics');
|
||||
}
|
||||
|
||||
// Rendered instantly; the Sentinel round-trip runs in the deferred lazy-load request.
|
||||
return view('livewire.project.application.analytics-placeholder');
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ class Proxy extends Component
|
||||
$this->redirectEnabled = data_get($this->server, 'proxy.redirect_enabled', true);
|
||||
$this->redirectUrl = data_get($this->server, 'proxy.redirect_url');
|
||||
$this->syncData(false);
|
||||
$this->loadProxyConfiguration();
|
||||
$this->clearAppliedTraefikBranchWarning();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\ConfigureTrafficAnalytics;
|
||||
use App\Livewire\Analytics;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\View\View;
|
||||
@@ -83,6 +84,7 @@ class TrafficAnalyticsSettings extends Component
|
||||
ConfigureTrafficAnalytics::run($this->server, $enable);
|
||||
$this->server->refresh();
|
||||
$this->isTrafficAnalyticsEnabled = $this->server->isTrafficAnalyticsEnabled();
|
||||
$this->dispatch('trafficAnalyticsStateChanged')->to(Analytics::class);
|
||||
$this->dispatch('success', $enable
|
||||
? 'Traffic analytics enabled. Restarting proxy and Sentinel.'
|
||||
: 'Traffic analytics disabled. Restarting proxy and Sentinel.');
|
||||
|
||||
@@ -170,15 +170,6 @@ class ServerSetting extends Model
|
||||
{
|
||||
static::creating(function ($setting) {
|
||||
try {
|
||||
// Enable traffic analytics by default for eligible servers, unless the
|
||||
// caller explicitly set a value. Swarm and build servers are ineligible,
|
||||
// mirroring the toggle guard so the flag never contradicts capability.
|
||||
// Runs before sentinel generation, which may throw and be swallowed below.
|
||||
if (! $setting->isDirty('is_traffic_analytics_enabled')) {
|
||||
$isSwarm = $setting->is_swarm_manager || $setting->is_swarm_worker;
|
||||
$isBuild = (bool) $setting->is_build_server;
|
||||
$setting->is_traffic_analytics_enabled = ! $isSwarm && ! $isBuild;
|
||||
}
|
||||
if (str($setting->sentinel_token)->isEmpty()) {
|
||||
$setting->generateSentinelToken(save: false, ignoreEvent: true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div class="flex w-full min-w-0 flex-col gap-6">
|
||||
@if (! ($hideSkeleton ?? false))
|
||||
{{-- Header (real chrome; only the data below is a skeleton) --}}
|
||||
<div class="flex flex-col gap-4">
|
||||
@if (empty($scopedServerUuid ?? null))
|
||||
@@ -54,4 +55,5 @@
|
||||
<x-application.settings-section id="analytics-country-section" title="Countries" flush>
|
||||
<x-skeleton class="h-64 w-full" />
|
||||
</x-application.settings-section>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -123,17 +123,17 @@ $appListboxOptions = array_merge(
|
||||
@endif
|
||||
|
||||
@if ($servers->isEmpty())
|
||||
<x-empty size="sm" title="Traffic analytics is not enabled"
|
||||
description="{{ $scopedServerUuid === null ? 'Enable traffic analytics on a server to see request analytics here.' : 'Enable traffic analytics in the settings below to begin collecting requests for this server.' }}"
|
||||
icon-name="analytics">
|
||||
@if ($scopedServerUuid === null)
|
||||
@if ($scopedServerUuid === null)
|
||||
<x-empty size="sm" title="Traffic analytics is not enabled"
|
||||
description="Enable traffic analytics on a server to see request analytics here."
|
||||
icon-name="analytics">
|
||||
<x-slot:contents>
|
||||
<a class="button" href="{{ route('server.index') }}" {{ wireNavigate() }}>
|
||||
View servers
|
||||
</a>
|
||||
</x-slot:contents>
|
||||
@endif
|
||||
</x-empty>
|
||||
</x-empty>
|
||||
@endif
|
||||
@elseif (! $overview)
|
||||
<x-empty size="sm" title="No analytics data yet"
|
||||
description="We could not load traffic analytics for the selected filters and range. Try a different range or check back shortly."
|
||||
|
||||
@@ -11,12 +11,15 @@
|
||||
$dashboardItemLimit = 8;
|
||||
$dashboardProjects = $projects->sortBy('name', SORT_NATURAL)->take($dashboardItemLimit);
|
||||
$dashboardServers = $servers->sortBy('name', SORT_NATURAL)->take($dashboardItemLimit);
|
||||
$hasTrafficAnalytics = $servers->contains(fn ($server) => $server->isTrafficAnalyticsEnabled());
|
||||
@endphp
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-8">
|
||||
<livewire:dashboard.active-deployments />
|
||||
|
||||
<livewire:dashboard.traffic-analytics />
|
||||
@if ($hasTrafficAnalytics)
|
||||
<livewire:dashboard.traffic-analytics />
|
||||
@endif
|
||||
|
||||
<section class="mb-0! min-w-0">
|
||||
<x-section-heading title="Projects" subtitle="Your deployment workspaces"
|
||||
|
||||
@@ -1,19 +1 @@
|
||||
<section class="mb-0! min-w-0">
|
||||
{{-- Real section header; only the KPI grid below is a skeleton. --}}
|
||||
<div class="mb-3 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-[14px]! leading-5! font-semibold! text-black dark:text-fg">
|
||||
Traffic analytics
|
||||
</h2>
|
||||
<p class="mt-0.5 text-[11px] text-neutral-500 dark:text-fg-faint">
|
||||
Team-wide request volume across servers with traffic analytics enabled
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<x-skeleton class="h-8 w-40 rounded-lg" />
|
||||
<x-skeleton class="h-4 w-24 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-skeleton.tiles :count="4" grid="grid-cols-1 sm:grid-cols-2 lg:grid-cols-4" rounded="rounded-xl" />
|
||||
</section>
|
||||
<div class="contents"></div>
|
||||
|
||||
@@ -7,6 +7,8 @@ $approxBadge = fn (string $tooltip) => '<span title="'.e($tooltip).'" class="ml-
|
||||
|
||||
$spark = 'refreshChartData-'.$chartId.'-status';
|
||||
?>
|
||||
<div class="contents">
|
||||
@if ($servers->isNotEmpty() && $overview)
|
||||
<section class="mb-0! min-w-0">
|
||||
<div class="mb-3 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
@@ -42,29 +44,14 @@ $spark = 'refreshChartData-'.$chartId.'-status';
|
||||
</div>
|
||||
@endif
|
||||
<a href="{{ route('analytics') }}" {{ wireNavigate() }}
|
||||
class="inline-flex shrink-0 items-center gap-1 text-[12px] font-medium text-neutral-500 transition-colors hover:text-black dark:text-fg-dim dark:hover:text-fg">
|
||||
class="group inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5 text-[12px] font-medium text-neutral-600 transition-[color,background-color,transform] duration-100 ease-out hover:bg-neutral-100 hover:text-black active:scale-[0.97] dark:border-white/[0.08] dark:bg-white/[0.06] dark:text-fg-dim dark:hover:bg-white/[0.1] dark:hover:text-fg">
|
||||
Open analytics
|
||||
<x-reicon name="arrow-right" class="size-3" />
|
||||
<x-reicon name="arrow-right" class="size-3 opacity-70 transition-transform duration-150 ease-out group-hover:translate-x-0.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($servers->isEmpty())
|
||||
<x-empty size="sm" title="Traffic analytics is not enabled"
|
||||
description="Enable Sentinel traffic analytics on a server to see a team-wide summary here."
|
||||
icon-name="network">
|
||||
<x-slot:contents>
|
||||
<a class="button" href="{{ route('server.index') }}" {{ wireNavigate() }}>
|
||||
View servers
|
||||
</a>
|
||||
</x-slot:contents>
|
||||
</x-empty>
|
||||
@elseif (! $overview)
|
||||
<x-empty size="sm" title="No analytics data yet"
|
||||
description="We could not load traffic analytics for the selected range. Try a different range or check back shortly."
|
||||
icon-name="network" />
|
||||
@else
|
||||
{{-- Sparkline KPI cards. Each links through to the full analytics page. --}}
|
||||
{{-- Sparkline KPI cards. Each links through to the full analytics page. --}}
|
||||
<div class="grid grid-cols-1 gap-px overflow-hidden rounded-xl border border-neutral-200 bg-neutral-200 sm:grid-cols-2 lg:grid-cols-4 dark:border-white/[0.08] dark:bg-white/[0.07]">
|
||||
<a href="{{ route('analytics') }}" {{ wireNavigate() }}
|
||||
class="group flex flex-col bg-white px-4 py-3 transition-colors hover:bg-neutral-50 dark:bg-[color-mix(in_srgb,var(--color-app)_95%,white)] dark:hover:bg-[color-mix(in_srgb,var(--color-app)_93%,white)]">
|
||||
@@ -128,5 +115,6 @@ $spark = 'refreshChartData-'.$chartId.'-status';
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ $analyticsServerUuid = $application->destination?->server?->uuid;
|
||||
<x-slot:actions>
|
||||
<a class="button" href="{{ route('server.analytics', ['server_uuid' => $analyticsServerUuid]) }}"
|
||||
{{ wireNavigate() }}>
|
||||
Server settings
|
||||
Server analytics
|
||||
<x-external-link />
|
||||
</a>
|
||||
</x-slot:actions>
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
@elseif ($currentRoute === 'project.application.metrics')
|
||||
<livewire:project.shared.metrics :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.analytics')
|
||||
<livewire:project.application.analytics :application="$application" />
|
||||
<livewire:project.application.analytics :application="$application"
|
||||
:lazy="$application->destination?->server?->isTrafficAnalyticsEnabled()" />
|
||||
@elseif ($currentRoute === 'project.application.tags')
|
||||
<livewire:project.shared.tags :resource="$application" />
|
||||
@elseif ($currentRoute === 'project.application.danger')
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
|
||||
</x-application.settings-section>
|
||||
|
||||
<livewire:project.application.traffic-overview :application="$application"
|
||||
:key="'application-traffic-overview-'.$application->id" />
|
||||
|
||||
<x-application.settings-section id="access-section" title="Access" helper="Manage how this application is reached publicly and from the Docker network.">
|
||||
<section id="public-access-section" @class([
|
||||
'border-b border-neutral-200 pb-5 dark:border-white/[0.07]' => $buildPack !== 'dockercompose',
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
<x-external-link />
|
||||
</a>
|
||||
</x-slot:actions>
|
||||
<x-callout type="info" title="Metrics are not enabled">
|
||||
Enable Sentinel and metrics for this server before collecting application usage data.
|
||||
</x-callout>
|
||||
<x-empty size="sm" title="Metrics are not enabled"
|
||||
description="Enable Sentinel and metrics for this server before collecting application usage data."
|
||||
icon-name="dashboard" />
|
||||
</x-application.settings-section>
|
||||
@elseif (!str($resource->status)->contains('running'))
|
||||
<x-application.settings-section id="metrics-overview-section" title="Metrics"
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
:key="'server-traffic-analytics-settings-'.$server->uuid" />
|
||||
@endcan
|
||||
|
||||
<livewire:analytics :scoped-server-uuid="$server->uuid" :key="'server-analytics-'.$server->uuid" />
|
||||
<livewire:analytics :scoped-server-uuid="$server->uuid" :key="'server-analytics-'.$server->uuid"
|
||||
:lazy="$server->isTrafficAnalyticsEnabled()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
|
||||
@if ($server->proxyType() === ProxyTypes::TRAEFIK->value || $server->proxyType() === 'CADDY')
|
||||
<x-application.settings-section id="server-proxy-file-section" :title="$proxyTitle"
|
||||
x-init="$wire.loadProxyConfiguration()"
|
||||
helper="Edit the generated proxy compose configuration used on this server.">
|
||||
<x-slot:actions>
|
||||
@can('update', $server)
|
||||
@@ -128,6 +129,11 @@
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div wire:loading.flex wire:target="loadProxyConfiguration"
|
||||
class="min-h-32 items-center justify-center">
|
||||
<x-loading text="Loading proxy configuration…" />
|
||||
</div>
|
||||
|
||||
@if ($proxySettings)
|
||||
<div class="relative mt-4" wire:loading.class="pointer-events-none opacity-50"
|
||||
wire:target="submit,resetProxyConfiguration" aria-live="polite">
|
||||
|
||||
@@ -8,10 +8,22 @@
|
||||
<x-application.settings-section id="server-traffic-analytics-settings-section" title="Traffic analytics"
|
||||
helper="Control proxy traffic collection, retention, and visitor geolocation for this server.">
|
||||
<x-slot:actions>
|
||||
<x-forms.button canGate="update" :canResource="$server" wire:click="toggleTrafficAnalytics"
|
||||
wire:confirm="{{ $isTrafficAnalyticsEnabled ? 'Disable' : 'Enable' }} traffic analytics? The proxy and Sentinel will restart, causing a brief connectivity blip for all applications on this server.">
|
||||
{{ $isTrafficAnalyticsEnabled ? 'Disable' : 'Enable' }} traffic analytics
|
||||
</x-forms.button>
|
||||
@if ($isTrafficAnalyticsEnabled)
|
||||
<div class="flex items-center gap-3">
|
||||
<x-loading wire:loading.flex wire:target="toggleTrafficAnalytics"
|
||||
text="Restarting Sentinel and proxy..." compact />
|
||||
<x-modal-confirmation title="Disable traffic analytics?"
|
||||
buttonTitle="Disable traffic analytics" submitAction="toggleTrafficAnalytics"
|
||||
:actions="[
|
||||
'Disabling traffic analytics will restart Sentinel and the proxy. Your applications will experience a brief interruption.',
|
||||
]"
|
||||
warningMessage="Application traffic may be interrupted while Sentinel and the proxy restart."
|
||||
:confirmWithText="false" :confirmWithPassword="false" :ignoreWire="false"
|
||||
step2ButtonText="Disable traffic analytics"
|
||||
:disabled="! auth()->user()->can('update', $server)"
|
||||
:authDisabled="! auth()->user()->can('update', $server)" />
|
||||
</div>
|
||||
@endif
|
||||
</x-slot:actions>
|
||||
|
||||
@if ($isTrafficAnalyticsEnabled)
|
||||
@@ -45,7 +57,24 @@
|
||||
@else
|
||||
<x-empty size="sm" title="Traffic analytics is disabled"
|
||||
description="Enable traffic analytics to collect proxy access logs and geolocate visitor traffic."
|
||||
icon-name="dashboard" />
|
||||
icon-name="dashboard">
|
||||
<x-slot:contents>
|
||||
<div class="flex items-center gap-3">
|
||||
<x-loading wire:loading.flex wire:target="toggleTrafficAnalytics"
|
||||
text="Restarting Sentinel and proxy..." compact />
|
||||
<x-modal-confirmation title="Enable traffic analytics?"
|
||||
buttonTitle="Enable traffic analytics" submitAction="toggleTrafficAnalytics"
|
||||
:actions="[
|
||||
'Enabling traffic analytics will restart Sentinel and the proxy. Your applications will experience a brief interruption.',
|
||||
]"
|
||||
warningMessage="Application traffic may be interrupted while Sentinel and the proxy restart."
|
||||
:confirmWithText="false" :confirmWithPassword="false" :ignoreWire="false"
|
||||
step2ButtonText="Enable traffic analytics" isHighlightedButton
|
||||
:disabled="! auth()->user()->can('update', $server)"
|
||||
:authDisabled="! auth()->user()->can('update', $server)" />
|
||||
</div>
|
||||
</x-slot:contents>
|
||||
</x-empty>
|
||||
@endif
|
||||
</x-application.settings-section>
|
||||
</form>
|
||||
|
||||
@@ -21,6 +21,12 @@ test('compose file loading waits for the user to confirm the file location', fun
|
||||
->not->toContain('x-init="$wire.dispatch(\'loadCompose\', true)"');
|
||||
});
|
||||
|
||||
test('traffic analytics is only shown on the dedicated analytics page', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
|
||||
|
||||
expect($view)->not->toContain('<livewire:project.application.traffic-overview');
|
||||
});
|
||||
|
||||
test('docker compose heading separates its title and action', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/application/general.blade.php'));
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
test('disabled application metrics use the standard empty state', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/project/shared/metrics.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('<x-empty size="sm" title="Metrics are not enabled"')
|
||||
->toContain('description="Enable Sentinel and metrics for this server before collecting application usage data."')
|
||||
->toContain('icon-name="dashboard"')
|
||||
->not->toContain('<x-callout type="info" title="Metrics are not enabled">');
|
||||
});
|
||||
@@ -10,3 +10,21 @@ it('disables proxy configuration controls and covers the editor while saving', f
|
||||
->toContain('Updating proxy configuration')
|
||||
->toContain('aria-live="polite"');
|
||||
});
|
||||
|
||||
it('loads only the compose file from the frontend and shows the shared loading indicator', function () {
|
||||
$page = file_get_contents(resource_path('views/livewire/server/proxy/show.blade.php'));
|
||||
$proxy = file_get_contents(resource_path('views/livewire/server/proxy.blade.php'));
|
||||
$component = file_get_contents(app_path('Livewire/Server/Proxy.php'));
|
||||
|
||||
expect($page)
|
||||
->toContain('<livewire:server.proxy :server="$server" />')
|
||||
->not->toContain('<livewire:server.proxy :server="$server" lazy />');
|
||||
|
||||
expect($proxy)
|
||||
->toContain('x-init="$wire.loadProxyConfiguration()"')
|
||||
->toContain('wire:loading.flex wire:target="loadProxyConfiguration"')
|
||||
->toContain('<x-loading text="Loading proxy configuration…" />');
|
||||
|
||||
expect($component)
|
||||
->not->toContain('$this->loadProxyConfiguration();');
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Livewire\Project\Application\Analytics;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
@@ -82,6 +83,7 @@ beforeEach(function () {
|
||||
// Server (from a prior enabled test) can leak into a later test and be treated as
|
||||
// analytics-enabled, mounting the component against an unreachable server.
|
||||
Server::flushIdentityMap();
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
@@ -114,6 +116,35 @@ function makeAnalyticsApplication(Team $team, PrivateKey $privateKey, Environmen
|
||||
]);
|
||||
}
|
||||
|
||||
it('only lazy loads application analytics when traffic analytics is enabled', function () {
|
||||
$configuration = file_get_contents(resource_path('views/livewire/project/application/configuration.blade.php'));
|
||||
|
||||
expect($configuration)
|
||||
->toContain(':lazy="$application->destination?->server?->isTrafficAnalyticsEnabled()"');
|
||||
});
|
||||
|
||||
it('renders the disabled state in the initial application analytics page response', function () {
|
||||
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, false);
|
||||
|
||||
$this->get(route('project.application.analytics', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'application_uuid' => $application->uuid,
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSee('Traffic analytics is not enabled')
|
||||
->assertDontSee('__lazyLoad', escape: false)
|
||||
->assertDontSee('analytics-range-section');
|
||||
});
|
||||
|
||||
it('guards the lazy placeholder when traffic analytics is disabled', function () {
|
||||
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, false);
|
||||
|
||||
Livewire::test(Analytics::class, ['application' => $application, 'lazy' => true])
|
||||
->assertSee('Traffic analytics is not enabled')
|
||||
->assertDontSee('analytics-range-section');
|
||||
});
|
||||
|
||||
it('renders KPIs from a mocked traffic client when analytics is enabled', function () {
|
||||
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, true);
|
||||
|
||||
@@ -189,9 +220,13 @@ it('falls back to the donut for the per-app chart when the series endpoint is ab
|
||||
it('shows an empty state when traffic analytics is disabled for the server', function () {
|
||||
$application = makeAnalyticsApplication($this->team, $this->privateKey, $this->environment, false);
|
||||
|
||||
loadLazy(Livewire::test(Analytics::class, ['application' => $application]))
|
||||
Livewire::test(Analytics::class, ['application' => $application, 'lazy' => false])
|
||||
->assertOk()
|
||||
->assertSee('Analytics')
|
||||
->assertSee('Traffic analytics is not enabled')
|
||||
->assertSee('Server analytics')
|
||||
->assertSeeHtml(route('server.analytics', ['server_uuid' => $application->destination->server->uuid]))
|
||||
->assertDontSee('__lazyLoad', escape: false)
|
||||
->assertDontSee('Unique visitors');
|
||||
});
|
||||
|
||||
@@ -204,8 +239,10 @@ it('renders the disabled empty-state without crashing when the application has n
|
||||
|
||||
expect($application->destination)->toBeNull();
|
||||
|
||||
loadLazy(Livewire::test(Analytics::class, ['application' => $application]))
|
||||
Livewire::test(Analytics::class, ['application' => $application, 'lazy' => false])
|
||||
->assertOk()
|
||||
->assertSee('Analytics')
|
||||
->assertDontSee('Server settings');
|
||||
->assertSee('Traffic analytics is not enabled')
|
||||
->assertDontSee('__lazyLoad', escape: false)
|
||||
->assertDontSee('Server analytics');
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Livewire\Dashboard\TrafficAnalytics;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
@@ -67,6 +68,19 @@ beforeEach(function () {
|
||||
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
||||
});
|
||||
|
||||
it('hides traffic analytics from the dashboard when no server has it enabled', function () {
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
]);
|
||||
$server->settings->is_traffic_analytics_enabled = false;
|
||||
$server->settings->save();
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertOk()
|
||||
->assertDontSee('Traffic analytics');
|
||||
});
|
||||
|
||||
it('renders the team traffic summary aggregated across servers with an approximate badge', function () {
|
||||
$serverOne = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
@@ -150,6 +164,14 @@ it('shows loading states while the dashboard range refreshes', function () {
|
||||
->toContain('aria-label="Loading analytics"');
|
||||
});
|
||||
|
||||
it('styles the open analytics link as a dashboard action button', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/dashboard/traffic-analytics.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('class="group inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-neutral-200 bg-white px-2.5')
|
||||
->toContain('group-hover:translate-x-0.5');
|
||||
});
|
||||
|
||||
it('uses the dashboard surface treatment for the analytics KPI group', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/dashboard/traffic-analytics.blade.php'));
|
||||
|
||||
@@ -162,7 +184,7 @@ it('uses the dashboard surface treatment for the analytics KPI group', function
|
||||
->not->toContain('dark:bg-base dark:hover:bg-white/[0.03]');
|
||||
});
|
||||
|
||||
it('shows a failure empty-state instead of an all-zero KPI panel when every server fetch fails', function () {
|
||||
it('hides dashboard analytics when every server fetch fails', function () {
|
||||
$serverOne = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
@@ -183,12 +205,14 @@ it('shows a failure empty-state instead of an all-zero KPI panel when every serv
|
||||
|
||||
loadLazy(Livewire::test(TrafficAnalytics::class))
|
||||
->assertOk()
|
||||
->assertSee('No analytics data yet')
|
||||
->assertSeeHtml('class="contents"')
|
||||
->assertDontSee('Traffic analytics')
|
||||
->assertDontSee('No analytics data yet')
|
||||
->assertDontSee('Unique visitors')
|
||||
->assertDontSee('Error rate');
|
||||
});
|
||||
|
||||
it('shows an empty state when no server in the team has traffic analytics enabled', function () {
|
||||
it('renders nothing when no server in the team has traffic analytics enabled', function () {
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $this->privateKey->id,
|
||||
@@ -200,5 +224,13 @@ it('shows an empty state when no server in the team has traffic analytics enable
|
||||
loadLazy(Livewire::test(TrafficAnalytics::class))
|
||||
->assertOk()
|
||||
->assertDontSee('Unique visitors')
|
||||
->assertSee('not enabled');
|
||||
->assertDontSee('Traffic analytics');
|
||||
});
|
||||
|
||||
it('uses an empty lazy placeholder so analytics only appears after data loads', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/dashboard/traffic-analytics-placeholder.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain('class="contents"')
|
||||
->not->toContain('Traffic analytics');
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Livewire\Analytics;
|
||||
use App\Livewire\Server\Analytics\Show;
|
||||
use App\Livewire\Server\TrafficAnalyticsSettings;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
@@ -13,6 +14,7 @@ use Livewire\Livewire;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = $this->user->teams()->first();
|
||||
$this->actingAs($this->user);
|
||||
@@ -60,6 +62,20 @@ it('moves traffic analytics configuration out of sentinel and onto analytics', f
|
||||
->not->toContain('id="trafficTopn"');
|
||||
});
|
||||
|
||||
it('matches the server metrics empty state when traffic analytics is disabled', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/traffic-analytics-settings.blade.php'));
|
||||
$disabledState = str($view)
|
||||
->after('@else')
|
||||
->before('@endif')
|
||||
->toString();
|
||||
|
||||
expect($disabledState)
|
||||
->toContain('title="Traffic analytics is disabled"')
|
||||
->toContain('<x-slot:contents>')
|
||||
->toContain('isHighlightedButton')
|
||||
->toContain('buttonTitle="Enable traffic analytics"');
|
||||
});
|
||||
|
||||
it('renders traffic analytics settings above the server analytics dashboard', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/analytics/show.blade.php'));
|
||||
|
||||
@@ -67,6 +83,13 @@ it('renders traffic analytics settings above the server analytics dashboard', fu
|
||||
->toBeLessThan(strpos($view, '<livewire:analytics'));
|
||||
});
|
||||
|
||||
it('only lazy loads the scoped dashboard when traffic analytics is enabled', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/analytics/show.blade.php'));
|
||||
|
||||
expect($view)
|
||||
->toContain(':lazy="$server->isTrafficAnalyticsEnabled()"');
|
||||
});
|
||||
|
||||
it('matches other server pages without a visible page title', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/analytics/show.blade.php'));
|
||||
|
||||
@@ -88,6 +111,41 @@ it('scopes the server analytics page to its route server', function () {
|
||||
->assertDontSee($otherServer->name);
|
||||
});
|
||||
|
||||
it('does not duplicate the disabled state on a scoped server analytics dashboard', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$server->settings->is_traffic_analytics_enabled = false;
|
||||
$server->settings->save();
|
||||
|
||||
Livewire::test(Analytics::class, ['scopedServerUuid' => $server->uuid])
|
||||
->assertDontSee('Traffic analytics is not enabled');
|
||||
});
|
||||
|
||||
it('removes stale analytics content when traffic analytics is disabled', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$server->settings->is_traffic_analytics_enabled = true;
|
||||
$server->settings->save();
|
||||
|
||||
$component = Livewire::test(Analytics::class, ['scopedServerUuid' => $server->uuid]);
|
||||
|
||||
$server->settings->is_traffic_analytics_enabled = false;
|
||||
$server->settings->save();
|
||||
|
||||
$component
|
||||
->dispatch('trafficAnalyticsStateChanged')
|
||||
->assertSet('servers', fn ($servers) => $servers->isEmpty())
|
||||
->assertDontSee('No analytics data yet');
|
||||
});
|
||||
|
||||
it('does not render a skeleton placeholder for a disabled scoped server', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$server->settings->is_traffic_analytics_enabled = false;
|
||||
$server->settings->save();
|
||||
|
||||
Livewire::test(Analytics::class, ['scopedServerUuid' => $server->uuid, 'lazy' => true])
|
||||
->assertDontSee('analytics-overview-section')
|
||||
->assertDontSee('analytics-requests-section');
|
||||
});
|
||||
|
||||
it('saves traffic analytics settings from the server analytics page', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ beforeEach(function () {
|
||||
$this->team = $user->teams()->first();
|
||||
});
|
||||
|
||||
it('defaults traffic analytics to enabled for a normal server and exposes a server helper', function () {
|
||||
it('defaults traffic analytics to disabled for a normal server and exposes a server helper', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
expect($server->settings->is_traffic_analytics_enabled)->toBeTrue();
|
||||
expect($server->isTrafficAnalyticsEnabled())->toBeTrue();
|
||||
expect($server->settings->is_traffic_analytics_enabled)->toBeFalse();
|
||||
expect($server->isTrafficAnalyticsEnabled())->toBeFalse();
|
||||
|
||||
$server->settings->is_traffic_analytics_enabled = false;
|
||||
$server->settings->is_traffic_analytics_enabled = true;
|
||||
$server->settings->save();
|
||||
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeFalse();
|
||||
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue();
|
||||
});
|
||||
|
||||
it('defaults traffic analytics to disabled for a swarm server', function () {
|
||||
@@ -44,14 +44,14 @@ it('defaults traffic analytics to disabled for a build server', function () {
|
||||
expect($setting->is_traffic_analytics_enabled)->toBeFalse();
|
||||
});
|
||||
|
||||
it('respects an explicit traffic analytics value on creation', function () {
|
||||
it('respects an explicit enabled traffic analytics value on creation', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$setting = ServerSetting::create([
|
||||
'server_id' => $server->id,
|
||||
'is_traffic_analytics_enabled' => false,
|
||||
'is_traffic_analytics_enabled' => true,
|
||||
]);
|
||||
|
||||
expect($setting->is_traffic_analytics_enabled)->toBeFalse();
|
||||
expect($setting->is_traffic_analytics_enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
it('defaults traffic collection and geoip settings to sentinel values', function () {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Actions\Server\ConfigureTrafficAnalytics;
|
||||
use App\Livewire\Analytics;
|
||||
use App\Livewire\Server\TrafficAnalyticsSettings;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -11,6 +13,7 @@ use Livewire\Livewire;
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = $this->user->teams()->first();
|
||||
$this->actingAs($this->user);
|
||||
@@ -32,11 +35,32 @@ it('toggles traffic analytics via the sentinel settings component', function ()
|
||||
|
||||
Livewire::test(TrafficAnalyticsSettings::class, ['server' => $server])
|
||||
->call('toggleTrafficAnalytics')
|
||||
->assertDispatchedTo(Analytics::class, 'trafficAnalyticsStateChanged')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue();
|
||||
});
|
||||
|
||||
it('warns about the application interruption before enabling traffic analytics', function () {
|
||||
$server = Server::factory()->create(['team_id' => $this->team->id]);
|
||||
$server->settings->is_traffic_analytics_enabled = false;
|
||||
$server->settings->save();
|
||||
|
||||
Livewire::test(TrafficAnalyticsSettings::class, ['server' => $server])
|
||||
->assertSee('Enable traffic analytics?')
|
||||
->assertDontSeeHtml('wire:confirm')
|
||||
->assertSeeHtml('wire:loading.flex')
|
||||
->assertSeeHtml('wire:target="toggleTrafficAnalytics"')
|
||||
->assertSee('Restarting Sentinel and proxy...')
|
||||
->assertSee('Enabling traffic analytics will restart Sentinel and the proxy. Your applications will experience a brief interruption.');
|
||||
});
|
||||
|
||||
it('allows the analytics toggle modal to update after the state changes', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/server/traffic-analytics-settings.blade.php'));
|
||||
|
||||
expect($view)->toContain(':ignoreWire="false"');
|
||||
});
|
||||
|
||||
it('does not enable traffic analytics on a swarm server', function () {
|
||||
ConfigureTrafficAnalytics::partialMock()->shouldReceive('handle')->never();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user