Skip to content

Show Schedules on timeline (#93) #234

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
68 changes: 68 additions & 0 deletions resources/views/components/incident-item.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@use('Cachet\Enums\IncidentStatusEnum')
@props([
'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-white/5">
<div @class([
'flex flex-col bg-zinc-50 p-4 dark:bg-accent-background gap-2',
'rounded-t-lg' => $incident->updates->isNotEmpty(),
'rounded-lg' => $incident->updates->isEmpty(),
])>
@if ($incident->components()->exists())
<div class="text-xs font-medium">
{{ $incident->components->pluck('name')->join(', ', ' and ') }}
</div>
@endif
<div class="flex flex-col sm:flex-row justify-between gap-2 flex-col-reverse items-start sm:items-center relative">
<div class="flex flex-col flex-1">
<x-cachet::timeline-badge icon="cachet-incident" :color="\Filament\Support\Colors\Color::Amber" label="" />
<div class="flex gap-2 items-center">
<h3 class="max-w-full text-base font-semibold break-words sm:text-xl">
<a href="{{ route('cachet.status-page.incident', $incident) }}">{{ $incident->name}}</a>
</h3>
@auth
<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') }}">
<x-heroicon-m-pencil-square class="size-4" />
</a>
@endauth
</div>
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
</div>
<div class="flex justify-start sm:justify-end">
<x-cachet::badge :status="$incident->latestStatus" />
</div>
</div>
</div>

<div class="relative">
<div class="absolute inset-y-0 -left-9">
<div class="ml-3.5 h-full border-l-2 border-dashed dark:border-zinc-700"></div>
<div class="absolute inset-x-0 top-0 w-full h-24 bg-gradient-to-t from-transparent to-[rgb(var(--accent-background))]"></div>
<div class="absolute inset-x-0 bottom-0 w-full h-24 bg-gradient-to-b from-transparent to-[rgb(var(--accent-background))]"></div>
</div>
<div class="flex flex-col px-4 divide-y dark:divide-zinc-700">
@foreach ($incident->updates as $update)
<div class="relative py-4" x-data="{ timestamp: new Date(@js($update->created_at)) }">
<x-cachet::incident-update-status :status="$update->status" />
<h3 class="text-lg font-semibold">{{ $update->status->getLabel() }}</h3>
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $update->created_at->diffForHumans() }} — <time datetime="{{ $update->created_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
<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>
</div>
@endforeach
<div class="relative py-4" x-data="{ timestamp: new Date(@js($incident->timestamp)) }">
<x-cachet::incident-update-status :status="IncidentStatusEnum::unknown" />

<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
<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>
</div>
</div>
</div>
</div>
66 changes: 5 additions & 61 deletions resources/views/components/incident.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,11 @@
<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)
<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">
<div @class([
'flex flex-col bg-zinc-50 p-4 dark:bg-accent-background gap-2',
'rounded-t-lg' => $incident->updates->isNotEmpty(),
'rounded-lg' => $incident->updates->isEmpty(),
])>
@if ($incident->components()->exists())
<div class="text-xs font-medium">
{{ $incident->components->pluck('name')->join(', ', ' and ') }}
</div>
@endif
<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">
<a href="{{ route('cachet.status-page.incident', $incident) }}">{{ $incident->name}}</a>
</h3>
@auth
<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') }}">
<x-heroicon-m-pencil-square class="size-4" />
</a>
@endauth
</div>
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
</div>
<div class="flex justify-start sm:justify-end">
<x-cachet::badge :status="$incident->latestStatus" />
</div>
</div>
</div>

<div class="relative">
<div class="absolute inset-y-0 -left-9">
<div class="ml-3.5 h-full border-l-2 border-dashed dark:border-zinc-700"></div>
<div class="absolute inset-x-0 top-0 w-full h-24 bg-gradient-to-t from-transparent to-[rgb(var(--accent-background))]"></div>
<div class="absolute inset-x-0 bottom-0 w-full h-24 bg-gradient-to-b from-transparent to-[rgb(var(--accent-background))]"></div>
</div>
<div class="flex flex-col px-4 divide-y dark:divide-zinc-700">
@foreach ($incident->updates as $update)
<div class="relative py-4" x-data="{ timestamp: new Date(@js($update->created_at)) }">
<x-cachet::incident-update-status :status="$update->status" />
<h3 class="text-lg font-semibold">{{ $update->status->getLabel() }}</h3>
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $update->created_at->diffForHumans() }} — <time datetime="{{ $update->created_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
<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>
</div>
@endforeach
<div class="relative py-4" x-data="{ timestamp: new Date(@js($incident->timestamp)) }">
<x-cachet::incident-update-status :status="IncidentStatusEnum::unknown" />

<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $incident->timestamp->diffForHumans() }} — <time datetime="{{ $incident->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
<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>
</div>
</div>
</div>
</div>
@if($incident instanceof \Cachet\Models\Incident)
<x-cachet::incident-item :incident="$incident" />
@elseif($incident instanceof \Cachet\Models\Schedule)
<x-cachet::schedule-item :schedule="$incident" />
@endif
@empty
<div class="bg-white border divide-y rounded-lg dark:divide-zinc-700 dark:border-zinc-700 dark:bg-white/5">
<div class="flex flex-col p-4 divide-y dark:divide-zinc-700">
Expand Down
69 changes: 69 additions & 0 deletions resources/views/components/schedule-item.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@use('Cachet\Enums\ScheduleStatusEnum')
@props([
'schedule',
])


<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">
<div @class([
'flex flex-col bg-zinc-50 p-4 dark:bg-accent-background gap-2',
'rounded-t-lg' => $schedule->updates->isNotEmpty(),
'rounded-lg' => $schedule->updates->isEmpty(),
])>
@if ($schedule->components()->exists())
<div class="text-xs font-medium">
{{ $schedule->components->pluck('name')->join(', ', ' and ') }}
</div>
@endif
<div class="flex flex-col sm:flex-row justify-between gap-2 flex-col-reverse items-start sm:items-center relative">
<div class="flex flex-col flex-1">
<x-cachet::timeline-badge icon="cachet-maintenance" :color="\Filament\Support\Colors\Color::Slate" label="" />
<div class="flex gap-2 items-center">
<h3 class="max-w-full text-base font-semibold break-words sm:text-xl">
<a href="javascript:void(0)">{{ $schedule->name}}</a>
</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="{{ __('cachet::incident.edit_button_title') }}">
<x-heroicon-m-pencil-square class="size-4" />
</a>
@endauth
</div>
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $schedule->timestamp->diffForHumans() }} — <time datetime="{{ $schedule->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
</div>
<div class="flex justify-start sm:justify-end">
<x-cachet::badge :status="$schedule->latestStatus" />

</div>
</div>
</div>

<div class="relative">
<div class="absolute inset-y-0 -left-9">
<div class="ml-3.5 h-full border-l-2 border-dashed dark:border-zinc-700"></div>
<div class="absolute inset-x-0 top-0 w-full h-24 bg-gradient-to-t from-transparent to-[rgb(var(--accent-background))]"></div>
<div class="absolute inset-x-0 bottom-0 w-full h-24 bg-gradient-to-b from-transparent to-[rgb(var(--accent-background))]"></div>
</div>
<div class="flex flex-col px-4 divide-y dark:divide-zinc-700">
@foreach ($schedule->updates as $update)
<div class="relative py-4" x-data="{ timestamp: new Date(@js($update->created_at)) }">
<x-cachet::schedule-update-status :status="$update->status" />
<h3 class="text-lg font-semibold">{{ $update->status->getLabel() }}</h3>
<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $update->created_at->diffForHumans() }} — <time datetime="{{ $update->created_at->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
<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>
</div>
@endforeach
<div class="relative py-4" x-data="{ timestamp: new Date(@js($schedule->timestamp)) }">
<x-cachet::schedule-update-status :status="ScheduleStatusEnum::complete" />

<span class="text-xs text-zinc-500 dark:text-zinc-400">
{{ $schedule->timestamp->diffForHumans() }} — <time datetime="{{ $schedule->timestamp->toW3cString() }}" x-text="timestamp.toLocaleString()"></time>
</span>
<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>
</div>
</div>
</div>
</div>
12 changes: 12 additions & 0 deletions resources/views/components/schedule-update-status.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div {{ $attributes->style([
Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [200, 400, 700, 900],
),
]),
])->merge(['title' => $title]) }}>
<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">
@svg($icon, 'size-5')
</div>
</div>
15 changes: 15 additions & 0 deletions resources/views/components/timeline-badge.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div {{ $attributes->style([
Illuminate\Support\Arr::toCssStyles([
\Filament\Support\get_color_css_variables(
$color,
shades: [200, 400, 700, 900],
),
]),
])->merge(['title' => $label]) }}>
<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">
@svg($icon, 'size-5')
</div>
</div>
{{-- <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">--}}
{{-- {{ $label }}--}}
{{-- </x-filament::badge>--}}
25 changes: 25 additions & 0 deletions src/Collections/TimelineCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Cachet\Collections;

use Cachet\Contracts\Support\Sequencable;
use Illuminate\Support\Collection;

class TimelineCollection extends Collection
{


public function getSorted($startDate, $endDate, $onlyDisruptedDays = false)
{
return $this->sortByDesc(fn (Sequencable $item) => $item->getSequenceTimestamp())
->groupBy(fn (Sequencable $item) => $item->getSequenceTimestamp()->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 ($items) => $items->isNotEmpty()))
->sortKeysDesc();
}
}
10 changes: 10 additions & 0 deletions src/Contracts/Support/Sequencable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Cachet\Contracts\Support;

use Carbon\CarbonInterface;

interface Sequencable
{
public function getSequenceTimestamp(): CarbonInterface;
}
10 changes: 9 additions & 1 deletion src/Models/Incident.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Cachet\Models;

use Cachet\Concerns\HasVisibility;
use Cachet\Contracts\Support\Sequencable;
use Cachet\Database\Factories\IncidentFactory;
use Cachet\Enums\IncidentStatusEnum;
use Cachet\Enums\ResourceVisibilityEnum;
Expand All @@ -11,6 +12,7 @@
use Cachet\Events\Incidents\IncidentUpdated;
use Cachet\Filament\Resources\IncidentResource;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
Expand Down Expand Up @@ -56,7 +58,7 @@
* @method static Builder<static>|static unresolved()
* @method static Builder<static>|static stickied()
*/
class Incident extends Model
class Incident extends Model implements Sequencable
{
/** @use HasFactory<IncidentFactory> */
use HasFactory;
Expand Down Expand Up @@ -181,6 +183,12 @@ protected function timestamp(): Attribute
);
}

public function getSequenceTimestamp(): CarbonInterface
{
return $this->timestamp;
}


/**
* Determine the latest status of the incident.
*
Expand Down
40 changes: 39 additions & 1 deletion src/Models/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace Cachet\Models;

use Cachet\Contracts\Support\Sequencable;
use Cachet\Database\Factories\ScheduleFactory;
use Cachet\Enums\ScheduleStatusEnum;
use Cachet\Filament\Resources\ScheduleResource;
use Carbon\CarbonInterface;
use Cachet\QueryBuilders\ScheduleBuilder;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
Expand Down Expand Up @@ -35,7 +38,7 @@
* @method static \Cachet\QueryBuilders\ScheduleBuilder inTheFuture()
* @method static \Cachet\QueryBuilders\ScheduleBuilder inThePast()
*/
class Schedule extends Model
class Schedule extends Model implements Sequencable
{
/** @use HasFactory<ScheduleFactory> */
use HasFactory;
Expand Down Expand Up @@ -108,6 +111,41 @@ public function formattedMessage(): string
return Str::of($this->message)->markdown();
}

/**
* Determine the latest status of the incident.
*
* @return Attribute<ScheduleStatusEnum|null, never>
*/
protected function latestStatus(): Attribute
{
return Attribute::make(
get: function ($value) {
return $this->updates()->latest()->first()->status ?? $this->status;
}
);
}

/**
* @return Attribute<Carbon, never>
*/
protected function timestamp(): Attribute
{
return Attribute::make(
get: fn () => $this->completed_at ?? $this->scheduled_at
);
}

public function getSequenceTimestamp(): CarbonInterface
{
return $this->timestamp;
}

public function filamentDashboardEditUrl(): string
{
return ScheduleResource::getUrl(name: 'edit', parameters: ['record' => $this->id]);
}


/**
* Create a new Eloquent query builder for the model.
*
Expand Down
Loading