Skip to content

Commit dfb9008

Browse files
authored
Implement basic OhDear integration (#134)
1 parent 35a4b24 commit dfb9008

File tree

11 files changed

+295
-0
lines changed

11 files changed

+295
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*/
12+
public function up(): void
13+
{
14+
Schema::table('incidents', function (Blueprint $table) {
15+
$table->string('external_provider')->nullable()->after('guid');
16+
$table->string('external_id')->nullable();
17+
});
18+
}
19+
};

resources/lang/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
"Custom Header HTML": "Custom Header HTML",
1717
"Dashboard": "Dashboard",
1818
"Edit Incident": "Edit Incident",
19+
"Enter the URL of your OhDear status page (e.g., https://status.example.com).": "Enter the URL of your OhDear status page (e.g., https://status.example.com).",
1920
"Fixed": "Fixed",
2021
"Guests": "Guests",
2122
"Identified": "Identified",
23+
"Import Incidents": "Import Incidents",
24+
"Import Sites as Components": "Import Sites as Components",
2225
"In Progress": "In Progress",
2326
"Investigating": "Investigating",
2427
"Laravel Blade": "Laravel Blade",
@@ -32,20 +35,24 @@
3235
"No incidents reported between :from and :to": "No incidents reported between :from and :to",
3336
"Notified Subscribers": "Notified Subscribers",
3437
"Notify Subscribers?": "Notify Subscribers?",
38+
"OhDear Status Page URL": "OhDear Status Page URL",
3539
"Operational": "Operational",
3640
"Partial Outage": "Partial Outage",
3741
"Past Incidents": "Past Incidents",
3842
"Performance Issues": "Performance Issues",
3943
"Previous": "Previous",
44+
"Recent incidents from Oh Dear will be imported as incidents in Cachet.": "Recent incidents from Oh Dear will be imported as incidents in Cachet.",
4045
"Record Update": "Record Update",
4146
"Resources": "Resources",
4247
"Settings": "Settings",
4348
"Setup Cachet": "Setup Cachet",
4449
"Show Dashboard Link": "Show Dashboard Link",
50+
"Sites configured in Oh Dear will be imported as components in Cachet.": "Sites configured in Oh Dear will be imported as components in Cachet.",
4551
"Status Page": "Status Page",
4652
"Subscriber": "Subscriber",
4753
"Sum": "Sum",
4854
"Support Cachet": "Support Cachet",
55+
"The component group to assign imported components to.": "The component group to assign imported components to.",
4956
"The incident's created timestamp will be used if left empty.": "The incident's created timestamp will be used if left empty.",
5057
"Today": "Today",
5158
"Total number of reported incidents.": "Total number of reported incidents.",

resources/svg/oh-dear.svg

Lines changed: 8 additions & 0 deletions
Loading
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<x-filament::page>
2+
<x-filament-panels::form wire:submit.prevent="importFeed">
3+
{{ $this->form }}
4+
5+
<div>
6+
<x-filament::button type="submit" color="primary">
7+
{{ __('Import Feed') }}
8+
</x-filament::button>
9+
</div>
10+
</x-filament-panels::form>
11+
</x-filament::page>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Cachet\Actions\Integrations;
4+
5+
use Cachet\Enums\ComponentStatusEnum;
6+
use Cachet\Enums\ExternalProviderEnum;
7+
use Cachet\Models\Component;
8+
use Cachet\Models\Incident;
9+
use Illuminate\Support\Carbon;
10+
11+
class ImportOhDearFeed
12+
{
13+
/**
14+
* Import an OhDear feed.
15+
*/
16+
public function __invoke(array $data, bool $importSites, ?int $componentGroupId, bool $importIncidents): void
17+
{
18+
if ($importSites) {
19+
$this->importSites($data['sites']['ungrouped'], $componentGroupId);
20+
}
21+
22+
if ($importIncidents) {
23+
$this->importIncidents($data['updatesPerDay']);
24+
}
25+
}
26+
27+
/**
28+
* Import OhDear sites as components.
29+
*/
30+
private function importSites(array $sites, ?int $componentGroupId): void
31+
{
32+
foreach ($sites as $site) {
33+
Component::updateOrCreate(
34+
['link' => $site['url']],
35+
[
36+
'name' => $site['label'],
37+
'component_group_id' => $componentGroupId,
38+
'status' => $site['status'] === 'up' ? ComponentStatusEnum::operational : ComponentStatusEnum::partial_outage,
39+
]
40+
);
41+
}
42+
}
43+
44+
/**
45+
* Import OhDear incidents.
46+
*/
47+
private function importIncidents(array $updatesPerDay): void
48+
{
49+
Incident::unguard();
50+
51+
foreach ($updatesPerDay as $day => $incidents) {
52+
foreach ($incidents as $incident) {
53+
Incident::updateOrCreate(
54+
[
55+
'external_provider' => $provider = ExternalProviderEnum::OhDear,
56+
'external_id' => $incident['id']
57+
],
58+
[
59+
'name' => $incident['title'],
60+
'status' => $provider->status($incident['severity']),
61+
'message' => $incident['text'],
62+
'occurred_at' => Carbon::createFromTimestamp($incident['time']),
63+
'created_at' => Carbon::createFromTimestamp($incident['time']),
64+
]
65+
);
66+
}
67+
}
68+
}
69+
}

src/CachetDashboardServiceProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public function panel(Panel $panel): Panel
4646
->label(__('Settings'))
4747
->collapsed()
4848
->icon('cachet-settings'),
49+
NavigationGroup::make('Integrations')
50+
->label(__('Integrations'))
51+
->collapsed(),
4952
NavigationGroup::make(__('Resources'))
5053
->collapsible(false),
5154
])

src/Enums/ExternalProviderEnum.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Cachet\Enums;
4+
5+
enum ExternalProviderEnum: string
6+
{
7+
case OhDear = 'OhDear';
8+
9+
/**
10+
* Match the status to the Cachet status.
11+
*/
12+
public function status(mixed $status): IncidentStatusEnum
13+
{
14+
if ($this === self::OhDear) {
15+
return match ($status) {
16+
'resolved' => IncidentStatusEnum::fixed,
17+
'warning' => IncidentStatusEnum::investigating,
18+
default => IncidentStatusEnum::unknown,
19+
};
20+
}
21+
}
22+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<?php
2+
3+
namespace Cachet\Filament\Pages\Integrations;
4+
5+
use Cachet\Actions\Integrations\ImportOhDearFeed;
6+
use Cachet\Filament\Resources\ComponentGroupResource;
7+
use Cachet\Models\Component;
8+
use Filament\Forms\Components\Section;
9+
use Filament\Forms\Components\Select;
10+
use Filament\Forms\Components\TextInput;
11+
use Filament\Forms\Components\Toggle;
12+
use Filament\Forms\Concerns\InteractsWithForms;
13+
use Filament\Forms\Form;
14+
use Filament\Forms\Get;
15+
use Filament\Notifications\Notification;
16+
use Filament\Pages\Page;
17+
use Illuminate\Http\Client\ConnectionException;
18+
use Illuminate\Http\Client\RequestException;
19+
use Illuminate\Support\Facades\Http;
20+
21+
class OhDear extends Page
22+
{
23+
use InteractsWithForms;
24+
25+
protected static ?string $navigationIcon = 'cachet-oh-dear';
26+
27+
protected static ?string $navigationGroup = 'Integrations';
28+
29+
protected static string $view = 'cachet::filament.pages.integrations.oh-dear';
30+
31+
public string $url;
32+
public bool $import_sites = false;
33+
public ?int $component_group_id = null;
34+
public bool $import_incidents = false;
35+
36+
/**
37+
* Mount the page.
38+
*/
39+
public function mount(): void
40+
{
41+
$this->form->fill([
42+
'url' => '',
43+
'import_sites' => true,
44+
'component_group_id' => null,
45+
'import_incidents' => true,
46+
]);
47+
}
48+
49+
/**
50+
* Get the form schema definition.
51+
*/
52+
protected function getFormSchema(): array
53+
{
54+
return [
55+
Section::make()->schema([
56+
TextInput::make('url')
57+
->label(__('OhDear Status Page URL'))
58+
->placeholder('https://status.example.com')
59+
->url()
60+
->required()
61+
->suffix('/json')
62+
->helperText(__('Enter the URL of your OhDear status page (e.g., https://status.example.com).')),
63+
64+
Toggle::make('import_sites')
65+
->label(__('Import Sites as Components'))
66+
->helperText(__('Sites configured in Oh Dear will be imported as components in Cachet.'))
67+
->default(true)
68+
->reactive(),
69+
70+
Select::make('component_group_id')
71+
->searchable()
72+
->visible(fn (Get $get) => $get('import_sites') === true)
73+
->relationship('group', 'name')
74+
->model(Component::class)
75+
->label(__('Component Group'))
76+
->helperText(__('The component group to assign imported components to.'))
77+
->createOptionForm(fn (Form $form) => ComponentGroupResource::form($form))
78+
->preload(),
79+
80+
Toggle::make('import_incidents')
81+
->label(__('Import Incidents'))
82+
->helperText(__('Recent incidents from Oh Dear will be imported as incidents in Cachet.'))
83+
->default(false),
84+
])
85+
];
86+
}
87+
88+
/**
89+
* Import the OhDear feed.
90+
*/
91+
public function importFeed(ImportOhDearFeed $importOhDearFeedAction): void
92+
{
93+
$this->validate();
94+
95+
try {
96+
$ohDear = Http::baseUrl(rtrim($this->url))
97+
->get('/json')
98+
->throw()
99+
->json();
100+
} catch (ConnectionException $e) {
101+
$this->addError('url', $e->getMessage());
102+
103+
return;
104+
} catch (RequestException $e) {
105+
$this->addError('url', 'The provided URL is not a valid OhDear status page endpoint.');
106+
107+
return;
108+
}
109+
110+
if (! isset($ohDear['sites'], $ohDear['summarizedStatus'])) {
111+
$this->addError('url', 'The provided URL is not a valid OhDear status page endpoint.');
112+
113+
return;
114+
}
115+
116+
$importOhDearFeedAction->__invoke($ohDear, $this->import_sites, $this->component_group_id, $this->import_incidents);
117+
118+
Notification::make()
119+
->title(__('OhDear feed imported successfully'))
120+
->success()
121+
->send();
122+
123+
$this->reset(['url', 'import_sites', 'import_incidents']);
124+
}
125+
}

src/Models/Incident.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class Incident extends Model
4141

4242
protected $fillable = [
4343
'guid',
44+
'external_provider',
45+
'external_id',
4446
'user_id',
4547
'component_id',
4648
'name',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Cachet\Actions\Integrations\ImportOhDearFeed;
4+
use Cachet\Enums\ComponentStatusEnum;
5+
use Cachet\Enums\ExternalProviderEnum;
6+
use Cachet\Enums\IncidentStatusEnum;
7+
8+
it('can import an OhDear feed', function () {
9+
$importOhDearFeed = new ImportOhDearFeed();
10+
11+
$data = json_decode(file_get_contents(__DIR__.'/../../../stubs/ohdear-feed-php.json'), true);
12+
13+
$importOhDearFeed($data, importSites: true, componentGroupId: 1, importIncidents: true);
14+
15+
$this->assertDatabaseHas('components', [
16+
'link' => 'https://www.php.net/',
17+
'name' => 'php.net',
18+
'component_group_id' => 1,
19+
'status' => ComponentStatusEnum::operational,
20+
]);
21+
22+
$this->assertDatabaseHas('incidents', [
23+
'external_provider' => ExternalProviderEnum::OhDear->value,
24+
'external_id' => "1274100",
25+
'name' => 'php.net has recovered.',
26+
'status' => IncidentStatusEnum::fixed,
27+
]);
28+
});

0 commit comments

Comments
 (0)