diff --git a/database/migrations/2025_01_21_121458_add_ordering_cols_to_component_groups_table.php b/database/migrations/2025_01_21_121458_add_ordering_cols_to_component_groups_table.php new file mode 100644 index 00000000..9daf70fc --- /dev/null +++ b/database/migrations/2025_01_21_121458_add_ordering_cols_to_component_groups_table.php @@ -0,0 +1,35 @@ +string('order_column')->nullable()->after('order'); + $table->char('order_direction', 4)->nullable()->after('order_column'); + }); + + DB::table('component_groups')->update(['order_column' => ResourceOrderColumnEnum::Manual->value]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('component_groups', function (Blueprint $table) { + $table->dropColumn([ + 'order_column', + 'order_direction', + ]); + }); + } +}; diff --git a/resources/lang/en/component_group.php b/resources/lang/en/component_group.php index 6eb88f3f..286604b7 100644 --- a/resources/lang/en/component_group.php +++ b/resources/lang/en/component_group.php @@ -13,6 +13,7 @@ 'name' => 'Name', 'visible' => 'Visible', 'collapsed' => 'Collapsed', + 'order_column' => 'Component Group Order', 'created_at' => 'Created at', 'updated_at' => 'Updated at', ], @@ -25,5 +26,7 @@ 'name_label' => 'Name', 'visible_label' => 'Visible', 'collapsed_label' => 'Collapsed', + 'order_column_label' => 'Component Group Order', + 'order_direction' => 'Order Direction', ], ]; diff --git a/resources/lang/en/resource.php b/resources/lang/en/resource.php index fdede18c..8ad1b6e3 100644 --- a/resources/lang/en/resource.php +++ b/resources/lang/en/resource.php @@ -1,6 +1,17 @@ [ + 'id' => 'ID', + 'last_updated' => 'Last Updated', + 'name' => 'Name', + 'manual' => 'Manual', + 'status' => 'Status', + ], + 'order_direction' => [ + 'asc' => 'Ascending', + 'desc' => 'Descending', + ], 'visibility' => [ 'authenticated' => 'Users', 'guest' => 'Guests', diff --git a/resources/views/components/component-groups.blade.php b/resources/views/components/component-groups.blade.php new file mode 100644 index 00000000..8fe49c62 --- /dev/null +++ b/resources/views/components/component-groups.blade.php @@ -0,0 +1,7 @@ +@foreach($componentGroups as $componentGroup) + +@endforeach + +@foreach($ungroupedComponents as $component) + +@endforeach diff --git a/resources/views/status-page/index.blade.php b/resources/views/status-page/index.blade.php index bb9092e9..00d9a0a5 100644 --- a/resources/views/status-page/index.blade.php +++ b/resources/views/status-page/index.blade.php @@ -5,14 +5,9 @@ - @foreach ($componentGroups as $componentGroup) - - @endforeach - - @foreach ($ungroupedComponents as $component) - - @endforeach + + @if ($schedules->isNotEmpty()) diff --git a/src/Enums/ResourceOrderColumnEnum.php b/src/Enums/ResourceOrderColumnEnum.php new file mode 100644 index 00000000..ad56af76 --- /dev/null +++ b/src/Enums/ResourceOrderColumnEnum.php @@ -0,0 +1,38 @@ + __('cachet::resource.order_column.id'), + self::LastUpdated => __('cachet::resource.order_column.last_updated'), + self::Name => __('cachet::resource.order_column.name'), + self::Manual => __('cachet::resource.order_column.manual'), + self::Status => __('cachet::resource.order_column.status'), + }; + } + + /** + * Determine if the column requires a direction. + */ + public static function requiresDirection(): array + { + return [ + self::Id, + self::LastUpdated, + self::Name, + self::Status, + ]; + } +} diff --git a/src/Enums/ResourceOrderDirectionEnum.php b/src/Enums/ResourceOrderDirectionEnum.php new file mode 100644 index 00000000..52e8f723 --- /dev/null +++ b/src/Enums/ResourceOrderDirectionEnum.php @@ -0,0 +1,29 @@ + __('cachet::resource.order_direction.asc'), + self::Desc => __('cachet::resource.order_direction.desc'), + }; + } + + public function ascending(): bool + { + return $this === self::Asc; + } + + public function descending(): bool + { + return $this === self::Desc; + } +} diff --git a/src/Filament/Resources/ComponentGroupResource.php b/src/Filament/Resources/ComponentGroupResource.php index fe3c99a9..8f3bfb53 100644 --- a/src/Filament/Resources/ComponentGroupResource.php +++ b/src/Filament/Resources/ComponentGroupResource.php @@ -3,6 +3,8 @@ namespace Cachet\Filament\Resources; use Cachet\Enums\ComponentGroupVisibilityEnum; +use Cachet\Enums\ResourceOrderColumnEnum; +use Cachet\Enums\ResourceOrderDirectionEnum; use Cachet\Enums\ResourceVisibilityEnum; use Cachet\Filament\Resources\ComponentGroupResource\Pages; use Cachet\Filament\Resources\ComponentResource\RelationManagers\ComponentsRelationManager; @@ -42,9 +44,21 @@ public static function form(Form $form): Form ->required() ->inline() ->options(ComponentGroupVisibilityEnum::class) - ->default(ComponentGroupVisibilityEnum::expanded) + ->default(ComponentGroupVisibilityEnum::expanded->value) ->columnSpanFull(), ]), + Forms\Components\Section::make()->schema([ + Forms\Components\Select::make('order_column') + ->label(__('cachet::component_group.form.order_column_label')) + ->options(ResourceOrderColumnEnum::class) + ->required() + ->reactive(), + Forms\Components\Select::make('order_direction') + ->label(__('cachet::component_group.form.order_direction')) + ->options(ResourceOrderDirectionEnum::class) + ->required(fn (Forms\Get $get) => $get('order_column') !== ResourceOrderColumnEnum::Manual->value) + ->visible(fn (Forms\Get $get) => $get('order_column') !== ResourceOrderColumnEnum::Manual->value), + ]) ]); } @@ -63,6 +77,15 @@ public static function table(Table $table): Table Tables\Columns\TextColumn::make('collapsed') ->label(__('cachet::component_group.list.headers.collapsed')) ->sortable(), + Tables\Columns\TextColumn::make('order_column') + ->icon(fn ($record) => match (true) { + $record->order_column === ResourceOrderColumnEnum::Manual => 'heroicon-o-chevron-up-down', + $record->order_direction === ResourceOrderDirectionEnum::Asc => 'heroicon-o-arrow-up', + $record->order_direction === ResourceOrderDirectionEnum::Desc => 'heroicon-o-arrow-down', + default => null, + }) + ->label(__('cachet::component_group.list.headers.order_column')) + ->sortable(), Tables\Columns\TextColumn::make('created_at') ->label(__('cachet::component_group.list.headers.created_at')) ->dateTime() diff --git a/src/Http/Controllers/StatusPage/StatusPageController.php b/src/Http/Controllers/StatusPage/StatusPageController.php index f4041f6e..868e0d9d 100644 --- a/src/Http/Controllers/StatusPage/StatusPageController.php +++ b/src/Http/Controllers/StatusPage/StatusPageController.php @@ -2,11 +2,14 @@ namespace Cachet\Http\Controllers\StatusPage; +use Cachet\Enums\ResourceOrderColumnEnum; use Cachet\Models\Component; use Cachet\Models\ComponentGroup; use Cachet\Models\Incident; use Cachet\Models\Schedule; +use Filament\Resources\Resource; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\DB; use Illuminate\View\View; class StatusPageController @@ -17,19 +20,6 @@ class StatusPageController public function index(): View { return view('cachet::status-page.index', [ - 'componentGroups' => ComponentGroup::query() - ->with(['components' => fn ($query) => $query->enabled()->orderBy('order')->withCount('incidents')]) - ->visible(auth()->check()) - ->orderBy('order') - ->when(auth()->check(), fn (Builder $query) => $query->users(), fn ($query) => $query->guests()) - ->get(), - 'ungroupedComponents' => Component::query() - ->enabled() - ->whereNull('component_group_id') - ->orderBy('order') - ->withCount('incidents') - ->get(), - 'schedules' => Schedule::query()->with('updates')->incomplete()->orderBy('scheduled_at')->get(), ]); } diff --git a/src/Models/Component.php b/src/Models/Component.php index a14cf9cf..390f6498 100644 --- a/src/Models/Component.php +++ b/src/Models/Component.php @@ -4,6 +4,7 @@ use Cachet\Database\Factories\ComponentFactory; use Cachet\Enums\ComponentStatusEnum; +use Cachet\Enums\ResourceOrderColumnEnum; use Cachet\Events\Components\ComponentCreated; use Cachet\Events\Components\ComponentDeleted; use Cachet\Events\Components\ComponentUpdated; @@ -138,6 +139,20 @@ public function latestStatus(): Attribute return Attribute::get(fn () => $this->incidents()->unresolved()->latest()->first()?->pivot->component_status ?? $this->status); } + /** + * Determine how to order the component. + */ + public function orderableBy(ComponentGroup $group): mixed + { + return match ($group->order_column) { + ResourceOrderColumnEnum::Id => $this->id, + ResourceOrderColumnEnum::LastUpdated => $this->updated_at, + ResourceOrderColumnEnum::Name => $this->name, + ResourceOrderColumnEnum::Manual => $this->order, + default => $this->status->value, + }; + } + /** * Create a new factory instance for the model. */ diff --git a/src/Models/ComponentGroup.php b/src/Models/ComponentGroup.php index e078911f..08ca405c 100644 --- a/src/Models/ComponentGroup.php +++ b/src/Models/ComponentGroup.php @@ -5,6 +5,8 @@ use Cachet\Concerns\HasVisibility; use Cachet\Database\Factories\ComponentGroupFactory; use Cachet\Enums\ComponentGroupVisibilityEnum; +use Cachet\Enums\ResourceOrderColumnEnum; +use Cachet\Enums\ResourceOrderDirectionEnum; use Cachet\Enums\ResourceVisibilityEnum; use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\Factory; @@ -34,7 +36,8 @@ class ComponentGroup extends Model /** @var array */ protected $casts = [ - 'order' => 'int', + 'order_column' => ResourceOrderColumnEnum::class, + 'order_direction' => ResourceOrderDirectionEnum::class, 'collapsed' => ComponentGroupVisibilityEnum::class, 'visible' => ResourceVisibilityEnum::class, ]; @@ -43,6 +46,8 @@ class ComponentGroup extends Model protected $fillable = [ 'name', 'order', + 'order_column', + 'order_direction', 'collapsed', 'visible', ]; @@ -54,7 +59,7 @@ class ComponentGroup extends Model */ public function components(): HasMany { - return $this->hasMany(Component::class); + return $this->hasMany(Component::class)->chaperone('group'); } public function isCollapsible(): bool diff --git a/src/Models/Incident.php b/src/Models/Incident.php index 30e334b9..1f7bc510 100644 --- a/src/Models/Incident.php +++ b/src/Models/Incident.php @@ -124,7 +124,7 @@ public function components(): BelongsToMany */ public function incidentComponents(): HasMany { - return $this->hasMany(IncidentComponent::class); + return $this->hasMany(IncidentComponent::class)->chaperone(); } /** diff --git a/src/View/Components/ComponentGroups.php b/src/View/Components/ComponentGroups.php new file mode 100644 index 00000000..eb9eca94 --- /dev/null +++ b/src/View/Components/ComponentGroups.php @@ -0,0 +1,47 @@ + $this->componentGroups(), + 'ungroupedComponents' => \Cachet\Models\Component::query() + ->enabled() + ->whereNull('component_group_id') + ->orderBy('order') + ->withCount('incidents') + ->get(), + ]); + } + + /** + * Fetch component groups with their components in the configured order. + */ + private function componentGroups(): Collection + { + return ComponentGroup::query() + ->with(['components' => fn ($query) => $query->enabled()->orderBy('order')->withCount('incidents')]) + ->visible(auth()->check()) + ->when(auth()->check(), fn (Builder $query) => $query->users(), fn ($query) => $query->guests()) + ->get() + ->map(function (ComponentGroup $group) { + $group->components = $group->components + ->sortBy( + fn (\Cachet\Models\Component $component) => $component->orderableBy($group), + descending: $group->order_direction?->descending() + ); + + return $group; + }); + } +}