From f17cdc1c7411b4dcc945370ebfe5dd41965a29d6 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:20:41 +0200 Subject: [PATCH] feat(analytics): add server analytics page and split settings Add a per-server Analytics page scoped to that host, move traffic settings out of Sentinel, and put metrics settings on Charts. Surface 4xx/5xx on path rows and restyle deployment logs. --- app/Data/Traffic/TrafficPathData.php | 4 + app/Livewire/Analytics.php | 62 +++-- app/Livewire/Server/Analytics/Show.php | 26 ++ app/Livewire/Server/Charts.php | 38 ++- app/Livewire/Server/Sentinel.php | 74 ------ .../Server/TrafficAnalyticsSettings.php | 109 +++++++++ resources/css/app.css | 222 ++++++++++++++++++ .../views/components/server/sidebar.blade.php | 8 + .../livewire/analytics-placeholder.blade.php | 18 +- resources/views/livewire/analytics.blade.php | 56 +++-- .../project/application/analytics.blade.php | 2 +- .../application/traffic-overview.blade.php | 2 +- .../project/shared/get-logs.blade.php | 56 ++++- .../livewire/server/analytics/show.blade.php | 21 ++ .../views/livewire/server/charts.blade.php | 88 ++++--- .../views/livewire/server/sentinel.blade.php | 61 +---- .../traffic-analytics-settings.blade.php | 52 ++++ .../livewire/traffic/_device-chart.blade.php | 18 +- .../livewire/traffic/_paths-list.blade.php | 9 + routes/web.php | 2 + tests/Feature/DeploymentLogsLayoutTest.php | 18 ++ tests/Feature/SentinelUnsavedBarFlashTest.php | 14 +- .../Server/ServerMetricsSettingsTest.php | 93 ++++++++ .../TrafficAnalytics/ChartTokensTest.php | 8 + .../TrafficAnalytics/GlobalAnalyticsTest.php | 6 +- .../ServerAnalyticsPageTest.php | 116 +++++++++ .../ToggleTrafficAnalyticsTest.php | 16 +- .../TrafficAnalytics/TrafficDataTest.php | 17 ++ 28 files changed, 970 insertions(+), 246 deletions(-) create mode 100644 app/Livewire/Server/Analytics/Show.php create mode 100644 app/Livewire/Server/TrafficAnalyticsSettings.php create mode 100644 resources/views/livewire/server/analytics/show.blade.php create mode 100644 resources/views/livewire/server/traffic-analytics-settings.blade.php create mode 100644 tests/Feature/Server/ServerMetricsSettingsTest.php create mode 100644 tests/Feature/TrafficAnalytics/ServerAnalyticsPageTest.php diff --git a/app/Data/Traffic/TrafficPathData.php b/app/Data/Traffic/TrafficPathData.php index 40549c9ebf..22fa8359da 100644 --- a/app/Data/Traffic/TrafficPathData.php +++ b/app/Data/Traffic/TrafficPathData.php @@ -10,6 +10,8 @@ class TrafficPathData extends Data public string $path, public int $requests, public int $bytesOut, + public int $s4xx, + public int $s5xx, public float $p50, public float $p95, // Owning app key (Sentinel returns this per path row so the UI can show the @@ -23,6 +25,8 @@ class TrafficPathData extends Data path: (string) data_get($row, 'path', ''), requests: (int) data_get($row, 'requests', 0), bytesOut: (int) data_get($row, 'bytes_out', 0), + s4xx: (int) data_get($row, 's4xx', 0), + s5xx: (int) data_get($row, 's5xx', 0), p50: (float) data_get($row, 'p50', 0.0), p95: (float) data_get($row, 'p95', 0.0), app: (string) data_get($row, 'app', ''), diff --git a/app/Livewire/Analytics.php b/app/Livewire/Analytics.php index 993d2aa4c1..24ba69d2d4 100644 --- a/app/Livewire/Analytics.php +++ b/app/Livewire/Analytics.php @@ -21,6 +21,8 @@ class Analytics extends Component public string $chartId = 'global-analytics'; + public ?string $scopedServerUuid = null; + /** Traffic-enabled servers owned by the current team. */ public Collection $servers; @@ -109,32 +111,46 @@ class Analytics extends Component */ protected array $appMetaCache = []; - public function mount(): void + public function mount(?string $scopedServerUuid = null): void { $allServers = Server::ownedByCurrentTeamCached(); - $this->servers = $allServers - ->filter(fn (Server $server) => $server->isTrafficAnalyticsEnabled()) - ->values(); + $this->scopedServerUuid = $scopedServerUuid; - $this->serverOptions = $this->servers - ->mapWithKeys(fn (Server $server) => [$server->uuid => $server->name]) - ->all(); + if ($this->scopedServerUuid !== null) { + $server = $allServers->firstWhere('uuid', $this->scopedServerUuid); + abort_if($server === null, 404); - $eligibleDisabled = $allServers - ->filter(fn (Server $server) => ! $server->isTrafficAnalyticsEnabled() - && ! $server->isSwarm() - && ! $server->isBuildServer()) - ->values(); + $this->serverUuid = $server->uuid; + $this->chartId = 'server-analytics-'.$server->uuid; + $this->servers = $server->isTrafficAnalyticsEnabled() ? collect([$server]) : collect(); + $this->serverOptions = [$server->uuid => $server->name]; + $this->eligibleDisabledServers = []; + $this->nudgeKey = ''; + } else { + $this->servers = $allServers + ->filter(fn (Server $server) => $server->isTrafficAnalyticsEnabled()) + ->values(); - $this->eligibleDisabledServers = $eligibleDisabled - ->map(fn (Server $server) => ['uuid' => $server->uuid, 'name' => $server->name]) - ->all(); - $this->nudgeKey = substr(md5($eligibleDisabled->pluck('uuid')->sort()->implode(',')), 0, 12); + $this->serverOptions = $this->servers + ->mapWithKeys(fn (Server $server) => [$server->uuid => $server->name]) + ->all(); - // A bookmarked ?server= may point at a server that is no longer enabled. - if ($this->serverUuid !== '' && ! array_key_exists($this->serverUuid, $this->serverOptions)) { - $this->serverUuid = ''; + $eligibleDisabled = $allServers + ->filter(fn (Server $server) => ! $server->isTrafficAnalyticsEnabled() + && ! $server->isSwarm() + && ! $server->isBuildServer()) + ->values(); + + $this->eligibleDisabledServers = $eligibleDisabled + ->map(fn (Server $server) => ['uuid' => $server->uuid, 'name' => $server->name]) + ->all(); + $this->nudgeKey = substr(md5($eligibleDisabled->pluck('uuid')->sort()->implode(',')), 0, 12); + + // A bookmarked ?server= may point at a server that is no longer enabled. + if ($this->serverUuid !== '' && ! array_key_exists($this->serverUuid, $this->serverOptions)) { + $this->serverUuid = ''; + } } $this->refreshAppOptions(); @@ -226,6 +242,10 @@ class Analytics extends Component */ protected function targetServers(): Collection { + if ($this->scopedServerUuid !== null) { + return $this->servers; + } + if ($this->appUuid !== '') { $server = Application::ownedByCurrentTeam()->whereUuid($this->appUuid)->first() ?->destination?->server; @@ -311,9 +331,11 @@ class Analytics extends Component $key = $resolveId."\n".$pathStr; $domain = $resolveId !== '' ? ($this->appMeta($resolveId)['domain'] ?? null) : null; - $pathTotals[$key] ??= ['path' => $pathStr, 'domain' => $domain, 'requests' => 0, 'bytesOut' => 0, 'p95' => 0.0]; + $pathTotals[$key] ??= ['path' => $pathStr, 'domain' => $domain, 'requests' => 0, 'bytesOut' => 0, 's4xx' => 0, 's5xx' => 0, 'p95' => 0.0]; $pathTotals[$key]['requests'] += (int) ($data['requests'] ?? 0); $pathTotals[$key]['bytesOut'] += (int) ($data['bytesOut'] ?? 0); + $pathTotals[$key]['s4xx'] += (int) ($data['s4xx'] ?? 0); + $pathTotals[$key]['s5xx'] += (int) ($data['s5xx'] ?? 0); $pathTotals[$key]['p95'] = max($pathTotals[$key]['p95'], (float) ($data['p95'] ?? 0)); } diff --git a/app/Livewire/Server/Analytics/Show.php b/app/Livewire/Server/Analytics/Show.php new file mode 100644 index 0000000000..abcf904e0c --- /dev/null +++ b/app/Livewire/Server/Analytics/Show.php @@ -0,0 +1,26 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->authorize('view', $this->server); + } + + public function render(): View + { + return view('livewire.server.analytics.show'); + } +} diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php index 1cda771a7c..567034c801 100644 --- a/app/Livewire/Server/Charts.php +++ b/app/Livewire/Server/Charts.php @@ -5,6 +5,7 @@ namespace App\Livewire\Server; use App\Actions\Server\StartSentinel; use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Attributes\Validate; use Livewire\Component; class Charts extends Component @@ -23,15 +24,44 @@ class Charts extends Component public bool $poll = true; + #[Validate(['required', 'integer', 'min:1'])] + public int|string $sentinelMetricsRefreshRateSeconds; + + #[Validate(['required', 'integer', 'min:1'])] + public int|string $sentinelMetricsHistoryDays; + + #[Validate(['required', 'integer', 'min:10'])] + public int|string $sentinelPushIntervalSeconds; + public function mount(string $server_uuid) { try { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; + $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; + $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; } catch (\Throwable $e) { return handleError($e, $this); } } + public function saveMetricsSettings(): void + { + try { + $this->authorize('update', $this->server); + $this->validate(); + + $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds; + $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays; + $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds; + $this->server->settings->save(); + + $this->dispatch('success', 'Metrics settings updated. Restarting Sentinel.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + public function toggleMetrics(): void { try { @@ -70,11 +100,9 @@ class Charts extends Component try { $cpuMetrics = $this->server->getCpuMetrics($this->interval); $memoryMetrics = $this->server->getMemoryMetrics($this->interval); - $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ - 'seriesData' => $cpuMetrics, - ]); - $this->dispatch("refreshChartData-{$this->chartId}-memory", [ - 'seriesData' => $memoryMetrics, + $this->dispatch("refreshChartData-{$this->chartId}-metrics", [ + 'cpuSeries' => $cpuMetrics, + 'memorySeries' => $memoryMetrics, ]); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Server/Sentinel.php b/app/Livewire/Server/Sentinel.php index d467380ba3..52010a91c4 100644 --- a/app/Livewire/Server/Sentinel.php +++ b/app/Livewire/Server/Sentinel.php @@ -2,7 +2,6 @@ namespace App\Livewire\Server; -use App\Actions\Server\ConfigureTrafficAnalytics; use App\Actions\Server\StartSentinel; use App\Actions\Server\StopSentinel; use App\Models\Server; @@ -23,15 +22,6 @@ class Sentinel extends Component public ?string $sentinelUpdatedAt = null; - #[Validate(['required', 'integer', 'min:1'])] - public int|string $sentinelMetricsRefreshRateSeconds; - - #[Validate(['required', 'integer', 'min:1'])] - public int|string $sentinelMetricsHistoryDays; - - #[Validate(['required', 'integer', 'min:10'])] - public int|string $sentinelPushIntervalSeconds; - #[Validate(['nullable', 'url'])] public ?string $sentinelCustomUrl = null; @@ -41,28 +31,6 @@ class Sentinel extends Component public ?string $sentinelCustomDockerImage = null; - public bool $isTrafficAnalyticsEnabled; - - #[Validate(['required', 'integer', 'min:1'])] - public int|string $trafficTopn; - - #[Validate(['required', 'integer', 'min:0'])] - public int|string $trafficSampleThreshold; - - #[Validate(['required', 'integer', 'min:1'])] - public int|string $trafficRetention1hDays; - - #[Validate(['required', 'integer', 'min:1'])] - public int|string $trafficRetention1dDays; - - public bool $isGeoipEnabled; - - #[Validate(['required', 'integer', 'min:1'])] - public int|string $geoipRefreshDays; - - #[Validate(['nullable', 'string', 'max:255'])] - public ?string $geoipMaxmindLicenseKey = null; - public function getListeners() { $teamId = $this->server->team_id ?? auth()->user()->currentTeam()->id; @@ -83,38 +51,17 @@ class Sentinel extends Component $this->validate(); $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled; $this->server->settings->sentinel_token = $this->sentinelToken; - $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds; - $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays; - $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds; $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; - $this->server->settings->traffic_topn = $this->trafficTopn; - $this->server->settings->traffic_sample_threshold = $this->trafficSampleThreshold; - $this->server->settings->traffic_retention_1h_days = $this->trafficRetention1hDays; - $this->server->settings->traffic_retention_1d_days = $this->trafficRetention1dDays; - $this->server->settings->is_geoip_enabled = $this->isGeoipEnabled; - $this->server->settings->geoip_refresh_days = $this->geoipRefreshDays; - $this->server->settings->geoip_maxmind_license_key = $this->geoipMaxmindLicenseKey; $this->server->settings->save(); } else { $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled; $this->sentinelToken = $this->server->settings->sentinel_token; - $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; - $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; - $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; $this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled; $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; $this->sentinelUpdatedAt = $this->server->sentinel_updated_at; - $this->isTrafficAnalyticsEnabled = $this->server->isTrafficAnalyticsEnabled(); - $this->trafficTopn = $this->server->settings->traffic_topn; - $this->trafficSampleThreshold = $this->server->settings->traffic_sample_threshold; - $this->trafficRetention1hDays = $this->server->settings->traffic_retention_1h_days; - $this->trafficRetention1dDays = $this->server->settings->traffic_retention_1d_days; - $this->isGeoipEnabled = (bool) $this->server->settings->is_geoip_enabled; - $this->geoipRefreshDays = $this->server->settings->geoip_refresh_days; - $this->geoipMaxmindLicenseKey = $this->server->settings->geoip_maxmind_license_key; } } @@ -168,27 +115,6 @@ class Sentinel extends Component } } - public function toggleTrafficAnalytics(): void - { - try { - $this->authorize('update', $this->server); - if ($this->server->isSwarm() || $this->server->isBuildServer()) { - $this->dispatch('error', 'Traffic analytics is not supported on Swarm/Build servers.'); - - return; - } - $enable = ! $this->server->isTrafficAnalyticsEnabled(); - ConfigureTrafficAnalytics::run($this->server, $enable); - $this->server->refresh(); - $this->isTrafficAnalyticsEnabled = $this->server->isTrafficAnalyticsEnabled(); - $this->dispatch('success', $enable - ? 'Traffic analytics enabled. Restarting proxy and Sentinel.' - : 'Traffic analytics disabled. Restarting proxy and Sentinel.'); - } catch (\Throwable $e) { - handleError($e, $this); - } - } - public function regenerateSentinelToken() { try { diff --git a/app/Livewire/Server/TrafficAnalyticsSettings.php b/app/Livewire/Server/TrafficAnalyticsSettings.php new file mode 100644 index 0000000000..48022d9aed --- /dev/null +++ b/app/Livewire/Server/TrafficAnalyticsSettings.php @@ -0,0 +1,109 @@ +authorize('update', $this->server); + $this->syncData(); + } + + private function syncData(bool $toModel = false): void + { + if ($toModel) { + $this->validate(); + $this->server->settings->traffic_topn = $this->trafficTopn; + $this->server->settings->traffic_sample_threshold = $this->trafficSampleThreshold; + $this->server->settings->traffic_retention_1h_days = $this->trafficRetention1hDays; + $this->server->settings->traffic_retention_1d_days = $this->trafficRetention1dDays; + $this->server->settings->is_geoip_enabled = $this->isGeoipEnabled; + $this->server->settings->geoip_refresh_days = $this->geoipRefreshDays; + $this->server->settings->geoip_maxmind_license_key = $this->geoipMaxmindLicenseKey; + $this->server->settings->save(); + + return; + } + + $this->isTrafficAnalyticsEnabled = $this->server->isTrafficAnalyticsEnabled(); + $this->trafficTopn = $this->server->settings->traffic_topn; + $this->trafficSampleThreshold = $this->server->settings->traffic_sample_threshold; + $this->trafficRetention1hDays = $this->server->settings->traffic_retention_1h_days; + $this->trafficRetention1dDays = $this->server->settings->traffic_retention_1d_days; + $this->isGeoipEnabled = (bool) $this->server->settings->is_geoip_enabled; + $this->geoipRefreshDays = $this->server->settings->geoip_refresh_days; + $this->geoipMaxmindLicenseKey = $this->server->settings->geoip_maxmind_license_key; + } + + public function toggleTrafficAnalytics(): void + { + try { + $this->authorize('update', $this->server); + if ($this->server->isSwarm() || $this->server->isBuildServer()) { + $this->dispatch('error', 'Traffic analytics is not supported on Swarm/Build servers.'); + + return; + } + + $enable = ! $this->server->isTrafficAnalyticsEnabled(); + ConfigureTrafficAnalytics::run($this->server, $enable); + $this->server->refresh(); + $this->isTrafficAnalyticsEnabled = $this->server->isTrafficAnalyticsEnabled(); + $this->dispatch('success', $enable + ? 'Traffic analytics enabled. Restarting proxy and Sentinel.' + : 'Traffic analytics disabled. Restarting proxy and Sentinel.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function saveTrafficAnalyticsSettings(): void + { + try { + $this->authorize('update', $this->server); + $this->syncData(true); + $this->dispatch('success', 'Traffic analytics settings updated. Restarting Sentinel.'); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function render(): View + { + return view('livewire.server.traffic-analytics-settings'); + } +} diff --git a/resources/css/app.css b/resources/css/app.css index 3de7cc1907..c5f7b5166d 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -3624,6 +3624,228 @@ html[data-theme="custom"] .logs-viewer-timestamp { color: var(--color-fg-dim); } +.runtime-log-panel { + --runtime-log-line: rgba(0, 0, 0, 0.08); + --runtime-log-muted: #66666f; + --runtime-log-hover: rgba(0, 0, 0, 0.04); + --runtime-log-detail: rgba(0, 0, 0, 0.03); + --runtime-log-columns: 12.75rem 5.5rem minmax(0, 1fr); +} + +.dark .runtime-log-panel { + --runtime-log-line: var(--glass-line, rgba(255, 255, 255, 0.065)); + --runtime-log-muted: #a09da5; + --runtime-log-hover: rgba(255, 255, 255, 0.035); + --runtime-log-detail: rgba(0, 0, 0, 0.24); +} + +.runtime-log-viewport.logs-viewer-viewport { + container: runtime-log-explorer / inline-size; + padding: 0; +} + +.runtime-log-viewport.logs-viewer-viewport::after { + display: none; +} + +.runtime-log-columns, +.runtime-log-viewport [data-log-line]:not(.hidden) { + display: grid; + grid-template-columns: var(--runtime-log-columns); + gap: 0.625rem; + box-sizing: border-box; + width: 100%; + min-height: 2.75rem; + align-items: center; + padding: 0.6875rem 1.875rem 0.6875rem 1rem; +} + +.runtime-log-columns { + position: sticky; + top: 0; + z-index: 10; + border-bottom: 1px solid var(--runtime-log-line); + background: #f0eff2; + color: var(--runtime-log-muted); + font-family: ui-sans-serif, system-ui, sans-serif; + font-size: 0.75rem; + font-weight: 500; +} + +.dark .runtime-log-columns { + background: var(--coollabs-elevated); +} + +html[data-theme="custom"] .runtime-log-columns { + background: var(--color-log-toolbar); +} + +.runtime-log-viewport [data-log-line] { + position: relative; + border-bottom: 1px solid var(--runtime-log-line); + border-radius: 0; + cursor: pointer; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 0.8125rem; + line-height: 1.6; +} + +.runtime-log-viewport [data-log-line]:hover, +.runtime-log-viewport [data-log-line][aria-expanded="true"] { + background: var(--runtime-log-hover); +} + +.runtime-log-viewport [data-log-line]:focus-visible { + outline: 2px solid var(--color-accent, #b93642); + outline-offset: -2px; +} + +.runtime-log-viewport [data-log-line]::before { + content: "[" attr(data-log-level) "]"; + grid-column: 2; + grid-row: 1; + color: var(--runtime-log-muted); + text-transform: uppercase; +} + +.runtime-log-viewport [data-log-line]::after { + content: ""; + position: absolute; + top: 1rem; + right: 0.75rem; + width: 0.375rem; + height: 0.375rem; + border-right: 1.5px solid var(--runtime-log-muted); + border-bottom: 1.5px solid var(--runtime-log-muted); + transform: rotate(45deg); +} + +.runtime-log-viewport [data-log-line][aria-expanded="true"]::after { + top: 1.1875rem; + transform: rotate(225deg); +} + +.runtime-log-viewport .log-error::before { color: #e55e73; } +.runtime-log-viewport .log-warning::before { color: #d79945; } +.runtime-log-viewport .log-debug::before { color: #929099; } +.runtime-log-viewport .log-info::before { color: #8891f0; } + +.runtime-log-viewport .logs-viewer-timestamp { + grid-column: 1; + grid-row: 1; + color: var(--runtime-log-muted); + font-size: 0.8125rem; + line-height: 1.6; + white-space: nowrap; +} + +.runtime-log-viewport [data-line-text] { + grid-column: 3; + grid-row: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.runtime-log-detail { + margin: 0 0 0.25rem; + padding: 1.25rem; + border-bottom: 1px solid var(--runtime-log-line); + background: var(--runtime-log-detail); + color: inherit; + overflow-wrap: anywhere; + white-space: pre-wrap; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 0.8125rem; + line-height: 1.8; +} + +.dark .runtime-log-detail { + color: #adbdc9; +} + +.runtime-log-viewport [data-log-line].hidden + .runtime-log-detail { + display: none !important; +} + +.runtime-log-without-time { + --runtime-log-columns: 5.5rem minmax(0, 1fr); +} + +.runtime-log-without-time [data-log-line]::before { + grid-column: 1; +} + +.runtime-log-without-time [data-line-text] { + grid-column: 2; +} + +@container runtime-log-explorer (max-width: 650px) { + .runtime-log-columns, + .runtime-log-viewport [data-log-line]:not(.hidden) { + grid-template-columns: minmax(0, 1fr) 5.5rem; + gap: 0.1875rem 0.5rem; + } + + .runtime-log-columns > :last-child, + .runtime-log-viewport [data-line-text] { + grid-column: 1 / -1; + grid-row: 2; + } + + .runtime-log-without-time [data-line-text] { + grid-column: 1 / -1; + } +} + +.runtime-log-empty { + display: flex; + min-height: 18rem; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 2rem; + color: var(--runtime-log-muted); + text-align: left; +} + +.runtime-log-loading { + min-height: 18rem; + align-items: center; + justify-content: center; + gap: 0.625rem; + padding: 2rem; + color: var(--runtime-log-muted); + font-size: 0.8125rem; + font-weight: 500; +} + +.runtime-log-empty-icon { + display: inline-flex; + width: 2.25rem; + height: 2.25rem; + flex-shrink: 0; + align-items: center; + justify-content: center; + border: 1px solid var(--runtime-log-line); + border-radius: 0.5rem; + background: var(--runtime-log-detail); +} + +.runtime-log-empty p { + color: inherit; + font-size: 0.8125rem; + font-weight: 500; +} + +.runtime-log-empty div > span { + display: block; + margin-top: 0.125rem; + font-size: 0.75rem; + line-height: 1.25rem; +} + .env-table-detail { padding: 0.25rem 1rem 1.25rem; } diff --git a/resources/views/components/server/sidebar.blade.php b/resources/views/components/server/sidebar.blade.php index 46e433e8a5..75fb43e2d7 100644 --- a/resources/views/components/server/sidebar.blade.php +++ b/resources/views/components/server/sidebar.blade.php @@ -132,6 +132,14 @@ 'group' => 'Operations', 'visible' => $server->isFunctional(), ], + [ + 'label' => 'Analytics', + 'route' => 'server.analytics', + 'active' => $activeMenu === 'analytics', + 'icon' => 'analytics', + 'group' => 'Operations', + 'visible' => $server->isFunctional() && ! $server->isSwarm() && ! $server->isBuildServer(), + ], [ 'label' => 'Security', 'route' => 'server.security.patches', diff --git a/resources/views/livewire/analytics-placeholder.blade.php b/resources/views/livewire/analytics-placeholder.blade.php index ca48f74145..cb6007b95b 100644 --- a/resources/views/livewire/analytics-placeholder.blade.php +++ b/resources/views/livewire/analytics-placeholder.blade.php @@ -1,16 +1,20 @@
- Request traffic across every application and server, reported by Sentinel. -
-+ Request traffic across every application and server, reported by Sentinel. +
+- Request traffic across every application and server, reported by Sentinel. -
-+ Request traffic across every application and server, reported by Sentinel. +
+No logs yet.+
No logs yet
+ Logs will appear here when the container produces output. ++ Five and ten minute ranges refresh automatically every five seconds. +
+