Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions resources/css/cachet.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,98 @@
display: none !important;
}
}

@layer components {
.status-header {
border-bottom: 1px solid rgb(24 24 27 / 10%);
background: rgb(255 255 255 / 70%);
backdrop-filter: blur(12px);
}

.status-overview {
overflow: hidden;
border: 1px solid rgb(24 24 27 / 12%);
border-radius: 0.75rem;
background: rgb(255 255 255 / 92%);
box-shadow: 0 12px 28px -24px rgb(24 39 30 / 36%);
}

.status-overview__masthead {
display: flex;
min-height: 3.75rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.875rem 1.25rem;
border-bottom: 1px solid rgb(24 24 27 / 10%);
}

.status-overview__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.status-summary {
padding: 1.25rem;
border-bottom: 1px solid rgb(24 24 27 / 10%);
background: linear-gradient(
115deg,
color-mix(in srgb, var(--color-100) 76%, white),
color-mix(in srgb, var(--color-200) 62%, white)
);
}

.status-summary__icon {
display: inline-flex;
width: 2rem;
height: 2rem;
flex: none;
align-items: center;
justify-content: center;
border-radius: 9999px;
background: color-mix(in srgb, currentColor 10%, transparent);
}

.status-overview .status-components {
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}

.status-overview .status-components > ul > :last-child,
.status-overview .status-components > ul > :last-child > *:last-child {
border-bottom: 0;
}

@media (min-width: 640px) {
.status-overview__masthead,
.status-summary {
padding-right: 1.5rem;
padding-left: 1.5rem;
}
}

.dark .status-header {
background: rgb(24 24 27 / 66%);
}

.dark .status-overview {
border-color: rgb(255 255 255 / 14%);
background: rgb(24 24 27 / 76%);
}

.dark .status-overview__masthead,
.dark .status-summary {
border-color: rgb(255 255 255 / 14%);
}

.dark .status-summary {
background: linear-gradient(
115deg,
color-mix(in srgb, var(--color-400) 20%, rgb(24 24 27)),
color-mix(in srgb, var(--color-400) 34%, rgb(24 24 27))
);
}
}
10 changes: 5 additions & 5 deletions resources/views/components/component-group.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
{{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENT_GROUPS_BEFORE) }}
@php($groupStatus = $componentGroup->worstComponentStatus())
<li x-data x-disclosure @if ($componentGroup->isExpanded(auth()->user())) default-open @endif>
<button x-disclosure:button class="relative flex w-full items-center justify-between gap-3 py-3 pl-8 pr-4 text-left transition hover:bg-zinc-50/60 dark:hover:bg-white/[0.02] sm:py-4 sm:pl-9 sm:pr-6">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400 dark:text-zinc-500 sm:left-3">
<button x-disclosure:button class="relative flex w-full flex-col items-start gap-2 py-4 pl-6 pr-4 text-left transition hover:bg-zinc-50/60 dark:hover:bg-white/[0.02] sm:flex-row sm:items-center sm:justify-between sm:gap-3 sm:py-5 sm:pl-6 sm:pr-6">
<span class="absolute left-2 top-7 -translate-y-1/2 text-zinc-400 dark:text-zinc-500 sm:top-1/2">
<x-heroicon-m-chevron-right ::class="$disclosure.isOpen && 'rotate-90'" class="size-3.5 transition" />
</span>

<h4 class="min-w-0 truncate font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
<h2 class="min-w-0 truncate font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
{{ $componentGroup->name }}
</h4>
</h2>

<span class="shrink-0 text-sm font-medium {{ $groupStatus->getTextColorClasses() }}">
<span class="shrink-0 text-sm font-semibold tracking-tight {{ $groupStatus->getTextColorClasses() }}">
{{ $groupStatus->getLabel() }}
</span>
</button>
Expand Down
4 changes: 1 addition & 3 deletions resources/views/components/component-groups.blade.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
@if ($componentGroups->isNotEmpty() || $ungroupedComponents->isNotEmpty())
<div class="group relative overflow-hidden rounded-lg bg-white shadow-sm ring-1 ring-zinc-900/10 dark:bg-zinc-900 dark:ring-white/15">
<div class="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-accent/40 to-transparent" aria-hidden="true"></div>

<div class="status-components group relative">
<ul class="divide-y divide-zinc-900/10 dark:divide-white/15">
@foreach ($componentGroups as $componentGroup)
<x-cachet::component-group :component-group="$componentGroup" />
Expand Down
36 changes: 21 additions & 15 deletions resources/views/components/component.blade.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
{{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_COMPONENTS_BEFORE) }}
<li class="relative px-4 py-3 transition hover:bg-zinc-50/60 dark:hover:bg-white/[0.02] sm:px-6 sm:py-4">
<div class="flex items-center justify-between gap-3">
<li class="relative px-4 py-4 transition hover:bg-zinc-50/60 dark:hover:bg-white/[0.02] sm:px-6 sm:py-5">
<div class="flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<div class="flex min-w-0 items-center gap-1.5">
<h4 @class([
'truncate tracking-tight',
'font-semibold text-zinc-900 dark:text-zinc-100' => ! ($nested ?? false),
'text-zinc-600 dark:text-zinc-300' => $nested ?? false,
])>
@if($component->formattedLink())
<a href="{{ $component->formattedLink() }}" target="_blank" rel="nofollow noopener" class="before:absolute before:inset-0 before:content-['']">{{ $component->name }}</a>
@else
{{ $component->name }}
@endif
</h4>
@if ($nested ?? false)
<h3 class="truncate tracking-tight text-zinc-600 dark:text-zinc-300">
@if($component->formattedLink())
<a href="{{ $component->formattedLink() }}" target="_blank" rel="nofollow noopener" class="before:absolute before:inset-0 before:content-['']">{{ $component->name }}</a>
@else
{{ $component->name }}
@endif
</h3>
@else
<h2 class="truncate font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
@if($component->formattedLink())
<a href="{{ $component->formattedLink() }}" target="_blank" rel="nofollow noopener" class="before:absolute before:inset-0 before:content-['']">{{ $component->name }}</a>
@else
{{ $component->name }}
@endif
</h2>
@endif

@if($component->description)
<div x-data x-popover class="relative flex shrink-0 items-center">
Expand All @@ -36,11 +42,11 @@
class="relative shrink-0">
<div x-ref="badgeAnchor">
@if ($component->impacting_incident)
<a href="{{ route('cachet.status-page.incident', [$component->impacting_incident]) }}" class="inline-flex text-sm font-medium {{ $component->latest_status->getTextColorClasses() }}">
<a href="{{ route('cachet.status-page.incident', [$component->impacting_incident]) }}" class="inline-flex text-sm font-semibold tracking-tight {{ $component->latest_status->getTextColorClasses() }}">
{{ $component->latest_status->getLabel() }}
</a>
@else
<span class="text-sm font-medium {{ $component->latest_status->getTextColorClasses() }}">{{ $component->latest_status->getLabel() }}</span>
<span class="text-sm font-semibold tracking-tight {{ $component->latest_status->getTextColorClasses() }}">{{ $component->latest_status->getLabel() }}</span>
@endif
</div>

Expand Down
5 changes: 2 additions & 3 deletions resources/views/components/header.blade.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
{{ \Cachet\Facades\CachetView::renderHook(\Cachet\View\RenderHook::STATUS_PAGE_NAVIGATION_BEFORE) }}
<header class="border-b border-zinc-900/10 bg-white/70 backdrop-blur dark:border-white/15 dark:bg-zinc-900/50">
<header class="status-header">
<div class="container mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-4 sm:px-6 lg:px-8">
<a href="{{ route('cachet.status-page') }}" class="inline-flex items-center transition hover:opacity-80">
@if($appBanner)
<img src="{{ Storage::url($appBanner) }}" alt="{{ $siteName }}" class="h-8 w-auto" />
@else
<x-cachet::logo class="hidden h-8 w-auto sm:block" />
<x-cachet::logomark class="h-8 w-auto sm:hidden" />
<x-cachet::logo class="h-8 w-auto" />
@endif
</a>

Expand Down
2 changes: 1 addition & 1 deletion resources/views/components/logo.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<svg viewBox="0 0 236 60" fill="none" xmlns="http://www.w3.org/2000/svg" {{ ($attributes ?? null)?->merge(['class']) }}>
<g class="text-[#141414] dark:text-white hidden sm:block">
<g class="text-[#141414] dark:text-white">
<path d="M223.239 46V14.08H230.919V46H223.239ZM218.727 27.232V20.512H235.863V27.232H218.727Z" fill="currentColor" />
<path d="M205.146 46.48C202.33 46.48 199.882 45.92 197.802 44.8C195.722 43.648 194.106 42.096 192.954 40.144C191.802 38.16 191.226 35.904 191.226 33.376C191.226 31.424 191.546 29.632 192.186 28C192.826 26.368 193.722 24.96 194.874 23.776C196.026 22.56 197.386 21.632 198.954 20.992C200.554 20.32 202.298 19.984 204.186 19.984C205.978 19.984 207.61 20.304 209.082 20.944C210.586 21.584 211.882 22.48 212.97 23.632C214.058 24.784 214.89 26.144 215.466 27.712C216.042 29.28 216.298 30.992 216.234 32.848L216.186 34.912H195.882L194.778 30.592H209.946L209.13 31.504V30.544C209.066 29.744 208.81 29.04 208.362 28.432C207.946 27.792 207.386 27.296 206.682 26.944C205.978 26.592 205.178 26.416 204.282 26.416C203.034 26.416 201.962 26.672 201.066 27.184C200.202 27.664 199.546 28.384 199.098 29.344C198.65 30.272 198.426 31.424 198.426 32.8C198.426 34.208 198.714 35.44 199.29 36.496C199.898 37.52 200.762 38.32 201.882 38.896C203.034 39.472 204.394 39.76 205.962 39.76C207.05 39.76 208.01 39.6 208.842 39.28C209.706 38.96 210.634 38.416 211.626 37.648L215.226 42.736C214.234 43.6 213.178 44.304 212.058 44.848C210.938 45.392 209.786 45.792 208.602 46.048C207.45 46.336 206.298 46.48 205.146 46.48Z" fill="currentColor" />
<path d="M162.533 46V10.48H170.069V25.696L168.533 26.272C168.885 25.12 169.509 24.08 170.405 23.152C171.333 22.192 172.437 21.424 173.717 20.848C174.997 20.272 176.341 19.984 177.749 19.984C179.669 19.984 181.285 20.384 182.597 21.184C183.909 21.952 184.901 23.12 185.573 24.688C186.245 26.224 186.581 28.112 186.581 30.352V46H178.853V30.976C178.853 29.952 178.709 29.104 178.421 28.432C178.133 27.76 177.685 27.264 177.077 26.944C176.501 26.592 175.781 26.432 174.917 26.464C174.245 26.464 173.621 26.576 173.045 26.8C172.469 26.992 171.973 27.296 171.557 27.712C171.141 28.096 170.805 28.544 170.549 29.056C170.325 29.568 170.213 30.128 170.213 30.736V46H166.421C165.525 46 164.757 46 164.117 46C163.477 46 162.949 46 162.533 46Z" fill="currentColor" />
Expand Down
24 changes: 19 additions & 5 deletions resources/views/components/status-bar.blade.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
<div {{ $attributes->class(['rounded-lg bg-custom-200 p-4 ring-1 ring-custom-400/60 dark:bg-custom-400/30 dark:ring-custom-400/40'])->style([
<div {{ $attributes->class(['status-summary'])->style([
Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$status->getColor(),
shades: [200, 400, 800],
shades: [100, 200, 400, 500, 800],
),
]),
]) }}>
<div class="flex items-center gap-3">
@svg($status->getIcon(), 'size-6 shrink-0 text-custom-800 dark:text-custom-200')
<p class="text-base font-semibold tracking-tight text-custom-800 dark:text-custom-200 sm:text-lg">{{ $status->getLabel() }}</p>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3" role="status" aria-live="polite">
<div class="flex items-center gap-3">
<span class="status-summary__icon text-custom-800 dark:text-custom-200">
@svg($status->getIcon(), 'size-5 shrink-0 text-custom-800 dark:text-custom-200')
</span>
@if ($isHeading)
<h1 class="text-base font-semibold tracking-tight text-custom-800 dark:text-custom-200">{{ $status->getLabel() }}</h1>
@else
<p class="text-base font-semibold tracking-tight text-custom-800 dark:text-custom-200">{{ $status->getLabel() }}</p>
@endif
</div>

@if ($lastUpdated)
<span class="text-xs font-medium text-custom-800/70 dark:text-custom-200/70">
{{ __('cachet::component.last_updated', ['timestamp' => $lastUpdated->diffForHumans()]) }}
</span>
@endif
</div>
</div>
24 changes: 18 additions & 6 deletions resources/views/status-page/index.blade.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
<x-cachet::cachet>
<x-cachet::header />

<div class="container mx-auto flex max-w-5xl flex-col space-y-8 px-4 py-10 sm:px-6 lg:px-8">
<x-cachet::status-bar />
@php($appSettings = app(\Cachet\Settings\AppSettings::class))
@php($siteName = $appSettings->name ?: config('cachet.title', 'Cachet'))

<x-cachet::about />
<main class="status-page container mx-auto flex max-w-5xl flex-col gap-8 px-4 py-10 sm:px-6 lg:px-8">
<section class="status-overview relative">
<div class="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-accent/40 to-transparent" aria-hidden="true"></div>
@if ($appSettings->show_site_name)
<div class="status-overview__masthead">
<h1 class="status-overview__title font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">{{ $siteName }}</h1>
</div>
@endif

<x-cachet::status-bar :is-heading="! $appSettings->show_site_name" />

<x-cachet::component-groups />
</section>

<x-cachet::about :show-site-name="false" />

<x-cachet::component-groups />

@if ($display_graphs)
<x-cachet::metrics />
@endif
Expand All @@ -17,7 +29,7 @@
@endif

<x-cachet::incident-timeline />
</div>
</main>

<x-cachet::footer />
</x-cachet::cachet>
12 changes: 12 additions & 0 deletions src/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Cachet\Models\Component;
use Cachet\Models\Incident;
use Cachet\Settings\AppSettings;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;

class Status
Expand Down Expand Up @@ -102,6 +103,17 @@ public function incidents(): object
->first();
}

/**
* Get the most recent update timestamp across enabled components.
*/
public function lastUpdated(): ?CarbonInterface
{
return Component::query()
->enabled()
->latest('updated_at')
->first(['updated_at'])?->updated_at;
}

/**
* Tally the enabled components by their effective status.
*
Expand Down
8 changes: 5 additions & 3 deletions src/View/Components/About.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ class About extends Component
/**
* Create a new component instance.
*/
public function __construct(private AppSettings $settings)
{
public function __construct(
private AppSettings $settings,
public readonly ?bool $showSiteName = null,
) {
//
}

Expand All @@ -27,7 +29,7 @@ public function render(): View|Closure|string
return view('cachet::components.about', [
'title' => $this->settings->name ?: config('cachet.title', 'Cachet'),
'about' => Cachet::markdown(Str::of($this->settings->about)->trim()->toString()),
'showSiteName' => $this->settings->show_site_name,
'showSiteName' => $this->showSiteName ?? $this->settings->show_site_name,
'showAbout' => $this->settings->show_about,
]);
}
Expand Down
7 changes: 5 additions & 2 deletions src/View/Components/StatusBar.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ class StatusBar extends ViewComponent
/**
* Create a new component instance.
*/
public function __construct(protected readonly Status $status)
{
public function __construct(
protected readonly Status $status,
public readonly bool $isHeading = false,
) {
//
}

Expand All @@ -24,6 +26,7 @@ public function render(): View|Closure|string
{
return view('cachet::components.status-bar', [
'status' => $this->status->current(),
'lastUpdated' => $this->status->lastUpdated(),
]);
}
}
29 changes: 29 additions & 0 deletions tests/Feature/StatusPage/StatusPageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Cachet\Enums\ComponentStatusEnum;
use Cachet\Models\Component;
use Cachet\Models\ComponentGroup;
use Cachet\Models\Incident;
use Cachet\Models\Schedule;
use Cachet\Settings\AppSettings;
Expand Down Expand Up @@ -32,6 +33,34 @@
->assertSee('<meta name="description" content="A private production system." />', escape: false);
});

it('shows the site name once when it is enabled', function () {
$settings = app(AppSettings::class);
$settings->name = 'Acme Status';
$settings->show_site_name = true;
$settings->show_about = false;
$settings->save();

$response = $this->get(route('cachet.status-page'))->assertOk();
$body = Str::after($response->getContent(), '<body');

expect(substr_count($body, 'Acme Status'))->toBe(1);
});

it('uses a logical heading hierarchy for components', function () {
$group = ComponentGroup::factory()->create(['name' => 'Core services']);
Component::factory()->create(['name' => 'Public API']);
Component::factory()->create(['name' => 'Core API', 'component_group_id' => $group->id]);

$page = $this->get(route('cachet.status-page'))
->assertOk()
->getContent();

expect($page)
->toMatch('/<h2[^>]*>\s*Core services\s*<\\/h2>/')
->toMatch('/<h3[^>]*>\s*Core API\s*<\\/h3>/')
->toMatch('/<h2[^>]*>\s*Public API\s*<\\/h2>/');
});

it('renders the status page in the configured locale', function () {
$settings = app(AppSettings::class);
$settings->locale = 'de';
Expand Down
Loading
Loading