Skip to content

Commit 218515c

Browse files
committed
Fixes cachethq#93
1 parent a86919b commit 218515c

12 files changed

+357
-75
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
@use('Cachet\Enums\IncidentStatusEnum')
2+
@props([
3+
'incident',
4+
])
5+
6+
7+
<div x-data="{ timestamp: new Date(@js($incident->timestamp)) }" class="bg-white border divide-y rounded-lg ml-9 dark:divide-zinc-700 dark:border-zinc-700 dark:bg-white/5">
8+
<div @class([
9+
'flex flex-col bg-zinc-50 p-4 dark:bg-accent-background gap-2',
10+
'rounded-t-lg' => $incident->updates->isNotEmpty(),
11+
'rounded-lg' => $incident->updates->isEmpty(),
12+
])>
13+
@if ($incident->components()->exists())
14+
<div class="text-xs font-medium">
15+
{{ $incident->components->pluck('name')->join(', ', ' and ') }}
16+
</div>
17+
@endif
18+
<div class="flex flex-col sm:flex-row justify-between gap-2 flex-col-reverse items-start sm:items-center relative">
19+
<div class="flex flex-col flex-1">
20+
<x-cachet::timeline-badge icon="cachet-incident" :color="\Filament\Support\Colors\Color::Amber" label="" />
21+
<div class="flex gap-2 items-center">
22+
<h3 class="max-w-full text-base font-semibold break-words sm:text-xl">
23+
<a href="{{ route('cachet.status-page.incident', $incident) }}">{{ $incident->name}}</a>
24+
</h3>
25+
@auth
26+
<a href="{{ $incident->filamentDashboardEditUrl() }}" class="underline text-right text-sm text-zinc-500 hover:text-zinc-400 dark:text-zinc-400 dark:hover:text-zinc-300" title="{{ __('cachet::incident.edit_button_title') }}">
27+
<x-heroicon-m-pencil-square class="size-4" />
28+
</a>
29+
@endauth
30+
</div>
31+
<span class="text-xs text-zinc-500 dark:text-zinc-400">
32+
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
33+
</span>
34+
</div>
35+
<div class="flex justify-start sm:justify-end">
36+
<x-cachet::badge :status="$incident->latestStatus" />
37+
</div>
38+
</div>
39+
</div>
40+
41+
<div class="relative">
42+
<div class="absolute inset-y-0 -left-9">
43+
<div class="ml-3.5 h-full border-l-2 border-dashed dark:border-zinc-700"></div>
44+
<div class="absolute inset-x-0 top-0 w-full h-24 bg-gradient-to-t from-transparent to-[rgb(var(--accent-background))]"></div>
45+
<div class="absolute inset-x-0 bottom-0 w-full h-24 bg-gradient-to-b from-transparent to-[rgb(var(--accent-background))]"></div>
46+
</div>
47+
<div class="flex flex-col px-4 divide-y dark:divide-zinc-700">
48+
@foreach ($incident->updates as $update)
49+
<div class="relative py-4" x-data="{ timestamp: new Date(@js($update->created_at)) }">
50+
<x-cachet::incident-update-status :status="$update->status" />
51+
<h3 class="text-lg font-semibold">{{ $update->status->getLabel() }}</h3>
52+
<span class="text-xs text-zinc-500 dark:text-zinc-400">
53+
{{ $update->created_at->diffForHumans() }} — <time datetime="{{ $update->created_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
54+
</span>
55+
<div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-accent-content prose-a:underline prose-p:leading-normal">{!! $update->formattedMessage() !!}</div>
56+
</div>
57+
@endforeach
58+
<div class="relative py-4" x-data="{ timestamp: new Date(@js($incident->timestamp)) }">
59+
<x-cachet::incident-update-status :status="IncidentStatusEnum::unknown" />
60+
61+
<span class="text-xs text-zinc-500 dark:text-zinc-400">
62+
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
63+
</span>
64+
<div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-accent-content prose-a:underline prose-p:leading-normal">{!! $incident->formattedMessage() !!}</div>
65+
</div>
66+
</div>
67+
</div>
68+
</div>

resources/views/components/incident.blade.php

+5-61
Original file line numberDiff line numberDiff line change
@@ -8,67 +8,11 @@
88
<div class="relative flex flex-col gap-5" x-data="{ forDate: new Date(@js($date)) }">
99
<h3 class="text-xl font-semibold"><time datetime="{{ $date }}" x-text="forDate.toLocaleDateString()"></time></h3>
1010
@forelse($incidents as $incident)
11-
<div x-data="{ timestamp: new Date(@js($incident->timestamp)) }" class="bg-white border divide-y rounded-lg ml-9 dark:divide-zinc-700 dark:border-zinc-700 dark:bg-white/5">
12-
<div @class([
13-
'flex flex-col bg-zinc-50 p-4 dark:bg-accent-background gap-2',
14-
'rounded-t-lg' => $incident->updates->isNotEmpty(),
15-
'rounded-lg' => $incident->updates->isEmpty(),
16-
])>
17-
@if ($incident->components()->exists())
18-
<div class="text-xs font-medium">
19-
{{ $incident->components->pluck('name')->join(', ', ' and ') }}
20-
</div>
21-
@endif
22-
<div class="flex flex-col sm:flex-row justify-between gap-2 flex-col-reverse items-start sm:items-center">
23-
<div class="flex flex-col flex-1">
24-
<div class="flex gap-2 items-center">
25-
<h3 class="max-w-full text-base font-semibold break-words sm:text-xl">
26-
<a href="{{ route('cachet.status-page.incident', $incident) }}">{{ $incident->name}}</a>
27-
</h3>
28-
@auth
29-
<a href="{{ $incident->filamentDashboardEditUrl() }}" class="underline text-right text-sm text-zinc-500 hover:text-zinc-400 dark:text-zinc-400 dark:hover:text-zinc-300" title="{{ __('cachet::incident.edit_button_title') }}">
30-
<x-heroicon-m-pencil-square class="size-4" />
31-
</a>
32-
@endauth
33-
</div>
34-
<span class="text-xs text-zinc-500 dark:text-zinc-400">
35-
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
36-
</span>
37-
</div>
38-
<div class="flex justify-start sm:justify-end">
39-
<x-cachet::badge :status="$incident->latestStatus" />
40-
</div>
41-
</div>
42-
</div>
43-
44-
<div class="relative">
45-
<div class="absolute inset-y-0 -left-9">
46-
<div class="ml-3.5 h-full border-l-2 border-dashed dark:border-zinc-700"></div>
47-
<div class="absolute inset-x-0 top-0 w-full h-24 bg-gradient-to-t from-transparent to-[rgb(var(--accent-background))]"></div>
48-
<div class="absolute inset-x-0 bottom-0 w-full h-24 bg-gradient-to-b from-transparent to-[rgb(var(--accent-background))]"></div>
49-
</div>
50-
<div class="flex flex-col px-4 divide-y dark:divide-zinc-700">
51-
@foreach ($incident->updates as $update)
52-
<div class="relative py-4" x-data="{ timestamp: new Date(@js($update->created_at)) }">
53-
<x-cachet::incident-update-status :status="$update->status" />
54-
<h3 class="text-lg font-semibold">{{ $update->status->getLabel() }}</h3>
55-
<span class="text-xs text-zinc-500 dark:text-zinc-400">
56-
{{ $update->created_at->diffForHumans() }} — <time datetime="{{ $update->created_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
57-
</span>
58-
<div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-accent-content prose-a:underline prose-p:leading-normal">{!! $update->formattedMessage() !!}</div>
59-
</div>
60-
@endforeach
61-
<div class="relative py-4" x-data="{ timestamp: new Date(@js($incident->timestamp)) }">
62-
<x-cachet::incident-update-status :status="IncidentStatusEnum::unknown" />
63-
64-
<span class="text-xs text-zinc-500 dark:text-zinc-400">
65-
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
66-
</span>
67-
<div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-accent-content prose-a:underline prose-p:leading-normal">{!! $incident->formattedMessage() !!}</div>
68-
</div>
69-
</div>
70-
</div>
71-
</div>
11+
@if($incident instanceof \Cachet\Models\Incident)
12+
<x-cachet::incident-item :incident="$incident" />
13+
@elseif($incident instanceof \Cachet\Models\Schedule)
14+
<x-cachet::schedule-item :schedule="$incident" />
15+
@endif
7216
@empty
7317
<div class="bg-white border divide-y rounded-lg dark:divide-zinc-700 dark:border-zinc-700 dark:bg-white/5">
7418
<div class="flex flex-col p-4 divide-y dark:divide-zinc-700">
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
@use('Cachet\Enums\ScheduleStatusEnum')
2+
@props([
3+
'schedule',
4+
])
5+
6+
7+
<div x-data="{ timestamp: new Date(@js($schedule->get)) }" class="bg-white border divide-y rounded-lg ml-9 dark:divide-zinc-700 dark:border-zinc-700 dark:bg-white/5">
8+
<div @class([
9+
'flex flex-col bg-zinc-50 p-4 dark:bg-accent-background gap-2',
10+
'rounded-t-lg' => $schedule->updates->isNotEmpty(),
11+
'rounded-lg' => $schedule->updates->isEmpty(),
12+
])>
13+
@if ($schedule->components()->exists())
14+
<div class="text-xs font-medium">
15+
{{ $schedule->components->pluck('name')->join(', ', ' and ') }}
16+
</div>
17+
@endif
18+
<div class="flex flex-col sm:flex-row justify-between gap-2 flex-col-reverse items-start sm:items-center relative">
19+
<div class="flex flex-col flex-1">
20+
<x-cachet::timeline-badge icon="cachet-maintenance" :color="\Filament\Support\Colors\Color::Slate" label="" />
21+
<div class="flex gap-2 items-center">
22+
<h3 class="max-w-full text-base font-semibold break-words sm:text-xl">
23+
<a href="javascript:void(0)">{{ $schedule->name}}</a>
24+
</h3>
25+
@auth
26+
<a href="{{ $schedule->filamentDashboardEditUrl() }}" class="underline text-right text-sm text-zinc-500 hover:text-zinc-400 dark:text-zinc-400 dark:hover:text-zinc-300" title="{{ __('cachet::incident.edit_button_title') }}">
27+
<x-heroicon-m-pencil-square class="size-4" />
28+
</a>
29+
@endauth
30+
</div>
31+
<span class="text-xs text-zinc-500 dark:text-zinc-400">
32+
{{ $schedule->timestamp->diffForHumans() }} — <time datetime="{{ $schedule->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
33+
</span>
34+
</div>
35+
<div class="flex justify-start sm:justify-end">
36+
<x-cachet::badge :status="$schedule->latestStatus" />
37+
38+
</div>
39+
</div>
40+
</div>
41+
42+
<div class="relative">
43+
<div class="absolute inset-y-0 -left-9">
44+
<div class="ml-3.5 h-full border-l-2 border-dashed dark:border-zinc-700"></div>
45+
<div class="absolute inset-x-0 top-0 w-full h-24 bg-gradient-to-t from-transparent to-[rgb(var(--accent-background))]"></div>
46+
<div class="absolute inset-x-0 bottom-0 w-full h-24 bg-gradient-to-b from-transparent to-[rgb(var(--accent-background))]"></div>
47+
</div>
48+
<div class="flex flex-col px-4 divide-y dark:divide-zinc-700">
49+
@foreach ($schedule->updates as $update)
50+
<div class="relative py-4" x-data="{ timestamp: new Date(@js($update->created_at)) }">
51+
<x-cachet::schedule-update-status :status="$update->status" />
52+
<h3 class="text-lg font-semibold">{{ $update->status->getLabel() }}</h3>
53+
<span class="text-xs text-zinc-500 dark:text-zinc-400">
54+
{{ $update->created_at->diffForHumans() }} — <time datetime="{{ $update->created_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
55+
</span>
56+
<div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-accent-content prose-a:underline prose-p:leading-normal">{!! $update->formattedMessage() !!}</div>
57+
</div>
58+
@endforeach
59+
<div class="relative py-4" x-data="{ timestamp: new Date(@js($schedule->timestamp)) }">
60+
<x-cachet::schedule-update-status :status="ScheduleStatusEnum::complete" />
61+
62+
<span class="text-xs text-zinc-500 dark:text-zinc-400">
63+
{{ $schedule->timestamp->diffForHumans() }} — <time datetime="{{ $schedule->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
64+
</span>
65+
<div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-accent-content prose-a:underline prose-p:leading-normal">{!! $schedule->formattedMessage() !!}</div>
66+
</div>
67+
</div>
68+
</div>
69+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<div {{ $attributes->style([
2+
Illuminate\Support\Arr::toCssStyles([
3+
\Filament\Support\get_color_css_variables(
4+
$color,
5+
shades: [200, 400, 700, 900],
6+
),
7+
]),
8+
])->merge(['title' => $title]) }}>
9+
<div class="absolute -left-[calc(28px+10px+13px)] top-4 flex h-7 w-7 items-center justify-center rounded-full bg-custom-200 dark:bg-custom-200/80 text-custom-700 isolate">
10+
@svg($icon, 'size-5')
11+
</div>
12+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div {{ $attributes->style([
2+
Illuminate\Support\Arr::toCssStyles([
3+
\Filament\Support\get_color_css_variables(
4+
$color,
5+
shades: [200, 400, 700, 900],
6+
),
7+
]),
8+
])->merge(['title' => $label]) }}>
9+
<div class="absolute -left-[calc(28px+10px+13px)] top-1 flex h-7 w-7 items-center justify-center rounded-full bg-custom-200 dark:bg-custom-200/80 text-custom-700 isolate">
10+
@svg($icon, 'size-5')
11+
</div>
12+
</div>
13+
{{-- <x-filament::badge :color="$color" :icon="$icon" :icon-size="\Filament\Support\Enums\IconSize::Large" class="bg-custom-400 text-custom-900 dark:text-custom-400">--}}
14+
{{-- {{ $label }}--}}
15+
{{-- </x-filament::badge>--}}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Cachet\Collections;
4+
5+
use Cachet\Contracts\Support\Sequencable;
6+
use Illuminate\Support\Collection;
7+
8+
class TimelineCollection extends Collection
9+
{
10+
11+
12+
public function getSorted($startDate, $endDate, $onlyDisruptedDays = false)
13+
{
14+
return $this->sortByDesc(fn (Sequencable $item) => $item->getSequenceTimestamp())
15+
->groupBy(fn (Sequencable $item) => $item->getSequenceTimestamp()->toDateString())
16+
->union(
17+
// Back-fill any missing dates...
18+
collect($endDate->toPeriod($startDate))
19+
->keyBy(fn ($period) => $period->toDateString())
20+
->map(fn ($period) => collect())
21+
)
22+
->when($onlyDisruptedDays, fn ($collection) => $collection->filter(fn ($items) => $items->isNotEmpty()))
23+
->sortKeysDesc();
24+
}
25+
}

src/Contracts/Support/Sequencable.php

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Cachet\Contracts\Support;
4+
5+
use Carbon\CarbonInterface;
6+
7+
interface Sequencable
8+
{
9+
public function getSequenceTimestamp(): CarbonInterface;
10+
}

src/Models/Incident.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Cachet\Models;
44

55
use Cachet\Concerns\HasVisibility;
6+
use Cachet\Contracts\Support\Sequencable;
67
use Cachet\Database\Factories\IncidentFactory;
78
use Cachet\Enums\IncidentStatusEnum;
89
use Cachet\Enums\ResourceVisibilityEnum;
@@ -11,6 +12,7 @@
1112
use Cachet\Events\Incidents\IncidentUpdated;
1213
use Cachet\Filament\Resources\IncidentResource;
1314
use Carbon\Carbon;
15+
use Carbon\CarbonInterface;
1416
use Illuminate\Contracts\Auth\Authenticatable;
1517
use Illuminate\Database\Eloquent\Builder;
1618
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -56,7 +58,7 @@
5658
* @method static Builder<static>|static unresolved()
5759
* @method static Builder<static>|static stickied()
5860
*/
59-
class Incident extends Model
61+
class Incident extends Model implements Sequencable
6062
{
6163
/** @use HasFactory<IncidentFactory> */
6264
use HasFactory;
@@ -181,6 +183,12 @@ protected function timestamp(): Attribute
181183
);
182184
}
183185

186+
public function getSequenceTimestamp(): CarbonInterface
187+
{
188+
return $this->timestamp;
189+
}
190+
191+
184192
/**
185193
* Determine the latest status of the incident.
186194
*

src/Models/Schedule.php

+40-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
namespace Cachet\Models;
44

5+
use Cachet\Contracts\Support\Sequencable;
56
use Cachet\Database\Factories\ScheduleFactory;
67
use Cachet\Enums\ScheduleStatusEnum;
8+
use Cachet\Filament\Resources\ScheduleResource;
9+
use Cachet\QueryBuilders\ScheduleBuilder;
10+
use Carbon\CarbonInterface;
711
use Illuminate\Database\Eloquent\Builder;
812
use Illuminate\Database\Eloquent\Casts\Attribute;
913
use Illuminate\Database\Eloquent\Factories\Factory;
@@ -35,7 +39,7 @@
3539
* @method static Builder<static>|static inTheFuture()
3640
* @method static Builder<static>|static inThePast()
3741
*/
38-
class Schedule extends Model
42+
class Schedule extends Model implements Sequencable
3943
{
4044
/** @use HasFactory<ScheduleFactory> */
4145
use HasFactory;
@@ -107,6 +111,41 @@ public function formattedMessage(): string
107111
return Str::of($this->message)->markdown();
108112
}
109113

114+
/**
115+
* Determine the latest status of the incident.
116+
*
117+
* @return Attribute<ScheduleStatusEnum|null, never>
118+
*/
119+
protected function latestStatus(): Attribute
120+
{
121+
return Attribute::make(
122+
get: function ($value) {
123+
return $this->updates()->latest()->first()->status ?? $this->status;
124+
}
125+
);
126+
}
127+
128+
/**
129+
* @return Attribute<Carbon, never>
130+
*/
131+
protected function timestamp(): Attribute
132+
{
133+
return Attribute::make(
134+
get: fn () => $this->completed_at ?? $this->scheduled_at
135+
);
136+
}
137+
138+
public function getSequenceTimestamp(): CarbonInterface
139+
{
140+
return $this->timestamp;
141+
}
142+
143+
public function filamentDashboardEditUrl(): string
144+
{
145+
return ScheduleResource::getUrl(name: 'edit', parameters: ['record' => $this->id]);
146+
}
147+
148+
110149
/**
111150
* Scope schedules that are incomplete.
112151
*

0 commit comments

Comments
 (0)