Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic OhDear integration #134

Merged
merged 3 commits into from
Nov 27, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('incidents', function (Blueprint $table) {
$table->string('external_provider')->nullable()->after('guid');
$table->string('external_id')->nullable();
});
}
};
7 changes: 7 additions & 0 deletions resources/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
"Custom Header HTML": "Custom Header HTML",
"Dashboard": "Dashboard",
"Edit Incident": "Edit Incident",
"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).",
"Fixed": "Fixed",
"Guests": "Guests",
"Identified": "Identified",
"Import Incidents": "Import Incidents",
"Import Sites as Components": "Import Sites as Components",
"In Progress": "In Progress",
"Investigating": "Investigating",
"Laravel Blade": "Laravel Blade",
Expand All @@ -32,20 +35,24 @@
"No incidents reported between :from and :to": "No incidents reported between :from and :to",
"Notified Subscribers": "Notified Subscribers",
"Notify Subscribers?": "Notify Subscribers?",
"OhDear Status Page URL": "OhDear Status Page URL",
"Operational": "Operational",
"Partial Outage": "Partial Outage",
"Past Incidents": "Past Incidents",
"Performance Issues": "Performance Issues",
"Previous": "Previous",
"Recent incidents from Oh Dear will be imported as incidents in Cachet.": "Recent incidents from Oh Dear will be imported as incidents in Cachet.",
"Record Update": "Record Update",
"Resources": "Resources",
"Settings": "Settings",
"Setup Cachet": "Setup Cachet",
"Show Dashboard Link": "Show Dashboard Link",
"Sites configured in Oh Dear will be imported as components in Cachet.": "Sites configured in Oh Dear will be imported as components in Cachet.",
"Status Page": "Status Page",
"Subscriber": "Subscriber",
"Sum": "Sum",
"Support Cachet": "Support Cachet",
"The component group to assign imported components to.": "The component group to assign imported components to.",
"The incident's created timestamp will be used if left empty.": "The incident's created timestamp will be used if left empty.",
"Today": "Today",
"Total number of reported incidents.": "Total number of reported incidents.",
Expand Down
8 changes: 8 additions & 0 deletions resources/svg/oh-dear.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions resources/views/filament/pages/integrations/oh-dear.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<x-filament::page>
<x-filament-panels::form wire:submit.prevent="importFeed">
{{ $this->form }}

<div>
<x-filament::button type="submit" color="primary">
{{ __('Import Feed') }}
</x-filament::button>
</div>
</x-filament-panels::form>
</x-filament::page>
69 changes: 69 additions & 0 deletions src/Actions/Integrations/ImportOhDearFeed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Cachet\Actions\Integrations;

use Cachet\Enums\ComponentStatusEnum;
use Cachet\Enums\ExternalProviderEnum;
use Cachet\Models\Component;
use Cachet\Models\Incident;
use Illuminate\Support\Carbon;

class ImportOhDearFeed
{
/**
* Import an OhDear feed.
*/
public function __invoke(array $data, bool $importSites, ?int $componentGroupId, bool $importIncidents): void
{
if ($importSites) {
$this->importSites($data['sites']['ungrouped'], $componentGroupId);
}

if ($importIncidents) {
$this->importIncidents($data['updatesPerDay']);
}
}

/**
* Import OhDear sites as components.
*/
private function importSites(array $sites, ?int $componentGroupId): void
{
foreach ($sites as $site) {
Component::updateOrCreate(
['link' => $site['url']],
[
'name' => $site['label'],
'component_group_id' => $componentGroupId,
'status' => $site['status'] === 'up' ? ComponentStatusEnum::operational : ComponentStatusEnum::partial_outage,
]
);
}
}

/**
* Import OhDear incidents.
*/
private function importIncidents(array $updatesPerDay): void
{
Incident::unguard();

foreach ($updatesPerDay as $day => $incidents) {
foreach ($incidents as $incident) {
Incident::updateOrCreate(
[
'external_provider' => $provider = ExternalProviderEnum::OhDear,
'external_id' => $incident['id']
],
[
'name' => $incident['title'],
'status' => $provider->status($incident['severity']),
'message' => $incident['text'],
'occurred_at' => Carbon::createFromTimestamp($incident['time']),
'created_at' => Carbon::createFromTimestamp($incident['time']),
]
);
}
}
}
}
3 changes: 3 additions & 0 deletions src/CachetDashboardServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public function panel(Panel $panel): Panel
->label(__('Settings'))
->collapsed()
->icon('cachet-settings'),
NavigationGroup::make('Integrations')
->label(__('Integrations'))
->collapsed(),
NavigationGroup::make(__('Resources'))
->collapsible(false),
])
Expand Down
22 changes: 22 additions & 0 deletions src/Enums/ExternalProviderEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Cachet\Enums;

enum ExternalProviderEnum: string
{
case OhDear = 'OhDear';

/**
* Match the status to the Cachet status.
*/
public function status(mixed $status): IncidentStatusEnum
{
if ($this === self::OhDear) {
return match ($status) {
'resolved' => IncidentStatusEnum::fixed,
'warning' => IncidentStatusEnum::investigating,
default => IncidentStatusEnum::unknown,
};
}
}
}
125 changes: 125 additions & 0 deletions src/Filament/Pages/Integrations/OhDear.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<?php

namespace Cachet\Filament\Pages\Integrations;

use Cachet\Actions\Integrations\ImportOhDearFeed;
use Cachet\Filament\Resources\ComponentGroupResource;
use Cachet\Models\Component;
use Filament\Forms\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Form;
use Filament\Forms\Get;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;

class OhDear extends Page
{
use InteractsWithForms;

protected static ?string $navigationIcon = 'cachet-oh-dear';

protected static ?string $navigationGroup = 'Integrations';

protected static string $view = 'cachet::filament.pages.integrations.oh-dear';

public string $url;
public bool $import_sites = false;
public ?int $component_group_id = null;
public bool $import_incidents = false;

/**
* Mount the page.
*/
public function mount(): void
{
$this->form->fill([
'url' => '',
'import_sites' => true,
'component_group_id' => null,
'import_incidents' => true,
]);
}

/**
* Get the form schema definition.
*/
protected function getFormSchema(): array
{
return [
Section::make()->schema([
TextInput::make('url')
->label(__('OhDear Status Page URL'))
->placeholder('https://status.example.com')
->url()
->required()
->suffix('/json')
->helperText(__('Enter the URL of your OhDear status page (e.g., https://status.example.com).')),

Toggle::make('import_sites')
->label(__('Import Sites as Components'))
->helperText(__('Sites configured in Oh Dear will be imported as components in Cachet.'))
->default(true)
->reactive(),

Select::make('component_group_id')
->searchable()
->visible(fn (Get $get) => $get('import_sites') === true)
->relationship('group', 'name')
->model(Component::class)
->label(__('Component Group'))
->helperText(__('The component group to assign imported components to.'))
->createOptionForm(fn (Form $form) => ComponentGroupResource::form($form))
->preload(),

Toggle::make('import_incidents')
->label(__('Import Incidents'))
->helperText(__('Recent incidents from Oh Dear will be imported as incidents in Cachet.'))
->default(false),
])
];
}

/**
* Import the OhDear feed.
*/
public function importFeed(ImportOhDearFeed $importOhDearFeedAction): void
{
$this->validate();

try {
$ohDear = Http::baseUrl(rtrim($this->url))
->get('/json')
->throw()
->json();
} catch (ConnectionException $e) {
$this->addError('url', $e->getMessage());

return;
} catch (RequestException $e) {
$this->addError('url', 'The provided URL is not a valid OhDear status page endpoint.');

return;
}

if (! isset($ohDear['sites'], $ohDear['summarizedStatus'])) {
$this->addError('url', 'The provided URL is not a valid OhDear status page endpoint.');

return;
}

$importOhDearFeedAction->__invoke($ohDear, $this->import_sites, $this->component_group_id, $this->import_incidents);

Notification::make()
->title(__('OhDear feed imported successfully'))
->success()
->send();

$this->reset(['url', 'import_sites', 'import_incidents']);
}
}
2 changes: 2 additions & 0 deletions src/Models/Incident.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class Incident extends Model

protected $fillable = [
'guid',
'external_provider',
'external_id',
'user_id',
'component_id',
'name',
Expand Down
28 changes: 28 additions & 0 deletions tests/Unit/Actions/Integrations/ImportOhDearFeedTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Cachet\Actions\Integrations\ImportOhDearFeed;
use Cachet\Enums\ComponentStatusEnum;
use Cachet\Enums\ExternalProviderEnum;
use Cachet\Enums\IncidentStatusEnum;

it('can import an OhDear feed', function () {
$importOhDearFeed = new ImportOhDearFeed();

$data = json_decode(file_get_contents(__DIR__.'/../../../stubs/ohdear-feed-php.json'), true);

$importOhDearFeed($data, importSites: true, componentGroupId: 1, importIncidents: true);

$this->assertDatabaseHas('components', [
'link' => 'https://www.php.net/',
'name' => 'php.net',
'component_group_id' => 1,
'status' => ComponentStatusEnum::operational,
]);

$this->assertDatabaseHas('incidents', [
'external_provider' => ExternalProviderEnum::OhDear->value,
'external_id' => "1274100",
'name' => 'php.net has recovered.',
'status' => IncidentStatusEnum::fixed,
]);
});
1 change: 1 addition & 0 deletions tests/stubs/ohdear-feed-php.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"title":"PHP.net Status","timezone":"UTC","pinnedUpdate":null,"sites":{"ungrouped":[{"label":"php.net","url":"https:\/\/www.php.net\/","status":"up"},{"label":"pecl.php.net","url":"https:\/\/pecl.php.net","status":"up"},{"label":"bugs.php.net","url":"https:\/\/bugs.php.net","status":"up"},{"label":"museum.php.net","url":"https:\/\/museum.php.net","status":"up"},{"label":"wiki.php.net","url":"https:\/\/wiki.php.net","status":"up"},{"label":"people.php.net","url":"https:\/\/people.php.net","status":"up"},{"label":"news-web.php.net","url":"https:\/\/news-web.php.net","status":"up"},{"label":"downloads.php.net","url":"https:\/\/downloads.php.net","status":"up"}]},"updatesPerDay":{"1732579200":[],"1732492800":[],"1732406400":[{"id":1274100,"title":"php.net has recovered.","text":"","pinned":false,"severity":"resolved","time":1732471160},{"id":1273622,"title":"php.net appears to be down.","text":"","pinned":false,"severity":"warning","time":1732429163},{"id":1273623,"title":"downloads.php.net appears to be down.","text":"","pinned":false,"severity":"warning","time":1732429163}],"1732320000":[],"1732233600":[],"1732147200":[],"1732060800":[]},"summarizedStatus":"up"}
Loading