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;
+ });
+ }
+}