Skip to content

Commit 844bfd6

Browse files
committed
Add status page badges
1 parent d627d22 commit 844bfd6

10 files changed

Lines changed: 186 additions & 0 deletions

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"require": {
2222
"php": "^8.3",
2323
"ext-simplexml": "*",
24+
"cachethq/badger": "^5.0",
2425
"doctrine/dbal": "^3.6",
2526
"filament/filament": "^5.0",
2627
"filament/spatie-laravel-settings-plugin": "^5.0",

src/CachetCoreServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Cachet;
44

55
use BladeUI\Icons\Factory;
6+
use Cachet\Badger\BadgerServiceProvider;
67
use Cachet\Commands\AssetsCommand;
78
use Cachet\Commands\CheckComponentsCommand;
89
use Cachet\Commands\MakeUserCommand;
@@ -64,6 +65,7 @@ public function register(): void
6465
$this->app->singleton(Cachet::class);
6566
$this->app->singleton(ViewManager::class);
6667
$this->app->scoped(Status::class);
68+
$this->app->register(BadgerServiceProvider::class);
6769

6870
$this->configureSettingsCache();
6971
}

src/Enums/ComponentStatusEnum.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@ public function getTextColorClasses(): string
7979
};
8080
}
8181

82+
public function getBadgeColor(): string
83+
{
84+
return match ($this) {
85+
self::operational => 'brightgreen',
86+
self::performance_issues => 'yellow',
87+
self::partial_outage => 'orange',
88+
self::major_outage => 'red',
89+
self::under_maintenance => 'orange',
90+
self::unknown => 'lightgray',
91+
};
92+
}
93+
8294
public function getColor(): array
8395
{
8496
return match ($this) {

src/Enums/SystemStatusEnum.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ public function getColor(): array
3434
};
3535
}
3636

37+
public function getBadgeColor(): string
38+
{
39+
return match ($this) {
40+
self::operational => 'brightgreen',
41+
self::partial_outage => 'orange',
42+
self::major_outage => 'red',
43+
self::under_maintenance => 'orange',
44+
};
45+
}
46+
3747
public function getIcon(): string
3848
{
3949
return match ($this) {

src/Http/BadgeResponseFactory.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Cachet\Http;
4+
5+
use Cachet\Badger\BadgeImage;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Http\Response;
8+
9+
class BadgeResponseFactory
10+
{
11+
/**
12+
* Build a cacheable SVG response.
13+
*/
14+
public function make(BadgeImage $badge): Response
15+
{
16+
return response((string) $badge, 200, [
17+
'Cache-Control' => 'public, max-age=60, s-maxage=60',
18+
'Content-Type' => 'image/svg+xml; charset=UTF-8',
19+
]);
20+
}
21+
22+
/**
23+
* Return the requested style, falling back to the default for unsupported values.
24+
*/
25+
public function style(Request $request): string
26+
{
27+
$style = $request->string('style', 'flat-square')->value();
28+
29+
return in_array($style, ['flat-square', 'plastic-flat', 'flat', 'plastic', 'social', 'svg'], true)
30+
? $style
31+
: 'flat-square';
32+
}
33+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Cachet\Http\Controllers\StatusPage;
4+
5+
use Cachet\Badger\Badger;
6+
use Cachet\Enums\ResourceVisibilityEnum;
7+
use Cachet\Http\BadgeResponseFactory;
8+
use Cachet\Models\Component;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Http\Response;
11+
12+
class ComponentBadgeController
13+
{
14+
/**
15+
* Render a badge for a publicly visible component.
16+
*/
17+
public function __invoke(Request $request, Component $component, Badger $badger, BadgeResponseFactory $badgeResponseFactory): Response
18+
{
19+
$component->loadMissing('group');
20+
21+
abort_if(! $component->enabled || ($component->group?->visible !== null && $component->group->visible !== ResourceVisibilityEnum::guest), 404);
22+
23+
return $badgeResponseFactory->make(
24+
$badger->generate(
25+
$component->name,
26+
$component->latest_status->getLabel(),
27+
$component->latest_status->getBadgeColor(),
28+
$badgeResponseFactory->style($request),
29+
),
30+
);
31+
}
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Cachet\Http\Controllers\StatusPage;
4+
5+
use Cachet\Badger\Badger;
6+
use Cachet\Http\BadgeResponseFactory;
7+
use Cachet\Status;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Http\Response;
10+
11+
class StatusBadgeController
12+
{
13+
/**
14+
* Render a badge for the status page's current system status.
15+
*/
16+
public function __invoke(Request $request, Status $status, Badger $badger, BadgeResponseFactory $badgeResponseFactory): Response
17+
{
18+
$systemStatus = $status->current();
19+
20+
return $badgeResponseFactory->make(
21+
$badger->generate('status', $systemStatus->getLabel(), $systemStatus->getBadgeColor(), $badgeResponseFactory->style($request)),
22+
);
23+
}
24+
}

src/PendingRouteRegistration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use Cachet\Http\Controllers\HealthController;
77
use Cachet\Http\Controllers\RssController;
88
use Cachet\Http\Controllers\Setup\SetupController;
9+
use Cachet\Http\Controllers\StatusPage\ComponentBadgeController;
10+
use Cachet\Http\Controllers\StatusPage\StatusBadgeController;
911
use Cachet\Http\Controllers\StatusPage\StatusPageController;
1012
use Cachet\Http\Controllers\Subscribers\SubscriberController;
1113
use Cachet\Http\Controllers\Subscribers\UnsubscribeSubscriberController;
@@ -36,6 +38,8 @@ public function register(): void
3638
$router->get('/', [StatusPageController::class, 'index'])->name('status-page');
3739
$router->get('/incidents/{incident:guid}', [StatusPageController::class, 'show'])->name('status-page.incident');
3840
$router->get('/schedules/{schedule}', [StatusPageController::class, 'schedule'])->name('status-page.schedule');
41+
$router->get('/badge.svg', StatusBadgeController::class)->name('status-page.badge');
42+
$router->get('/components/{component}/badge.svg', ComponentBadgeController::class)->name('status-page.component.badge');
3943

4044
$router->get('/setup', [SetupController::class, 'index'])->name('setup.index');
4145
$router->post('/setup', [SetupController::class, 'store'])->name('setup.store');

tests/Feature/StatusPage/StatusPageTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,50 @@
1414
->assertOk();
1515
});
1616

17+
it('renders an SVG badge for the overall status page status', function () {
18+
Component::factory()->create(['status' => ComponentStatusEnum::operational]);
19+
20+
$response = $this->get(route('cachet.status-page.badge'))
21+
->assertOk()
22+
->assertHeader('content-type', 'image/svg+xml; charset=UTF-8')
23+
->assertSee('<svg', escape: false)
24+
->assertSee('All systems are operational.');
25+
26+
expect($response->headers->get('cache-control'))
27+
->toContain('public')
28+
->toContain('max-age=60')
29+
->toContain('s-maxage=60');
30+
});
31+
32+
it('renders an SVG badge for a public component', function () {
33+
$component = Component::factory()->create([
34+
'name' => 'Public API',
35+
'status' => ComponentStatusEnum::major_outage,
36+
]);
37+
38+
$this->get(route('cachet.status-page.component.badge', $component))
39+
->assertOk()
40+
->assertHeader('content-type', 'image/svg+xml; charset=UTF-8')
41+
->assertSee('<svg', escape: false)
42+
->assertSee('Public API')
43+
->assertSee('Major outage');
44+
});
45+
46+
it('does not render a badge for a component in a private group', function () {
47+
$group = ComponentGroup::factory()->create(['visible' => 0]);
48+
$component = Component::factory()->create(['component_group_id' => $group->id]);
49+
50+
$this->get(route('cachet.status-page.component.badge', $component))
51+
->assertNotFound();
52+
});
53+
54+
it('does not render a badge for a disabled component', function () {
55+
$component = Component::factory()->create(['enabled' => false]);
56+
57+
$this->get(route('cachet.status-page.component.badge', $component))
58+
->assertNotFound();
59+
});
60+
1761
it('can hide the site name and about content without changing status page metadata', function () {
1862
$settings = app(AppSettings::class);
1963
$settings->name = 'Acme Status';
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
use Cachet\Enums\ComponentStatusEnum;
4+
use Cachet\Enums\SystemStatusEnum;
5+
6+
it('maps component statuses to badge colors', function (ComponentStatusEnum $status, string $color) {
7+
expect($status->getBadgeColor())->toBe($color);
8+
})->with([
9+
[ComponentStatusEnum::operational, 'brightgreen'],
10+
[ComponentStatusEnum::performance_issues, 'yellow'],
11+
[ComponentStatusEnum::partial_outage, 'orange'],
12+
[ComponentStatusEnum::major_outage, 'red'],
13+
[ComponentStatusEnum::unknown, 'lightgray'],
14+
[ComponentStatusEnum::under_maintenance, 'orange'],
15+
]);
16+
17+
it('maps system statuses to badge colors', function (SystemStatusEnum $status, string $color) {
18+
expect($status->getBadgeColor())->toBe($color);
19+
})->with([
20+
[SystemStatusEnum::operational, 'brightgreen'],
21+
[SystemStatusEnum::partial_outage, 'orange'],
22+
[SystemStatusEnum::major_outage, 'red'],
23+
[SystemStatusEnum::under_maintenance, 'orange'],
24+
]);

0 commit comments

Comments
 (0)