diff --git a/public/build/manifest.json b/public/build/manifest.json index 0a1c654f..d692ffcd 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -1,18 +1,17 @@ { - "resources/css/cachet.css": { - "file": "assets/cachet-D-feVIY7.css", - "src": "resources/css/cachet.css", + "resources/js/cachet.js": { + "file": "assets/cachet.6122e927.js", + "src": "resources/js/cachet.js", "isEntry": true }, "resources/css/dashboard/theme.css": { - "file": "assets/theme-C6ZKRDAs.css", + "file": "assets/theme.8251fad4.css", "src": "resources/css/dashboard/theme.css", "isEntry": true }, - "resources/js/cachet.js": { - "file": "assets/cachet-DCZQ8JcZ.js", - "name": "cachet", - "src": "resources/js/cachet.js", + "resources/css/cachet.css": { + "file": "assets/cachet.30cb9e62.css", + "src": "resources/css/cachet.css", "isEntry": true } } \ No newline at end of file diff --git a/resources/views/components/incident.blade.php b/resources/views/components/incident.blade.php index 5548b27e..d8d9d9db 100644 --- a/resources/views/components/incident.blade.php +++ b/resources/views/components/incident.blade.php @@ -4,9 +4,7 @@ 'incidents', ]) -<div class="relative flex flex-col gap-5" x-data="{ forDate: new Date(@js($date)) }"> - <h3 class="text-xl font-semibold"><time datetime="{{ $date }}" x-text="forDate.toLocaleDateString()"></time></h3> - @forelse($incidents as $incident) + @foreach($incidents as $incident) <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-zinc-800"> <div @class([ 'flex flex-col bg-zinc-50 p-4 dark:bg-zinc-900 gap-2', @@ -68,13 +66,4 @@ </div> </div> </div> - @empty - <div class="bg-white border divide-y rounded-lg ml-9 dark:divide-zinc-700 dark:border-zinc-700 dark:bg-zinc-800"> - <div class="flex flex-col p-4 divide-y dark:divide-zinc-700"> - <div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-primary-500 prose-a:underline prose-p:leading-normal"> - {{ __('No incidents reported.') }} - </div> - </div> - </div> - @endforelse -</div> + @endforeach diff --git a/resources/views/components/schedule-timeline.blade.php b/resources/views/components/schedule-timeline.blade.php new file mode 100644 index 00000000..7225c677 --- /dev/null +++ b/resources/views/components/schedule-timeline.blade.php @@ -0,0 +1,43 @@ +@use('Cachet\Enums\ScheduleStatusEnum') +@props([ + 'date', + 'schedules', +]) + + @foreach($schedules as $schedule) + <div x-data="{ timestamp: new Date(@js($schedule->completed_at)) }" class="bg-white border divide-y rounded-lg ml-9 dark:divide-zinc-700 dark:border-zinc-700 dark:bg-zinc-800"> + <div @class([ + 'flex flex-col bg-zinc-50 p-4 dark:bg-zinc-900 gap-2', + 'rounded-lg', + ])> + <div class="flex flex-col sm:flex-row justify-between gap-2 flex-col-reverse items-start sm:items-center"> + <div class="flex flex-col flex-1"> + <div class="flex gap-2 items-center"> + <h3 class="max-w-full text-base font-semibold break-words sm:text-xl"> + {{ $schedule->name}} + </h3> + @auth + <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="{{ __('Edit Incident') }}"> + <x-heroicon-m-pencil-square class="size-4" /> + </a> + @endauth + </div> + <span class="text-xs text-zinc-500 dark:text-zinc-400"> + {{ $schedule->completed_at->diffForHumans() }} — <time datetime="{{ $schedule->completed_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time> + </span> + </div> + <div class="flex justify-start sm:justify-end"> + <x-cachet::badge :status="$schedule->status" /> + </div> + </div> + </div> + + <div class="relative"> + <div class="flex flex-col px-4 divide-y dark:divide-zinc-700"> + <div class="relative py-4" x-data="{ timestamp: new Date(@js($schedule->completed_at)) }"> + <div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-primary-500 prose-a:underline prose-p:leading-normal">{!! $schedule->formattedMessage() !!}</div> + </div> + </div> + </div> + </div> + @endforeach diff --git a/resources/views/components/schedule.blade.php b/resources/views/components/schedule.blade.php index 1ec66c48..1abb3c1b 100644 --- a/resources/views/components/schedule.blade.php +++ b/resources/views/components/schedule.blade.php @@ -13,6 +13,11 @@ {{ $schedule->scheduled_at->diffForHumans() }} — <time datetime="{{ $schedule->scheduled_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time> </span> </div> + @auth + <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="{{ __('Edit Incident') }}"> + <x-heroicon-m-pencil-square class="size-4" /> + </a> + @endauth <div class="flex justify-start sm:justify-end"> <x-cachet::badge :status="$schedule->status" /> diff --git a/resources/views/components/incident-timeline.blade.php b/resources/views/components/timeline.blade.php similarity index 66% rename from resources/views/components/incident-timeline.blade.php rename to resources/views/components/timeline.blade.php index cda4b2d0..55a4107b 100644 --- a/resources/views/components/incident-timeline.blade.php +++ b/resources/views/components/timeline.blade.php @@ -29,13 +29,22 @@ </div> <div class="flex flex-col gap-14 w-full"> - @forelse ($incidents as $date => $incident) - <x-cachet::incident :date="$date" :incidents="$incident" /> - @empty - <div class="text-zinc-500 dark:text-zinc-400 text-center"> - {{ __('No incidents reported between :from and :to.', ['from' => $from, 'to' => $to]) }} + @foreach ($incidents as $date => $incident) + <div class="relative flex flex-col gap-5" x-data="{ forDate: new Date(@js($date)) }"> + <h3 class="text-xl font-semibold"><time datetime="{{ $date }}" x-text="forDate.toLocaleDateString()"></time></h3> + <x-cachet::incident :date="$date" :incidents="$incident" /> + <x-cachet::schedule-timeline :date="$date" :schedules="$schedules[$date]" /> + @if(count($incident) === 0 && count($schedules[$date]) === 0) + <div class="bg-white border divide-y rounded-lg ml-9 dark:divide-zinc-700 dark:border-zinc-700 dark:bg-zinc-800"> + <div class="flex flex-col p-4 divide-y dark:divide-zinc-700"> + <div class="prose-sm md:prose prose-zinc dark:prose-invert prose-a:text-primary-500 prose-a:underline prose-p:leading-normal"> + {{ __('No incidents reported.') }} + </div> + </div> + </div> + @endif + @endforeach </div> - @endforelse </div> <div class="flex justify-between"> diff --git a/resources/views/status-page/index.blade.php b/resources/views/status-page/index.blade.php index 5fef4e98..6d076988 100644 --- a/resources/views/status-page/index.blade.php +++ b/resources/views/status-page/index.blade.php @@ -18,7 +18,7 @@ <x-cachet::schedules :schedules="$schedules" /> @endif - <x-cachet::incident-timeline /> + <x-cachet::timeline /> </div> <x-cachet::footer /> diff --git a/src/Models/Schedule.php b/src/Models/Schedule.php index bbbb5b2f..492e67f1 100644 --- a/src/Models/Schedule.php +++ b/src/Models/Schedule.php @@ -4,6 +4,7 @@ use Cachet\Database\Factories\ScheduleFactory; use Cachet\Enums\ScheduleStatusEnum; +use Cachet\Filament\Resources\ScheduleResource; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\Factory; @@ -113,6 +114,19 @@ public function scopeInThePast(Builder $query): Builder return $query->where('completed_at', '<=', Carbon::now()); } + /** + * Get the URL to the schedule page within the dashboard. + */ + public function filamentDashboardEditUrl(): string + { + return ScheduleResource::getUrl(name: 'edit', parameters: ['record' => $this->id]); + } + + public function timestamp(): Attribute + { + return Attribute::get(fn () => $this->completed_at ?: $this->scheduled_at); + } + /** * Create a new factory instance for the model. */ diff --git a/src/View/Components/IncidentTimeline.php b/src/View/Components/Timeline.php similarity index 65% rename from src/View/Components/IncidentTimeline.php rename to src/View/Components/Timeline.php index c9cfeab6..6921a4d0 100644 --- a/src/View/Components/IncidentTimeline.php +++ b/src/View/Components/Timeline.php @@ -3,6 +3,7 @@ namespace Cachet\View\Components; use Cachet\Models\Incident; +use Cachet\Models\Schedule; use Cachet\Settings\AppSettings; use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Builder; @@ -10,7 +11,7 @@ use Illuminate\Support\Collection; use Illuminate\View\Component; -class IncidentTimeline extends Component +class Timeline extends Component { public function __construct(private AppSettings $appSettings) { @@ -23,12 +24,17 @@ public function render(): View $startDate = Carbon::createFromFormat('Y-m-d', request('from', now()->toDateString())); $endDate = $startDate->clone()->subDays($incidentDays); - return view('cachet::components.incident-timeline', [ + return view('cachet::components.timeline', [ 'incidents' => $this->incidents( $startDate, $endDate, $this->appSettings->only_disrupted_days ), + 'schedules' => $this->schedules( + $startDate, + $endDate, + $this->appSettings->only_disrupted_days + ), 'from' => $startDate->toDateString(), 'to' => $endDate->toDateString(), 'nextPeriodFrom' => $startDate->clone()->subDays($incidentDays + 1)->toDateString(), @@ -72,4 +78,33 @@ private function incidents(Carbon $startDate, Carbon $endDate, bool $onlyDisrupt ->when($onlyDisruptedDays, fn ($collection) => $collection->filter(fn ($incidents) => $incidents->isNotEmpty())) ->sortKeysDesc(); } + + /** + * Fetch the schedules that occurred between the given start and end date. + * Schedules will be grouped by days. + */ + private function schedules(Carbon $startDate, Carbon $endDate, bool $onlyDisruptedDays = false): Collection + { + return Schedule::query() + ->with([ + 'components', + ]) + ->where(function (Builder $query) use ($endDate, $startDate) { + $query->whereBetween('completed_at', [ + $endDate->startOfDay()->toDateTimeString(), + $startDate->endofDay()->toDateTimeString(), + ]); + }) + ->orderBy('completed_at', 'desc') + ->get() + ->groupBy(fn (Schedule $schedule) => $schedule->completed_at->toDateString()) + ->union( + // Back-fill any missing dates... + collect($endDate->toPeriod($startDate)) + ->keyBy(fn ($period) => $period->toDateString()) + ->map(fn ($period) => collect()) + ) + ->when($onlyDisruptedDays, fn ($collection) => $collection->filter(fn ($schedules) => $schedules->isNotEmpty())) + ->sortKeysDesc(); + } }